diff --git a/android/src/main/java/com/reactnativestripesdk/FakeOnrampSdkModule.kt b/android/src/main/java/com/reactnativestripesdk/FakeOnrampSdkModule.kt index be7acb4ec2..b44b6362b7 100644 --- a/android/src/main/java/com/reactnativestripesdk/FakeOnrampSdkModule.kt +++ b/android/src/main/java/com/reactnativestripesdk/FakeOnrampSdkModule.kt @@ -118,6 +118,14 @@ class FakeOnrampSdkModule( promise?.resolveNotImplemented() } + @ReactMethod + override fun getCryptoTokenDisplayData( + token: ReadableMap, + promise: Promise, + ) { + promise?.resolveNotImplemented() + } + private fun Promise.resolveNotImplemented() { this.resolve( createFailedError( diff --git a/android/src/oldarch/java/com/reactnativestripesdk/NativeOnrampSdkModuleSpec.java b/android/src/oldarch/java/com/reactnativestripesdk/NativeOnrampSdkModuleSpec.java index 8e7bceaeec..e53820d429 100644 --- a/android/src/oldarch/java/com/reactnativestripesdk/NativeOnrampSdkModuleSpec.java +++ b/android/src/oldarch/java/com/reactnativestripesdk/NativeOnrampSdkModuleSpec.java @@ -93,6 +93,10 @@ protected final void emitOnCheckoutClientSecretRequested(ReadableMap value) { @DoNotStrip public abstract void onrampAuthorize(String linkAuthIntentId, Promise promise); + @ReactMethod + @DoNotStrip + public abstract void getCryptoTokenDisplayData(ReadableMap token, Promise promise); + @ReactMethod @DoNotStrip public abstract void logout(Promise promise); diff --git a/android/src/onramp/java/com/reactnativestripesdk/OnrampSdkModule.kt b/android/src/onramp/java/com/reactnativestripesdk/OnrampSdkModule.kt index 2eedb4dfb2..4ea8ab3692 100644 --- a/android/src/onramp/java/com/reactnativestripesdk/OnrampSdkModule.kt +++ b/android/src/onramp/java/com/reactnativestripesdk/OnrampSdkModule.kt @@ -49,6 +49,9 @@ import com.stripe.android.link.LinkAppearance import com.stripe.android.link.LinkAppearance.Colors import com.stripe.android.link.LinkAppearance.PrimaryButton import com.stripe.android.link.LinkAppearance.Style +import com.stripe.android.link.LinkController.PaymentMethodPreview +import com.stripe.android.link.PaymentMethodPreviewDetails +import com.stripe.android.model.CardBrand import com.stripe.android.paymentsheet.PaymentSheet import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -526,6 +529,80 @@ class OnrampSdkModule( presenter.authorize(linkAuthIntentId) } + @ReactMethod + override fun getCryptoTokenDisplayData( + token: ReadableMap, + promise: Promise, + ) { + val context = reactApplicationContext + + val paymentDetails: PaymentMethodPreview? = + when { + token.hasKey("card") -> { + val cardMap = token.getMap("card") + if (cardMap != null) { + val brand = cardMap.getString("brand") ?: "" + val funding = cardMap.getString("funding") ?: "" + val last4 = cardMap.getString("last4") ?: "" + val cardBrand = CardBrand.fromCode(brand) + + PaymentMethodPreview.create( + context = context, + details = + PaymentMethodPreviewDetails.Card( + brand = cardBrand, + funding = funding, + last4 = last4, + ), + ) + } else { + null + } + } + token.hasKey("us_bank_account") -> { + val bankMap = token.getMap("us_bank_account") + if (bankMap != null) { + val bankName = bankMap.getString("bank_name") + val last4 = bankMap.getString("last4") ?: "" + PaymentMethodPreview.create( + context = context, + details = + PaymentMethodPreviewDetails.BankAccount( + bankIconCode = null, + bankName = bankName, + last4 = last4, + ), + ) + } else { + null + } + } + else -> null + } + + if (paymentDetails == null) { + promise.resolve( + createFailedError( + IllegalArgumentException("Unsupported payment method"), + ), + ) + return + } + + val icon = + currentActivity + ?.let { ContextCompat.getDrawable(it, paymentDetails.iconRes) } + ?.let { "data:image/png;base64," + getBase64FromBitmap(getBitmapFromDrawable(it)) } + + val displayData = Arguments.createMap() + + displayData.putString("icon", icon) + displayData.putString("label", paymentDetails.label) + displayData.putString("sublabel", paymentDetails.sublabel) + + promise.resolve(createResult("displayData", displayData)) + } + @ReactMethod override fun logout(promise: Promise) { val coordinator = diff --git a/example/src/screens/Onramp/CryptoOnrampFlow.tsx b/example/src/screens/Onramp/CryptoOnrampFlow.tsx index 5bd5c1fa5b..0d8518b531 100644 --- a/example/src/screens/Onramp/CryptoOnrampFlow.tsx +++ b/example/src/screens/Onramp/CryptoOnrampFlow.tsx @@ -55,6 +55,7 @@ export default function CryptoOnrampFlow() { createCryptoPaymentToken, performCheckout, authorize, + getCryptoTokenDisplayData, logOut, isAuthError, } = useOnramp(); @@ -75,7 +76,7 @@ export default function CryptoOnrampFlow() { const [cardPaymentMethod] = useState('Card'); const [bankAccountPaymentMethod] = useState('BankAccount'); - const [paymentDisplayData, setPaymentDisplayData] = + const [currentPaymentDisplayData, setCurrentPaymentDisplayData] = useState(null); const [cryptoPaymentToken, setCryptoPaymentToken] = useState( @@ -133,6 +134,33 @@ export default function CryptoOnrampFlow() { } }, [userInfo.email, hasLinkAccount]); + const showPaymentData = useCallback(async () => { + const cardParams: Onramp.CryptoPaymentToken = { + card: { + brand: 'visa', + funding: 'credit', + last4: '1234', + }, + }; + + const bankParams: Onramp.CryptoPaymentToken = { + us_bank_account: { + bank_name: 'Bank of America', + last4: '5678', + }, + }; + + const cardData = (await getCryptoTokenDisplayData(cardParams)).displayData; + const bankData = (await getCryptoTokenDisplayData(bankParams)).displayData; + + if (cardData) { + setCurrentPaymentDisplayData(cardData); + console.log('Bank Payment Data:', bankData); + } else { + Alert.alert('No Payment Data', 'No payment data available to display.'); + } + }, [getCryptoTokenDisplayData]); + const handlePresentVerification = useCallback(async () => { if (!userInfo.email) { showError('Please enter an email address first.'); @@ -299,7 +327,7 @@ export default function CryptoOnrampFlow() { if (result?.error) { showError(`Could not collect payment: ${result.error.message}.`); } else if (result?.displayData) { - setPaymentDisplayData(result.displayData); + setCurrentPaymentDisplayData(result.displayData); } else { showCanceled('Payment collection cancelled, please try again.'); } @@ -345,7 +373,8 @@ export default function CryptoOnrampFlow() { if (!customerId) missingItems.push('customer authentication'); if (!walletAddress || !walletNetwork) missingItems.push('wallet address registration'); - if (!paymentDisplayData) missingItems.push('payment method selection'); + if (!currentPaymentDisplayData) + missingItems.push('payment method selection'); if (!cryptoPaymentToken) missingItems.push('crypto payment token creation'); if (!authToken) missingItems.push('authentication token'); @@ -359,7 +388,7 @@ export default function CryptoOnrampFlow() { customerId, walletAddress, walletNetwork, - paymentDisplayData, + currentPaymentDisplayData, cryptoPaymentToken, authToken, ]); @@ -480,7 +509,7 @@ export default function CryptoOnrampFlow() { setResponse(null); setIsLinkUser(false); setCustomerId(null); - setPaymentDisplayData(null); + setCurrentPaymentDisplayData(null); setCryptoPaymentToken(null); setAuthToken(null); setWalletAddress(null); @@ -528,7 +557,7 @@ export default function CryptoOnrampFlow() { + +