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() + } }