From fc381b6b3f0a00c6c4c526fbd5f4e92ca2d2c0f7 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Wed, 25 Dec 2024 12:21:17 +0100 Subject: [PATCH] Add SignUpParams model class --- .../networkinglinksignup/LinkSignupHandler.kt | 73 ++++++++++++++---- ...ialConnectionsConsumerSessionRepository.kt | 61 ++++++++++++--- .../com/stripe/android/model/SignUpParams.kt | 54 +++++++++++++ .../android/repository/ConsumersApiService.kt | 75 +++++++++---------- .../link/repositories/LinkApiRepository.kt | 23 +++--- 5 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 payments-model/src/main/java/com/stripe/android/model/SignUpParams.kt 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 f935fe760a0..12ff1f64706 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 suspend 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,31 @@ 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 +94,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 +113,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 9fb625db051..7a035756add 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 @@ -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 @@ -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?, @@ -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() 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..35e21ec0e7a --- /dev/null +++ b/payments-model/src/main/java/com/stripe/android/model/SignUpParams.kt @@ -0,0 +1,54 @@ +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 73e1203cbbd..966949f0635 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 @@ -18,6 +18,7 @@ import com.stripe.android.model.ConsumerSignUpConsentAction import com.stripe.android.model.CustomEmailType import com.stripe.android.model.IncentiveEligibilitySession import com.stripe.android.model.SharePaymentDetails +import com.stripe.android.model.SignUpParams import com.stripe.android.model.VerificationType import com.stripe.android.model.parsers.AttachConsumerToLinkAccountSessionJsonParser import com.stripe.android.model.parsers.ConsumerPaymentDetailsJsonParser @@ -31,19 +32,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, @@ -118,16 +115,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( @@ -136,26 +124,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, ) @@ -359,6 +347,13 @@ 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/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, ) }