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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ sealed interface AppRoute : NavKey, Parcelable {
val emailVerificationCode: String? = null,
val target: AppRoute? = null,
val fullScreen: Boolean = false,
val linkForPayment: Boolean = false,
) : AppRoute, FlowRouteWithResult<VerificationResult> {
override val initialStack: List<NavKey>
get() = buildVerificationInitialStack(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
implementation(libs.bundles.kotlinx.serialization)

implementation(project(":apps:flipcash:shared:analytics"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:phone"))
implementation(project(":libs:messaging"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ private fun verificationEntryProvider(
PhoneVerificationContent(isInModal = !route.fullScreen)
}
annotatedEntry<VerificationStep.PhoneCode> {
PhoneCodeContent(includeEmail = route.includeEmail, isInModal = !route.fullScreen)
PhoneCodeContent(
includeEmail = route.includeEmail,
isInModal = !route.fullScreen,
linkForPayment = route.linkForPayment,
)
}
annotatedEntry<VerificationStep.PhoneCountryCode> {
PhoneCountryCodeContent(isInModal = !route.fullScreen)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.viewModelScope
import com.flipcash.app.core.extensions.onResult
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.phone.CountryLocale
import com.flipcash.app.phone.PhoneUtils
import com.flipcash.features.contact.verification.R
import com.flipcash.libs.coroutines.DispatcherProvider
import com.flipcash.services.controllers.ContactVerificationController
import com.flipcash.services.controllers.ProfileController
import com.flipcash.services.models.ContactMethod
import com.flipcash.services.user.UserManager
import com.flipcash.services.models.PhoneVerificationError
import com.getcode.manager.BottomBarManager
import com.getcode.util.resources.ResourceHelper
Expand All @@ -19,6 +22,7 @@ import com.getcode.view.LoadingSuccessState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
Expand All @@ -37,6 +41,8 @@ internal class PhoneVerificationViewModel @Inject constructor(
private val phoneUtils: PhoneUtils,
private val verificationController: ContactVerificationController,
private val profileController: ProfileController,
private val userManager: UserManager,
private val featureFlags: FeatureFlagController,
private val resources: ResourceHelper,
private val dispatchers: DispatcherProvider,
) : BaseViewModel<PhoneVerificationViewModel.State, PhoneVerificationViewModel.Event>(
Expand Down Expand Up @@ -88,6 +94,7 @@ internal class PhoneVerificationViewModel @Inject constructor(

data object OnVerifyCodeClicked : Event
data object OnCodeVerified : Event
data object LinkForPayment : Event

data object OnMaxAttemptsReached : Event
}
Expand Down Expand Up @@ -196,6 +203,21 @@ internal class PhoneVerificationViewModel @Inject constructor(
)
}
).launchIn(viewModelScope)

eventFlow
.filterIsInstance<Event.LinkForPayment>()
.filter {
featureFlags.observe(FeatureFlag.PhoneNumberSend).value ||
userManager.state.value.flags?.enablePhoneNumberSend == true
}
.map {
val number = stateFlow.value.numberTextFieldState.text.toString()
val locale = stateFlow.value.selectedLocale
val cleanedNumber = phoneUtils.cleanNumber(number, locale)
ContactMethod.Phone(cleanedNumber)
}
.map { verificationController.linkForPayment(it) }
.launchIn(viewModelScope)
}

private suspend fun handleSendVerificationCode(method: ContactMethod) {
Expand Down Expand Up @@ -296,6 +318,7 @@ internal class PhoneVerificationViewModel @Inject constructor(
}
Event.OnVerifyCodeClicked -> { state -> state }
Event.OnCodeVerified -> { state -> state }
Event.LinkForPayment -> { state -> state }
is Event.OnPhoneNumberFormatted -> { state -> state.copy(formattedPhone = event.formatted) }
Event.OnSendCodeClicked -> { state -> state.copy(attempts = state.attempts + 1) }
Event.OnMaxAttemptsReached -> { state -> state }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.onEach
fun PhoneCodeContent(
includeEmail: Boolean,
isInModal: Boolean = true,
linkForPayment: Boolean = false,
) {
val flowNavigator = rememberFlowNavigator<VerificationStep, VerificationResult>()
val viewModel = flowSharedViewModel<PhoneVerificationViewModel>()
Expand Down Expand Up @@ -55,10 +56,13 @@ fun PhoneCodeContent(
.launchIn(this)
}

LaunchedEffect(viewModel, includeEmail) {
LaunchedEffect(viewModel, includeEmail, linkForPayment) {
viewModel.eventFlow
.filterIsInstance<PhoneVerificationViewModel.Event.OnCodeVerified>()
.onEach {
if (linkForPayment) {
viewModel.dispatchEvent(PhoneVerificationViewModel.Event.LinkForPayment)
}
if (includeEmail) {
flowNavigator.navigateTo(VerificationStep.EmailEntry)
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.flipcash.app.contact.verification.internal.phone

import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.phone.PhoneUtils
import com.flipcash.features.contact.verification.R
import com.flipcash.services.controllers.ContactVerificationController
import com.flipcash.services.controllers.ProfileController
import com.flipcash.services.models.PhoneVerificationError
import com.flipcash.services.user.UserManager
import com.getcode.manager.BottomBarManager
import com.getcode.util.resources.ResourceHelper
import com.flipcash.app.core.MainCoroutineRule
Expand Down Expand Up @@ -34,6 +36,8 @@ class PhoneVerificationViewModelErrorTest {
// Mockito for Result-returning methods (MockK double-boxes Result inline class)
private val verificationController: ContactVerificationController = mock()
private val profileController = mockk<ProfileController>(relaxed = true)
private val userManager = mockk<UserManager>(relaxed = true)
private val featureFlags = mockk<FeatureFlagController>(relaxed = true)
private val resources = mockk<ResourceHelper>(relaxed = true)

private lateinit var dispatchers: TestDispatchers
Expand Down Expand Up @@ -62,6 +66,8 @@ class PhoneVerificationViewModelErrorTest {
phoneUtils = phoneUtils,
verificationController = verificationController,
profileController = profileController,
userManager = userManager,
featureFlags = featureFlags,
resources = resources,
dispatchers = dispatchers,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.flipcash.app.featureflags.LocalFeatureFlags
import com.flipcash.app.core.onboarding.OnboardingResult
import com.flipcash.app.core.onboarding.OnboardingStep
import com.flipcash.app.login.internal.LoginAccessKeyViewModel
import com.flipcash.app.login.internal.OnboardingViewModel
import com.flipcash.app.login.internal.screens.PhotoAccessKeyScreen
import com.flipcash.app.login.internal.screens.AccessKeyScreen
import com.flipcash.app.login.internal.screens.LoginRouterScreenContent
Expand Down Expand Up @@ -73,13 +74,13 @@ import kotlinx.coroutines.flow.onEach
* ```
* 1. New account (ResumePoint.Login → ProceedToVerification)
*
* Start → Verification³ → AccessKey ──┬──→ Contacts¹ → Notifications → Scanner
* Start → Verification² → AccessKey ──┬──────────────→ Contacts¹ → Notifications → Scanner
* └→ Purchase ─┘
*
* 2. Seed restore (ResumePoint.Login → LoggedIn via SeedInput)
*
* Start → SeedInput ──┬──────────────────────→ Contacts¹ → Notifications → Scanner
* └→ Purchase → Verification² ─┘
* Start → SeedInput ──┬──────────────→ Contacts¹ → Notifications → Scanner
* └→ Purchase ─┘
*
* 3. App resume (ResumePoint.PostAccessKey)
*
Expand All @@ -95,9 +96,9 @@ import kotlinx.coroutines.flow.onEach
* **and** [FeatureFlag.ContactPickerMode] is off. When ContactPickerMode is on,
* contacts are accessed via the system picker at call site (no READ_CONTACTS needed).
* Already-granted permissions are auto-skipped via [PermissionsPhaseFlowHost].
* ² Verification is skipped if a phone number is already linked.
* ³ Phone verification is shown only when [FeatureFlag.PhoneNumberSend] is enabled
* and no phone is linked. Uses `target` to replace nav stack with AccessKey on success.
* ² Phone verification is shown only when [FeatureFlag.PhoneNumberSend] is enabled
* and no phone is linked. Skipped entirely when the flag is off.
* Uses `target` to replace the nav stack with AccessKey on success.
*/
@Composable
fun OnboardingFlowScreen(
Expand Down Expand Up @@ -134,6 +135,7 @@ private fun PermissionsPhaseFlowHost(
resultStateRegistry: NavResultStateRegistry,
) {
val outerNavigator = LocalCodeNavigator.current
val onboardingViewModel = hiltViewModel<OnboardingViewModel>()
val checker = LocalPermissionChecker.current
val contactConfig = PermissionConfigs.contacts()
val notificationConfig = PermissionConfigs.notifications()
Expand Down Expand Up @@ -178,6 +180,7 @@ private fun PermissionsPhaseFlowHost(
when (reason) {
is FlowExitReason.Completed -> {
analytics.action(Action.CompletedOnboarding)
onboardingViewModel.linkPhoneForPayment()
outerNavigator.navigate(
route = AppRoute.Main.Scanner,
options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll),
Expand All @@ -186,6 +189,7 @@ private fun PermissionsPhaseFlowHost(

FlowExitReason.BackedOutOfRoot -> {
// All permissions already granted
onboardingViewModel.linkPhoneForPayment()
outerNavigator.navigate(
route = AppRoute.Main.Scanner,
options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll),
Expand All @@ -205,19 +209,16 @@ private fun AccountPhaseFlowHost(
resultStateRegistry: NavResultStateRegistry,
) {
val outerNavigator = LocalCodeNavigator.current
val userManager = LocalUserManager.current!!
val userState by userManager.state.collectAsStateWithLifecycle()

val initialStack = route.rememberInitialStack<OnboardingStep>()

FlowHost<OnboardingStep, OnboardingResult>(
FlowHost(
initialStack = initialStack,
resultStateRegistry = resultStateRegistry,
onExit = { reason, _ ->
when (reason) {
is FlowExitReason.Completed -> {
val hasLinkedPhone = userState.userProfile?.verifiedPhoneNumber != null
val route = resolvePostAccountRoute(reason.result, hasLinkedPhone)
val route = resolvePostAccountRoute(reason.result)
route?.let { outerNavigator.replace(it) }
}

Expand All @@ -231,27 +232,14 @@ private fun AccountPhaseFlowHost(

internal fun resolvePostAccountRoute(
result: OnboardingResult,
hasLinkedPhone: Boolean,
skipContacts: Boolean = false,
): AppRoute? {
val permissionsRoute = AppRoute.OnboardingFlow(
phase = AppRoute.OnboardingFlow.Phase.Permissions,
skipContacts = skipContacts,
)
return when (result) {
is OnboardingResult.ProceedToVerification -> {
if (hasLinkedPhone) {
permissionsRoute
} else {
AppRoute.Verification(
origin = AppRoute.Onboarding.AccessKey,
includePhone = true,
includeEmail = false,
target = permissionsRoute,
fullScreen = true,
)
}
}
is OnboardingResult.ProceedToVerification -> permissionsRoute
OnboardingResult.LoggedIn -> permissionsRoute
OnboardingResult.Completed -> null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.flipcash.app.login.internal

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.services.controllers.ContactVerificationController
import com.flipcash.services.models.ContactMethod
import com.flipcash.services.user.UserManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
internal class OnboardingViewModel @Inject constructor(
private val userManager: UserManager,
private val featureFlags: FeatureFlagController,
private val contactVerificationController: ContactVerificationController,
) : ViewModel() {

fun linkPhoneForPayment() {
val phone = userManager.profile?.verifiedPhoneNumber ?: return
val enabled = featureFlags.observe(FeatureFlag.PhoneNumberSend).value ||
userManager.state.value.flags?.enablePhoneNumberSend == true
if (!enabled) return
viewModelScope.launch {
contactVerificationController.linkForPayment(ContactMethod.Phone(phone))
}
}
}
Loading
Loading