Skip to content

Commit e91951c

Browse files
committed
fix(auth): resolve account switching race condition
MenuScreen lives inside a sheet with its own navigator, so replaceAll(Login(entropy)) was targeting the sheet backstack instead of the root navigator — the entropy never reached the login screen. Fix: store pending switch entropy in AuthManager before logout, then let App.kt's auth guard consume it and navigate to Login(entropy) on the root navigator. Also crossfade Scanner↔Login transitions and fire-and-forget slow network cleanup (FCM/push token deletion) so the transition isn't blocked. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent c367725 commit e91951c

7 files changed

Lines changed: 58 additions & 25 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,32 +167,38 @@ internal fun App(
167167
)
168168
} then SinglePaneSceneStrategy(),
169169
transitionSpec = {
170-
val hasLoading = initialState.key == AppRoute.Loading.toString() ||
171-
targetState.key == AppRoute.Loading.toString()
170+
val shouldCrossfade = initialState.key == AppRoute.Loading.toString() ||
171+
targetState.key == AppRoute.Loading.toString() ||
172+
initialState.key.toString().startsWith("Login") ||
173+
targetState.key.toString().startsWith("Login")
172174
when {
173-
hasLoading -> fadeIn(tween(300)) togetherWith fadeOut(tween(300))
175+
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(tween(300))
174176
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
175177
EnterTransition.None togetherWith ExitTransition.None
176178
else -> slideInHorizontally(initialOffsetX = { it }) togetherWith
177179
slideOutHorizontally(targetOffsetX = { -it })
178180
}
179181
},
180182
popTransitionSpec = {
181-
val hasLoading = initialState.key == AppRoute.Loading.toString() ||
182-
targetState.key == AppRoute.Loading.toString()
183+
val shouldCrossfade = initialState.key == AppRoute.Loading.toString() ||
184+
targetState.key == AppRoute.Loading.toString() ||
185+
initialState.key.toString().startsWith("Login") ||
186+
targetState.key.toString().startsWith("Login")
183187
when {
184-
hasLoading -> fadeIn(tween(300)) togetherWith fadeOut(tween(300))
188+
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(tween(300))
185189
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
186190
EnterTransition.None togetherWith ExitTransition.None
187191
else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
188192
slideOutHorizontally(targetOffsetX = { it })
189193
}
190194
},
191195
predictivePopTransitionSpec = {
192-
val hasLoading = initialState.key == AppRoute.Loading.toString() ||
193-
targetState.key == AppRoute.Loading.toString()
196+
val shouldCrossfade = initialState.key == AppRoute.Loading.toString() ||
197+
targetState.key == AppRoute.Loading.toString() ||
198+
initialState.key.toString().startsWith("Login") ||
199+
targetState.key.toString().startsWith("Login")
194200
when {
195-
hasLoading -> fadeIn(tween(300)) togetherWith fadeOut(tween(300))
201+
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(tween(300))
196202
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
197203
EnterTransition.None togetherWith ExitTransition.None
198204
else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
@@ -257,7 +263,8 @@ internal fun App(
257263
val current = codeNavigator.currentRouteKey
258264
if (current !is AppRoute.Loading && current !is AppRoute.Onboarding) {
259265
codeNavigator.pendingSheetDismiss = null
260-
codeNavigator.replaceAll(AppRoute.Onboarding.Login())
266+
val switchEntropy = viewModel.consumePendingSwitchEntropy()
267+
codeNavigator.replaceAll(AppRoute.Onboarding.Login(switchEntropy))
261268
}
262269
}
263270
}

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/HomeViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ internal class HomeViewModel @Inject constructor(
112112
)
113113
}
114114

115+
fun consumePendingSwitchEntropy(): String? {
116+
return authManager.consumePendingSwitchEntropy()
117+
}
118+
115119
suspend fun logout(): Result<Unit> {
116120
return authManager.logout()
117121
}

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ private data class LaunchNavGraph(
173173
*/
174174
private fun List<NavKey>.startsWith(prefix: List<NavKey>): Boolean {
175175
if (size < prefix.size) return false
176-
return prefix.indices.all { i -> this[i] == prefix[i] }
176+
return prefix.indices.all { i -> this[i]::class == prefix[i]::class }
177177
}
178178

179179
private fun buildNavGraphForLaunch(

apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/MenuScreen.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ fun MenuScreen() {
3939
LaunchedEffect(viewModel) {
4040
viewModel.eventFlow
4141
.filterIsInstance<MenuScreenViewModel.Event.OnSwitchAccountTo>()
42-
.map { it.entropy }
43-
.onEach {
44-
navigator.hide()
45-
navigator.replaceAll(AppRoute.Onboarding.Login(it)) }
42+
.onEach { navigator.hide() }
4643
.launchIn(this)
4744
}
4845
}

apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,7 @@ internal class MenuScreenViewModel @Inject constructor(
179179
onFailure = { Result.failure(it) }
180180
)
181181
}.onResult(
182-
onError = {
183-
184-
},
182+
onError = { },
185183
onSuccess = { dispatchEvent(Event.OnSwitchAccountTo(it)) }
186184
).launchIn(viewModelScope)
187185

apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ class AuthManager @Inject constructor(
4343
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
4444
private var softLoginDisabled: Boolean = false
4545

46+
/**
47+
* Entropy for the account being switched to. Set before logout so App.kt's
48+
* auth guard can navigate to Login(entropy) instead of seedless Login().
49+
*/
50+
var pendingSwitchEntropy: String? = null
51+
private set
52+
4653
companion object {
4754
private const val TAG = "AuthManager"
4855
internal fun taggedTrace(
@@ -174,15 +181,20 @@ class AuthManager @Inject constructor(
174181
return logout()
175182
}
176183

184+
suspend fun logoutAndSwitchAccount(entropy: String): Result<String> {
185+
pendingSwitchEntropy = entropy
186+
return logout().map { entropy }
187+
}
188+
189+
fun consumePendingSwitchEntropy(): String? {
190+
return pendingSwitchEntropy.also { pendingSwitchEntropy = null }
191+
}
192+
177193
suspend fun logout(): Result<Unit> {
178194
return credentialManager.logout()
179195
.onSuccess { resetStateForUser() }
180196
}
181197

182-
suspend fun logoutAndSwitchAccount(entropy: String): Result<String> {
183-
return logout().map { entropy }
184-
}
185-
186198
private fun loginAnalytics() {
187199
// analytics.login(
188200
// ownerPublicKey = owner.getPublicKeyBase58(),
@@ -192,8 +204,11 @@ class AuthManager @Inject constructor(
192204
}
193205

194206
private suspend fun resetStateForUser() {
195-
FirebaseMessaging.getInstance().deleteToken()
196-
pushController.deleteTokens()
207+
// Fire-and-forget slow network operations to avoid blocking navigation
208+
launch {
209+
FirebaseMessaging.getInstance().deleteToken()
210+
pushController.deleteTokens()
211+
}
197212
notificationManager.cancelAll()
198213
userManager.clear()
199214
tokenCoordinator.reset()

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ class RealSessionController @Inject constructor(
136136

137137
private val scannedRendezvous = mutableMapOf<String, Long>()
138138

139+
@Volatile
140+
private var lastForegroundTimestamp = 0L
141+
139142
private val giftCardClaimInProgress = MutableStateFlow<String?>(null)
140143

141144
init {
@@ -145,6 +148,7 @@ class RealSessionController @Inject constructor(
145148
.filterIsInstance<AuthState.LoggedOut>()
146149
.onEach {
147150
_state.update { SessionState() }
151+
lastForegroundTimestamp = 0L
148152
}.launchIn(scope)
149153

150154
userManager.state
@@ -200,6 +204,13 @@ class RealSessionController @Inject constructor(
200204
* 7. If the user is registered, connects to the billing client.
201205
*/
202206
override fun onAppInForeground() {
207+
val now = Clock.System.now().toEpochMilliseconds()
208+
if (now - lastForegroundTimestamp < FOREGROUND_DEDUP_WINDOW_MS) {
209+
trace(tag = "Session", message = "onAppInForeground skipped (dedup)", type = TraceType.Process)
210+
return
211+
}
212+
lastForegroundTimestamp = now
213+
203214
trace(
204215
tag = "Session",
205216
message = "onAppInForeground",
@@ -856,4 +867,5 @@ class RealSessionController @Inject constructor(
856867
}
857868

858869
private val AIRDROP_INITIAL_DELAY = 1.seconds
859-
private val CASH_LINK_CONFIRMATION_DELAY = 500.milliseconds
870+
private val CASH_LINK_CONFIRMATION_DELAY = 500.milliseconds
871+
private const val FOREGROUND_DEDUP_WINDOW_MS = 2_000L

0 commit comments

Comments
 (0)