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 10, 2025
1 parent ad9e8a8 commit 7d91d37
Show file tree
Hide file tree
Showing 26 changed files with 338 additions and 65 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 @@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.stripe.android.financialconnections.debug.DebugConfiguration
import com.stripe.android.financialconnections.di.FinancialConnectionsSingletonSharedComponentHolder
import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity

/**
Expand All @@ -20,7 +21,9 @@ class FinancialConnectionsSheetRedirectActivity : AppCompatActivity() {
*/
intent.data
?.let { uri ->
val uriWithDebugConfiguration = uri.overrideWithDebugConfiguration()
val uriWithDebugConfiguration = uri
.overrideWithDebugConfiguration()
.overrideIfIntegrityFailed()
uriWithDebugConfiguration.toIntent()
?.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
?.also { it.data = uriWithDebugConfiguration }
Expand Down Expand Up @@ -67,6 +70,21 @@ class FinancialConnectionsSheetRedirectActivity : AppCompatActivity() {
null -> this
}

/**
* When an integrity verdict fails, clients will switch to the web flow locally but backend will still
* consider the flow native. This checks the local verdict state and overrides native deep links to web.
*/
private fun Uri.overrideIfIntegrityFailed(): Uri =
when (
FinancialConnectionsSingletonSharedComponentHolder
.getComponent(application)
.integrityVerdictManager()
.verdictFailed()
) {
true -> Uri.parse(toString().replace(HOST_NATIVE_LINK_ACCOUNTS, HOST_LINK_ACCOUNTS))
else -> this
}

private fun Uri.isFinancialConnectionsScheme(): Boolean {
return (this.scheme == "stripe-auth" || this.scheme == "stripe")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsS
import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSessionForToken
import com.stripe.android.financialconnections.domain.GetOrFetchSync
import com.stripe.android.financialconnections.domain.GetOrFetchSync.RefetchCondition.Always
import com.stripe.android.financialconnections.domain.IntegrityVerdictManager
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 @@ -71,6 +73,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val getOrFetchSync: GetOrFetchSync,
private val integrityRequestManager: IntegrityRequestManager,
private val integrityVerdictManager: IntegrityVerdictManager,
private val fetchFinancialConnectionsSession: FetchFinancialConnectionsSession,
private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken,
private val logger: Logger,
Expand Down Expand Up @@ -130,6 +133,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
}

private suspend fun prepareStandardRequestManager(): Boolean {
// If previously within the application session an integrity check failed
// do not initialize the request manager and directly launch the web flow.
if (integrityVerdictManager.verdictFailed()) {
return false
}
val result = integrityRequestManager.prepare()
result.onFailure {
analyticsTracker.track(
Expand Down Expand Up @@ -523,6 +531,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 @@ -538,6 +550,28 @@ 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() {
integrityVerdictManager.setVerdictFailed()
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 @@ -2,6 +2,7 @@ package com.stripe.android.financialconnections.di

import android.app.Application
import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.domain.IntegrityVerdictManager
import com.stripe.attestation.BuildConfig
import com.stripe.attestation.IntegrityRequestManager
import com.stripe.attestation.IntegrityStandardRequestManager
Expand Down Expand Up @@ -38,7 +39,9 @@ internal object FinancialConnectionsSingletonSharedComponentHolder {
@Component(modules = [FinancialConnectionsSingletonSharedModule::class])
internal interface FinancialConnectionsSingletonSharedComponent {

fun providesIntegrityRequestManager(): IntegrityRequestManager
fun integrityRequestManager(): IntegrityRequestManager

fun integrityVerdictManager(): IntegrityVerdictManager

@Component.Factory
interface Factory {
Expand All @@ -58,4 +61,8 @@ internal class FinancialConnectionsSingletonSharedModule {
logError = { message, error -> Logger.getInstance(BuildConfig.DEBUG).error(message, error) },
factory = RealStandardIntegrityManagerFactory(context)
)

@Provides
@Singleton
fun providesIntegrityVerdictManager(): IntegrityVerdictManager = IntegrityVerdictManager()
}
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
@@ -0,0 +1,20 @@
package com.stripe.android.financialconnections.domain

/**
* Manages the verdict of the integrity check. If the verdict is failed, the user will be switched to web flow.
*
* The scope of this is the application session. Subsequent launches of the AuthFlow within the hosting app after
* a verdict failure will directly launch the web flow.
*/
internal class IntegrityVerdictManager {

private var verdictFailed: Boolean = false

fun setVerdictFailed() {
verdictFailed = true
}

fun verdictFailed(): Boolean {
return verdictFailed
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
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.android.model.EmailSource
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,
emailSource: EmailSource,
verifiedFlow: Boolean
): ConsumerSessionLookup {
return if (verifiedFlow) {
requireNotNull(
consumerSessionRepository.mobileLookupConsumerSession(
email = email.lowercase().trim(),
emailSource = emailSource,
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
@@ -1,6 +1,7 @@
package com.stripe.android.financialconnections.domain

import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.EmailSource
import com.stripe.android.model.VerificationType
import javax.inject.Inject

Expand All @@ -20,15 +21,23 @@ internal class LookupConsumerAndStartVerification @Inject constructor(
*/
suspend operator fun invoke(
email: String,
emailSource: EmailSource,
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 = email,
emailSource = emailSource,
verifiedFlow = appVerificationEnabled
)
}
.onSuccess { session ->
if (session.exists) {
onStartVerification()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.stripe.android.financialconnections.features.error

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

internal fun Throwable.isAttestationError(): Boolean = when (this) {
// Stripe backend could not verify the intregrity of the request
is APIException -> stripeError?.code == "link_failed_to_attest_request"
// Interaction with Integrity API to generate tokens resulted in a failure
is AttestationError -> true
else -> false
}
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
Loading

0 comments on commit 7d91d37

Please sign in to comment.