diff --git a/financial-connections/detekt-baseline.xml b/financial-connections/detekt-baseline.xml
index 2ec88779b7f..d0dfef94ba7 100644
--- a/financial-connections/detekt-baseline.xml
+++ b/financial-connections/detekt-baseline.xml
@@ -31,6 +31,8 @@
SwallowedException:PollAttachPaymentAccount.kt$PollAttachPaymentAccount$e: StripeException
SwallowedException:PollAuthorizationSessionAccounts.kt$PollAuthorizationSessionAccounts$e: StripeException
SwallowedException:PostAuthorizationSession.kt$PostAuthorizationSession$e: StripeException
+ TooManyFunctions:FinancialConnectionsConsumerSessionRepository.kt$FinancialConnectionsConsumerSessionRepository
+ TooManyFunctions:FinancialConnectionsConsumerSessionRepository.kt$FinancialConnectionsConsumerSessionRepositoryImpl : FinancialConnectionsConsumerSessionRepository
TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepository
TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepositoryImpl : FinancialConnectionsManifestRepository
TooManyFunctions:FinancialConnectionsSheetNativeViewModel.kt$FinancialConnectionsSheetNativeViewModel : FinancialConnectionsViewModelTopAppBarHost
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandler.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandler.kt
index 1adbac0b4a8..dfcf0df5aa5 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandler.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandler.kt
@@ -4,6 +4,7 @@ import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
import com.stripe.android.financialconnections.analytics.logError
+import com.stripe.android.financialconnections.di.APPLICATION_ID
import com.stripe.android.financialconnections.domain.AttachConsumerToLinkAccountSession
import com.stripe.android.financialconnections.domain.GetCachedAccounts
import com.stripe.android.financialconnections.domain.GetOrFetchSync
@@ -19,12 +20,14 @@ import com.stripe.android.financialconnections.navigation.Destination.Networking
import com.stripe.android.financialconnections.navigation.Destination.Success
import com.stripe.android.financialconnections.navigation.NavigationManager
import com.stripe.android.financialconnections.repository.FinancialConnectionsConsumerSessionRepository
+import com.stripe.attestation.IntegrityRequestManager
import javax.inject.Inject
+import javax.inject.Named
internal interface LinkSignupHandler {
suspend fun performSignup(
- state: NetworkingLinkSignupState,
+ state: NetworkingLinkSignupState
): Pane
fun handleSignupFailure(
@@ -37,8 +40,10 @@ internal interface LinkSignupHandler {
internal class LinkSignupHandlerForInstantDebits @Inject constructor(
private val consumerRepository: FinancialConnectionsConsumerSessionRepository,
private val attachConsumerToLinkAccountSession: AttachConsumerToLinkAccountSession,
+ private val integrityRequestManager: IntegrityRequestManager,
private val getOrFetchSync: GetOrFetchSync,
private val navigationManager: NavigationManager,
+ @Named(APPLICATION_ID) private val applicationId: String,
private val handleError: HandleError,
) : LinkSignupHandler {
@@ -47,18 +52,30 @@ internal class LinkSignupHandlerForInstantDebits @Inject constructor(
): Pane {
val phoneController = state.payload()!!.phoneController
- val signup = consumerRepository.signUp(
- email = state.validEmail!!,
- phoneNumber = state.validPhone!!,
- country = phoneController.getCountryCode(),
- )
+ val manifest = getOrFetchSync().manifest
+ val signup = if (manifest.appVerificationEnabled) {
+ val token = integrityRequestManager.requestToken().getOrThrow()
+ consumerRepository.mobileSignUp(
+ email = state.validEmail!!,
+ phoneNumber = state.validPhone!!,
+ country = phoneController.getCountryCode(),
+ verificationToken = token,
+ appId = applicationId
+ )
+ } else {
+ consumerRepository.signUp(
+ email = state.validEmail!!,
+ phoneNumber = state.validPhone!!,
+ country = phoneController.getCountryCode(),
+ )
+ }
attachConsumerToLinkAccountSession(
consumerSessionClientSecret = signup.consumerSession.clientSecret,
)
- val manifest = getOrFetchSync(refetchCondition = Always).manifest
- return manifest.nextPane
+ // Refresh manifest to get the next pane
+ return getOrFetchSync(refetchCondition = Always).manifest.nextPane
}
override fun navigateToVerification() {
@@ -76,11 +93,14 @@ internal class LinkSignupHandlerForInstantDebits @Inject constructor(
}
internal class LinkSignupHandlerForNetworking @Inject constructor(
+ private val consumerRepository: FinancialConnectionsConsumerSessionRepository,
private val getOrFetchSync: GetOrFetchSync,
private val getCachedAccounts: GetCachedAccounts,
+ private val integrityRequestManager: IntegrityRequestManager,
private val saveAccountToLink: SaveAccountToLink,
private val eventTracker: FinancialConnectionsAnalyticsTracker,
private val navigationManager: NavigationManager,
+ @Named(APPLICATION_ID) private val applicationId: String,
private val logger: Logger,
) : LinkSignupHandler {
@@ -92,14 +112,36 @@ internal class LinkSignupHandlerForNetworking @Inject constructor(
val manifest = getOrFetchSync().manifest
val phoneController = state.payload()!!.phoneController
require(state.valid) { "Form invalid! ${state.validEmail} ${state.validPhone}" }
- saveAccountToLink.new(
- country = phoneController.getCountryCode(),
- email = state.validEmail!!,
- phoneNumber = state.validPhone!!,
- selectedAccounts = selectedAccounts,
- shouldPollAccountNumbers = manifest.isDataFlow,
- )
+ if (manifest.appVerificationEnabled) {
+ // ** New signup flow on verified flows: 2 requests **
+ // 1. Mobile signup endpoint providing email + phone number
+ // 2. Separately call SaveAccountToLink with the newly created account.
+ val token = integrityRequestManager.requestToken().getOrThrow()
+ val signup = consumerRepository.mobileSignUp(
+ email = state.validEmail!!,
+ phoneNumber = state.validPhone!!,
+ country = phoneController.getCountryCode(),
+ verificationToken = token,
+ appId = applicationId,
+ )
+ saveAccountToLink.existing(
+ consumerSessionClientSecret = signup.consumerSession.clientSecret,
+ selectedAccounts = selectedAccounts,
+ shouldPollAccountNumbers = manifest.isDataFlow,
+ )
+ } else {
+ // ** Legacy signup endpoint on unverified flows: 1 request **
+ // SaveAccountToLink endpoint Signs up when providing email + phone number
+ // **and** saves accounts to Link in the same request.
+ saveAccountToLink.new(
+ country = phoneController.getCountryCode(),
+ email = state.validEmail!!,
+ phoneNumber = state.validPhone!!,
+ selectedAccounts = selectedAccounts,
+ shouldPollAccountNumbers = manifest.isDataFlow,
+ )
+ }
return Pane.SUCCESS
}
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 2c39342c7db..9149dfb1a7d 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
@@ -2,6 +2,7 @@ package com.stripe.android.financialconnections.repository
import com.stripe.android.core.Logger
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
+import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext
import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext.BillingDetails
import com.stripe.android.financialconnections.domain.IsLinkWithStripe
@@ -18,6 +19,7 @@ import com.stripe.android.model.ConsumerSignUpConsentAction.EnteredPhoneNumberCl
import com.stripe.android.model.CustomEmailType
import com.stripe.android.model.EmailSource
import com.stripe.android.model.SharePaymentDetails
+import com.stripe.android.model.SignUpParams
import com.stripe.android.model.UpdateAvailableIncentives
import com.stripe.android.model.VerificationType
import com.stripe.android.repository.ConsumersApiService
@@ -48,6 +50,14 @@ internal interface FinancialConnectionsConsumerSessionRepository {
country: String,
): ConsumerSessionSignup
+ suspend fun mobileSignUp(
+ email: String,
+ phoneNumber: String,
+ country: String,
+ verificationToken: String,
+ appId: String
+ ): ConsumerSessionSignup
+
suspend fun startConsumerVerification(
consumerSessionClientSecret: String,
connectionsMerchantName: String?,
@@ -140,11 +150,40 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl(
}
override suspend fun signUp(
+ email: String,
+ phoneNumber: String,
+ country: String
+ ): ConsumerSessionSignup = performSignUp(
+ email = email,
+ phoneNumber = phoneNumber,
+ country = country,
+ signupCall = consumersApiService::signUp
+ )
+
+ override suspend fun mobileSignUp(
email: String,
phoneNumber: String,
country: String,
+ verificationToken: String,
+ appId: String
+ ): ConsumerSessionSignup = performSignUp(
+ email = email,
+ phoneNumber = phoneNumber,
+ country = country,
+ verificationToken = verificationToken,
+ appId = appId,
+ signupCall = consumersApiService::mobileSignUp
+ )
+
+ private suspend fun performSignUp(
+ email: String,
+ phoneNumber: String,
+ country: String,
+ verificationToken: String? = null,
+ appId: String? = null,
+ signupCall: suspend (SignUpParams, ApiRequest.Options) -> Result
): ConsumerSessionSignup = mutex.withLock {
- consumersApiService.signUp(
+ val signUpParams = SignUpParams(
email = email,
phoneNumber = phoneNumber,
country = country,
@@ -153,12 +192,16 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl(
amount = elementsSessionContext?.amount,
currency = elementsSessionContext?.currency,
incentiveEligibilitySession = elementsSessionContext?.incentiveEligibilitySession,
- requestOptions = provideApiRequestOptions(useConsumerPublishableKey = false),
requestSurface = requestSurface,
consentAction = EnteredPhoneNumberClickedSaveToLink,
- ).onSuccess { signup ->
- updateCachedConsumerSessionFromSignup(signup)
- }.getOrThrow()
+ verificationToken = verificationToken,
+ appId = appId
+ )
+
+ // Make the API call using the given lambda function
+ signupCall(signUpParams, provideApiRequestOptions(useConsumerPublishableKey = false))
+ .onSuccess { signup -> updateCachedConsumerSessionFromSignup(signup) }
+ .getOrThrow()
}
override suspend fun startConsumerVerification(
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandlerForInstantDebitsTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandlerForInstantDebitsTest.kt
new file mode 100644
index 00000000000..fba510aa9af
--- /dev/null
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandlerForInstantDebitsTest.kt
@@ -0,0 +1,107 @@
+package com.stripe.android.financialconnections.features.networkinglinksignup
+
+import com.stripe.android.financialconnections.ApiKeyFixtures.consumerSessionSignup
+import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest
+import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse
+import com.stripe.android.financialconnections.domain.AttachConsumerToLinkAccountSession
+import com.stripe.android.financialconnections.domain.GetOrFetchSync
+import com.stripe.android.financialconnections.domain.HandleError
+import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.Payload
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.presentation.Async
+import com.stripe.android.financialconnections.repository.FinancialConnectionsConsumerSessionRepository
+import com.stripe.android.financialconnections.utils.TestIntegrityRequestManager
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class LinkSignupHandlerForInstantDebitsTest {
+
+ private lateinit var handler: LinkSignupHandlerForInstantDebits
+ private val consumerRepository = mock()
+ private val getOrFetchSync = mock()
+ private val attachConsumerToLinkAccountSession = mock()
+ private val integrityRequestManager = TestIntegrityRequestManager()
+ private val navigationManager = mock()
+ private val handleError = mock()
+
+ @Before
+ fun setUp() {
+ handler = LinkSignupHandlerForInstantDebits(
+ consumerRepository,
+ attachConsumerToLinkAccountSession,
+ integrityRequestManager,
+ getOrFetchSync,
+ navigationManager,
+ "applicationId",
+ handleError
+ )
+ }
+
+ private val validPayload = Payload(
+ merchantName = "Mock Merchant",
+ emailController = mock(),
+ prefilledEmail = "test@stripe.com",
+ sessionId = "fcsess_1234",
+ appVerificationEnabled = false,
+ phoneController = mock {
+ whenever(it.getCountryCode()).thenReturn("US")
+ },
+ isInstantDebits = true,
+ content = mock()
+ )
+
+ @Test
+ fun `performSignup should navigate to next pane on success`() = runTest {
+ val testState = NetworkingLinkSignupState(
+ validEmail = "test@example.com",
+ validPhone = "+123456789",
+ isInstantDebits = true,
+ payload = Async.Success(validPayload)
+ )
+
+ val expectedPane = Pane.INSTITUTION_PICKER
+ whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(
+ syncResponse(
+ sessionManifest().copy(
+ nextPane = expectedPane,
+ appVerificationEnabled = true
+ )
+ )
+ )
+ whenever(
+ consumerRepository.mobileSignUp(
+ email = any(),
+ phoneNumber = any(),
+ country = any(),
+ verificationToken = any(),
+ appId = any()
+ )
+ ).thenReturn(consumerSessionSignup())
+
+ val result = handler.performSignup(testState)
+
+ verify(attachConsumerToLinkAccountSession).invoke(any())
+ assertEquals(expectedPane, result)
+ }
+
+ @Test
+ fun `handleSignupFailure should call handleError with correct parameters`() = runTest {
+ val error = RuntimeException("Test Error")
+ handler.handleSignupFailure(error)
+
+ verify(handleError).invoke(
+ extraMessage = "Error creating a Link account",
+ error = error,
+ pane = Pane.LINK_LOGIN,
+ displayErrorScreen = true
+ )
+ }
+}
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandlerForNetworkingTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandlerForNetworkingTest.kt
new file mode 100644
index 00000000000..df17b4d1cd8
--- /dev/null
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/LinkSignupHandlerForNetworkingTest.kt
@@ -0,0 +1,157 @@
+package com.stripe.android.financialconnections.features.networkinglinksignup
+
+import com.stripe.android.core.Logger
+import com.stripe.android.financialconnections.ApiKeyFixtures.consumerSessionSignup
+import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest
+import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.analytics.logError
+import com.stripe.android.financialconnections.domain.CachedPartnerAccount
+import com.stripe.android.financialconnections.domain.GetCachedAccounts
+import com.stripe.android.financialconnections.domain.GetOrFetchSync
+import com.stripe.android.financialconnections.domain.SaveAccountToLink
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.presentation.Async
+import com.stripe.android.financialconnections.repository.FinancialConnectionsConsumerSessionRepository
+import com.stripe.android.financialconnections.utils.TestIntegrityRequestManager
+import com.stripe.android.financialconnections.utils.TestNavigationManager
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoInteractions
+import org.mockito.kotlin.whenever
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class LinkSignupHandlerForNetworkingTest {
+
+ private lateinit var handler: LinkSignupHandlerForNetworking
+ private val consumerRepository = mock()
+ private val getOrFetchSync = mock()
+ private val getCachedAccounts = mock()
+ private val saveAccountToLink = mock()
+ private val integrityRequestManager = TestIntegrityRequestManager()
+ private val navigationManager = TestNavigationManager()
+ private val eventTracker = mock()
+ private val logger = mock()
+
+ @Before
+ fun setUp() {
+ handler = LinkSignupHandlerForNetworking(
+ consumerRepository,
+ getOrFetchSync,
+ getCachedAccounts,
+ integrityRequestManager,
+ saveAccountToLink,
+ eventTracker,
+ navigationManager,
+ "applicationId",
+ logger
+ )
+ }
+
+ private val validPayload = NetworkingLinkSignupState.Payload(
+ merchantName = "Mock Merchant",
+ emailController = mock(),
+ prefilledEmail = "test@networking.com",
+ sessionId = "fcsess_5678",
+ appVerificationEnabled = true,
+ phoneController = mock {
+ whenever(it.getCountryCode()).thenReturn("US")
+ },
+ isInstantDebits = false,
+ content = mock()
+ )
+
+ @Test
+ fun `performSignup on verified flows should save account and return success pane on success`() = runTest {
+ val testState = NetworkingLinkSignupState(
+ validEmail = "test@networking.com",
+ validPhone = "+1987654321",
+ isInstantDebits = false,
+ payload = Async.Success(validPayload)
+ )
+
+ val expectedPane = Pane.SUCCESS
+
+ whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(
+ syncResponse(sessionManifest().copy(appVerificationEnabled = true))
+ )
+ whenever(getCachedAccounts()).thenReturn(listOf(mock())) // Mock a list of cached accounts
+ whenever(
+ consumerRepository.mobileSignUp(
+ email = any(),
+ phoneNumber = any(),
+ country = any(),
+ verificationToken = any(),
+ appId = any()
+ )
+ ).thenReturn(consumerSessionSignup())
+
+ val result = handler.performSignup(testState)
+
+ verify(consumerRepository).mobileSignUp(
+ email = eq("test@networking.com"),
+ phoneNumber = eq("+1987654321"),
+ country = eq("US"),
+ verificationToken = eq(integrityRequestManager.requestTokenResult.getOrThrow()),
+ appId = eq("applicationId")
+ )
+ verify(saveAccountToLink).existing(any(), any(), any())
+ assertEquals(expectedPane, result)
+ }
+
+ @Test
+ fun `handleSignupFailure should log error and navigate to Success pane`() = runTest {
+ val error = RuntimeException("Test Error")
+
+ handler.handleSignupFailure(error)
+
+ verify(eventTracker).logError(
+ extraMessage = "Error saving account to Link",
+ error = error,
+ logger = logger,
+ pane = Pane.NETWORKING_LINK_SIGNUP_PANE
+ )
+
+ navigationManager.assertNavigatedTo(
+ destination = Destination.Success,
+ pane = Pane.NETWORKING_LINK_SIGNUP_PANE
+ )
+ }
+
+ @Test
+ fun `performSignup should use legacy signup flow when verification is false`() = runTest {
+ val testState = NetworkingLinkSignupState(
+ validEmail = "legacy@example.com",
+ validPhone = "+1987654321",
+ isInstantDebits = false,
+ payload = Async.Success(validPayload)
+ )
+
+ whenever(getOrFetchSync(anyOrNull(), anyOrNull())).thenReturn(
+ syncResponse(sessionManifest().copy(appVerificationEnabled = false))
+ )
+ val cachedAccounts = listOf(mock()) // Ensure this matches what you return
+ whenever(getCachedAccounts()).thenReturn(cachedAccounts)
+
+ val expectedPane = Pane.SUCCESS
+
+ val result = handler.performSignup(testState)
+
+ verifyNoInteractions(consumerRepository)
+ verify(saveAccountToLink).new(
+ email = eq("legacy@example.com"),
+ phoneNumber = eq("+1987654321"),
+ selectedAccounts = eq(cachedAccounts),
+ country = eq("US"),
+ shouldPollAccountNumbers = eq(true)
+ )
+ assertEquals(expectedPane, result)
+ }
+}
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 9911116f1dd..1cd3d15dd8a 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
@@ -619,10 +619,13 @@ class NetworkingLinkSignupViewModelTest {
return LinkSignupHandlerForNetworking(
getOrFetchSync = getOrFetchSync,
+ consumerRepository = consumerSessionRepository(failOnSignup = false),
getCachedAccounts = getCachedAccounts,
saveAccountToLink = saveAccountToLink,
eventTracker = eventTracker,
navigationManager = navigationManager,
+ integrityRequestManager = mock(),
+ applicationId = "test",
logger = Logger.noop(),
)
}
@@ -646,15 +649,7 @@ class NetworkingLinkSignupViewModelTest {
)
}
- val consumerRepository = mock {
- if (failOnSignup) {
- onBlocking { signUp(any(), any(), any()) } doAnswer {
- throw APIConnectionException()
- }
- } else {
- onBlocking { signUp(any(), any(), any()) } doReturn consumerSessionSignup()
- }
- }
+ val consumerRepository = consumerSessionRepository(failOnSignup)
return LinkSignupHandlerForInstantDebits(
getOrFetchSync = getOrFetchSync,
@@ -664,9 +659,28 @@ class NetworkingLinkSignupViewModelTest {
},
navigationManager = navigationManager,
handleError = handleError,
+ integrityRequestManager = mock(),
+ applicationId = "test",
)
}
+ private fun consumerSessionRepository(failOnSignup: Boolean): FinancialConnectionsConsumerSessionRepository {
+ val consumerRepository = mock {
+ if (failOnSignup) {
+ onBlocking { signUp(any(), any(), any()) } doAnswer {
+ throw APIConnectionException()
+ }
+ onBlocking { mobileSignUp(any(), any(), any(), any(), any()) } doAnswer {
+ throw APIConnectionException()
+ }
+ } else {
+ onBlocking { signUp(any(), any(), any()) } doReturn consumerSessionSignup()
+ onBlocking { mobileSignUp(any(), any(), any(), any(), any()) } doReturn consumerSessionSignup()
+ }
+ }
+ return consumerRepository
+ }
+
private fun networkingLinkSignupPane() = NetworkingLinkSignupPane(
aboveCta = "Above CTA",
body = NetworkingLinkSignupBody(emptyList()),
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 710d9d8c6c6..8abc319b4f7 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
@@ -26,7 +26,9 @@ import com.stripe.android.repository.ConsumersApiService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
+import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -81,17 +83,8 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
whenever(
consumersApiService.signUp(
- email = anyOrNull(),
- phoneNumber = anyOrNull(),
- country = anyOrNull(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- consentAction = anyOrNull(),
- requestSurface = anyOrNull(),
- requestOptions = anyOrNull(),
+ params = any(),
+ requestOptions = anyOrNull()
)
).thenReturn(Result.success(consumerSessionSignup))
@@ -138,16 +131,10 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
whenever(
consumersApiService.signUp(
- email = anyOrNull(),
- phoneNumber = anyOrNull(),
- country = anyOrNull(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = eq(1234),
- currency = eq("cad"),
- incentiveEligibilitySession = eq(IncentiveEligibilitySession.PaymentIntent("pi_123")),
- consentAction = anyOrNull(),
- requestSurface = anyOrNull(),
+ params = argThat {
+ currency == "cad" &&
+ incentiveEligibilitySession == IncentiveEligibilitySession.PaymentIntent("pi_123")
+ },
requestOptions = anyOrNull(),
)
).thenReturn(
@@ -331,17 +318,8 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
whenever(
consumersApiService.signUp(
- email = anyOrNull(),
- phoneNumber = anyOrNull(),
- country = anyOrNull(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- consentAction = anyOrNull(),
- requestSurface = anyOrNull(),
- requestOptions = anyOrNull(),
+ params = any(),
+ requestOptions = anyOrNull()
)
).thenReturn(Result.success(consumerSessionSignup()))
@@ -352,17 +330,8 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
)
verify(consumersApiService).signUp(
- email = anyOrNull(),
- phoneNumber = anyOrNull(),
- country = anyOrNull(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- requestSurface = eq("android_connections"),
- consentAction = anyOrNull(),
- requestOptions = anyOrNull(),
+ params = argThat { requestSurface == "android_connections" },
+ requestOptions = anyOrNull()
)
}
@@ -372,17 +341,8 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
whenever(
consumersApiService.signUp(
- email = anyOrNull(),
- phoneNumber = anyOrNull(),
- country = anyOrNull(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- consentAction = anyOrNull(),
- requestSurface = anyOrNull(),
- requestOptions = anyOrNull(),
+ params = any(),
+ requestOptions = anyOrNull()
)
).thenReturn(Result.success(consumerSessionSignup()))
@@ -393,17 +353,10 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
)
verify(consumersApiService).signUp(
- email = anyOrNull(),
- phoneNumber = anyOrNull(),
- country = anyOrNull(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- requestSurface = eq("android_instant_debits"),
- consentAction = anyOrNull(),
- requestOptions = anyOrNull(),
+ params = argThat {
+ requestSurface == "android_instant_debits"
+ },
+ requestOptions = anyOrNull()
)
}
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestIntegrityRequestManager.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestIntegrityRequestManager.kt
index a922a9f9e04..c5604896fd2 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestIntegrityRequestManager.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestIntegrityRequestManager.kt
@@ -6,6 +6,7 @@ internal class TestIntegrityRequestManager(
val prepareResult: Result = Result.success(Unit),
val requestTokenResult: Result = Result.success("token")
) : IntegrityRequestManager {
+
override suspend fun prepare(): Result = prepareResult
override suspend fun requestToken(requestIdentifier: String?): Result = requestTokenResult
diff --git a/payments-model/src/main/java/com/stripe/android/model/SignUpParams.kt b/payments-model/src/main/java/com/stripe/android/model/SignUpParams.kt
new file mode 100644
index 00000000000..6ba65987d20
--- /dev/null
+++ b/payments-model/src/main/java/com/stripe/android/model/SignUpParams.kt
@@ -0,0 +1,53 @@
+package com.stripe.android.model
+
+import androidx.annotation.RestrictTo
+import java.util.Locale
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+data class SignUpParams(
+ val email: String,
+ val phoneNumber: String,
+ val country: String,
+ val name: String?,
+ val locale: Locale?,
+ val amount: Long?,
+ val currency: String?,
+ val incentiveEligibilitySession: IncentiveEligibilitySession?,
+ val requestSurface: String,
+ val consentAction: ConsumerSignUpConsentAction,
+ val verificationToken: String? = null,
+ val appId: String? = null
+) {
+ fun toParamMap(): Map {
+ val params = mutableMapOf(
+ "email_address" to email.lowercase(),
+ "phone_number" to phoneNumber,
+ "country" to country,
+ "country_inferring_method" to "PHONE_NUMBER",
+ "amount" to amount,
+ "currency" to currency,
+ "consent_action" to consentAction.value,
+ "request_surface" to requestSurface
+ )
+
+ locale?.let {
+ params["locale"] = it.toLanguageTag()
+ }
+
+ name?.let {
+ params["legal_name"] = it
+ }
+
+ verificationToken?.let {
+ params["android_verification_token"] = it
+ }
+
+ appId?.let {
+ params["app_id"] = it
+ }
+
+ params.putAll(incentiveEligibilitySession?.toParamMap().orEmpty())
+
+ return params.toMap()
+ }
+}
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 f8d1825ad16..d1ad4eeb6e4 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
@@ -14,11 +14,10 @@ import com.stripe.android.model.ConsumerPaymentDetailsCreateParams
import com.stripe.android.model.ConsumerSession
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.SignUpParams
import com.stripe.android.model.UpdateAvailableIncentives
import com.stripe.android.model.VerificationType
import com.stripe.android.model.parsers.AttachConsumerToLinkAccountSessionJsonParser
@@ -34,19 +33,15 @@ import java.util.Locale
interface ConsumersApiService {
suspend fun signUp(
- email: String,
- phoneNumber: String,
- country: String,
- name: String?,
- locale: Locale?,
- amount: Long?,
- currency: String?,
- incentiveEligibilitySession: IncentiveEligibilitySession?,
- requestSurface: String,
- consentAction: ConsumerSignUpConsentAction,
+ params: SignUpParams,
requestOptions: ApiRequest.Options,
): Result
+ suspend fun mobileSignUp(
+ params: SignUpParams,
+ requestOptions: ApiRequest.Options
+ ): Result
+
suspend fun lookupConsumerSession(
email: String,
requestSurface: String,
@@ -131,16 +126,7 @@ class ConsumersApiServiceImpl(
)
override suspend fun signUp(
- email: String,
- phoneNumber: String,
- country: String,
- name: String?,
- locale: Locale?,
- amount: Long?,
- currency: String?,
- incentiveEligibilitySession: IncentiveEligibilitySession?,
- requestSurface: String,
- consentAction: ConsumerSignUpConsentAction,
+ params: SignUpParams,
requestOptions: ApiRequest.Options,
): Result {
return executeRequestWithResultParser(
@@ -149,26 +135,26 @@ class ConsumersApiServiceImpl(
request = apiRequestFactory.createPost(
url = consumerAccountsSignUpUrl,
options = requestOptions,
- params = mapOf(
- "email_address" to email.lowercase(),
- "phone_number" to phoneNumber,
- "country" to country,
- "country_inferring_method" to "PHONE_NUMBER",
- "amount" to amount,
- "currency" to currency,
- "consent_action" to consentAction.value,
- "request_surface" to requestSurface,
- ).plus(
- locale?.let {
- mapOf("locale" to it.toLanguageTag())
- } ?: emptyMap()
- ).plus(
- name?.let {
- mapOf("legal_name" to it)
- } ?: emptyMap()
- ).plus(
- incentiveEligibilitySession?.toParamMap().orEmpty()
- ),
+ params = params.toParamMap()
+ ),
+ responseJsonParser = ConsumerSessionSignupJsonParser,
+ )
+ }
+
+ /**
+ * Retrieves the ConsumerSession if the given email is associated with a Link account.
+ */
+ override suspend fun mobileSignUp(
+ params: SignUpParams,
+ requestOptions: ApiRequest.Options
+ ): Result {
+ return executeRequestWithResultParser(
+ stripeErrorJsonParser = stripeErrorJsonParser,
+ stripeNetworkClient = stripeNetworkClient,
+ request = apiRequestFactory.createPost(
+ url = consumerMobileSignUpUrl,
+ options = requestOptions,
+ params = params.toParamMap()
),
responseJsonParser = ConsumerSessionSignupJsonParser,
)
@@ -400,6 +386,12 @@ class ConsumersApiServiceImpl(
internal val consumerAccountsSignUpUrl: String =
getApiUrl("consumers/accounts/sign_up")
+ /**
+ * @return `https://api.stripe.com/v1/consumers/mobile/sign_up`
+ */
+ internal val consumerMobileSignUpUrl: String =
+ getApiUrl("consumers/mobile/sign_up")
+
/**
* @return `https://api.stripe.com/v1/consumers/sessions/lookup`
*/
diff --git a/payments-model/src/test/java/com/stripe/android/repository/ConsumersApiServiceImplTest.kt b/payments-model/src/test/java/com/stripe/android/repository/ConsumersApiServiceImplTest.kt
index 2125799ba47..a79761ad737 100644
--- a/payments-model/src/test/java/com/stripe/android/repository/ConsumersApiServiceImplTest.kt
+++ b/payments-model/src/test/java/com/stripe/android/repository/ConsumersApiServiceImplTest.kt
@@ -13,6 +13,7 @@ import com.stripe.android.model.ConsumerPaymentDetailsCreateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.IncentiveEligibilitySession
+import com.stripe.android.model.SignUpParams
import com.stripe.android.model.VerificationType
import com.stripe.android.networktesting.NetworkRule
import com.stripe.android.networktesting.RequestMatchers.bodyPart
@@ -61,16 +62,18 @@ class ConsumersApiServiceImplTest {
}
val signup = consumersApiService.signUp(
- email = email,
- phoneNumber = "+15555555568",
- country = "US",
- name = null,
- locale = Locale.US,
- amount = 1234,
- currency = "cad",
- incentiveEligibilitySession = IncentiveEligibilitySession.PaymentIntent("pi_123"),
- consentAction = ConsumerSignUpConsentAction.Checkbox,
- requestSurface = requestSurface,
+ SignUpParams(
+ email = email,
+ phoneNumber = "+15555555568",
+ country = "US",
+ name = null,
+ locale = Locale.US,
+ amount = 1234,
+ currency = "cad",
+ incentiveEligibilitySession = IncentiveEligibilitySession.PaymentIntent("pi_123"),
+ consentAction = ConsumerSignUpConsentAction.Checkbox,
+ requestSurface = requestSurface,
+ ),
requestOptions = DEFAULT_OPTIONS,
).getOrThrow()
diff --git a/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt b/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt
index 5487bd06286..481c6aea344 100644
--- a/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/link/repositories/LinkApiRepository.kt
@@ -15,6 +15,7 @@ import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.PaymentMethodCreateParams
+import com.stripe.android.model.SignUpParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.VerificationType
import com.stripe.android.networking.StripeRepository
@@ -61,17 +62,19 @@ internal class LinkApiRepository @Inject constructor(
consentAction: ConsumerSignUpConsentAction
): Result = withContext(workContext) {
consumersApiService.signUp(
- email = email,
- phoneNumber = phone,
- country = country,
- name = name,
- locale = locale,
- amount = null,
- currency = null,
- incentiveEligibilitySession = null,
- consentAction = consentAction,
+ SignUpParams(
+ email = email,
+ phoneNumber = phone,
+ country = country,
+ name = name,
+ locale = locale,
+ amount = null,
+ currency = null,
+ incentiveEligibilitySession = null,
+ consentAction = consentAction,
+ requestSurface = REQUEST_SURFACE
+ ),
requestOptions = buildRequestOptions(),
- requestSurface = REQUEST_SURFACE,
)
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt
index 36870455e88..a58721a0676 100644
--- a/paymentsheet/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/link/repositories/LinkApiRepositoryTest.kt
@@ -14,6 +14,7 @@ import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentMethodCreateParams
+import com.stripe.android.model.SignUpParams
import com.stripe.android.model.VerificationType
import com.stripe.android.networking.StripeRepository
import com.stripe.android.payments.core.analytics.ErrorReporter
@@ -124,16 +125,18 @@ class LinkApiRepositoryTest {
)
verify(consumersApiService).signUp(
- email = email,
- phoneNumber = phone,
- country = country,
- name = name,
- locale = Locale.US,
- amount = null,
- currency = null,
- incentiveEligibilitySession = null,
- requestSurface = "android_payment_element",
- consentAction = ConsumerSignUpConsentAction.Checkbox,
+ SignUpParams(
+ email = email,
+ phoneNumber = phone,
+ country = country,
+ name = name,
+ locale = Locale.US,
+ amount = null,
+ currency = null,
+ incentiveEligibilitySession = null,
+ requestSurface = "android_payment_element",
+ consentAction = ConsumerSignUpConsentAction.Checkbox,
+ ),
requestOptions = ApiRequest.Options(PUBLISHABLE_KEY, STRIPE_ACCOUNT_ID),
)
}
@@ -143,16 +146,7 @@ class LinkApiRepositoryTest {
val consumerSession = mock()
whenever(
consumersApiService.signUp(
- email = any(),
- phoneNumber = any(),
- country = any(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- requestSurface = any(),
- consentAction = any(),
+ params = any(),
requestOptions = any()
)
).thenReturn(Result.success(consumerSession))
@@ -173,16 +167,7 @@ class LinkApiRepositoryTest {
fun `consumerSignUp catches exception and returns failure`() = runTest {
whenever(
consumersApiService.signUp(
- email = any(),
- phoneNumber = any(),
- country = any(),
- name = anyOrNull(),
- locale = anyOrNull(),
- amount = anyOrNull(),
- currency = anyOrNull(),
- incentiveEligibilitySession = anyOrNull(),
- requestSurface = any(),
- consentAction = any(),
+ params = any(),
requestOptions = any()
)
).thenReturn(Result.failure(RuntimeException("error")))