Skip to content

Commit

Permalink
Replace LookupAndStartVerification with StartVerification in step…
Browse files Browse the repository at this point in the history
…-up pane
  • Loading branch information
tillh-stripe committed Aug 15, 2024
1 parent 3296b4b commit 647da7e
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +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.LookupConsumerAndStartVerification.Result
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 @@ -52,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 @@ -62,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 @@ -74,38 +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()) }

val result = 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,
)

when (result) {
is Result.ConsumerNotFound -> {
eventTracker.track(VerificationStepUpError(PANE, ConsumerNotFoundError))
navigationManager.tryNavigateTo(InstitutionPicker(referrer = PANE))
}
is Result.LookupError -> {
eventTracker.track(VerificationStepUpError(PANE, LookupConsumerSession))
setState { copy(payload = Fail(result.error)) }
}
is Result.VerificationError -> {
eventTracker.track(VerificationStepUpError(PANE, StartVerificationError))
setState { copy(payload = Fail(result.error)) }
}
is Result.Success -> {
val payload = buildPayload(result.consumerSession)
setState { copy(payload = Success(payload)) }
}
}
}
buildPayload(consumerSession)
}.execute { copy(payload = it) }
}

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

private fun logErrors() {
private fun observeErrors() {
onAsync(
LinkStepUpVerificationState::payload,
onSuccess = {
Expand Down Expand Up @@ -183,42 +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()) }

val result = 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,
)

when (result) {
is Result.ConsumerNotFound -> {
eventTracker.track(VerificationStepUpError(PANE, ConsumerNotFoundError))
navigationManager.tryNavigateTo(InstitutionPicker(referrer = PANE))
}
is Result.LookupError -> {
eventTracker.track(VerificationStepUpError(PANE, LookupConsumerSession))
setState { copy(resendOtp = Fail(result.error)) }
}
is Result.VerificationError -> {
eventTracker.track(VerificationStepUpError(PANE, StartVerificationError))
setState { copy(resendOtp = Fail(result.error)) }
}
is Result.Success -> {
setState { copy(resendOtp = Success(Unit)) }
}
}
}
Unit
}.execute { copy(resendOtp = it) }
}

@AssistedFactory
interface Factory {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.stripe.android.financialconnections.features.linkstepupverification

import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.ApiKeyFixtures
Expand All @@ -11,16 +10,16 @@ import com.stripe.android.financialconnections.TestFinancialConnectionsAnalytics
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.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.presentation.Async.Loading
import com.stripe.android.financialconnections.repository.CachedConsumerSession
import com.stripe.android.financialconnections.repository.ConsumerSessionProvider
import com.stripe.android.financialconnections.utils.TestNavigationManager
import com.stripe.android.model.VerificationType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
Expand All @@ -39,9 +38,10 @@ class LinkStepUpVerificationViewModelTest {

private val getOrFetchSync = mock<GetOrFetchSync>()
private val navigationManager = TestNavigationManager()
private val consumerSessionProvider = mock<ConsumerSessionProvider>()
private val startVerification = mock<StartVerification>()
private val confirmVerification = mock<ConfirmVerification>()
private val getCachedAccounts = mock<GetCachedAccounts>()
private val lookupConsumerAndStartVerification = mock<LookupConsumerAndStartVerification>()
private val selectNetworkedAccounts = mock<SelectNetworkedAccounts>()
private val markLinkVerified = mock<MarkLinkStepUpVerified>()
private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
Expand All @@ -53,11 +53,12 @@ class LinkStepUpVerificationViewModelTest {
getOrFetchSync = getOrFetchSync,
navigationManager = navigationManager,
eventTracker = eventTracker,
consumerSessionProvider = consumerSessionProvider,
startVerification = startVerification,
confirmVerification = confirmVerification,
markLinkStepUpVerified = markLinkVerified,
getCachedAccounts = getCachedAccounts,
selectNetworkedAccounts = selectNetworkedAccounts,
lookupConsumerAndStartVerification = lookupConsumerAndStartVerification,
logger = Logger.noop(),
initialState = state,
nativeAuthFlowCoordinator = nativeAuthFlowCoordinator,
Expand All @@ -69,16 +70,22 @@ class LinkStepUpVerificationViewModelTest {
val consumerSession = ApiKeyFixtures.consumerSession()
getManifestReturnsManifestWithEmail(email)

whenever(lookupConsumerAndStartVerification(any(), anyOrNull(), any())).thenReturn(
LookupConsumerAndStartVerification.Result.Success(consumerSession)
whenever(consumerSessionProvider.provideConsumerSession()).thenReturn(
CachedConsumerSession(
emailAddress = consumerSession.emailAddress,
phoneNumber = consumerSession.redactedFormattedPhoneNumber,
clientSecret = consumerSession.clientSecret,
publishableKey = "pk_123",
isVerified = true,
)
)
whenever(startVerification.email(any(), anyOrNull())).thenReturn(consumerSession)

val viewModel = buildViewModel()

verify(lookupConsumerAndStartVerification).invoke(
email = eq(email),
verify(startVerification).email(
consumerSessionClientSecret = eq(consumerSession.clientSecret),
businessName = anyOrNull(),
verificationType = eq(VerificationType.EMAIL),
)

val state = viewModel.stateFlow.value
Expand All @@ -87,43 +94,6 @@ class LinkStepUpVerificationViewModelTest {
.isEqualTo(consumerSession.clientSecret)
}

@Test
fun `init - ConsumerNotFound sends analytics and navigates to institution picker`() = runTest {
val email = "[email protected]"

getManifestReturnsManifestWithEmail(email)

whenever(lookupConsumerAndStartVerification(any(), anyOrNull(), any())).thenReturn(
LookupConsumerAndStartVerification.Result.ConsumerNotFound
)

buildViewModel().stateFlow.test {
assertThat(awaitItem().payload).isInstanceOf(Loading::class.java)

verify(lookupConsumerAndStartVerification).invoke(
email = eq(email),
businessName = anyOrNull(),
verificationType = eq(VerificationType.EMAIL),
)

// we don't expect any state updates if the consumer is not found
expectNoEvents()

navigationManager.assertNavigatedTo(
destination = Destination.InstitutionPicker,
pane = Pane.LINK_STEP_UP_VERIFICATION
)

eventTracker.assertContainsEvent(
"linked_accounts.networking.verification.step_up.error",
mapOf(
"pane" to "networking_link_step_up_verification",
"error" to "ConsumerNotFoundError"
)
)
}
}

@Test
fun `otpEntered - on valid OTP confirms, verifies, selects account and navigates to success`() =
runTest {
Expand All @@ -140,16 +110,23 @@ class LinkStepUpVerificationViewModelTest {
// link succeeds
markLinkVerifiedReturns(linkVerifiedManifest)

whenever(lookupConsumerAndStartVerification(any(), anyOrNull(), any())).thenReturn(
LookupConsumerAndStartVerification.Result.Success(consumerSession)
whenever(consumerSessionProvider.provideConsumerSession()).thenReturn(
CachedConsumerSession(
emailAddress = consumerSession.emailAddress,
phoneNumber = consumerSession.redactedFormattedPhoneNumber,
clientSecret = consumerSession.clientSecret,
publishableKey = "pk_123",
isVerified = true,
)
)

whenever(startVerification.email(any(), anyOrNull())).thenReturn(consumerSession)

val viewModel = buildViewModel()

verify(lookupConsumerAndStartVerification).invoke(
email = eq(email),
verify(startVerification).email(
consumerSessionClientSecret = eq(consumerSession.clientSecret),
businessName = anyOrNull(),
verificationType = eq(VerificationType.EMAIL),
)

val otpController = viewModel.stateFlow.value.payload()!!.otpElement.controller
Expand Down

0 comments on commit 647da7e

Please sign in to comment.