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

[FC] Updates lookup call to use mobile endpoint on verified flows #9820

Draft
wants to merge 1 commit into
base: carlosmuvi/12-21-sends_supports_app_verification_to_syncrhonize_when_integrity_available
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,18 @@ 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 {
operator fun invoke(
suspend operator fun invoke(
extraMessage: String,
error: Throwable,
pane: FinancialConnectionsSessionManifest.Pane,
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 @@ -38,11 +43,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 +56,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,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
Loading