Skip to content

Commit a595b08

Browse files
committed
feat(onboarding): unify login into a single OnboardingFlow with linear FlowHost API
Replace the fragmented login routing (LoginRouter, standalone screens, and scattered verification logic in MainRoot) with a single OnboardingFlowScreen backed by two FlowHost phases: Account and Permissions. FlowHost API changes: - Add linear flow overload with `steps`, `resumeAt`, and `completedResult` parameters for ordered step-by-step flows - Add `FlowNavigator.proceed()` to advance through the step list, exit with completedResult at the end, or delegate to `onProceed` for custom behavior - Rename the existing overload as the non-linear variant for flows that manage their own navigation via navigateTo/exitWithResult - Extract shared logic into FlowHostImpl; support re-seeding when initialStack changes before user navigation (async flag settling) Onboarding routing: - All AuthState.Registered cases now route to AppRoute.OnboardingFlow with a ResumePoint (Login, AccessKey, AccessKeyThenPurchase, or PostAccessKey) — MainRoot no longer routes directly to Verification - PostAccessKeyRedirect checks UserProfile.verifiedPhoneNumber to skip verification when phone is already linked - Seed restore (LoggedIn) skips verification and goes straight to permissions — existing users encounter phone verification in-app via the send flow - Permissions phase uses the linear FlowHost with resumeAt to skip already-granted permissions Login module restructuring: - Delete LoginRouter, AccessKeyScreen, SeedInputScreen (standalone wrappers) — all step content is now composed inline by OnboardingFlowScreen via the entryProvider - Move ViewModels to internal package, screen content to internal/screens - Add OnboardingStep sealed interface and OnboardingResult for flow step/result modeling Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 2be9942 commit a595b08

29 files changed

Lines changed: 880 additions & 337 deletions

File tree

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ internal fun App(
266266
action.entropy,
267267
onSwitchAccount = {
268268
codeNavigator.replaceAll(
269-
AppRoute.Onboarding.Login(
270-
action.entropy,
269+
AppRoute.OnboardingFlow(
270+
seed = action.entropy,
271271
fromDeeplink = true
272272
)
273273
)
@@ -287,13 +287,13 @@ internal fun App(
287287
LaunchedEffect(userState.authState) {
288288
if (userState.authState == AuthState.LoggedOut) {
289289
val current = codeNavigator.currentRouteKey
290-
if (current !is AppRoute.Loading && current !is AppRoute.Onboarding) {
290+
if (current !is AppRoute.Loading && current !is AppRoute.OnboardingFlow) {
291291
codeNavigator.pendingSheetDismiss = null
292292
val switchEntropy =
293293
viewModel.consumePendingSwitchEntropy()
294294
codeNavigator.replaceAll(
295-
AppRoute.Onboarding.Login(
296-
switchEntropy
295+
AppRoute.OnboardingFlow(
296+
seed = switchEntropy
297297
)
298298
)
299299
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ fun AppRestrictedScreen(restrictionType: RestrictionType) {
1919
coroutineScope.launch {
2020
homeViewModel.logout()
2121
.onSuccess {
22-
navigator.replaceAll(
23-
AppRoute.Onboarding.Login()
24-
)
22+
navigator.replaceAll(AppRoute.OnboardingFlow())
2523
}
2624
}
2725
}

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

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,9 @@ import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEn
3434
import com.flipcash.app.lab.LabsScreen
3535
import com.flipcash.app.lab.NavBarSettingsScreen
3636
import com.flipcash.app.lab.StandaloneLabsScreen
37-
import com.flipcash.app.login.accesskey.AccessKeyScreen
38-
import com.flipcash.app.login.accesskey.PhotoAccessKeyScreen
39-
import com.flipcash.app.login.router.LoginRouter
40-
import com.flipcash.app.login.seed.SeedInputScreen
37+
import com.flipcash.app.login.OnboardingFlowScreen
4138
import com.flipcash.app.menu.MenuScreen
4239
import com.flipcash.app.myaccount.MyAccountScreen
43-
import com.flipcash.app.permissions.ContactPermissionScreen
44-
import com.flipcash.app.permissions.NotificationPermissionRationaleScreen
45-
import com.flipcash.app.permissions.NotificationPermissionScreen
46-
import com.flipcash.app.purchase.PurchaseAccountScreen
4740
import com.flipcash.app.scanner.ScannerScreen
4841
import com.flipcash.app.shareapp.ShareAppScreen
4942
import com.flipcash.app.tokens.SwapFlowScreen
@@ -75,16 +68,10 @@ fun appEntryProvider(
7568
// Loading / splash
7669
annotatedEntry<AppRoute.Loading> { MainRoot(deepLink) }
7770

78-
// Onboarding
79-
annotatedEntry<AppRoute.Onboarding.Login> { key -> LoginRouter(key.seed, key.fromDeeplink) }
80-
annotatedEntry<AppRoute.Onboarding.SeedInput> { SeedInputScreen() }
81-
annotatedEntry<AppRoute.Onboarding.AccessKey> { AccessKeyScreen() }
82-
annotatedEntry<AppRoute.Onboarding.AccessKeySavedLocation> { PhotoAccessKeyScreen() }
83-
annotatedEntry<AppRoute.Onboarding.Purchase> { key -> PurchaseAccountScreen(key.fromLogin) }
84-
annotatedEntry<AppRoute.Onboarding.ContactPermission> { key -> ContactPermissionScreen(key.postCreate) }
85-
annotatedEntry<AppRoute.Onboarding.NotificationPermission> { key -> NotificationPermissionScreen(key.postCreate) }
86-
annotatedEntry<AppRoute.Onboarding.NotificationPermissionRationale> { key -> NotificationPermissionRationaleScreen(key.permanentlyDenied) }
87-
annotatedEntry<AppRoute.Onboarding.CameraPermission> { }
71+
// Onboarding flow
72+
annotatedEntry<AppRoute.OnboardingFlow> { key ->
73+
OnboardingFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
74+
}
8875

8976
// Main
9077
annotatedEntry<AppRoute.Main.Sheet> { key ->

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -179,25 +179,13 @@ private fun buildNavGraphForLaunch(
179179
): LaunchNavGraph? {
180180
return when (state) {
181181
is AuthState.Registered -> {
182-
if (state.seenAccessKey) {
183-
val routes = if (userFlags?.requiresIapForRegistration == true) {
184-
listOf(
185-
AppRoute.Onboarding.Login(),
186-
AppRoute.Onboarding.AccessKey,
187-
AppRoute.Onboarding.Purchase()
188-
)
189-
} else {
190-
listOf(AppRoute.Main.Scanner)
191-
}
192-
LaunchNavGraph(routes)
193-
} else {
194-
LaunchNavGraph(
195-
listOf(
196-
AppRoute.Onboarding.Login(),
197-
AppRoute.Onboarding.AccessKey
198-
)
199-
)
182+
val resumePoint = when {
183+
!state.seenAccessKey -> AppRoute.OnboardingFlow.ResumePoint.AccessKey
184+
userFlags?.requiresIapForRegistration == true ->
185+
AppRoute.OnboardingFlow.ResumePoint.AccessKeyThenPurchase
186+
else -> AppRoute.OnboardingFlow.ResumePoint.PostAccessKey
200187
}
188+
LaunchNavGraph(listOf(AppRoute.OnboardingFlow(resumeAt = resumePoint)))
201189
}
202190

203191
AuthState.LoggedInWithUser -> {
@@ -223,10 +211,10 @@ private fun buildNavGraphForLaunch(
223211
if (link != null) {
224212
when (val action = router.dispatch(link)) {
225213
is DeeplinkAction.Navigate -> LaunchNavGraph(action.routes)
226-
else -> LaunchNavGraph(listOf(AppRoute.Onboarding.Login()))
214+
else -> LaunchNavGraph(listOf(AppRoute.OnboardingFlow()))
227215
}
228216
} else {
229-
LaunchNavGraph(listOf(AppRoute.Onboarding.Login()))
217+
LaunchNavGraph(listOf(AppRoute.OnboardingFlow()))
230218
}
231219
}
232220

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import com.flipcash.app.core.verification.VerificationResult
1414
import com.flipcash.app.core.verification.VerificationStep
1515
import com.flipcash.app.core.withdrawal.WithdrawalResult
1616
import com.flipcash.app.core.withdrawal.WithdrawalStep
17+
import com.flipcash.app.core.onboarding.OnboardingStep
1718
import com.getcode.navigation.NonDismissableRoute
1819
import com.getcode.navigation.NonDraggableRoute
20+
import com.getcode.navigation.flow.FlowRoute
1921
import com.getcode.navigation.flow.FlowRouteWithResult
2022
import com.getcode.opencode.exchange.VerifiedFiat
2123
import com.getcode.opencode.internal.solana.model.SwapId
@@ -62,6 +64,30 @@ sealed interface AppRoute : NavKey, Parcelable {
6264
}
6365

6466

67+
@Serializable
68+
@Parcelize
69+
data class OnboardingFlow(
70+
val phase: Phase = Phase.Account,
71+
val seed: String? = null,
72+
val fromDeeplink: Boolean = false,
73+
val resumeAt: ResumePoint = ResumePoint.Login,
74+
) : AppRoute, FlowRoute {
75+
enum class Phase { Account, Permissions }
76+
enum class ResumePoint { Login, AccessKey, AccessKeyThenPurchase, PostAccessKey }
77+
78+
override val initialStack: List<NavKey>
79+
get() = when (phase) {
80+
Phase.Account -> when (resumeAt) {
81+
ResumePoint.Login -> listOf(OnboardingStep.Start(seed, fromDeeplink))
82+
ResumePoint.AccessKey -> listOf(OnboardingStep.Start(), OnboardingStep.AccessKey)
83+
ResumePoint.AccessKeyThenPurchase ->
84+
listOf(OnboardingStep.Start(), OnboardingStep.AccessKey, OnboardingStep.Purchase)
85+
ResumePoint.PostAccessKey -> emptyList()
86+
}
87+
Phase.Permissions -> listOf(OnboardingStep.ContactPermission, OnboardingStep.NotificationPermission)
88+
}
89+
}
90+
6591
@Serializable
6692
@Parcelize
6793
sealed interface Main : AppRoute {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.flipcash.app.core.onboarding
2+
3+
import android.os.Parcelable
4+
import com.getcode.navigation.NonDismissableRoute
5+
import com.getcode.navigation.flow.FlowStep
6+
import kotlinx.parcelize.Parcelize
7+
import kotlinx.serialization.Serializable
8+
9+
/**
10+
* Steps inside the Onboarding flow. Owned by [com.flipcash.app.core.AppRoute.OnboardingFlow]
11+
* and rendered inside a [com.getcode.navigation.flow.FlowHost].
12+
*/
13+
@Serializable
14+
sealed interface OnboardingStep : FlowStep, Parcelable {
15+
@Parcelize
16+
@Serializable
17+
data class Start(val seed: String? = null, val fromDeeplink: Boolean = false) : OnboardingStep
18+
19+
@Parcelize
20+
@Serializable
21+
data object SeedInput : OnboardingStep
22+
23+
@Parcelize
24+
@Serializable
25+
data object AccessKey : OnboardingStep
26+
27+
@Parcelize
28+
@Serializable
29+
data object AccessKeySavedLocation : OnboardingStep
30+
31+
@Parcelize
32+
@Serializable
33+
data object Purchase : OnboardingStep
34+
35+
@Parcelize
36+
@Serializable
37+
data object ContactPermission : OnboardingStep
38+
39+
@Parcelize
40+
@Serializable
41+
data object NotificationPermission : OnboardingStep
42+
43+
@Parcelize
44+
@Serializable
45+
data class NotificationPermissionRationale(
46+
val permanentlyDenied: Boolean = false,
47+
) : OnboardingStep, NonDismissableRoute
48+
}
49+
50+
@Serializable
51+
sealed interface OnboardingResult : Parcelable {
52+
@Parcelize
53+
@Serializable
54+
data object ProceedToVerification : OnboardingResult
55+
56+
@Parcelize
57+
@Serializable
58+
data object LoggedIn : OnboardingResult
59+
60+
@Parcelize
61+
@Serializable
62+
data object Completed : OnboardingResult
63+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ dependencies {
1616
implementation(project(":apps:flipcash:shared:analytics"))
1717
implementation(project(":apps:flipcash:shared:authentication"))
1818
implementation(project(":apps:flipcash:shared:featureflags"))
19+
implementation(project(":apps:flipcash:shared:permissions"))
1920
implementation(project(":apps:flipcash:shared:userflags"))
21+
implementation(project(":apps:flipcash:features:purchase"))
2022
implementation(project(":libs:datetime"))
2123
implementation(project(":libs:messaging"))
2224
implementation(project(":libs:permissions:bindings"))

0 commit comments

Comments
 (0)