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`
*/