Skip to content

[POC] Add bank account on Link in MPE #10814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions payments-core/api/payments-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -2545,14 +2545,6 @@ public final class com/stripe/android/model/ExpirationDate$Validated : com/strip
public fun toString ()Ljava/lang/String;
}

public final class com/stripe/android/model/FinancialConnectionsSession$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/model/FinancialConnectionsSession;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/model/FinancialConnectionsSession;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/model/GooglePayResult : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
Expand Down
8 changes: 8 additions & 0 deletions payments-model/api/payments-model.api
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@ public final class com/stripe/android/model/ConsumerSessionSignup$Creator : andr
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/model/FinancialConnectionsSession$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/model/FinancialConnectionsSession;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/model/FinancialConnectionsSession;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/model/IncentiveEligibilitySession$DeferredIntent$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/model/IncentiveEligibilitySession$DeferredIntent;
Expand Down
2 changes: 2 additions & 0 deletions payments-model/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
<ID>MagicNumber:CardJsonParser.kt$CardJsonParser$12</ID>
<ID>MagicNumber:CardUtils.kt$CardUtils$10</ID>
<ID>MagicNumber:CardUtils.kt$CardUtils$9</ID>
<ID>TooManyFunctions:ConsumersApiService.kt$ConsumersApiService</ID>
<ID>TooManyFunctions:ConsumersApiService.kt$ConsumersApiServiceImpl : ConsumersApiService</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.stripe.android.model.parsers

import androidx.annotation.RestrictTo
import com.stripe.android.core.model.StripeJsonUtils
import com.stripe.android.core.model.parsers.ModelJsonParser
import com.stripe.android.model.FinancialConnectionsSession
import org.json.JSONObject

internal class FinancialConnectionsSessionJsonParser :
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class FinancialConnectionsSessionJsonParser :
ModelJsonParser<FinancialConnectionsSession> {
override fun parse(json: JSONObject): FinancialConnectionsSession {
return FinancialConnectionsSession(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.CustomEmailType
import com.stripe.android.model.EmailSource
import com.stripe.android.model.FinancialConnectionsSession
import com.stripe.android.model.LinkMode
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.model.SignUpParams
import com.stripe.android.model.UpdateAvailableIncentives
Expand All @@ -25,6 +27,7 @@ import com.stripe.android.model.parsers.ConsumerPaymentDetailsJsonParser
import com.stripe.android.model.parsers.ConsumerSessionJsonParser
import com.stripe.android.model.parsers.ConsumerSessionLookupJsonParser
import com.stripe.android.model.parsers.ConsumerSessionSignupJsonParser
import com.stripe.android.model.parsers.FinancialConnectionsSessionJsonParser
import com.stripe.android.model.parsers.SharePaymentDetailsJsonParser
import com.stripe.android.model.parsers.UpdateAvailableIncentivesJsonParser
import java.util.Locale
Expand Down Expand Up @@ -108,6 +111,14 @@ interface ConsumersApiService {
requestSurface: String,
requestOptions: ApiRequest.Options,
): Result<UpdateAvailableIncentives>

suspend fun createLinkAccountSession(
consumerSessionClientSecret: String,
intentToken: String?,
linkMode: LinkMode,
requestSurface: String,
requestOptions: ApiRequest.Options
): Result<FinancialConnectionsSession>
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
Expand Down Expand Up @@ -385,6 +396,31 @@ class ConsumersApiServiceImpl(
)
}

override suspend fun createLinkAccountSession(
consumerSessionClientSecret: String,
intentToken: String?,
linkMode: LinkMode,
requestSurface: String,
requestOptions: ApiRequest.Options
): Result<FinancialConnectionsSession> {
return executeRequestWithResultParser(
stripeErrorJsonParser = stripeErrorJsonParser,
stripeNetworkClient = stripeNetworkClient,
request = apiRequestFactory.createPost(
url = createLinkAccountSession,
options = requestOptions,
params = mapOf(
"request_surface" to requestSurface,
"link_mode" to linkMode.value,
"credentials" to mapOf(
"consumer_session_client_secret" to consumerSessionClientSecret
),
),
),
responseJsonParser = FinancialConnectionsSessionJsonParser(),
)
}

internal companion object {

/**
Expand Down Expand Up @@ -434,6 +470,11 @@ class ConsumersApiServiceImpl(
*/
private val createPaymentDetails: String = getApiUrl("consumers/payment_details")

/**
* @return `https://api.stripe.com/v1/consumers/link_account_sessions`
*/
private val createLinkAccountSession: String = getApiUrl("consumers/link_account_sessions")

/**
* @return `https://api.stripe.com/v1/consumers/payment_details/share`
*/
Expand Down
1 change: 0 additions & 1 deletion paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,5 @@
<ID>TooManyFunctions:PaymentMethodMetadata.kt$PaymentMethodMetadata : Parcelable</ID>
<ID>TooManyFunctions:PaymentMethodsUiExtension.kt$com.stripe.android.paymentsheet.ui.PaymentMethodsUiExtension.kt</ID>
<ID>UnusedPrivateClass:PaymentOptionsViewModelTest.kt$PaymentOptionsViewModelTest$MyHostActivity : AppCompatActivity</ID>
<ID>UnusedPrivateProperty:SignUpScreenshotTest.kt$SignUpScreenshotTest.Companion$val signUpEnabledStates = listOf(true to "SignUpEnabled", false to "")</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ internal class LinkActivity : ComponentActivity() {
LinkActivityContract.Args(
configuration = configuration,
startWithVerificationDialog = false,
consumerSessionPublishableKey = null,
linkAccount = null,
launchMode = LinkLaunchMode.Full
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal class LinkActivityContract @Inject internal constructor(

data class Args internal constructor(
internal val configuration: LinkConfiguration,
internal val consumerSessionPublishableKey: String?,
internal val startWithVerificationDialog: Boolean,
internal val linkAccount: LinkAccount?,
internal val launchMode: LinkLaunchMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ internal class LinkActivityViewModel @Inject constructor(
val canDismissSheet: Boolean
get() = activityRetainedComponent.dismissalCoordinator.canDismiss

init {
linkAccountManager.consumerPublishableKey = getArgs(savedStateHandle)?.consumerPublishableKey
}

fun handleViewAction(action: LinkAction) {
when (action) {
LinkAction.BackPressed -> handleBackPressed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import javax.inject.Singleton
internal class LinkPaymentLauncher @Inject internal constructor(
linkAnalyticsComponentBuilder: LinkAnalyticsComponent.Builder,
private val linkActivityContract: LinkActivityContract,
private val linkStore: LinkStore,
private val linkStore: LinkStore
) {
private val analyticsHelper = linkAnalyticsComponentBuilder.build().linkAnalyticsHelper

Expand Down Expand Up @@ -75,12 +75,14 @@ internal class LinkPaymentLauncher @Inject internal constructor(
configuration: LinkConfiguration,
linkAccount: LinkAccount?,
launchMode: LinkLaunchMode,
consumerSessionPublishableKey: String?,
useLinkExpress: Boolean
) {
val args = LinkActivityContract.Args(
configuration = configuration,
linkAccount = linkAccount,
launchMode = launchMode,
consumerSessionPublishableKey = consumerSessionPublishableKey,
startWithVerificationDialog = useLinkExpress
)
linkActivityResultLauncher?.launch(args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import javax.inject.Inject
* Contract used to explicitly launch Link natively.
*/
internal class NativeLinkActivityContract @Inject constructor(
@PaymentElementCallbackIdentifier private val paymentElementCallbackIdentifier: String,
@PaymentElementCallbackIdentifier private val paymentElementCallbackIdentifier: String
) :
ActivityResultContract<LinkActivityContract.Args, LinkActivityResult>() {
override fun createIntent(context: Context, input: LinkActivityContract.Args): Intent {
Expand All @@ -24,6 +24,7 @@ internal class NativeLinkActivityContract @Inject constructor(
configuration = input.configuration,
stripeAccountId = paymentConfiguration.stripeAccountId,
publishableKey = paymentConfiguration.publishableKey,
consumerPublishableKey = input.consumerSessionPublishableKey,
startWithVerificationDialog = input.startWithVerificationDialog,
launchMode = input.launchMode,
paymentElementCallbackIdentifier = paymentElementCallbackIdentifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal data class NativeLinkArgs(
val stripeAccountId: String?,
val startWithVerificationDialog: Boolean,
val linkAccount: LinkAccount?,
val consumerPublishableKey: String?,
val paymentElementCallbackIdentifier: String,
val launchMode: LinkLaunchMode,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.model.FinancialConnectionsSession
import com.stripe.android.model.LinkMode
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.payments.core.analytics.ErrorReporter
Expand Down Expand Up @@ -54,7 +56,6 @@ internal class DefaultLinkAccountManager @Inject constructor(
* The publishable key for the signed in Link account.
*/
@Volatile
@VisibleForTesting
override var consumerPublishableKey: String? = null

override val accountStatus = linkAccountHolder.linkAccount.map { it.fetchAccountStatus() }
Expand Down Expand Up @@ -96,6 +97,20 @@ internal class DefaultLinkAccountManager @Inject constructor(
}
}

override suspend fun createLinkAccountSession(
linkMode: LinkMode
): Result<FinancialConnectionsSession> {
return runCatching {
val linkAccount = requireNotNull(linkAccountHolder.linkAccount.value)
linkRepository.createLinkAccountSession(
consumerSessionClientSecret = linkAccount.clientSecret,
stripeIntent = config.stripeIntent,
linkMode = config.linkMode!!,
consumerPublishableKey = consumerPublishableKey,
).getOrThrow()
}
}

override suspend fun signInWithUserInput(
userInput: UserInput
): Result<LinkAccount> =
Expand Down Expand Up @@ -266,6 +281,26 @@ internal class DefaultLinkAccountManager @Inject constructor(
}
}

override suspend fun createBankAccountPaymentDetails(
bankAccountId: String
): Result<ConsumerPaymentDetails> {
val linkAccountValue = linkAccountHolder.linkAccount.value
return if (linkAccountValue != null) {
linkAccountValue.let { account ->
linkRepository.createBankAccountPaymentDetails(
bankAccountId = bankAccountId,
userEmail = account.email,
consumerSessionClientSecret = account.clientSecret,
consumerPublishableKey = if (config.passthroughModeEnabled) null else consumerPublishableKey,
)
}
} else {
errorReporter.report(ErrorReporter.UnexpectedErrorEvent.LINK_ATTACH_CARD_WITH_NULL_ACCOUNT)
Result.failure(
IllegalStateException("A non-null Link account is needed to create payment details")
)
}
}
override suspend fun sharePaymentDetails(
paymentDetailsId: String,
expectedPaymentMethodType: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.EmailSource
import com.stripe.android.model.FinancialConnectionsSession
import com.stripe.android.model.LinkMode
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -94,6 +96,10 @@ internal interface LinkAccountManager {
paymentMethodCreateParams: PaymentMethodCreateParams
): Result<LinkPaymentDetails>

suspend fun createBankAccountPaymentDetails(
bankAccountId: String,
): Result<ConsumerPaymentDetails>

suspend fun sharePaymentDetails(
paymentDetailsId: String,
expectedPaymentMethodType: String,
Expand All @@ -104,6 +110,10 @@ internal interface LinkAccountManager {
startSession: Boolean,
): LinkAccount?

suspend fun createLinkAccountSession(
linkMode: LinkMode
): Result<FinancialConnectionsSession>

/**
* Triggers sending a verification code to the user.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.model.FinancialConnectionsSession
import com.stripe.android.model.IncentiveEligibilitySession
import com.stripe.android.model.LinkMode
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
Expand Down Expand Up @@ -199,6 +201,26 @@ internal class LinkApiRepository @Inject constructor(
}
}

override suspend fun createBankAccountPaymentDetails(
bankAccountId: String,
consumerSessionClientSecret: String,
consumerPublishableKey: String?,
userEmail: String
): Result<ConsumerPaymentDetails> = withContext(workContext) {
consumersApiService.createPaymentDetails(
consumerSessionClientSecret = consumerSessionClientSecret,
paymentDetailsCreateParams = ConsumerPaymentDetailsCreateParams.BankAccount(
bankAccountId = bankAccountId,
billingEmailAddress = null,
billingAddress = null
),
requestSurface = REQUEST_SURFACE,
requestOptions = buildRequestOptions(consumerPublishableKey),
).onFailure {
errorReporter.report(ErrorReporter.ExpectedErrorEvent.LINK_CREATE_CARD_FAILURE, StripeException.create(it))
}
}

override suspend fun shareCardPaymentDetails(
paymentMethodCreateParams: PaymentMethodCreateParams,
id: String,
Expand Down Expand Up @@ -362,6 +384,26 @@ internal class LinkApiRepository @Inject constructor(
)
}

override suspend fun createLinkAccountSession(
consumerSessionClientSecret: String,
stripeIntent: StripeIntent,
linkMode: LinkMode,
consumerPublishableKey: String?
): Result<FinancialConnectionsSession> {
return consumersApiService.createLinkAccountSession(
consumerSessionClientSecret = consumerSessionClientSecret,
intentToken = stripeIntent.clientSecret,
linkMode = linkMode,
requestSurface = REQUEST_SURFACE,
requestOptions = consumerPublishableKey?.let {
ApiRequest.Options(it)
} ?: ApiRequest.Options(
publishableKeyProvider(),
stripeAccountIdProvider()
)
)
}

private fun buildRequestOptions(
consumerAccountPublishableKey: String? = null,
): ApiRequest.Options {
Expand Down
Loading
Loading