Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create consumer session before verification pane #9032

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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 @@ -9,30 +9,24 @@ internal class LookupConsumerAndStartVerification @Inject constructor(
private val startVerification: StartVerification,
) {

/**
* Looks up a consumer account and starts verification.
*
* If the consumer account exists, starts verification.
* If the consumer account does not exist, calls [onConsumerNotFound].
* If there is an error looking up the consumer account, calls [onLookupError].
* If there is an error starting verification, calls [onStartVerificationError].
* If verification is started successfully, calls [onVerificationStarted].
*/
sealed interface Result {
data class Success(val consumerSession: ConsumerSession) : Result
data object ConsumerNotFound : Result
data class LookupError(val error: Throwable) : Result
data class VerificationError(val error: Throwable) : Result
}
Comment on lines +12 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ much better.


suspend operator fun invoke(
email: String,
businessName: String?,
verificationType: VerificationType,
onConsumerNotFound: suspend () -> Unit,
onLookupError: suspend (Throwable) -> Unit,
onStartVerification: suspend () -> Unit,
onVerificationStarted: suspend (ConsumerSession) -> Unit,
onStartVerificationError: suspend (Throwable) -> Unit
) {
runCatching { lookupAccount(email) }
.onSuccess { session ->
): Result {
return runCatching {
lookupAccount(email)
}.fold(
onSuccess = { session ->
if (session.exists) {
onStartVerification()
kotlin.runCatching {
runCatching {
val consumerSecret = session.consumerSession!!.clientSecret
when (verificationType) {
VerificationType.EMAIL -> startVerification.email(
Expand All @@ -43,12 +37,17 @@ internal class LookupConsumerAndStartVerification @Inject constructor(
consumerSessionClientSecret = consumerSecret
)
}
}
.onSuccess { onVerificationStarted(it) }
.onFailure { onStartVerificationError(it) }
}.fold(
onSuccess = { Result.Success(it) },
onFailure = { Result.VerificationError(it) }
)
} else {
onConsumerNotFound()
Result.ConsumerNotFound
}
}.onFailure { onLookupError(it) }
},
onFailure = {
Result.LookupError(it)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,30 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationStepUpError
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationStepUpError.Error.ConsumerNotFoundError
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationStepUpError.Error.LookupConsumerSession
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationStepUpError.Error.MarkLinkVerifiedError
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationStepUpError.Error.StartVerificationError
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationStepUpSuccess
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
import com.stripe.android.financialconnections.analytics.logError
import com.stripe.android.financialconnections.di.FinancialConnectionsSheetNativeComponent
import com.stripe.android.financialconnections.domain.ConfirmVerification
import com.stripe.android.financialconnections.domain.GetCachedAccounts
import com.stripe.android.financialconnections.domain.GetOrFetchSync
import com.stripe.android.financialconnections.domain.LookupConsumerAndStartVerification
import com.stripe.android.financialconnections.domain.MarkLinkStepUpVerified
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
import com.stripe.android.financialconnections.domain.SelectNetworkedAccounts
import com.stripe.android.financialconnections.domain.StartVerification
import com.stripe.android.financialconnections.features.linkstepupverification.LinkStepUpVerificationState.Payload
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.navigation.Destination.InstitutionPicker
import com.stripe.android.financialconnections.navigation.NavigationManager
import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarStateUpdate
import com.stripe.android.financialconnections.presentation.Async
import com.stripe.android.financialconnections.presentation.Async.Fail
import com.stripe.android.financialconnections.presentation.Async.Loading
import com.stripe.android.financialconnections.presentation.Async.Success
import com.stripe.android.financialconnections.presentation.Async.Uninitialized
import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel
import com.stripe.android.financialconnections.repository.ConsumerSessionProvider
import com.stripe.android.financialconnections.utils.error
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.VerificationType
import com.stripe.android.uicore.elements.IdentifierSpec
import com.stripe.android.uicore.elements.OTPController
import com.stripe.android.uicore.elements.OTPElement
Expand All @@ -51,7 +45,8 @@ internal class LinkStepUpVerificationViewModel @AssistedInject constructor(
nativeAuthFlowCoordinator: NativeAuthFlowCoordinator,
private val eventTracker: FinancialConnectionsAnalyticsTracker,
private val getOrFetchSync: GetOrFetchSync,
private val lookupConsumerAndStartVerification: LookupConsumerAndStartVerification,
private val consumerSessionProvider: ConsumerSessionProvider,
private val startVerification: StartVerification,
private val confirmVerification: ConfirmVerification,
private val selectNetworkedAccounts: SelectNetworkedAccounts,
private val getCachedAccounts: GetCachedAccounts,
Expand All @@ -61,8 +56,8 @@ internal class LinkStepUpVerificationViewModel @AssistedInject constructor(
) : FinancialConnectionsViewModel<LinkStepUpVerificationState>(initialState, nativeAuthFlowCoordinator) {

init {
logErrors()
viewModelScope.launch { lookupAndStartVerification() }
observeErrors()
startVerification()
}

override fun updateTopAppBar(state: LinkStepUpVerificationState): TopAppBarStateUpdate {
Expand All @@ -73,35 +68,17 @@ internal class LinkStepUpVerificationViewModel @AssistedInject constructor(
)
}

private suspend fun lookupAndStartVerification() = runCatching {
getOrFetchSync().manifest.also { requireNotNull(it.accountholderCustomerEmailAddress) }
}
.onFailure { setState { copy(payload = Fail(it)) } }
.onSuccess { manifest ->
setState { copy(payload = Loading()) }
lookupConsumerAndStartVerification(
email = requireNotNull(manifest.accountholderCustomerEmailAddress),
private fun startVerification() {
suspend {
val cachedConsumerSession = requireNotNull(consumerSessionProvider.provideConsumerSession())
val manifest = getOrFetchSync().manifest
val consumerSession = startVerification.email(
consumerSessionClientSecret = cachedConsumerSession.clientSecret,
businessName = manifest.businessName,
verificationType = VerificationType.EMAIL,
onConsumerNotFound = {
eventTracker.track(VerificationStepUpError(PANE, ConsumerNotFoundError))
navigationManager.tryNavigateTo(InstitutionPicker(referrer = PANE))
},
onLookupError = { error ->
eventTracker.track(VerificationStepUpError(PANE, LookupConsumerSession))
setState { copy(payload = Fail(error)) }
},
onStartVerification = { /* no-op */ },
onVerificationStarted = { consumerSession ->
val payload = buildPayload(consumerSession)
setState { copy(payload = Success(payload)) }
},
onStartVerificationError = { error ->
eventTracker.track(VerificationStepUpError(PANE, StartVerificationError))
setState { copy(payload = Fail(error)) }
}
)
}
buildPayload(consumerSession)
}.execute { copy(payload = it) }
}

private fun buildPayload(consumerSession: ConsumerSession) = Payload(
email = consumerSession.emailAddress,
Expand All @@ -113,7 +90,7 @@ internal class LinkStepUpVerificationViewModel @AssistedInject constructor(
)
)

private fun logErrors() {
private fun observeErrors() {
onAsync(
LinkStepUpVerificationState::payload,
onSuccess = {
Expand Down Expand Up @@ -179,37 +156,22 @@ internal class LinkStepUpVerificationViewModel @AssistedInject constructor(

fun onClickableTextClick(text: String) {
when (text) {
CLICKABLE_TEXT_RESEND_CODE -> viewModelScope.launch { onResendOtp() }
CLICKABLE_TEXT_RESEND_CODE -> onResendOtp()
else -> logger.error("Unknown clicked text $text")
}
}

private suspend fun onResendOtp() = runCatching {
getOrFetchSync().manifest.also { requireNotNull(it.accountholderCustomerEmailAddress) }
}
.onFailure { setState { copy(resendOtp = Fail(it)) } }
.onSuccess { manifest ->
setState { copy(resendOtp = Loading()) }
lookupConsumerAndStartVerification(
email = requireNotNull(manifest.accountholderCustomerEmailAddress),
private fun onResendOtp() {
suspend {
val cachedConsumerSession = requireNotNull(consumerSessionProvider.provideConsumerSession())
val manifest = getOrFetchSync().manifest
startVerification.email(
consumerSessionClientSecret = cachedConsumerSession.clientSecret,
businessName = manifest.businessName,
verificationType = VerificationType.EMAIL,
onConsumerNotFound = {
eventTracker.track(VerificationStepUpError(PANE, ConsumerNotFoundError))
navigationManager.tryNavigateTo(InstitutionPicker(referrer = PANE))
},
onLookupError = { error ->
eventTracker.track(VerificationStepUpError(PANE, LookupConsumerSession))
setState { copy(resendOtp = Fail(error)) }
},
onStartVerification = { /* no-op */ },
onVerificationStarted = { setState { copy(resendOtp = Success(Unit)) } },
onStartVerificationError = { error ->
eventTracker.track(VerificationStepUpError(PANE, StartVerificationError))
setState { copy(resendOtp = Fail(error)) }
}
)
}
Unit
}.execute { copy(resendOtp = it) }
}

@AssistedFactory
interface Factory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ private fun NetworkingLinkLoginWarmupContent(
},
footer = {
Footer(
loadingPrimary = state.continueAsync is Loading,
loading = state.disableNetworkingAsync is Loading || state.payload() == null,
secondaryButtonLabel = state.secondaryButtonLabel,
onContinueClick = onContinueClick,
Expand Down Expand Up @@ -115,14 +116,15 @@ private fun HeaderSection() {
@Composable
@OptIn(ExperimentalComposeUiApi::class)
private fun Footer(
loadingPrimary: Boolean,
loading: Boolean,
secondaryButtonLabel: Int,
Comment on lines +119 to 121
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - these 3 are not named consistently. Maybe something like:

Suggested change
loadingPrimary: Boolean,
loading: Boolean,
secondaryButtonLabel: Int,
primaryButtonLoading: Boolean,
footerLoading: Boolean,
secondaryButtonLabel: Int,

onContinueClick: () -> Unit,
onSkipClicked: () -> Unit
) {
Column {
FinancialConnectionsButton(
loading = false,
loading = loadingPrimary,
enabled = loading.not(),
type = Type.Primary,
onClick = onContinueClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.stripe.android.financialconnections.features.networkinglinkloginwarm

import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.financialconnections.R
Expand All @@ -13,11 +12,13 @@ import com.stripe.android.financialconnections.di.FinancialConnectionsSheetNativ
import com.stripe.android.financialconnections.domain.DisableNetworking
import com.stripe.android.financialconnections.domain.GetOrFetchSync
import com.stripe.android.financialconnections.domain.HandleError
import com.stripe.android.financialconnections.domain.LookupAccount
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
import com.stripe.android.financialconnections.features.common.getBusinessName
import com.stripe.android.financialconnections.features.common.getRedactedEmail
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane.NETWORKING_LINK_VERIFICATION
import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.navigation.Destination.Companion.KEY_NEXT_PANE_ON_DISABLE_NETWORKING
import com.stripe.android.financialconnections.navigation.NavigationManager
Expand All @@ -31,7 +32,6 @@ import com.stripe.android.financialconnections.presentation.FinancialConnections
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch

internal class NetworkingLinkLoginWarmupViewModel @AssistedInject constructor(
@Assisted initialState: NetworkingLinkLoginWarmupState,
Expand All @@ -40,7 +40,8 @@ internal class NetworkingLinkLoginWarmupViewModel @AssistedInject constructor(
private val handleError: HandleError,
private val getOrFetchSync: GetOrFetchSync,
private val disableNetworking: DisableNetworking,
private val navigationManager: NavigationManager
private val navigationManager: NavigationManager,
private val lookupAccount: LookupAccount,
) : FinancialConnectionsViewModel<NetworkingLinkLoginWarmupState>(initialState, nativeAuthFlowCoordinator) {

init {
Expand Down Expand Up @@ -84,9 +85,16 @@ internal class NetworkingLinkLoginWarmupViewModel @AssistedInject constructor(
)
}

fun onContinueClick() = viewModelScope.launch {
eventTracker.track(Click("click.continue", PANE))
navigationManager.tryNavigateTo(Destination.NetworkingLinkVerification(referrer = PANE))
fun onContinueClick() {
val payload = stateFlow.value.payload() ?: return

suspend {
eventTracker.track(Click("click.continue", PANE))
lookupAccount(payload.email)
navigationManager.tryNavigateTo(NETWORKING_LINK_VERIFICATION.destination(referrer = PANE))
}.execute {
copy(continueAsync = it)
}
}

fun onSecondaryButtonClicked() {
Expand All @@ -99,6 +107,38 @@ internal class NetworkingLinkLoginWarmupViewModel @AssistedInject constructor(
}
}

// private suspend fun startVerification(
// payload: NetworkingLinkLoginWarmupState.Payload,
// ): Pane? {
// val verificationResult = lookupConsumerAndStartVerification(
// email = payload.email,
// businessName = payload.merchantName,
// verificationType = VerificationType.SMS,
// )
//
// return when (verificationResult) {
// is Result.ConsumerNotFound -> {
// eventTracker.track(VerificationError(NetworkingLinkVerificationViewModel.PANE, ConsumerNotFoundError))
// Pane.INSTITUTION_PICKER
// }
// is Result.LookupError -> {
// eventTracker.track(VerificationError(NetworkingLinkVerificationViewModel.PANE, LookupConsumerSession))
// setState { copy(payload = Fail(verificationResult.error)) }
// null
// }
// is Result.VerificationError -> {
// eventTracker.track(
// VerificationError(NetworkingLinkVerificationViewModel.PANE, StartVerificationSessionError)
// )
// setState { copy(payload = Fail(verificationResult.error)) }
// null
// }
// is Result.Success -> {
// NETWORKING_LINK_VERIFICATION
// }
// }
// }

private fun skipNetworking() {
suspend {
eventTracker.track(Click("click.skip_sign_in", PANE))
Expand Down Expand Up @@ -159,6 +199,7 @@ internal data class NetworkingLinkLoginWarmupState(
val nextPaneOnDisableNetworking: String? = null,
val payload: Async<Payload> = Uninitialized,
val disableNetworkingAsync: Async<FinancialConnectionsSessionManifest> = Uninitialized,
val continueAsync: Async<Unit> = Uninitialized,
val isInstantDebits: Boolean = false,
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.stripe.android.financialconnections.domain.GetOrFetchSync
import com.stripe.android.financialconnections.domain.GetOrFetchSync.RefetchCondition
import com.stripe.android.financialconnections.domain.LookupAccount
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
import com.stripe.android.financialconnections.domain.StartVerification
import com.stripe.android.financialconnections.features.common.getBusinessName
import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.ViewEffect.OpenUrl
import com.stripe.android.financialconnections.features.notice.NoticeSheetState.NoticeSheetContent.Legal
Expand Down Expand Up @@ -61,6 +62,7 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor(
@Assisted initialState: NetworkingLinkSignupState,
nativeAuthFlowCoordinator: NativeAuthFlowCoordinator,
private val lookupAccount: LookupAccount,
private val startVerification: StartVerification,
private val uriUtils: UriUtils,
private val eventTracker: FinancialConnectionsAnalyticsTracker,
private val getOrFetchSync: GetOrFetchSync,
Expand Down Expand Up @@ -198,7 +200,15 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor(
logger.debug("VALID EMAIL ADDRESS $validEmail.")
searchJob += suspend {
delay(getLookupDelayMs(validEmail))
lookupAccount(validEmail)

val lookup = lookupAccount(validEmail)
val clientSecret = lookup.consumerSession?.clientSecret

if (lookup.exists && clientSecret != null) {
startVerification.sms(consumerSessionClientSecret = clientSecret)
}

lookup
}.execute { copy(lookupAccount = if (it.isCancellationError()) Uninitialized else it) }
} else {
setState { copy(lookupAccount = Uninitialized) }
Expand Down
Loading
Loading