Skip to content

Commit

Permalink
Updates lookup call to use mobile endpoint on verified flows
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosmuvi-stripe committed Jan 2, 2025
1 parent 75b1536 commit 69327fb
Show file tree
Hide file tree
Showing 23 changed files with 210 additions and 61 deletions.
4 changes: 1 addition & 3 deletions .idea/codestyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -442,9 +442,7 @@ enum class Merchant(
Networking("networking"),
LiveTesting("live_testing", canSwitchBetweenTestAndLive = false),
TestMode("testmode", canSwitchBetweenTestAndLive = false),
NmeDefaultVerification("nme", canSwitchBetweenTestAndLive = true),
NmeABAVVerification("nme_abav", canSwitchBetweenTestAndLive = true),
NmeSkipVerification("nme_skip", canSwitchBetweenTestAndLive = true),
Trusted("trusted", canSwitchBetweenTestAndLive = false),
Custom("other");

companion object {
Expand Down
1 change: 1 addition & 0 deletions financial-connections/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ID>LongMethod:AccountItem.kt$@Composable @Preview internal fun AccountItemPreview()</ID>
<ID>LongMethod:Button.kt$@Composable internal fun FinancialConnectionsButton( onClick: () -> Unit, modifier: Modifier = Modifier, type: Type = Primary, size: FinancialConnectionsButton.Size = FinancialConnectionsButton.Size.Regular, enabled: Boolean = true, loading: Boolean = false, content: @Composable (RowScope.() -> Unit) )</ID>
<ID>LongMethod:FinancialConnectionsSheetNativeActivity.kt$FinancialConnectionsSheetNativeActivity$@Composable fun NavHost( initialPane: Pane, testMode: Boolean, )</ID>
<ID>LongMethod:FinancialConnectionsSheetNativeViewModel.kt$FinancialConnectionsSheetNativeViewModel$private fun closeAuthFlow( earlyTerminationCause: EarlyTerminationCause? = null, closeAuthFlowError: Throwable? = null )</ID>
<ID>LongMethod:InstitutionPickerScreen.kt$private fun LazyListScope.searchResults( isInputEmpty: Boolean, payload: Payload, selectedInstitutionId: String?, onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit, institutions: Async&lt;InstitutionResponse>, onManualEntryClick: () -> Unit, onSearchMoreClick: () -> Unit )</ID>
<ID>LongMethod:LinkAccountPickerPreviewParameterProvider.kt$LinkAccountPickerPreviewParameterProvider$private fun partnerAccountList()</ID>
<ID>LongMethod:NetworkingSaveToLinkVerificationScreen.kt$@Composable private fun NetworkingSaveToLinkVerificationLoaded( confirmVerificationAsync: Async&lt;Unit>, payload: Payload, onCloseFromErrorClick: (Throwable) -> Unit, onSkipClick: () -> Unit, )</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
import com.stripe.android.financialconnections.domain.NativeAuthFlowRouter
import com.stripe.android.financialconnections.exception.AppInitializationError
import com.stripe.android.financialconnections.exception.CustomManualEntryRequiredError
import com.stripe.android.financialconnections.features.error.isAttestationError
import com.stripe.android.financialconnections.features.manualentry.isCustomManualEntryError
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.ForData
Expand Down Expand Up @@ -521,6 +522,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
fromNative: Boolean = false,
@StringRes finishMessage: Int? = null,
) {
if (result is Failed && result.error.isAttestationError()) {
switchToWebFlow()
return
}
eventReporter.onResult(state.initialArgs.configuration, result)
// Native emits its own events before finishing.
if (fromNative.not()) {
Expand All @@ -536,6 +541,27 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
setState { copy(viewEffect = FinishWithResult(result, finishMessage)) }
}

/**
* On scenarios where native failed mid flow due to attestation errors, switch back to web flow.
*/
private fun switchToWebFlow() {
viewModelScope.launch {
val sync = getOrFetchSync()
val hostedAuthUrl = HostedAuthUrlBuilder.create(
args = initialState.initialArgs,
manifest = sync.manifest,
)!!
setState {
copy(
manifest = manifest,
// Use intermediate state to prevent the flow from closing in [onResume].
webAuthFlowStatus = AuthFlowStatus.INTERMEDIATE_DEEPLINK,
viewEffect = OpenAuthFlowWithUrl(hostedAuthUrl)
)
}
}
}

companion object {

val Factory = viewModelFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ package com.stripe.android.financialconnections.domain
import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
import com.stripe.android.financialconnections.analytics.logError
import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message.CloseWithError
import com.stripe.android.financialconnections.features.error.isAttestationError
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.navigation.Destination
import com.stripe.android.financialconnections.navigation.NavigationManager
import com.stripe.android.financialconnections.repository.FinancialConnectionsErrorRepository
import javax.inject.Inject

internal interface HandleError {
operator fun invoke(
suspend operator fun invoke(
extraMessage: String,
error: Throwable,
pane: FinancialConnectionsSessionManifest.Pane,
Expand All @@ -21,6 +23,7 @@ internal interface HandleError {
internal class RealHandleError @Inject constructor(
private val errorRepository: FinancialConnectionsErrorRepository,
private val analyticsTracker: FinancialConnectionsAnalyticsTracker,
private val nativeAuthFlowCoordinator: NativeAuthFlowCoordinator,
private val logger: Logger,
private val navigationManager: NavigationManager
) : HandleError {
Expand All @@ -38,11 +41,11 @@ internal class RealHandleError @Inject constructor(
* @param displayErrorScreen whether to navigate to the error screen
*
*/
override operator fun invoke(
override suspend operator fun invoke(
extraMessage: String,
error: Throwable,
pane: FinancialConnectionsSessionManifest.Pane,
displayErrorScreen: Boolean
displayErrorScreen: Boolean,
) {
analyticsTracker.logError(
extraMessage = extraMessage,
Expand All @@ -51,8 +54,10 @@ internal class RealHandleError @Inject constructor(
pane = pane
)

// Navigate to error screen
if (displayErrorScreen) {
if (error.isAttestationError()) {
nativeAuthFlowCoordinator().emit(CloseWithError(cause = error))
} else if (displayErrorScreen) {
// Navigate to error screen
errorRepository.set(error)
navigationManager.tryNavigateTo(route = Destination.Error(referrer = pane))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
package com.stripe.android.financialconnections.domain

import android.app.Application
import com.stripe.android.financialconnections.FinancialConnectionsSheet
import com.stripe.android.financialconnections.repository.FinancialConnectionsConsumerSessionRepository
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.attestation.IntegrityRequestManager
import javax.inject.Inject

internal class LookupAccount @Inject constructor(
private val application: Application,
private val integrityRequestManager: IntegrityRequestManager,
private val consumerSessionRepository: FinancialConnectionsConsumerSessionRepository,
val configuration: FinancialConnectionsSheet.Configuration,
) {

suspend operator fun invoke(
email: String
): ConsumerSessionLookup = requireNotNull(
consumerSessionRepository.lookupConsumerSession(
email = email.lowercase().trim(),
clientSecret = configuration.financialConnectionsSessionClientSecret
)
)
email: String,
verifiedFlow: Boolean
): ConsumerSessionLookup {
return if (verifiedFlow) {
requireNotNull(
consumerSessionRepository.mobileLookupConsumerSession(
email = email.lowercase().trim(),
verificationToken = integrityRequestManager.requestToken().getOrThrow(),
appId = application.packageName
)
)
} else {
requireNotNull(
consumerSessionRepository.postConsumerSession(
email = email.lowercase().trim(),
clientSecret = configuration.financialConnectionsSessionClientSecret
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ internal class LookupConsumerAndStartVerification @Inject constructor(
email: String,
businessName: String?,
verificationType: VerificationType,
appVerificationEnabled: Boolean,
onConsumerNotFound: suspend () -> Unit,
onLookupError: suspend (Throwable) -> Unit,
onStartVerification: suspend () -> Unit,
onVerificationStarted: suspend (ConsumerSession) -> Unit,
onStartVerificationError: suspend (Throwable) -> Unit
) {
runCatching { lookupAccount(email) }
runCatching { lookupAccount(email, appVerificationEnabled) }
.onSuccess { session ->
if (session.exists) {
onStartVerification()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stripe.android.financialconnections.features.error

import com.stripe.android.core.exception.APIException

internal fun Throwable.isAttestationError(): Boolean {
return this is APIException && stripeError?.code == "link_failed_to_attest_request"
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal interface LinkSignupHandler {
state: NetworkingLinkSignupState,
): Pane

fun handleSignupFailure(
suspend fun handleSignupFailure(
error: Throwable,
)

Expand Down Expand Up @@ -65,7 +65,7 @@ internal class LinkSignupHandlerForInstantDebits @Inject constructor(
navigationManager.tryNavigateTo(NetworkingLinkVerification(referrer = LINK_LOGIN))
}

override fun handleSignupFailure(error: Throwable) {
override suspend fun handleSignupFailure(error: Throwable) {
handleError(
extraMessage = "Error creating a Link account",
error = error,
Expand Down Expand Up @@ -107,7 +107,7 @@ internal class LinkSignupHandlerForNetworking @Inject constructor(
navigationManager.tryNavigateTo(NetworkingSaveToLinkVerification(referrer = NETWORKING_LINK_SIGNUP_PANE))
}

override fun handleSignupFailure(error: Throwable) {
override suspend fun handleSignupFailure(error: Throwable) {
eventTracker.logError(
extraMessage = "Error saving account to Link",
error = error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class NetworkingLinkSignupPreviewParameterProvider :
initiallySelectedCountryCode = null,
),
isInstantDebits = false,
appVerificationEnabled = false,
content = networkingLinkSignupPane(),
)
),
Expand All @@ -49,6 +50,7 @@ internal class NetworkingLinkSignupPreviewParameterProvider :
initiallySelectedCountryCode = null,
),
isInstantDebits = false,
appVerificationEnabled = false,
content = networkingLinkSignupPane(),
)
),
Expand All @@ -74,6 +76,7 @@ internal class NetworkingLinkSignupPreviewParameterProvider :
initiallySelectedCountryCode = null,
),
isInstantDebits = false,
appVerificationEnabled = false,
content = networkingLinkSignupPane(),
)
),
Expand All @@ -99,6 +102,7 @@ internal class NetworkingLinkSignupPreviewParameterProvider :
initiallySelectedCountryCode = null,
),
isInstantDebits = true,
appVerificationEnabled = false,
content = linkLoginPane(),
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor(
NetworkingLinkSignupState.Payload(
content = requireNotNull(content),
merchantName = sync.manifest.getBusinessName(),
appVerificationEnabled = sync.manifest.appVerificationEnabled,
emailController = SimpleTextFieldController(
textFieldConfig = EmailConfig(label = R.string.stripe_networking_signup_email_label),
initialValue = sync.manifest.accountholderCustomerEmailAddress ?: prefillDetails?.email,
Expand Down Expand Up @@ -195,15 +196,18 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor(
/**
* @param validEmail valid email, or null if entered email is invalid.
*/
private suspend fun onEmailEntered(
private fun onEmailEntered(
validEmail: String?
) {
setState { copy(validEmail = validEmail) }
if (validEmail != null) {
logger.debug("VALID EMAIL ADDRESS $validEmail.")
searchJob += suspend {
delay(getLookupDelayMs(validEmail))
lookupAccount(validEmail)
lookupAccount(
email = validEmail,
verifiedFlow = stateFlow.value.payload()?.appVerificationEnabled == true
)
}.execute { copy(lookupAccount = if (it.isCancellationError()) Uninitialized else it) }
} else {
setState { copy(lookupAccount = Uninitialized) }
Expand Down Expand Up @@ -342,6 +346,7 @@ internal data class NetworkingLinkSignupState(
data class Payload(
val merchantName: String?,
val emailController: SimpleTextFieldController,
val appVerificationEnabled: Boolean,
val phoneController: PhoneNumberController,
val isInstantDebits: Boolean,
val content: Content,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ internal class NetworkingLinkVerificationViewModel @AssistedInject constructor(
return InitData(
businessName = manifest.businessName,
emailAddress = requireNotNull(email),
appVerificationEnabled = manifest.appVerificationEnabled,
initialInstitution = manifest.initialInstitution,
)
}
Expand All @@ -108,6 +109,7 @@ internal class NetworkingLinkVerificationViewModel @AssistedInject constructor(
) {
lookupConsumerAndStartVerification(
email = initData.emailAddress,
appVerificationEnabled = initData.appVerificationEnabled,
businessName = initData.businessName,
verificationType = VerificationType.SMS,
onConsumerNotFound = {
Expand Down Expand Up @@ -228,6 +230,7 @@ internal class NetworkingLinkVerificationViewModel @AssistedInject constructor(
private data class InitData(
val businessName: String?,
val emailAddress: String,
val appVerificationEnabled: Boolean,
val initialInstitution: FinancialConnectionsInstitution?,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ internal data class FinancialConnectionsSessionManifest(
@SerialName(value = "institution_search_disabled")
val institutionSearchDisabled: Boolean,

@SerialName(value = "app_verification_enabled")
val appVerificationEnabled: Boolean,

@SerialName(value = "livemode")
val livemode: Boolean,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.
import com.stripe.android.financialconnections.exception.CustomManualEntryRequiredError
import com.stripe.android.financialconnections.exception.FinancialConnectionsError
import com.stripe.android.financialconnections.exception.UnclassifiedError
import com.stripe.android.financialconnections.features.error.isAttestationError
import com.stripe.android.financialconnections.features.manualentry.isCustomManualEntryError
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled
Expand Down Expand Up @@ -307,9 +308,15 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
if (state.completed) {
return@launch
}

setState { copy(completed = true) }

if (closeAuthFlowError?.isAttestationError() == true) {
// Attestation error is a special case where we need to close the native flow
// and continue with the AuthFlow on a web browser.
finishWithResult(Failed(error = closeAuthFlowError))
return@launch
}

runCatching {
val completionResult = completeFinancialConnectionsSession(earlyTerminationCause, closeAuthFlowError)
val session = completionResult.session
Expand Down
Loading

0 comments on commit 69327fb

Please sign in to comment.