Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Boolean>,
clear: suspend () -> Unit,
isLoggedIn: Flow<Boolean>,
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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
}
Loading