Skip to content

Commit 9932271

Browse files
committed
feat: add UserFlagsCoordinator shared module for managing user flags with local overrides
Introduce :apps:flipcash:shared:userflags module that combines server-backed user flags with DataStore-persisted local overrides for staff testing. Exposes a single non-nullable resolvedFlags StateFlow with ResolvedFlag wrappers that track server values alongside override state. Move UserFlags model from internal to services/models for public access. Migrate consumers (BillController, login ViewModels, AppUpdateController) from UserManager.userFlags to the new coordinator. Simplify Result chaining in login ViewModels and add override-aware update availability and reset support in AppUpdateController. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent f9a6bda commit 9932271

26 files changed

Lines changed: 362 additions & 78 deletions

File tree

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
@@ -29,7 +29,7 @@ import com.flipcash.app.core.extensions.navigateTo
2929
import com.flipcash.app.core.extensions.resolveRoutes
3030
import com.flipcash.app.router.LocalRouter
3131
import com.flipcash.app.router.Router
32-
import com.flipcash.services.internal.model.account.UserFlags
32+
import com.flipcash.services.models.UserFlags
3333
import com.flipcash.services.user.AuthState
3434
import com.getcode.navigation.core.CodeNavigator
3535
import com.getcode.navigation.core.LocalCodeNavigator

apps/flipcash/core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
api(project(":libs:permissions:public"))
3535
implementation(project(":libs:vibrator:public"))
3636

37+
implementation(project(":apps:flipcash:shared:userflags"))
3738
api(project(":apps:flipcash:shared:theme"))
3839

3940
api(libs.sodium.bindings)

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/internal/bill/BillController.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.app.core.internal.bill
22

33
import com.flipcash.app.core.bill.BillState
4+
import com.flipcash.app.userflags.UserFlagsCoordinator
45
import com.flipcash.services.user.UserManager
56
import com.getcode.opencode.internal.manager.VerifiedState
67
import com.getcode.opencode.model.accounts.AccountCluster
@@ -19,7 +20,7 @@ import javax.inject.Singleton
1920
@Singleton
2021
class BillController @Inject constructor(
2122
private val transactionManager: BillTransactionManager,
22-
private val userManager: UserManager,
23+
private val userFlags: UserFlagsCoordinator,
2324
) {
2425
private val _state = MutableStateFlow(BillState.Default)
2526
val state: StateFlow<BillState>
@@ -50,7 +51,7 @@ class BillController @Inject constructor(
5051
amount = amount,
5152
owner = owner,
5253
verifiedState = verifiedState,
53-
billExchangeDataTimeout = userManager.userFlags?.billExchangeDataTimeout,
54+
billExchangeDataTimeout = userFlags.resolvedFlags.value.billExchangeDataTimeout.effectiveValue,
5455
present = present,
5556
onGrabbed = onGrabbed,
5657
onTimeout = onTimeout,

apps/flipcash/features/appupdates/src/main/kotlin/com/flipcash/app/updates/UpdateRequiredView.kt

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview
2727
import androidx.lifecycle.Lifecycle
2828
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2929
import com.flipcash.app.theme.FlipcashPreview
30+
import com.flipcash.app.updates.internal.NoUpdateAvailableException
3031
import com.flipcash.features.appupdates.R
3132
import com.getcode.theme.CodeTheme
3233
import com.getcode.ui.biometrics.BiometricsState
@@ -68,6 +69,11 @@ fun UpdateRequiredBlockingView(
6869
onClick = {
6970
composeScope.launch {
7071
appUpdater.startUpdate()
72+
.onFailure {
73+
if (it is NoUpdateAvailableException) {
74+
appUpdater.reset()
75+
}
76+
}
7177
}
7278
},
7379
text = stringResource(id = R.string.action_updateNow),
@@ -130,24 +136,17 @@ fun UpdateRequiredBlockingView(
130136
private fun PreviewUpdateRequiredView() {
131137
FlipcashPreview {
132138
val appUpdater = remember {
133-
object : AppUpdateController {
134-
override val availableUpdate = MutableStateFlow<UpdateInfo?>(
135-
UpdateInfo(
136-
updateAvailability = UpdateAvailability.UPDATE_AVAILABLE,
137-
updatePriority = 5,
138-
clientVersionStalenessDays = null,
139-
bytesDownloaded = 0,
140-
totalBytesToDownload = 100,
141-
installStatus = InstallStatus.UNKNOWN,
142-
availableVersionCode = 3000
143-
)
139+
StubAppUpdateController(
140+
UpdateInfo(
141+
updateAvailability = UpdateAvailability.UPDATE_AVAILABLE,
142+
updatePriority = 5,
143+
clientVersionStalenessDays = null,
144+
bytesDownloaded = 0,
145+
totalBytesToDownload = 100,
146+
installStatus = InstallStatus.UNKNOWN,
147+
availableVersionCode = 3000
144148
)
145-
146-
override suspend fun checkForUpdate() = Unit
147-
148-
override suspend fun startUpdate(): Result<Unit> = Result.success(Unit)
149-
150-
}
149+
)
151150
}
152151
CompositionLocalProvider(LocalAppUpdater provides appUpdater) {
153152
UpdateRequiredBlockingView(

apps/flipcash/features/login/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
implementation(project(":apps:flipcash:shared:analytics"))
1919
implementation(project(":apps:flipcash:shared:authentication"))
2020
implementation(project(":apps:flipcash:shared:featureflags"))
21+
implementation(project(":apps:flipcash:shared:userflags"))
2122
implementation(project(":libs:datetime"))
2223
implementation(project(":libs:messaging"))
2324
implementation(project(":libs:permissions:bindings"))

apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/accesskey/LoginAccessKeyViewModel.kt

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.flipcash.app.analytics.Button
66
import com.flipcash.app.analytics.FlipcashAnalyticsService
77
import com.flipcash.app.auth.AuthManager
88
import com.flipcash.app.core.storage.MediaScanner
9+
import com.flipcash.app.userflags.UserFlagsCoordinator
910
import com.flipcash.services.user.UserManager
1011
import com.getcode.libs.qr.QRCodeGenerator
1112
import com.getcode.opencode.managers.MnemonicManager
@@ -22,43 +23,35 @@ class LoginAccessKeyViewModel @Inject constructor(
2223
mnemonicManager: MnemonicManager,
2324
qrCodeGenerator: QRCodeGenerator,
2425
mediaScanner: MediaScanner,
25-
private val userManager: UserManager,
26+
userManager: UserManager,
27+
private val userFlags: UserFlagsCoordinator,
2628
private val authManager: AuthManager,
2729
private val analytics: FlipcashAnalyticsService,
2830
): BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, userManager, qrCodeGenerator) {
2931

30-
suspend fun saveImage(): Result<Boolean> = trackButton(Button.SaveAccessKey)
31-
.fold(
32-
onSuccess = { saveBitmapToFile() },
33-
onFailure = { Result.failure(it) }
34-
)
35-
.onSuccess { authManager.onUserAccessKeySeen() }
36-
.map { authManager.presentCredentialStorage() }
37-
.map { userManager.userFlags?.requiresIapForRegistration == true }
38-
.map {
32+
suspend fun onWroteDownInstead(): Result<Boolean> {
33+
trackButton(Button.WroteAccessKey)
34+
uiFlow.update { it.copy(skipState = LoadingSuccessState(loading = true)) }
35+
return runCatching {
36+
authManager.onUserAccessKeySeen()
37+
authManager.presentCredentialStorage()
3938
delay(150)
40-
uiFlow.update { s -> s.copy(exportState = LoadingSuccessState(success = true)) }
41-
it
39+
uiFlow.update { s -> s.copy(skipState = LoadingSuccessState(success = true)) }
40+
userFlags.resolvedFlags.value.requiresIapForRegistration.effectiveValue
4241
}
42+
}
4343

44-
suspend fun onWroteDownInstead(): Result<Boolean> = trackButton(Button.WroteAccessKey)
45-
.map {
46-
uiFlow.update { it.copy(skipState = LoadingSuccessState(loading = true)) }
47-
}
48-
.fold(
49-
onSuccess = { authManager.onUserAccessKeySeen() },
50-
onFailure = {
51-
uiFlow.update { s -> s.copy(skipState = LoadingSuccessState()) }
52-
Result.failure(it)
44+
suspend fun saveImage(): Result<Boolean> {
45+
trackButton(Button.SaveAccessKey)
46+
return saveBitmapToFile()
47+
.onSuccess { authManager.onUserAccessKeySeen() }
48+
.mapCatching {
49+
authManager.presentCredentialStorage()
50+
delay(150)
51+
uiFlow.update { s -> s.copy(exportState = LoadingSuccessState(success = true)) }
52+
userFlags.resolvedFlags.value.requiresIapForRegistration.effectiveValue
5353
}
54-
)
55-
.map { authManager.presentCredentialStorage() }
56-
.map { userManager.userFlags?.requiresIapForRegistration == true }
57-
.map {
58-
delay(150)
59-
uiFlow.update { s -> s.copy(skipState = LoadingSuccessState(success = true)) }
60-
it
61-
}
54+
}
6255

6356
private fun trackButton(button: Button): Result<Unit> {
6457
analytics.buttonTapped(button)

apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/seed/SeedInputViewModel.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import androidx.lifecycle.viewModelScope
55
import com.flipcash.app.auth.AuthManager
66
import com.flipcash.app.auth.internal.credentials.SelectCredentialError
77
import com.flipcash.app.core.AppRoute
8+
import com.flipcash.app.userflags.ResolvedFlag
9+
import com.flipcash.app.userflags.ResolvedUserFlags
10+
import com.flipcash.app.userflags.UserFlagsCoordinator
811
import com.flipcash.features.login.R
912
import com.flipcash.services.controllers.AccountController
10-
import com.flipcash.services.internal.model.account.UserFlags
13+
import com.flipcash.services.models.UserFlags
1114
import com.flipcash.services.user.UserManager
1215
import com.getcode.crypt.MnemonicPhrase
1316
import com.getcode.manager.BottomBarAction
@@ -41,6 +44,7 @@ class SeedInputViewModel @Inject constructor(
4144
private val authManager: AuthManager,
4245
private val accountController: AccountController,
4346
private val userManager: UserManager,
47+
private val userFlags: UserFlagsCoordinator,
4448
private val resources: ResourceHelper,
4549
private val mnemonicManager: MnemonicManager,
4650
) : BaseViewModel(resources) {
@@ -105,11 +109,12 @@ class SeedInputViewModel @Inject constructor(
105109
setState(isLoading = false, isSuccess = false, isContinueEnabled = true)
106110
}
107111
.onSuccess {
108-
val userFlags = userManager.userFlags
109-
if (userFlags == null) {
112+
val resolvedFlags = userFlags.resolvedFlags.value
113+
// check if we have server backed flags
114+
if (resolvedFlags.minimumVersion.serverValue == null) {
110115
accountController.getUserFlags()
111116
.onSuccess {
112-
postLoginNavigation(navigator, it)
117+
postLoginNavigation(navigator, userFlags.resolvedFlags.value)
113118
}.onFailure {
114119
setState(isLoading = false, isSuccess = false, isContinueEnabled = false)
115120
BottomBarManager.showError(
@@ -118,20 +123,20 @@ class SeedInputViewModel @Inject constructor(
118123
)
119124
}
120125
} else {
121-
postLoginNavigation(navigator, userFlags)
126+
postLoginNavigation(navigator, resolvedFlags)
122127
}
123128
}
124129
}
125130
}
126131

127132
private suspend fun postLoginNavigation(
128133
navigator: CodeNavigator,
129-
flags: UserFlags,
134+
flags: ResolvedUserFlags?,
130135
) {
131136
setState(isLoading = false, isSuccess = true, isContinueEnabled = false)
132137
delay(1.seconds)
133138
when {
134-
!flags.isRegistered && flags.requiresIapForRegistration -> {
139+
flags?.isRegistered?.effectiveValue == true && flags.requiresIapForRegistration.effectiveValue -> {
135140
navigator.push(AppRoute.Onboarding.Purchase(true))
136141
}
137142

apps/flipcash/shared/appupdates/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ dependencies {
1010
api(libs.google.play.app.updates.runtime)
1111
api(libs.google.play.app.updates.ktx)
1212

13+
implementation(project(":apps:flipcash:shared:userflags"))
1314
}

apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/AppUpdateController.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ interface AppUpdateController {
3030
suspend fun checkForUpdate()
3131

3232
suspend fun startUpdate(): Result<Unit>
33+
suspend fun reset()
3334
}

apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import kotlinx.coroutines.flow.StateFlow
66

77
val LocalAppUpdater = compositionLocalOf<AppUpdateController> { StubAppUpdateController() }
88

9-
private class StubAppUpdateController: AppUpdateController {
10-
override val availableUpdate: StateFlow<UpdateInfo?> = MutableStateFlow(null)
9+
class StubAppUpdateController(updateInfo: UpdateInfo? = null): AppUpdateController {
10+
override val availableUpdate: StateFlow<UpdateInfo?> = MutableStateFlow(updateInfo)
1111

1212
override suspend fun checkForUpdate() = Unit
1313

1414
override suspend fun startUpdate(): Result<Unit> {
1515
return Result.failure(Throwable("This is a stub implementation"))
1616
}
17+
18+
override suspend fun reset() = Unit
1719
}

0 commit comments

Comments
 (0)