Skip to content

Commit

Permalink
Enable bank payments in native Link
Browse files Browse the repository at this point in the history
GIT_VALID_PII_OVERRIDE
  • Loading branch information
tillh-stripe committed Feb 7, 2025
1 parent 0c5ab32 commit 1782b5c
Show file tree
Hide file tree
Showing 16 changed files with 243 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import com.stripe.android.payments.core.analytics.ErrorReporter
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmOverloads
internal fun DefaultFraudDetectionDataRepository(
fun DefaultFraudDetectionDataRepository(
context: Context,
workContext: CoroutineContext = Dispatchers.IO,
): DefaultFraudDetectionDataRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.paymentsheet.BuildConfig
import com.stripe.android.paymentsheet.model.amount
Expand Down Expand Up @@ -255,6 +256,19 @@ internal class DefaultLinkAccountManager @Inject constructor(
}
}

override suspend fun shareLinkCardBrand(
paymentDetailsId: String,
): Result<SharePaymentDetails> {
return runCatching {
requireNotNull(linkAccountHolder.linkAccount.value)
}.mapCatching { account ->
linkRepository.shareLinkCardBrand(
paymentDetailsId = paymentDetailsId,
consumerSessionClientSecret = account.clientSecret,
).getOrThrow()
}
}

private fun setAccount(
consumerSession: ConsumerSession,
publishableKey: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.EmailSource
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

Expand Down Expand Up @@ -86,6 +87,10 @@ internal interface LinkAccountManager {
paymentMethodCreateParams: PaymentMethodCreateParams
): Result<LinkPaymentDetails>

suspend fun shareLinkCardBrand(
paymentDetailsId: String,
): Result<SharePaymentDetails>

fun setLinkAccountFromLookupResult(
lookup: ConsumerSessionLookup,
startSession: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.wallets.Wallet
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentelement.confirmation.link.LinkCardBrandConfirmationOption
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.R
import javax.inject.Inject
Expand Down Expand Up @@ -112,17 +113,25 @@ internal class DefaultLinkConfirmationHandler @Inject constructor(
linkAccount: LinkAccount,
cvc: String?
): ConfirmationHandler.Args {
return ConfirmationHandler.Args(
intent = configuration.stripeIntent,
confirmationOption = PaymentMethodConfirmationOption.New(
val confirmationOption = if (paymentDetails.type == "bank_account" && configuration.passthroughModeEnabled) {
LinkCardBrandConfirmationOption(
paymentDetailsId = paymentDetails.id,
)
} else {
PaymentMethodConfirmationOption.New(
createParams = createPaymentMethodCreateParams(
selectedPaymentDetails = paymentDetails,
linkAccount = linkAccount,
cvc = cvc
),
optionsParams = null,
shouldSave = false
),
)
}

return ConfirmationHandler.Args(
intent = configuration.stripeIntent,
confirmationOption = confirmationOption,
appearance = PaymentSheet.Appearance(),
initializationMode = configuration.initializationMode,
shippingDetails = configuration.shippingDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import com.stripe.android.link.repositories.LinkRepository
import com.stripe.android.networking.StripeApiRepository
import com.stripe.android.networking.StripeRepository
import com.stripe.android.paymentelement.confirmation.ALLOWS_MANUAL_CONFIRMATION
import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
import com.stripe.android.paymentelement.confirmation.link.LinkCardBrandConfirmationDefinition
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.payments.core.analytics.RealErrorReporter
import com.stripe.android.payments.core.injection.PRODUCT_USAGE
Expand All @@ -52,6 +54,7 @@ import com.stripe.attestation.RealStandardIntegrityManagerFactory
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import kotlinx.coroutines.Dispatchers
import javax.inject.Named
import kotlin.coroutines.CoroutineContext
Expand Down Expand Up @@ -215,5 +218,16 @@ internal interface NativeLinkModule {
): String {
return application.packageName
}

@JvmSuppressWildcards
@Provides
@IntoSet
fun providesLinkCardBrandConfirmationDefinition(
linkAccountManager: DefaultLinkAccountManager
): ConfirmationDefinition<*, *, *, *> {
return LinkCardBrandConfirmationDefinition(
linkAccountManager = linkAccountManager,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ import com.stripe.android.model.StripeIntent
* The supported payment methods are read from [StripeIntent.linkFundingSources], and fallback to
* card only if the list is empty or none of them is valid.
*/
internal fun StripeIntent.supportedPaymentMethodTypes(linkAccount: LinkAccount) =
internal fun StripeIntent.supportedPaymentMethodTypes(linkAccount: LinkAccount): Set<String> {
if (!isLiveMode && linkAccount.email.contains("+multiple_funding_sources@")) {
supportedPaymentMethodTypes
} else {
linkFundingSources.filter { supportedPaymentMethodTypes.contains(it) }
.takeIf { it.isNotEmpty() }?.toSet()
?: setOf(ConsumerPaymentDetails.Card.TYPE)
return supportedPaymentMethodTypes
}

val allowedFundingSources = linkFundingSources.filter { it in supportedPaymentMethodTypes }
return allowedFundingSources.toSet().takeIf { it.isNotEmpty() } ?: setOf(ConsumerPaymentDetails.Card.TYPE)
}

private val supportedPaymentMethodTypes = setOf(
ConsumerPaymentDetails.Card.TYPE,
ConsumerPaymentDetails.Passthrough.TYPE
ConsumerPaymentDetails.Passthrough.TYPE,
ConsumerPaymentDetails.BankAccount.TYPE,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.stripe.android.link.repositories

import android.content.Context
import com.stripe.android.DefaultFraudDetectionDataRepository
import com.stripe.android.core.exception.StripeException
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.injection.PUBLISHABLE_KEY
import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
Expand All @@ -17,6 +20,7 @@ import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.model.IncentiveEligibilitySession
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.model.SignUpParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.VerificationType
Expand All @@ -34,6 +38,7 @@ import kotlin.coroutines.CoroutineContext
*/
@SuppressWarnings("TooManyFunctions")
internal class LinkApiRepository @Inject constructor(
context: Context,
@Named(PUBLISHABLE_KEY) private val publishableKeyProvider: () -> String,
@Named(STRIPE_ACCOUNT_ID) private val stripeAccountIdProvider: () -> String?,
private val stripeRepository: StripeRepository,
Expand All @@ -43,6 +48,13 @@ internal class LinkApiRepository @Inject constructor(
private val errorReporter: ErrorReporter,
) : LinkRepository {

private val fraudDetectionDataRepository: FraudDetectionDataRepository =
DefaultFraudDetectionDataRepository(context, workContext)

init {
fraudDetectionDataRepository.refresh()
}

override suspend fun lookupConsumer(
email: String,
): Result<ConsumerSessionLookup> = withContext(workContext) {
Expand Down Expand Up @@ -199,6 +211,24 @@ internal class LinkApiRepository @Inject constructor(
}
}

override suspend fun shareLinkCardBrand(
consumerSessionClientSecret: String,
paymentDetailsId: String,
): Result<SharePaymentDetails> = withContext(workContext) {
val fraudParams = fraudDetectionDataRepository.getCached()?.params.orEmpty()
val paymentMethodParams = mapOf("expand" to listOf("payment_method"))

consumersApiService.sharePaymentDetails(
consumerSessionClientSecret = consumerSessionClientSecret,
paymentDetailsId = paymentDetailsId,
expectedPaymentMethodType = "card", // Link card brand
requestOptions = buildRequestOptions(),
requestSurface = REQUEST_SURFACE,
extraParams = paymentMethodParams + fraudParams,
billingPhone = null,
)
}

override suspend fun logOut(
consumerSessionClientSecret: String,
consumerAccountPublishableKey: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.model.IncentiveEligibilitySession
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.model.StripeIntent

/**
Expand Down Expand Up @@ -76,6 +77,11 @@ internal interface LinkRepository {
consumerSessionClientSecret: String,
): Result<LinkPaymentDetails>

suspend fun shareLinkCardBrand(
consumerSessionClientSecret: String,
paymentDetailsId: String,
): Result<SharePaymentDetails>

suspend fun logOut(
consumerSessionClientSecret: String,
consumerAccountPublishableKey: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.Result
import kotlin.String
import kotlin.Throwable
import kotlin.Unit
import kotlin.fold
import kotlin.takeIf
import com.stripe.android.link.confirmation.Result as LinkConfirmationResult

internal class WalletViewModel @Inject constructor(
Expand Down Expand Up @@ -102,7 +97,7 @@ internal class WalletViewModel @Inject constructor(

viewModelScope.launch {
linkAccountManager.listPaymentDetails(
paymentMethodTypes = stripeIntent.supportedPaymentMethodTypes(linkAccount)
paymentMethodTypes = stripeIntent.supportedPaymentMethodTypes(linkAccount),
).fold(
onSuccess = { response ->
_uiState.update {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.stripe.android.paymentelement.confirmation.link

import android.os.Parcelable
import androidx.activity.result.ActivityResultCaller
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.parsers.PaymentMethodJsonParser
import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfirmationType
import com.stripe.android.paymentsheet.R
import kotlinx.parcelize.Parcelize
import org.json.JSONObject

internal class LinkCardBrandConfirmationDefinition(
private val linkAccountManager: LinkAccountManager,
) : ConfirmationDefinition<
LinkCardBrandConfirmationOption,
LinkCardBrandConfirmationDefinition.Launcher,
LinkCardBrandConfirmationDefinition.LauncherArguments,
LinkCardBrandConfirmationDefinition.Result,
> {
override val key: String = "LinkCardBrand"

override fun option(confirmationOption: ConfirmationHandler.Option): LinkCardBrandConfirmationOption? {
return confirmationOption as? LinkCardBrandConfirmationOption
}

override suspend fun action(
confirmationOption: LinkCardBrandConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters
): ConfirmationDefinition.Action<LauncherArguments> {
return createPaymentMethodConfirmationOption(confirmationOption).fold(
onSuccess = { nextConfirmationOption ->
ConfirmationDefinition.Action.Launch(
launcherArguments = LauncherArguments(nextConfirmationOption),
receivesResultInProcess = true,
deferredIntentConfirmationType = null,
)
},
onFailure = { error ->
ConfirmationDefinition.Action.Fail(
cause = error,
message = resolvableString(R.string.stripe_something_went_wrong),
errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal,
)
},
)
}

override fun createLauncher(
activityResultCaller: ActivityResultCaller,
onResult: (Result) -> Unit
): Launcher {
return Launcher(onResult)
}

override fun launch(
launcher: Launcher,
arguments: LauncherArguments,
confirmationOption: LinkCardBrandConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters,
) {
launcher.onResult(Result(arguments.nextConfirmationOption))
}

override fun toResult(
confirmationOption: LinkCardBrandConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters,
deferredIntentConfirmationType: DeferredIntentConfirmationType?,
result: Result,
): ConfirmationDefinition.Result {
return ConfirmationDefinition.Result.NextStep(
confirmationOption = result.nextConfirmationOption,
parameters = confirmationParameters,
)
}

private suspend fun createPaymentMethodConfirmationOption(
confirmationOption: LinkCardBrandConfirmationOption,
): kotlin.Result<PaymentMethodConfirmationOption> {
return linkAccountManager.shareLinkCardBrand(
paymentDetailsId = confirmationOption.paymentDetailsId,
).mapCatching {
requireNotNull(it.encodedPaymentMethod.parsePaymentMethod())
}.map { paymentMethod ->
PaymentMethodConfirmationOption.Saved(
paymentMethod = paymentMethod,
optionsParams = null,
)
}
}

@Parcelize
data class Result(
val nextConfirmationOption: PaymentMethodConfirmationOption,
) : Parcelable

data class LauncherArguments(
val nextConfirmationOption: PaymentMethodConfirmationOption,
)

class Launcher(
val onResult: (Result) -> Unit,
)
}

private fun String.parsePaymentMethod(): PaymentMethod? = try {
val json = JSONObject(this)
val paymentMethod = PaymentMethodJsonParser().parse(json)
paymentMethod
} catch (e: Exception) {
null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stripe.android.paymentelement.confirmation.link

import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import kotlinx.parcelize.Parcelize

@Parcelize
internal data class LinkCardBrandConfirmationOption(
val paymentDetailsId: String,
) : ConfirmationHandler.Option
Loading

0 comments on commit 1782b5c

Please sign in to comment.