Skip to content

Commit

Permalink
Add SignUpParams model class
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosmuvi-stripe committed Jan 16, 2025
1 parent 2b5b02c commit 2b2a4e4
Show file tree
Hide file tree
Showing 13 changed files with 541 additions and 186 deletions.
2 changes: 2 additions & 0 deletions financial-connections/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<ID>SwallowedException:PollAttachPaymentAccount.kt$PollAttachPaymentAccount$e: StripeException</ID>
<ID>SwallowedException:PollAuthorizationSessionAccounts.kt$PollAuthorizationSessionAccounts$e: StripeException</ID>
<ID>SwallowedException:PostAuthorizationSession.kt$PostAuthorizationSession$e: StripeException</ID>
<ID>TooManyFunctions:FinancialConnectionsConsumerSessionRepository.kt$FinancialConnectionsConsumerSessionRepository</ID>
<ID>TooManyFunctions:FinancialConnectionsConsumerSessionRepository.kt$FinancialConnectionsConsumerSessionRepositoryImpl : FinancialConnectionsConsumerSessionRepository</ID>
<ID>TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepository</ID>
<ID>TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepositoryImpl : FinancialConnectionsManifestRepository</ID>
<ID>TooManyFunctions:FinancialConnectionsSheetNativeViewModel.kt$FinancialConnectionsSheetNativeViewModel : FinancialConnectionsViewModelTopAppBarHost</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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 {

Expand All @@ -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() {
Expand All @@ -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 {

Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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?,
Expand Down Expand Up @@ -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>
): ConsumerSessionSignup = mutex.withLock {
consumersApiService.signUp(
val signUpParams = SignUpParams(
email = email,
phoneNumber = phoneNumber,
country = country,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FinancialConnectionsConsumerSessionRepository>()
private val getOrFetchSync = mock<GetOrFetchSync>()
private val attachConsumerToLinkAccountSession = mock<AttachConsumerToLinkAccountSession>()
private val integrityRequestManager = TestIntegrityRequestManager()
private val navigationManager = mock<NavigationManager>()
private val handleError = mock<HandleError>()

@Before
fun setUp() {
handler = LinkSignupHandlerForInstantDebits(
consumerRepository,
attachConsumerToLinkAccountSession,
integrityRequestManager,
getOrFetchSync,
navigationManager,
"applicationId",
handleError
)
}

private val validPayload = Payload(
merchantName = "Mock Merchant",
emailController = mock(),
prefilledEmail = "[email protected]",
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 = "[email protected]",
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
)
}
}
Loading

0 comments on commit 2b2a4e4

Please sign in to comment.