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

[Refactor] Merge the post verification logic between verification panes. #8653

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
24 changes: 24 additions & 0 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,22 @@ public final class com/stripe/android/financialconnections/model/PaymentAccount$
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/stripe/android/financialconnections/model/PaymentAccountParams$BankAccount$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/PaymentAccountParams$BankAccount;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/model/PaymentAccountParams$BankAccount;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/model/PaymentAccountParams$LinkedAccount$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/PaymentAccountParams$LinkedAccount;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/model/PaymentAccountParams$LinkedAccount;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/model/ReturningNetworkingUserAccountPicker$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/ReturningNetworkingUserAccountPicker;
Expand Down Expand Up @@ -1807,6 +1823,14 @@ public final class com/stripe/android/financialconnections/repository/AccountUpd
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/repository/AttachedPaymentAccountRepository$State$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/repository/AttachedPaymentAccountRepository$State;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/repository/AttachedPaymentAccountRepository$State;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository$State$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository$State;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ internal sealed class FinancialConnectionsAnalyticsEvent(
ConsumerNotFoundError("ConsumerNotFoundError"),
LookupConsumerSession("LookupConsumerSession"),
StartVerificationSessionError("StartVerificationSessionError"),
SaveToLinkError("SaveToLinkError"),
ConfirmVerificationSessionError("ConfirmVerificationSessionError"),
MarkLinkVerifiedError("MarkLinkVerifiedError"),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.stripe.android.financialconnections.domain

import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationError
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationError.Error.MarkLinkVerifiedError
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.VerificationSuccess
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
import com.stripe.android.financialconnections.analytics.logError
import com.stripe.android.financialconnections.features.common.isDataFlow
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
import com.stripe.android.financialconnections.model.PaymentAccountParams
import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.navigation.Destination.Success
import com.stripe.android.financialconnections.navigation.NavigationManager
import com.stripe.android.financialconnections.navigation.destination
import com.stripe.android.financialconnections.repository.AttachedPaymentAccountRepository
import javax.inject.Inject

internal class CompleteVerification @Inject constructor(
Copy link
Collaborator Author

@carlosmuvi-stripe carlosmuvi-stripe Jun 28, 2024

Choose a reason for hiding this comment

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

equivalent web code: https://git.corp.stripe.com/stripe-internal/stripe-js-v3/blob/02e07063/src/linkedAccounts/inner/components/panes/v3/LinkVerificationPane/index.tsx#L177

Note this is just merging the logic across verification panes into a usecase. No new code here besides checking if an account of type bank_account has been previously attached.

private val cachedAccounts: GetCachedAccounts,
private val saveAccountToLink: SaveAccountToLink,
private val getOrFetchSync: GetOrFetchSync,
private val analyticsTracker: FinancialConnectionsAnalyticsTracker,
private val navigationManager: NavigationManager,
private val logger: Logger,
private val handleError: HandleError,
private val markLinkVerified: MarkLinkVerified,
private val attachedPaymentAccountRepository: AttachedPaymentAccountRepository,
) {
suspend operator fun invoke(
pane: Pane,
consumerSessionClientSecret: String,
) {
val manifest = getOrFetchSync().manifest
val accounts = cachedAccounts()
if (accounts.isNotEmpty()) {
// If there are accounts to share (ie. user linked accounts first and then logged in) then save those
// when we have already connected accounts, we first saveToNetworkAndLink and then markLinkVerified
saveToNetworkAndLinkRequest(
accounts = accounts,
consumerSessionClientSecret = consumerSessionClientSecret,
isDataFlow = manifest.isDataFlow,
pane = pane
)
} else if (attachedPaymentAccountRepository.get()?.attachedPaymentAccount is PaymentAccountParams.BankAccount) {
// we come here if the user has already attached their bank account and has then logged in
// to an existing Link account. now we want network that bank account
// we do not need to pass anything here since the bank account is already attached
// and the backend pulls it from the LAS
saveToNetworkAndLinkRequest(
accounts = null,
consumerSessionClientSecret = consumerSessionClientSecret,
isDataFlow = manifest.isDataFlow,
pane = pane
)
} else {
// if the user is verifying at the beginning of the flow and hasn't connected accounts yet,
// we just markLinkVerified and move onto returning user account picker.
runCatching { markLinkVerified() }.fold(
// TODO(carlosmuvi): once `/link_verified` is updated to return correct next_pane we should consume that
onSuccess = {
analyticsTracker.track(VerificationSuccess(pane))
navigationManager.tryNavigateTo(Destination.LinkAccountPicker(referrer = pane))
},
onFailure = {
val nextPaneOnFailure = manifest.initialInstitution
?.let { Pane.PARTNER_AUTH }
?: Pane.INSTITUTION_PICKER
analyticsTracker.logError(
extraMessage = "Error confirming verification or marking link as verified",
error = it,
logger = logger,
pane = pane
)
analyticsTracker.track(VerificationError(pane, MarkLinkVerifiedError))
navigationManager.tryNavigateTo(nextPaneOnFailure.destination(referrer = pane))
}
)
}
}

private suspend fun saveToNetworkAndLinkRequest(
accounts: List<CachedPartnerAccount>?,
consumerSessionClientSecret: String,
isDataFlow: Boolean,
pane: Pane
) {
runCatching {
saveAccountToLink.existing(
consumerSessionClientSecret = consumerSessionClientSecret,
selectedAccounts = accounts,
shouldPollAccountNumbers = isDataFlow
)
}.onSuccess {
analyticsTracker.track(VerificationSuccess(pane))
}.onFailure {
logger.error("Error saving account to Link", it)
analyticsTracker.track(VerificationError(pane, VerificationError.Error.SaveToLinkError))
}

runCatching { markLinkVerified() }
.onSuccess {
// navigate to success regardless of save result.
// the save use case takes care of updating the success screen message (that will be a failure in case
// of something going wrong)
navigationManager.tryNavigateTo(Success(referrer = pane))
}.onFailure {
// mimicking behavior from v2 where if this request fails we use default error handling
handleError(
extraMessage = "Error marking link as verified",
error = it,
pane = pane,
displayErrorScreen = true
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal class GetCachedAccounts @Inject constructor(
) {

suspend operator fun invoke(): List<CachedPartnerAccount> {
return requireNotNull(repository.getCachedAccounts())
return repository.getCachedAccounts() ?: emptyList()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsInstitu
import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount
import com.stripe.android.financialconnections.model.PaymentAccountParams
import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
import com.stripe.android.financialconnections.repository.AttachedPaymentAccountRepository
import com.stripe.android.financialconnections.repository.FinancialConnectionsAccountsRepository
import com.stripe.android.financialconnections.utils.PollTimingOptions
import com.stripe.android.financialconnections.utils.retryOnException
Expand All @@ -17,6 +18,7 @@ import kotlin.time.Duration.Companion.seconds

internal class PollAttachPaymentAccount @Inject constructor(
private val repository: FinancialConnectionsAccountsRepository,
private val attachedPaymentAccountRepository: AttachedPaymentAccountRepository,
private val configuration: FinancialConnectionsSheet.Configuration
) {

Expand All @@ -35,11 +37,13 @@ internal class PollAttachPaymentAccount @Inject constructor(
retryCondition = { exception -> exception.shouldRetry }
) {
try {
repository.postLinkAccountSessionPaymentAccount(
repository.postAttachPaymentAccountToLinkAccountSession(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

renamed this api request to be more precise.

clientSecret = configuration.financialConnectionsSessionClientSecret,
paymentAccount = params,
consumerSessionClientSecret = consumerSessionClientSecret
)
).also {
attachedPaymentAccountRepository.set(params)
}
} catch (e: StripeException) {
throw e.toDomainException(
activeInstitution,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.stripe.android.financialconnections.domain
import com.stripe.android.financialconnections.FinancialConnectionsSheet
import com.stripe.android.financialconnections.R
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.PaymentAccountParams.BankAccount
import com.stripe.android.financialconnections.repository.AttachedPaymentAccountRepository
import com.stripe.android.financialconnections.repository.FinancialConnectionsAccountsRepository
import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
import com.stripe.android.financialconnections.repository.SuccessContentRepository
Expand All @@ -17,6 +19,7 @@ import kotlin.time.Duration.Companion.seconds
internal class SaveAccountToLink @Inject constructor(
private val locale: Locale?,
private val configuration: FinancialConnectionsSheet.Configuration,
private val attachedPaymentAccountRepository: AttachedPaymentAccountRepository,
private val successContentRepository: SuccessContentRepository,
private val repository: FinancialConnectionsManifestRepository,
private val accountsRepository: FinancialConnectionsAccountsRepository,
Expand Down Expand Up @@ -44,7 +47,7 @@ internal class SaveAccountToLink @Inject constructor(

suspend fun existing(
consumerSessionClientSecret: String,
selectedAccounts: List<CachedPartnerAccount>,
selectedAccounts: List<CachedPartnerAccount>?,
shouldPollAccountNumbers: Boolean,
): FinancialConnectionsSessionManifest {
return ensureReadyAccounts(shouldPollAccountNumbers, selectedAccounts) { selectedAccountIds ->
Expand All @@ -62,16 +65,23 @@ internal class SaveAccountToLink @Inject constructor(

private suspend fun ensureReadyAccounts(
shouldPollAccountNumbers: Boolean,
partnerAccounts: List<CachedPartnerAccount>,
action: suspend (Set<String>) -> FinancialConnectionsSessionManifest,
partnerAccounts: List<CachedPartnerAccount>?,
action: suspend (Set<String>?) -> FinancialConnectionsSessionManifest,
): FinancialConnectionsSessionManifest {
val selectedAccountIds = partnerAccounts.map { it.id }.toSet()
val linkedAccountIds = partnerAccounts.mapNotNull { it.linkedAccountId }.toSet()
val selectedAccountIds = partnerAccounts?.map { it.id }?.toSet() ?: emptySet()
val linkedAccountIds = partnerAccounts?.mapNotNull { it.linkedAccountId }?.toSet() ?: emptySet()

val pollingResult = if (shouldPollAccountNumbers) {
runCatching { awaitAccountNumbersReady(linkedAccountIds) }
} else {
Result.success(Unit)
val pollingResult = when {
partnerAccounts.isNullOrEmpty() -> when (attachedPaymentAccountRepository.get()?.attachedPaymentAccount) {
is BankAccount -> Result.success(Unit)
else -> Result.failure(
IllegalStateException(
"Must have a bank account attached if no accounts are selected"
)
)
}
shouldPollAccountNumbers -> runCatching { awaitAccountNumbersReady(linkedAccountIds) }
else -> Result.success(Unit)
}

return pollingResult.onFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ private fun createUpdateRequiredContent(
AccountUpdateRequiredState.Payload(
iconUrl = partnerAccount.institution?.icon?.default,
type = Repair(
authorization = partnerToCoreAuths?.getValue(partnerAccount.authorization),
authorization = partnerAccount.authorization?.let { partnerToCoreAuths?.getValue(it) },
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsSession
import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount
import com.stripe.android.financialconnections.model.ManualEntryMode
import com.stripe.android.financialconnections.model.PaymentAccountParams
import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.navigation.NavigationManager
import com.stripe.android.financialconnections.navigation.destination
import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarStateUpdate
import com.stripe.android.financialconnections.presentation.Async
import com.stripe.android.financialconnections.presentation.Async.Uninitialized
Expand Down Expand Up @@ -145,8 +145,8 @@ internal class ManualEntryViewModel @AssistedInject constructor(
activeInstitution = null,
consumerSessionClientSecret = null,
params = PaymentAccountParams.BankAccount(
routingNumber = requireNotNull(routing),
accountNumber = requireNotNull(account)
routingNumber = routing,
accountNumber = account
)
).also {
if (sync.manifest.manualEntryUsesMicrodeposits) {
Expand All @@ -157,7 +157,8 @@ internal class ManualEntryViewModel @AssistedInject constructor(
)
)
}
navigationManager.tryNavigateTo(Destination.ManualEntrySuccess(referrer = PANE))
val nextPane = (it.nextPane ?: Pane.MANUAL_ENTRY_SUCCESS).destination(referrer = PANE)
navigationManager.tryNavigateTo(nextPane)
}
}.execute { copy(linkPaymentAccount = it) }
}
Expand Down
Loading
Loading