diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt index 980b11cf72..f76bf29b7a 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt @@ -302,11 +302,11 @@ internal class MenuScreenViewModel @Inject constructor( combine( userManager.state .filter { it.authState is AuthState.Ready } - .map { it.userProfile?.username }, + .map { it.userProfile?.username to (it.userProfile?.isUsernameAutoAssigned == true) }, userFlags.resolvedFlags.map { it.usernameMinBalance.effectiveValue }, totalBalance.observeTotalBalance(), - ) { username, minimum, balance -> - val progress = when (val gate = usernameGate(username, minimum, balance)) { + ) { (username, isAutoAssigned), minimum, balance -> + val progress = when (val gate = usernameGate(username, isAutoAssigned, minimum, balance)) { UsernameGate.Claimed -> null UsernameGate.Unlocked -> UsernameProgress.Unlocked is UsernameGate.Locked -> UsernameProgress.Locked( diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/UsernameGate.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/UsernameGate.kt index 48ce924e9e..f4d1e0f65b 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/UsernameGate.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/UsernameGate.kt @@ -16,7 +16,10 @@ import com.getcode.opencode.model.financial.minus * Mirrors iOS `usernameGate(session:minimum:)` in `UsernameGate.swift`. */ internal sealed interface UsernameGate { - /** A handle is already claimed — the nudge is spent, and changing it lives in My Account. */ + /** + * A handle the user chose is already claimed — the nudge is spent, and changing it lives in + * My Account. A handle the server assigned does not count: the user hasn't picked it. + */ data object Claimed : UsernameGate /** Nothing in the way: either the balance clears the minimum, or there is no minimum. */ @@ -35,16 +38,19 @@ internal sealed interface UsernameGate { } /** - * @param username the account's claimed handle, null or blank when it hasn't claimed one. + * @param username the account's handle, null or blank when it has none. + * @param isAutoAssigned whether [username] was assigned by the server rather than chosen by the + * user. An assigned handle still counts as unclaimed, so the nudge keeps showing. * @param minimum the balance the account must hold to claim, from the `usernameMinBalance` flag. * @param balance the account's total balance, in the same currency as [minimum]. */ internal fun usernameGate( username: String?, + isAutoAssigned: Boolean, minimum: Fiat, balance: Fiat, ): UsernameGate = when { - !username.isNullOrBlank() -> UsernameGate.Claimed + !username.isNullOrBlank() && !isAutoAssigned -> UsernameGate.Claimed // A zero minimum is no gate at all — which is also what an unresolved flag looks like. Either // reading leaves nothing holding the account back, so both fail open. !minimum.isPositive -> UsernameGate.Unlocked diff --git a/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/UsernameGateTest.kt b/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/UsernameGateTest.kt index 30628e48ad..bd14478968 100644 --- a/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/UsernameGateTest.kt +++ b/apps/flipcash/features/menu/src/test/kotlin/com/flipcash/app/menu/internal/UsernameGateTest.kt @@ -14,14 +14,33 @@ class UsernameGateTest { fun `a claimed handle spends the nudge, whatever the balance`() { assertEquals( UsernameGate.Claimed, - usernameGate(username = "mcansh", minimum = usd(25.0), balance = Fiat.Zero), + usernameGate(username = "mcansh", isAutoAssigned = false, minimum = usd(25.0), balance = Fiat.Zero), ) } @Test fun `a blank handle counts as unclaimed`() { assertIs( - usernameGate(username = " ", minimum = usd(25.0), balance = Fiat.Zero), + usernameGate(username = " ", isAutoAssigned = false, minimum = usd(25.0), balance = Fiat.Zero), + ) + } + + @Test + fun `an auto-assigned handle still counts as unclaimed`() { + assertIs( + usernameGate(username = "user4821", isAutoAssigned = true, minimum = usd(25.0), balance = Fiat.Zero), + ) + } + + @Test + fun `an auto-assigned handle follows the balance rules of no handle`() { + assertEquals( + UsernameGate.Unlocked, + usernameGate(username = "user4821", isAutoAssigned = true, minimum = usd(25.0), balance = usd(25.0)), + ) + assertEquals( + UsernameGate.Unlocked, + usernameGate(username = "user4821", isAutoAssigned = true, minimum = Fiat.Zero, balance = Fiat.Zero), ) } @@ -29,7 +48,7 @@ class UsernameGateTest { fun `a zero minimum fails open`() { assertEquals( UsernameGate.Unlocked, - usernameGate(username = null, minimum = Fiat.Zero, balance = Fiat.Zero), + usernameGate(username = null, isAutoAssigned = false, minimum = Fiat.Zero, balance = Fiat.Zero), ) } @@ -37,7 +56,7 @@ class UsernameGateTest { fun `exactly the minimum unlocks`() { assertEquals( UsernameGate.Unlocked, - usernameGate(username = null, minimum = usd(25.0), balance = usd(25.0)), + usernameGate(username = null, isAutoAssigned = false, minimum = usd(25.0), balance = usd(25.0)), ) } @@ -45,13 +64,13 @@ class UsernameGateTest { fun `above the minimum unlocks`() { assertEquals( UsernameGate.Unlocked, - usernameGate(username = null, minimum = usd(25.0), balance = usd(25.01)), + usernameGate(username = null, isAutoAssigned = false, minimum = usd(25.0), balance = usd(25.01)), ) } @Test fun `below the minimum reports the shortfall and how far along it is`() { - val gate = usernameGate(username = null, minimum = usd(25.0), balance = usd(20.0)) + val gate = usernameGate(username = null, isAutoAssigned = false, minimum = usd(25.0), balance = usd(20.0)) assertIs(gate) assertEquals(usd(25.0), gate.minimum) @@ -61,7 +80,7 @@ class UsernameGateTest { @Test fun `an empty balance is zero progress, not a divide by zero`() { - val gate = usernameGate(username = null, minimum = usd(25.0), balance = Fiat.Zero) + val gate = usernameGate(username = null, isAutoAssigned = false, minimum = usd(25.0), balance = Fiat.Zero) assertIs(gate) assertEquals(usd(25.0), gate.shortfall) @@ -70,7 +89,7 @@ class UsernameGateTest { @Test fun `a negative balance clamps to zero progress rather than a backwards bar`() { - val gate = usernameGate(username = null, minimum = usd(25.0), balance = usd(-5.0)) + val gate = usernameGate(username = null, isAutoAssigned = false, minimum = usd(25.0), balance = usd(-5.0)) assertIs(gate) assertEquals(0f, gate.fraction)