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 2, 2025
1 parent 69327fb commit 5a1f7e4
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 151 deletions.
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

suspend 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 @@ -17,6 +17,7 @@ import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.ConsumerSignUpConsentAction.EnteredPhoneNumberClickedSaveToLink
import com.stripe.android.model.CustomEmailType
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.model.SignUpParams
import com.stripe.android.model.VerificationType
import com.stripe.android.repository.ConsumersApiService
import kotlinx.coroutines.sync.Mutex
Expand Down Expand Up @@ -44,6 +45,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 @@ -135,17 +144,49 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl(
country: String,
): ConsumerSessionSignup = mutex.withLock {
consumersApiService.signUp(
email = email,
phoneNumber = phoneNumber,
country = country,
name = null,
locale = locale,
amount = elementsSessionContext?.amount,
currency = elementsSessionContext?.currency,
incentiveEligibilitySession = elementsSessionContext?.incentiveEligibilitySession,
SignUpParams(
email = email,
phoneNumber = phoneNumber,
country = country,
name = null,
locale = locale,
amount = elementsSessionContext?.amount,
currency = elementsSessionContext?.currency,
incentiveEligibilitySession = elementsSessionContext?.incentiveEligibilitySession,
requestSurface = requestSurface,
consentAction = EnteredPhoneNumberClickedSaveToLink,
verificationToken = null,
appId = null,
),
requestOptions = provideApiRequestOptions(useConsumerPublishableKey = false)
).onSuccess { signup ->
updateCachedConsumerSessionFromSignup(signup)
}.getOrThrow()
}

override suspend fun mobileSignUp(
email: String,
phoneNumber: String,
country: String,
verificationToken: String,
appId: String
): ConsumerSessionSignup = mutex.withLock {
consumersApiService.mobileSignUp(
SignUpParams(
email = email,
phoneNumber = phoneNumber,
country = country,
name = null,
locale = locale,
amount = elementsSessionContext?.amount,
currency = elementsSessionContext?.currency,
incentiveEligibilitySession = elementsSessionContext?.incentiveEligibilitySession,
requestSurface = requestSurface,
consentAction = EnteredPhoneNumberClickedSaveToLink,
verificationToken = verificationToken,
appId = appId,
),
requestOptions = provideApiRequestOptions(useConsumerPublishableKey = false),
requestSurface = requestSurface,
consentAction = EnteredPhoneNumberClickedSaveToLink,
).onSuccess { signup ->
updateCachedConsumerSessionFromSignup(signup)
}.getOrThrow()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.stripe.android.financialconnections.features.networkinglinksignup

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.attestation.IntegrityRequestManager
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.mockito.kotlin.any
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 = mock<IntegrityRequestManager>()
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(),
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()).thenReturn(
mock {
whenever(it.manifest).thenReturn(
mock {
whenever(it.nextPane).thenReturn(expectedPane)
}
)
}
)
whenever(
consumerRepository.mobileSignUp(
email = any(),
phoneNumber = any(),
country = any(),
verificationToken = any(),
appId = any()
)
).thenReturn(mock())

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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
Expand All @@ -646,15 +649,7 @@ class NetworkingLinkSignupViewModelTest {
)
}

val consumerRepository = mock<FinancialConnectionsConsumerSessionRepository> {
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,
Expand All @@ -664,9 +659,24 @@ class NetworkingLinkSignupViewModelTest {
},
navigationManager = navigationManager,
handleError = handleError,
integrityRequestManager = mock(),
applicationId = "test",
)
}

private fun consumerSessionRepository(failOnSignup: Boolean): FinancialConnectionsConsumerSessionRepository {
val consumerRepository = mock<FinancialConnectionsConsumerSessionRepository> {
if (failOnSignup) {
onBlocking { signUp(any(), any(), any()) } doAnswer {
throw APIConnectionException()
}
} else {
onBlocking { signUp(any(), any(), any()) } doReturn consumerSessionSignup()
}
}
return consumerRepository
}

private fun networkingLinkSignupPane() = NetworkingLinkSignupPane(
aboveCta = "Above CTA",
body = NetworkingLinkSignupBody(emptyList()),
Expand Down
Loading

0 comments on commit 5a1f7e4

Please sign in to comment.