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 Dec 24, 2024
1 parent 60ad1f8 commit 8886acf
Show file tree
Hide file tree
Showing 16 changed files with 172 additions and 37 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,10 +3,14 @@ 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 kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject

internal interface HandleError {
Expand All @@ -21,6 +25,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 @@ -42,7 +47,7 @@ internal class RealHandleError @Inject constructor(
extraMessage: String,
error: Throwable,
pane: FinancialConnectionsSessionManifest.Pane,
displayErrorScreen: Boolean
displayErrorScreen: Boolean,
) {
analyticsTracker.logError(
extraMessage = extraMessage,
Expand All @@ -51,8 +56,14 @@ internal class RealHandleError @Inject constructor(
pane = pane
)

// Navigate to error screen
if (displayErrorScreen) {
if (error.isAttestationError()) {
GlobalScope.launch {
nativeAuthFlowCoordinator().emit(
value = 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
Expand Up @@ -34,7 +34,6 @@ import com.stripe.android.financialconnections.utils.Experiment.CONNECTIONS_CONS
import com.stripe.android.financialconnections.utils.error
import com.stripe.android.financialconnections.utils.experimentAssignment
import com.stripe.android.financialconnections.utils.trackExposure
import com.stripe.attestation.IntegrityRequestManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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 @@ -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,8 @@ internal data class FinancialConnectionsSessionManifest(
@SerialName(value = "institution_search_disabled")
val institutionSearchDisabled: Boolean,

val appVerificationEnabled: Boolean = true,

@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 8886acf

Please sign in to comment.