From edd4af285ffc25b1492bc06361377e10ec99f92c Mon Sep 17 00:00:00 2001 From: dadachi Date: Fri, 16 Oct 2026 06:56:56 +0900 Subject: [PATCH] Close the shop-deleted notice on sign-out In MyTurnTagCreator-Android, "Organization switched successfully." stayed over the sign-in screen after Settings > Sign Out; the same mechanism here keeps "Shop deleted" up after a sign-out, since nothing dismissed the root snackbar. showNoticeStoredForLaunch, which shows the notices stored before the app restarts itself, now cancels the notice when the shopkeeper signs out, which dismisses its snackbar. The notices that explain a sign-out (email changed, account deleted) are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YDK5huvuRECyVrsQEdTN9q --- CHANGELOG.md | 1 + .../ui/app_root/NativeAppTemplateApp.kt | 1 + .../ui/app_root/StoredNotice.kt | 17 +++++++- .../ui/app_root/StoredNoticeTest.kt | 39 ++++++++++++++++++- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d355d6..e2c994a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - Error messages are in the app language and say what the user can do, with the same wording as NativeAppTemplate-Free-iOS; the error codes are unchanged. A request that got no answer (no network, a timeout) or an error status without a message (e.g. a 502 page) shows `[NATIVEAPPTEMPLATE-2002]` "Something went wrong while communicating with the server. Please try again later." (サーバーとの通信中に問題が発生しました。しばらくしてからもう一度お試しください。) instead of the English system text (e.g. "Processing error: Unable to resolve host ..." or "Processing error: HTTP 502"). A response the app could not process (`2002`) and `1001` show "An unexpected error occurred." (予期しないエラーが発生しました。). The server's own messages are shown as is, without the developer suffix " [Status: 422]" (`2001`) or the "Processing error: " prefix (sign-in's "Invalid login credentials"). The details are logged with the code instead (`adb logcat -s ApiError`) - An error snackbar no longer stays after the screen recovers (e.g. the offline `[NATIVEAPPTEMPLATE-2002]` error still showing after the network is back and Reload succeeded). Every screen's load (Reload, pull to refresh, resume) and every sign-in attempt now clears the previous attempt's error when it starts, which closes its snackbar; an error of the new attempt still shows. Ported from MyTurnTagCreator-Android #139 +- The "Shop deleted" notice no longer stays over the sign-in screen after signing out. The notices stored before the app restarts itself are about the session, so signing out closes the one still on screen (`showNoticeStoredForLaunch`); the notices that explain a sign-out (email changed, account deleted) are unchanged. Ported from MyTurnTagCreator-Android #143 ## [3.2.6] - 2026-05-24 diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/NativeAppTemplateApp.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/NativeAppTemplateApp.kt index 533090a..d39e622 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/NativeAppTemplateApp.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/NativeAppTemplateApp.kt @@ -101,6 +101,7 @@ fun NativeAppTemplateApp(appState: NativeAppTemplateAppState) { showNoticeStoredForLaunch( flag = loginRepository.isShopDeleted(), clear = { loginRepository.setIsShopDeleted(false) }, + isLoggedIn = loginRepository.isLoggedIn(), show = showNotice(R.string.message_shop_deleted), ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNotice.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNotice.kt index 32cb416..49913c1 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNotice.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNotice.kt @@ -1,8 +1,10 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch // Notices stored as flags in DataStore by the screen that caused them. Each is cleared as soon as it // is taken for display, not after the snackbar is dismissed: an Indefinite snackbar returns only @@ -30,14 +32,27 @@ internal suspend fun showStoredNotice( * restarts itself (shop deleted). The process that stored the * flag must not take it: its snackbar would be gone a moment later with the process, and the * restarted app would find the flag cleared. + * + * These notices are about the session, so signing out ([isLoggedIn] turning false) closes one that + * is still up: cancelling [show] dismisses its snackbar, which otherwise stayed over the sign-in + * screen. */ internal suspend fun showNoticeStoredForLaunch( flag: Flow, clear: suspend () -> Unit, + isLoggedIn: Flow, show: suspend () -> Unit, ) { if (flag.first()) { clear() - show() + coroutineScope { + val notice = launch { show() } + val signOut = launch { + isLoggedIn.first { !it } + notice.cancel() + } + notice.join() + signOut.cancel() + } } } diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNoticeTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNoticeTest.kt index be753e5..5f9ab47 100644 --- a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNoticeTest.kt +++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/StoredNoticeTest.kt @@ -14,10 +14,11 @@ import org.junit.Test class StoredNoticeTest { // Outlives the "launches" below, as DataStore's file outlives the app process. private val preferences = NativeAppTemplatePreferencesDataSource( - InMemoryDataStore(UserPreferences.getDefaultInstance()), + InMemoryDataStore(UserPreferences.getDefaultInstance().toBuilder().setIsLoggedIn(true).build()), ) private var shopDeletedShownCount = 0 + private var shopDeletedClosedCount = 0 private var emailUpdatedShownCount = 0 /** An Indefinite snackbar: showSnackbar returns only when the shopkeeper dismisses it. */ @@ -31,9 +32,14 @@ class StoredNoticeTest { showNoticeStoredForLaunch( flag = preferences.isShopDeleted(), clear = { preferences.setIsShopDeleted(false) }, + isLoggedIn = preferences.isLoggedIn(), ) { shopDeletedShownCount++ - snackbar(isDismissed) + try { + snackbar(isDismissed) + } finally { + shopDeletedClosedCount++ + } } } launch { @@ -91,4 +97,33 @@ class StoredNoticeTest { assertEquals(2, emailUpdatedShownCount) app.cancel() } + + // Bug (found in MyTurnTagCreator-Android on 2026-10-16): a success snackbar shown after the app + // restarted itself ("Organization switched successfully." there; "Shop deleted" here) stayed over + // the sign-in screen after Settings > Sign Out. + @Test + fun shopDeleted_whileItShows_isClosedBySigningOut() = runTest(UnconfinedTestDispatcher()) { + preferences.setIsShopDeleted(true) + val app = launchApp() + assertEquals(1, shopDeletedShownCount) + assertEquals(0, shopDeletedClosedCount) + + // LoginRepositoryImpl.signOutLocally. + preferences.clearSession() + + assertEquals(1, shopDeletedClosedCount) + app.cancel() + } + + @Test + fun shopDeleted_whileSignedIn_staysUntilDismissed() = runTest(UnconfinedTestDispatcher()) { + preferences.setIsShopDeleted(true) + val app = launchApp() + + // Any other write while signed in. + preferences.setIsEmailUpdated(false) + + assertEquals(0, shopDeletedClosedCount) + app.cancel() + } }