-
Notifications
You must be signed in to change notification settings - Fork 260
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
feat: add support for ACHv2 on Android #879
Changes from all commits
dcb8987
f98c688
09cc702
efc42fe
dad048b
4301b04
0a27298
497c460
0be3dd0
e71c7f5
87cf6a0
7b38e49
2b7e0f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package com.reactnativestripesdk | ||
|
||
import android.os.Bundle | ||
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import android.widget.FrameLayout | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.fragment.app.Fragment | ||
import com.facebook.react.bridge.Promise | ||
import com.stripe.android.model.PaymentIntent | ||
import com.stripe.android.model.SetupIntent | ||
import com.stripe.android.model.StripeIntent | ||
import com.stripe.android.payments.bankaccount.CollectBankAccountConfiguration | ||
import com.stripe.android.payments.bankaccount.CollectBankAccountLauncher | ||
import com.stripe.android.payments.bankaccount.navigation.CollectBankAccountResult | ||
|
||
class CollectBankAccountLauncherFragment( | ||
private val activity: AppCompatActivity, | ||
private val publishableKey: String, | ||
private val clientSecret: String, | ||
private val isPaymentIntent: Boolean, | ||
private val collectParams: CollectBankAccountConfiguration.USBankAccount, | ||
private val promise: Promise | ||
) : Fragment() { | ||
private lateinit var collectBankAccountLauncher: CollectBankAccountLauncher | ||
|
||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, | ||
savedInstanceState: Bundle?): View { | ||
collectBankAccountLauncher = createBankAccountLauncher() | ||
|
||
return FrameLayout(requireActivity()).also { | ||
it.visibility = View.GONE | ||
} | ||
} | ||
|
||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
super.onViewCreated( | ||
view, | ||
savedInstanceState) | ||
|
||
if (isPaymentIntent) { | ||
collectBankAccountLauncher.presentWithPaymentIntent( | ||
publishableKey, | ||
clientSecret, | ||
collectParams | ||
) | ||
} else { | ||
collectBankAccountLauncher.presentWithSetupIntent( | ||
publishableKey, | ||
clientSecret, | ||
collectParams | ||
) | ||
} | ||
} | ||
|
||
private fun createBankAccountLauncher(): CollectBankAccountLauncher { | ||
return CollectBankAccountLauncher.create(this) { result -> | ||
when (result) { | ||
is CollectBankAccountResult.Completed -> { | ||
val intent = result.response.intent | ||
if (intent.status === StripeIntent.Status.RequiresPaymentMethod) { | ||
promise.resolve(createError(ErrorType.Canceled.toString(), "Bank account collection was canceled.")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this customer facing? If it is, should this be translated? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is developer facing |
||
} else if (intent.status === StripeIntent.Status.RequiresConfirmation) { | ||
promise.resolve( | ||
if (isPaymentIntent) | ||
createResult("paymentIntent", mapFromPaymentIntentResult(intent as PaymentIntent)) | ||
else | ||
createResult("setupIntent", mapFromSetupIntentResult(intent as SetupIntent)) | ||
) | ||
} | ||
} | ||
is CollectBankAccountResult.Cancelled -> { | ||
promise.resolve(createError(ErrorType.Canceled.toString(), "Bank account collection was canceled.")) | ||
} | ||
is CollectBankAccountResult.Failed -> { | ||
promise.resolve(createError(ErrorType.Failed.toString(), result.error)) | ||
} | ||
} | ||
activity.supportFragmentManager.beginTransaction().remove(this).commit() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,6 +106,7 @@ internal fun mapPaymentMethodType(type: PaymentMethod.Type?): String { | |
PaymentMethod.Type.Upi -> "Upi" | ||
PaymentMethod.Type.WeChatPay -> "WeChatPay" | ||
PaymentMethod.Type.Klarna -> "Klarna" | ||
PaymentMethod.Type.USBankAccount -> "USBankAccount" | ||
else -> "Unknown" | ||
} | ||
} | ||
|
@@ -132,6 +133,7 @@ internal fun mapToPaymentMethodType(type: String?): PaymentMethod.Type? { | |
"Upi" -> PaymentMethod.Type.Upi | ||
"WeChatPay" -> PaymentMethod.Type.WeChatPay | ||
"Klarna" -> PaymentMethod.Type.Klarna | ||
"USBankAccount" -> PaymentMethod.Type.USBankAccount | ||
else -> null | ||
} | ||
} | ||
|
@@ -213,6 +215,38 @@ internal fun mapFromBankAccount(bankAccount: BankAccount?): WritableMap? { | |
return bankAccountMap | ||
} | ||
|
||
internal fun mapToUSBankAccountHolderType(type: String?): PaymentMethod.USBankAccount.USBankAccountHolderType { | ||
return when (type) { | ||
"Company" -> PaymentMethod.USBankAccount.USBankAccountHolderType.COMPANY | ||
"Individual" -> PaymentMethod.USBankAccount.USBankAccountHolderType.INDIVIDUAL | ||
else -> PaymentMethod.USBankAccount.USBankAccountHolderType.INDIVIDUAL | ||
} | ||
} | ||
|
||
internal fun mapFromUSBankAccountHolderType(type: PaymentMethod.USBankAccount.USBankAccountHolderType): String { | ||
return when (type) { | ||
PaymentMethod.USBankAccount.USBankAccountHolderType.COMPANY -> "Company" | ||
PaymentMethod.USBankAccount.USBankAccountHolderType.INDIVIDUAL -> "Individual" | ||
else -> "Unknown" | ||
} | ||
} | ||
|
||
internal fun mapToUSBankAccountType(type: String?): PaymentMethod.USBankAccount.USBankAccountType { | ||
return when (type) { | ||
"Savings" -> PaymentMethod.USBankAccount.USBankAccountType.SAVINGS | ||
"Checking" -> PaymentMethod.USBankAccount.USBankAccountType.CHECKING | ||
else -> PaymentMethod.USBankAccount.USBankAccountType.CHECKING | ||
} | ||
} | ||
|
||
internal fun mapFromUSBankAccountType(type: PaymentMethod.USBankAccount.USBankAccountType): String { | ||
return when (type) { | ||
PaymentMethod.USBankAccount.USBankAccountType.CHECKING -> "Checking" | ||
PaymentMethod.USBankAccount.USBankAccountType.SAVINGS -> "Savings" | ||
else -> "Unknown" | ||
} | ||
} | ||
|
||
internal fun mapFromCard(card: Card?): WritableMap? { | ||
val cardMap: WritableMap = WritableNativeMap() | ||
|
||
|
@@ -279,6 +313,7 @@ internal fun mapFromPaymentMethod(paymentMethod: PaymentMethod): WritableMap { | |
val ideal: WritableMap = WritableNativeMap() | ||
val fpx: WritableMap = WritableNativeMap() | ||
val upi: WritableMap = WritableNativeMap() | ||
val usBankAccount: WritableMap = WritableNativeMap() | ||
|
||
card.putString("brand", mapCardBrand(paymentMethod.card?.brand)) | ||
card.putString("country", paymentMethod.card?.country) | ||
|
@@ -315,6 +350,28 @@ internal fun mapFromPaymentMethod(paymentMethod: PaymentMethod): WritableMap { | |
|
||
upi.putString("vpa", paymentMethod.upi?.vpa) | ||
|
||
// TODO: Remove reflection once USBankAccount is public payment method in stripe-android | ||
try { | ||
PaymentMethod::class.java.getDeclaredField("usBankAccount").let { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reflection for now for testing, won't need this once feature is public |
||
it.isAccessible = true | ||
val bankAccountPM = it.get(paymentMethod) as PaymentMethod.USBankAccount | ||
usBankAccount.putString("routingNumber", bankAccountPM.routingNumber) | ||
usBankAccount.putString("accountHolderType", mapFromUSBankAccountHolderType(bankAccountPM.accountHolderType)) | ||
usBankAccount.putString("accountType", mapFromUSBankAccountType(bankAccountPM.accountType)) | ||
usBankAccount.putString("last4", bankAccountPM.last4) | ||
usBankAccount.putString("bankName", bankAccountPM.bankName) | ||
usBankAccount.putString("linkedAccount", bankAccountPM.linkedAccount) | ||
usBankAccount.putString("fingerprint", bankAccountPM.fingerprint) | ||
usBankAccount.putString("fingerprint", bankAccountPM.fingerprint) | ||
usBankAccount.putString("preferredNetworks", bankAccountPM.networks?.preferred) | ||
usBankAccount.putArray("supportedNetworks", bankAccountPM.networks?.supported as? ReadableArray) | ||
} | ||
} catch (e: java.lang.Exception) { | ||
Log.w( | ||
"StripeReactNative", | ||
"Unable to find USBankAccount method:" + e.message) | ||
} | ||
|
||
pm.putString("id", paymentMethod.id) | ||
pm.putString("type", mapPaymentMethodType(paymentMethod.type)) | ||
pm.putBoolean("livemode", paymentMethod.liveMode) | ||
|
@@ -328,6 +385,7 @@ internal fun mapFromPaymentMethod(paymentMethod: PaymentMethod): WritableMap { | |
pm.putMap("Ideal", ideal) | ||
pm.putMap("Fpx", fpx) | ||
pm.putMap("Upi", upi) | ||
pm.putMap("USBankAccount", usBankAccount) | ||
|
||
return pm | ||
} | ||
|
@@ -377,6 +435,14 @@ internal fun mapFromPaymentIntentResult(paymentIntent: PaymentIntent): WritableM | |
return map | ||
} | ||
|
||
internal fun mapFromMicrodepositType(type: MicrodepositType): String { | ||
return when (type) { | ||
MicrodepositType.AMOUNTS -> "amounts" | ||
MicrodepositType.DESCRIPTOR_CODE -> "descriptorCode" | ||
else -> "unknown" | ||
} | ||
} | ||
|
||
internal fun mapNextAction(type: NextActionType?, data: NextActionData?): WritableNativeMap? { | ||
val nextActionMap = WritableNativeMap() | ||
when (type) { | ||
|
@@ -386,15 +452,14 @@ internal fun mapNextAction(type: NextActionType?, data: NextActionData?): Writab | |
nextActionMap.putString("redirectUrl", it.url.toString()) | ||
} | ||
} | ||
// TODO: This is currently private. Uncomment when ACHv2 is available on Android. | ||
// NextActionType.VerifyWithMicrodeposits -> { | ||
// (data as? NextActionData.VerifyWithMicrodeposits)?.let { | ||
// nextActionMap.putString("type", "verifyWithMicrodeposits") | ||
// nextActionMap.putString("arrivalDate", it.arrivalDate.toString()) | ||
// nextActionMap.putString("redirectUrl", it.hostedVerificationUrl) | ||
// nextActionMap.putString("microdepositType", it.microdepositType.toString()) | ||
// } | ||
// } | ||
NextActionType.VerifyWithMicrodeposits -> { | ||
(data as? NextActionData.VerifyWithMicrodeposits)?.let { | ||
nextActionMap.putString("type", "verifyWithMicrodeposits") | ||
nextActionMap.putString("arrivalDate", it.arrivalDate.toString()) | ||
nextActionMap.putString("redirectUrl", it.hostedVerificationUrl) | ||
nextActionMap.putString("microdepositType", mapFromMicrodepositType(it.microdepositType)) | ||
} | ||
} | ||
NextActionType.DisplayOxxoDetails -> { | ||
(data as? NextActionData.DisplayOxxoDetails)?.let { | ||
nextActionMap.putString("type", "oxxoVoucher") | ||
|
@@ -461,14 +526,14 @@ internal fun mapToAddress(addressMap: ReadableMap?, cardAddress: Address?): Addr | |
.setLine2(getValOr(addressMap, "line2")) | ||
.setState(getValOr(addressMap, "state")) | ||
|
||
cardAddress?.let { ca -> | ||
ca.postalCode?.let { | ||
address.setPostalCode(it) | ||
} | ||
ca.country?.let { | ||
address.setCountry(it) | ||
} | ||
cardAddress?.let { ca -> | ||
ca.postalCode?.let { | ||
address.setPostalCode(it) | ||
} | ||
ca.country?.let { | ||
address.setCountry(it) | ||
} | ||
} | ||
|
||
return address.build() | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Undecided on if this is included by default or we require users to add it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We require our users to add it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We require it for merchants that support ACH, but it's not mandatory for everyone. If you don't want to include the entire library on android for everyone, you can do
compileOnly
instead, and mention in the docs that users need to add it to theirandroid/build.gradle
file.CollectBankAccountLauncher
will fail at runtime if the connections code is not available on the classpath.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep- my question here is if the added size of the connections library is large enough to make users add this to their build.gradle manually. Even small additional steps like that can really cause issues for RN users. And then they're in charge of keeping that version up-to-date with the version of
com.stripe:stripe-android
we use is stripe-react-native internally. So I lean towards including it in our own library so users don't have to think about it.