From 96e9ba3db138503db6a300fa996c61c6bb602064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mu=C3=B1oz?= <99293320+carlosmuvi-stripe@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:39:10 -0800 Subject: [PATCH] [FC] Updates lookup call to use mobile endpoint on verified flows (#9820) # Summary - Uses new `mobile/lookup` on verified flows. - Pass verification token and app_id, required for this endpoint - Also `session_id` and `email_source`, optional before, are now required. - Non verified flows continue to use `POST consumer_sessions` (does some method renaming to match this) - Modifies `ErrorHandler` to, on attestation failures, close the native AuthFlow and continue on web. # Motivation https://docs.google.com/document/d/1joKz5UZHLVazmecfMHbq6gB6n4wj5u8To6AtqYgq_tc/edit?tab=t.0#heading=h.cz1xkpga7giy # Testing - [x] Added tests - [x] Modified tests - [x] Manually verified --- ...FinancialConnectionsPlaygroundViewModel.kt | 4 +- financial-connections/detekt-baseline.xml | 1 + ...nancialConnectionsSheetRedirectActivity.kt | 20 ++++++- .../FinancialConnectionsSheetViewModel.kt | 42 +++++++++++++++ ...nnectionsSingletonSharedComponentHolder.kt | 9 +++- .../domain/HandleError.kt | 18 +++++-- .../domain/IntegrityVerdictManager.kt | 20 +++++++ .../domain/LookupAccount.kt | 36 ++++++++++--- .../features/error/ErrorExt.kt | 13 +++++ ...LinkLoginWarmupPreviewParameterProvider.kt | 16 ++++-- .../NetworkingLinkLoginWarmupViewModel.kt | 16 ++++-- ...rkingLinkSignupPreviewParameterProvider.kt | 12 +++++ .../NetworkingLinkSignupViewModel.kt | 21 +++++++- .../FinancialConnectionsSessionManifest.kt | 3 ++ ...inancialConnectionsSheetNativeViewModel.kt | 9 +++- ...ialConnectionsConsumerSessionRepository.kt | 52 +++++++++++++------ .../financialconnections/ApiKeyFixtures.kt | 1 + .../FinancialConnectionsSheetViewModelTest.kt | 1 + .../NetworkingLinkLoginWarmupViewModelTest.kt | 11 +++- .../NetworkingLinkSignupViewModelTest.kt | 24 ++++----- ...ctionsConsumerSessionRepositoryImplTest.kt | 2 +- .../com/stripe/android/model/EmailSource.kt | 12 +++++ .../android/repository/ConsumersApiService.kt | 48 +++++++++++++++++ 23 files changed, 335 insertions(+), 56 deletions(-) create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IntegrityVerdictManager.kt create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorExt.kt create mode 100644 payments-model/src/main/java/com/stripe/android/model/EmailSource.kt diff --git a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt index a63255e0bfb..77dda6e7678 100644 --- a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt +++ b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundViewModel.kt @@ -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 { diff --git a/financial-connections/detekt-baseline.xml b/financial-connections/detekt-baseline.xml index 9a70a986a90..2ec88779b7f 100644 --- a/financial-connections/detekt-baseline.xml +++ b/financial-connections/detekt-baseline.xml @@ -8,6 +8,7 @@ LongMethod:AccountItem.kt$@Composable @Preview internal fun AccountItemPreview() 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) ) LongMethod:FinancialConnectionsSheetNativeActivity.kt$FinancialConnectionsSheetNativeActivity$@Composable fun NavHost( initialPane: Pane, testMode: Boolean, ) + LongMethod:FinancialConnectionsSheetNativeViewModel.kt$FinancialConnectionsSheetNativeViewModel$private fun closeAuthFlow( earlyTerminationCause: EarlyTerminationCause? = null, closeAuthFlowError: Throwable? = null ) LongMethod:InstitutionPickerScreen.kt$private fun LazyListScope.searchResults( isInputEmpty: Boolean, payload: Payload, selectedInstitutionId: String?, onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit, institutions: Async<InstitutionResponse>, onManualEntryClick: () -> Unit, onSearchMoreClick: () -> Unit ) LongMethod:LinkAccountPickerPreviewParameterProvider.kt$LinkAccountPickerPreviewParameterProvider$private fun partnerAccountList() LongMethod:NetworkingSaveToLinkVerificationScreen.kt$@Composable private fun NetworkingSaveToLinkVerificationLoaded( confirmVerificationAsync: Async<Unit>, payload: Payload, onCloseFromErrorClick: (Throwable) -> Unit, onSkipClick: () -> Unit, ) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetRedirectActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetRedirectActivity.kt index bbc0e3a6085..8d41e4e57f9 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetRedirectActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetRedirectActivity.kt @@ -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 /** @@ -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 } @@ -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") } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index 793e885f053..979fcc5c8f5 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -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 @@ -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, @@ -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( @@ -525,6 +533,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( fromNative: Boolean = false, @StringRes finishMessage: Int? = null, ) { + if (result is Failed && result.error.isAttestationError) { + integrityVerdictManager.setVerdictFailed() + switchToWebFlow() + return + } eventReporter.onResult(state.initialArgs.configuration, result) // Native emits its own events before finishing. if (fromNative.not()) { @@ -540,6 +553,35 @@ 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, + ) + + if (hostedAuthUrl != null) { + setState { + copy( + manifest = manifest, + // Use intermediate state to prevent the flow from closing in [onResume]. + webAuthFlowStatus = AuthFlowStatus.INTERMEDIATE_DEEPLINK, + viewEffect = OpenAuthFlowWithUrl(hostedAuthUrl) + ) + } + } else { + finishWithResult( + state = stateFlow.value, + result = Failed(IllegalArgumentException("hostedAuthUrl is required to switch to web flow!")) + ) + } + } + } + companion object { val Factory = viewModelFactory { diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt index 20506c22ba2..71c5253b3ae 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSingletonSharedComponentHolder.kt @@ -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 @@ -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 { @@ -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() } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt index ffc56b6c0fd..bf639faf5c8 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt @@ -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 { @@ -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 { @@ -42,7 +47,7 @@ internal class RealHandleError @Inject constructor( extraMessage: String, error: Throwable, pane: FinancialConnectionsSessionManifest.Pane, - displayErrorScreen: Boolean + displayErrorScreen: Boolean, ) { analyticsTracker.logError( extraMessage = extraMessage, @@ -51,8 +56,15 @@ internal class RealHandleError @Inject constructor( pane = pane ) - // Navigate to error screen - if (displayErrorScreen) { + if (error.isAttestationError) { + /* + An attestation error (verification token generation error, unsatisfactory attestation verdict, etc) + Happened mid flow -> Close the native flow with the error (right after we'll open a web browser to finish + the flow) + */ + GlobalScope.launch { nativeAuthFlowCoordinator().emit(CloseWithError(cause = error)) } + } else if (displayErrorScreen) { + // Navigate to error screen errorRepository.set(error) navigationManager.tryNavigateTo(route = Destination.Error(referrer = pane)) } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IntegrityVerdictManager.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IntegrityVerdictManager.kt new file mode 100644 index 00000000000..196d328cb97 --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/IntegrityVerdictManager.kt @@ -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 + } +} diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/LookupAccount.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/LookupAccount.kt index 1b14c57fa9e..3562b69833a 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/LookupAccount.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/LookupAccount.kt @@ -1,21 +1,43 @@ 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, + sessionId: String + ): ConsumerSessionLookup { + return if (verifiedFlow) { + requireNotNull( + consumerSessionRepository.mobileLookupConsumerSession( + email = email.lowercase().trim(), + emailSource = emailSource, + verificationToken = integrityRequestManager.requestToken().getOrThrow(), + appId = application.packageName, + sessionId = sessionId + ) + ) + } else { + requireNotNull( + consumerSessionRepository.postConsumerSession( + email = email.lowercase().trim(), + clientSecret = configuration.financialConnectionsSessionClientSecret + ) + ) + } + } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorExt.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorExt.kt new file mode 100644 index 00000000000..795c7c3eafd --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorExt.kt @@ -0,0 +1,13 @@ +package com.stripe.android.financialconnections.features.error + +import com.stripe.android.core.exception.APIException +import com.stripe.attestation.AttestationError + +internal val Throwable.isAttestationError: Boolean + get() = 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 + } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt index 74384f33fd8..7b0f1c2e8aa 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt @@ -22,7 +22,9 @@ internal class NetworkingLinkLoginWarmupPreviewParameterProvider : NetworkingLinkLoginWarmupState.Payload( merchantName = "Test", redactedEmail = "emai•••@test.com", - email = "email@test.com" + email = "email@test.com", + verifiedFlow = false, + sessionId = "sessionId" ) ), disableNetworkingAsync = Uninitialized, @@ -46,7 +48,9 @@ internal class NetworkingLinkLoginWarmupPreviewParameterProvider : NetworkingLinkLoginWarmupState.Payload( merchantName = "Test", redactedEmail = "emai•••@test.com", - email = "email@test.com" + email = "email@test.com", + verifiedFlow = false, + sessionId = "sessionId" ) ), disableNetworkingAsync = Fail(Exception("Error")), @@ -58,7 +62,9 @@ internal class NetworkingLinkLoginWarmupPreviewParameterProvider : NetworkingLinkLoginWarmupState.Payload( merchantName = "Test", redactedEmail = "emai•••@test.com", - email = "email@test.com" + email = "email@test.com", + verifiedFlow = false, + sessionId = "sessionId" ) ), disableNetworkingAsync = Loading(), @@ -70,7 +76,9 @@ internal class NetworkingLinkLoginWarmupPreviewParameterProvider : NetworkingLinkLoginWarmupState.Payload( merchantName = "Test", redactedEmail = "emai•••@test.com", - email = "email@test.com" + email = "email@test.com", + verifiedFlow = false, + sessionId = "sessionId" ) ), disableNetworkingAsync = Uninitialized, diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt index e26f25f388a..ee125f32269 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt @@ -28,6 +28,7 @@ import com.stripe.android.financialconnections.presentation.Async import com.stripe.android.financialconnections.presentation.Async.Uninitialized import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeState import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel +import com.stripe.android.model.EmailSource import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -51,7 +52,9 @@ internal class NetworkingLinkLoginWarmupViewModel @AssistedInject constructor( NetworkingLinkLoginWarmupState.Payload( merchantName = manifest.getBusinessName(), redactedEmail = requireNotNull(manifest.getRedactedEmail()), - email = requireNotNull(manifest.accountholderCustomerEmailAddress) + email = requireNotNull(manifest.accountholderCustomerEmailAddress), + sessionId = manifest.id, + verifiedFlow = manifest.appVerificationEnabled ) }.execute { copy(payload = it) } } @@ -91,7 +94,12 @@ internal class NetworkingLinkLoginWarmupViewModel @AssistedInject constructor( suspend { eventTracker.track(Click("click.continue", PANE)) // Trigger a lookup call to ensure we cache a consumer session for posterior verification. - lookupAccount(payload.email) + lookupAccount( + email = payload.email, + emailSource = EmailSource.CUSTOMER_OBJECT, + sessionId = payload.sessionId, + verifiedFlow = payload.verifiedFlow + ) navigationManager.tryNavigateTo(Destination.NetworkingLinkVerification(referrer = PANE)) }.execute { copy(continueAsync = it) @@ -193,6 +201,8 @@ internal data class NetworkingLinkLoginWarmupState( data class Payload( val merchantName: String?, val email: String, - val redactedEmail: String + val redactedEmail: String, + val verifiedFlow: Boolean, + val sessionId: String ) } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt index b15b00e9fa6..5bd680ea953 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt @@ -25,12 +25,15 @@ internal class NetworkingLinkSignupPreviewParameterProvider : NetworkingLinkSignupState.Payload( merchantName = "Test", emailController = EmailConfig.createController(""), + appVerificationEnabled = false, + prefilledEmail = null, phoneController = PhoneNumberController.createPhoneNumberController( initialValue = "", initiallySelectedCountryCode = null, ), isInstantDebits = false, content = networkingLinkSignupPane(), + sessionId = "fcsess_1234", ) ), validEmail = null, @@ -44,12 +47,15 @@ internal class NetworkingLinkSignupPreviewParameterProvider : NetworkingLinkSignupState.Payload( merchantName = "Test", emailController = EmailConfig.createController("valid@email.com"), + appVerificationEnabled = false, + prefilledEmail = null, phoneController = PhoneNumberController.createPhoneNumberController( initialValue = "", initiallySelectedCountryCode = null, ), isInstantDebits = false, content = networkingLinkSignupPane(), + sessionId = "fcsess_1234", ) ), validEmail = "test@test.com", @@ -69,12 +75,15 @@ internal class NetworkingLinkSignupPreviewParameterProvider : NetworkingLinkSignupState.Payload( merchantName = "Test", emailController = EmailConfig.createController("invalid_email.com"), + appVerificationEnabled = false, + prefilledEmail = null, phoneController = PhoneNumberController.createPhoneNumberController( initialValue = "", initiallySelectedCountryCode = null, ), isInstantDebits = false, content = networkingLinkSignupPane(), + sessionId = "fcsess_1234", ) ), validEmail = "test@test.com", @@ -94,12 +103,15 @@ internal class NetworkingLinkSignupPreviewParameterProvider : NetworkingLinkSignupState.Payload( merchantName = "Test", emailController = EmailConfig.createController(initialValue = null), + appVerificationEnabled = false, + prefilledEmail = null, phoneController = PhoneNumberController.createPhoneNumberController( initialValue = "", initiallySelectedCountryCode = null, ), isInstantDebits = true, content = linkLoginPane(), + sessionId = "fcsess_1234", ) ), validEmail = null, diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt index aee86f1c895..e576be5b68b 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt @@ -44,6 +44,8 @@ import com.stripe.android.financialconnections.utils.UriUtils import com.stripe.android.financialconnections.utils.error import com.stripe.android.financialconnections.utils.isCancellationError import com.stripe.android.model.ConsumerSessionLookup +import com.stripe.android.model.EmailSource.CUSTOMER_OBJECT +import com.stripe.android.model.EmailSource.USER_ACTION import com.stripe.android.uicore.elements.EmailConfig import com.stripe.android.uicore.elements.InputController import com.stripe.android.uicore.elements.PhoneNumberController @@ -100,12 +102,18 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor( val prefillDetails = elementsSessionContext?.prefillDetails + val initialEmail = (sync.manifest.accountholderCustomerEmailAddress ?: prefillDetails?.email) + ?.takeIf { it.isNotBlank() } + NetworkingLinkSignupState.Payload( content = requireNotNull(content), merchantName = sync.manifest.getBusinessName(), + sessionId = sync.manifest.id, + appVerificationEnabled = sync.manifest.appVerificationEnabled, + prefilledEmail = initialEmail, emailController = SimpleTextFieldController( textFieldConfig = EmailConfig(label = R.string.stripe_networking_signup_email_label), - initialValue = sync.manifest.accountholderCustomerEmailAddress ?: prefillDetails?.email, + initialValue = initialEmail, showOptionalLabel = false ), phoneController = PhoneNumberController.createPhoneNumberController( @@ -203,7 +211,13 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor( logger.debug("VALID EMAIL ADDRESS $validEmail.") searchJob += suspend { delay(getLookupDelayMs(validEmail)) - lookupAccount(validEmail) + val payload = stateFlow.value.payload() + lookupAccount( + email = validEmail, + emailSource = if (payload?.prefilledEmail == validEmail) CUSTOMER_OBJECT else USER_ACTION, + sessionId = payload?.sessionId ?: "", + verifiedFlow = payload?.appVerificationEnabled == true + ) }.execute { copy(lookupAccount = if (it.isCancellationError()) Uninitialized else it) } } else { setState { copy(lookupAccount = Uninitialized) } @@ -342,9 +356,12 @@ internal data class NetworkingLinkSignupState( data class Payload( val merchantName: String?, val emailController: SimpleTextFieldController, + val appVerificationEnabled: Boolean, + val prefilledEmail: String?, val phoneController: PhoneNumberController, val isInstantDebits: Boolean, val content: Content, + val sessionId: String, ) { val focusEmailField: Boolean diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt index d4605c0aadd..87897989356 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt @@ -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, diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt index 09d5023a004..502a5d7b61d 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt @@ -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 @@ -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 diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt index 6417a14c1e1..2c39342c7db 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepository.kt @@ -16,6 +16,7 @@ import com.stripe.android.model.ConsumerSessionLookup import com.stripe.android.model.ConsumerSessionSignup import com.stripe.android.model.ConsumerSignUpConsentAction.EnteredPhoneNumberClickedSaveToLink import com.stripe.android.model.CustomEmailType +import com.stripe.android.model.EmailSource import com.stripe.android.model.SharePaymentDetails import com.stripe.android.model.UpdateAvailableIncentives import com.stripe.android.model.VerificationType @@ -28,17 +29,25 @@ internal interface FinancialConnectionsConsumerSessionRepository { suspend fun getCachedConsumerSession(): CachedConsumerSession? + suspend fun postConsumerSession( + email: String, + clientSecret: String + ): ConsumerSessionLookup + + suspend fun mobileLookupConsumerSession( + email: String, + emailSource: EmailSource, + verificationToken: String, + sessionId: String, + appId: String + ): ConsumerSessionLookup + suspend fun signUp( email: String, phoneNumber: String, country: String, ): ConsumerSessionSignup - suspend fun lookupConsumerSession( - email: String, - clientSecret: String - ): ConsumerSessionLookup - suspend fun startConsumerVerification( consumerSessionClientSecret: String, connectionsMerchantName: String?, @@ -130,15 +139,6 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl( consumerSessionRepository.provideConsumerSession() } - override suspend fun lookupConsumerSession( - email: String, - clientSecret: String - ): ConsumerSessionLookup = mutex.withLock { - postConsumerSession(email, clientSecret).also { lookup -> - updateCachedConsumerSessionFromLookup(lookup) - } - } - override suspend fun signUp( email: String, phoneNumber: String, @@ -259,14 +259,34 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl( ) } - private suspend fun postConsumerSession( + override suspend fun postConsumerSession( email: String, clientSecret: String ): ConsumerSessionLookup = financialConnectionsConsumersApiService.postConsumerSession( email = email, clientSecret = clientSecret, requestSurface = requestSurface, - ) + ).also { + updateCachedConsumerSessionFromLookup(it) + } + + override suspend fun mobileLookupConsumerSession( + email: String, + emailSource: EmailSource, + verificationToken: String, + sessionId: String, + appId: String + ): ConsumerSessionLookup = consumersApiService.mobileLookupConsumerSession( + email = email, + emailSource = emailSource, + requestSurface = requestSurface, + verificationToken = verificationToken, + appId = appId, + sessionId = sessionId, + requestOptions = provideApiRequestOptions(useConsumerPublishableKey = false), + ).also { + updateCachedConsumerSessionFromLookup(it) + } private fun updateCachedConsumerSession( source: String, diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/ApiKeyFixtures.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/ApiKeyFixtures.kt index 099c096e5cd..cfa45e5d813 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/ApiKeyFixtures.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/ApiKeyFixtures.kt @@ -59,6 +59,7 @@ internal object ApiKeyFixtures { id = "1234", instantVerificationDisabled = true, institutionSearchDisabled = true, + appVerificationEnabled = false, livemode = true, businessName = "businessName", manualEntryUsesMicrodeposits = true, diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index a79cbc7ff12..fa87439353f 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -845,6 +845,7 @@ class FinancialConnectionsSheetViewModelTest { savedStateHandle = SavedStateHandle(), nativeAuthFlowCoordinator = mock(), integrityRequestManager = integrityRequestManager, + integrityVerdictManager = mock(), logger = Logger.noop() ) } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt index a1afc06ea3a..bada03132cb 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt @@ -77,7 +77,14 @@ class NetworkingLinkLoginWarmupViewModelTest { ) ) ) - whenever(lookupAccount(anyOrNull())).thenReturn( + whenever( + lookupAccount( + email = anyOrNull(), + emailSource = anyOrNull(), + verifiedFlow = anyOrNull(), + sessionId = anyOrNull() + ) + ).thenReturn( ConsumerSessionLookup( exists = true, errorMessage = null, @@ -88,7 +95,7 @@ class NetworkingLinkLoginWarmupViewModelTest { val viewModel = buildViewModel(NetworkingLinkLoginWarmupState()) viewModel.onContinueClick() - verify(lookupAccount).invoke(any()) + verify(lookupAccount).invoke(any(), any(), any(), any()) navigationManager.assertNavigatedTo( destination = Destination.NetworkingLinkVerification, pane = Pane.NETWORKING_LINK_LOGIN_WARMUP diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt index 93c4e805ef4..9911116f1dd 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt @@ -96,7 +96,7 @@ class NetworkingLinkSignupViewModelTest { ) ) ) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = false)) val viewModel = buildViewModel(NetworkingLinkSignupState()) val state = viewModel.stateFlow.value @@ -117,7 +117,7 @@ class NetworkingLinkSignupViewModelTest { ) ) ) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = false)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(), @@ -201,7 +201,7 @@ class NetworkingLinkSignupViewModelTest { ) ) whenever(getOrFetchSync().manifest).thenReturn(manifest) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = true)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = true)) val viewModel = buildViewModel(NetworkingLinkSignupState()) @@ -236,7 +236,7 @@ class NetworkingLinkSignupViewModelTest { ) ) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = true)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = true)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(isInstantDebits = true), @@ -274,7 +274,7 @@ class NetworkingLinkSignupViewModelTest { ) ) whenever(getOrFetchSync().manifest).thenReturn(manifest) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = true)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = true)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(), @@ -368,7 +368,7 @@ class NetworkingLinkSignupViewModelTest { ) whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(syncResponse) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = false)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(isInstantDebits = false), @@ -402,7 +402,7 @@ class NetworkingLinkSignupViewModelTest { ) whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(initialSyncResponse) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = false)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(isInstantDebits = true), @@ -436,7 +436,7 @@ class NetworkingLinkSignupViewModelTest { ) whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(syncResponse) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = false)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(isInstantDebits = false), @@ -470,7 +470,7 @@ class NetworkingLinkSignupViewModelTest { ) whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(initialSyncResponse) - whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false)) + whenever(lookupAccount(any(), any(), any(), any())).thenReturn(ConsumerSessionLookup(exists = false)) val viewModel = buildViewModel( state = NetworkingLinkSignupState(isInstantDebits = true), @@ -502,7 +502,7 @@ class NetworkingLinkSignupViewModelTest { val permissionException = PermissionException(stripeError = StripeError()) whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(initialSyncResponse) - whenever(lookupAccount(any())).then { + whenever(lookupAccount(any(), any(), any(), any())).then { throw permissionException } @@ -534,7 +534,7 @@ class NetworkingLinkSignupViewModelTest { val apiException = APIConnectionException() whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(initialSyncResponse) - whenever(lookupAccount(any())).then { + whenever(lookupAccount(any(), any(), any(), any())).then { throw apiException } @@ -566,7 +566,7 @@ class NetworkingLinkSignupViewModelTest { val permissionException = PermissionException(stripeError = StripeError()) whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(initialSyncResponse) - whenever(lookupAccount(any())).then { + whenever(lookupAccount(any(), any(), any(), any())).then { throw permissionException } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt index 9053aceebfc..710d9d8c6c6 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/repository/FinancialConnectionsConsumerSessionRepositoryImplTest.kt @@ -199,7 +199,7 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest { // ensures there's no cached consumer session assertThat(repository.getCachedConsumerSession()).isNull() - val result = repository.lookupConsumerSession(email, clientSecret) + val result = repository.postConsumerSession(email, clientSecret) assertThat(result).isEqualTo(consumerSessionLookup) diff --git a/payments-model/src/main/java/com/stripe/android/model/EmailSource.kt b/payments-model/src/main/java/com/stripe/android/model/EmailSource.kt new file mode 100644 index 00000000000..8f26c8faece --- /dev/null +++ b/payments-model/src/main/java/com/stripe/android/model/EmailSource.kt @@ -0,0 +1,12 @@ +package com.stripe.android.model + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +enum class EmailSource(val backendValue: String) { + // describes a user-entered email + USER_ACTION("user_action"), + + // describes a read-only, merchant-passed email + CUSTOMER_OBJECT("customer_object"), +} diff --git a/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt b/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt index a31291a6993..f8d1825ad16 100644 --- a/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt +++ b/payments-model/src/main/java/com/stripe/android/repository/ConsumersApiService.kt @@ -16,6 +16,7 @@ import com.stripe.android.model.ConsumerSessionLookup import com.stripe.android.model.ConsumerSessionSignup import com.stripe.android.model.ConsumerSignUpConsentAction import com.stripe.android.model.CustomEmailType +import com.stripe.android.model.EmailSource import com.stripe.android.model.IncentiveEligibilitySession import com.stripe.android.model.SharePaymentDetails import com.stripe.android.model.UpdateAvailableIncentives @@ -52,6 +53,16 @@ interface ConsumersApiService { requestOptions: ApiRequest.Options ): ConsumerSessionLookup + suspend fun mobileLookupConsumerSession( + email: String, + emailSource: EmailSource, + requestSurface: String, + verificationToken: String, + appId: String, + requestOptions: ApiRequest.Options, + sessionId: String + ): ConsumerSessionLookup + suspend fun startConsumerVerification( consumerSessionClientSecret: String, locale: Locale, @@ -186,6 +197,37 @@ class ConsumersApiServiceImpl( ) } + /** + * Retrieves the ConsumerSession if the given email is associated with a Link account. + */ + override suspend fun mobileLookupConsumerSession( + email: String, + emailSource: EmailSource, + requestSurface: String, + verificationToken: String, + appId: String, + requestOptions: ApiRequest.Options, + sessionId: String + ): ConsumerSessionLookup { + return executeRequestWithModelJsonParser( + stripeErrorJsonParser = stripeErrorJsonParser, + stripeNetworkClient = stripeNetworkClient, + request = apiRequestFactory.createPost( + mobileConsumerSessionLookupUrl, + requestOptions, + mapOf( + "request_surface" to requestSurface, + "email_address" to email.lowercase(), + "android_verification_token" to verificationToken, + "session_id" to sessionId, + "email_source" to emailSource.backendValue, + "app_id" to appId + ) + ), + responseJsonParser = ConsumerSessionLookupJsonParser() + ) + } + /** * Triggers a verification for the consumer corresponding to the given client secret. */ @@ -364,6 +406,12 @@ class ConsumersApiServiceImpl( internal val consumerSessionLookupUrl: String = getApiUrl("consumers/sessions/lookup") + /** + * @return `https://api.stripe.com/v1/consumers/mobile/sessions/lookup` + */ + internal val mobileConsumerSessionLookupUrl: String = + getApiUrl("consumers/mobile/sessions/lookup") + /** * @return `https://api.stripe.com/v1/consumers/sessions/start_verification` */