Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f8f55fe
Initial implementation of returning display data
Twigz Sep 16, 2025
121eded
Allow bank account as well
Twigz Sep 16, 2025
429d68d
Initial implementation on both platforms
Twigz Sep 17, 2025
b21119e
Remove bankIconCode
Twigz Sep 17, 2025
2362dd7
update for latest android changes
Twigz Sep 17, 2025
a4b2694
Fills out more of the `paymentDisplayData` implementation on iOS
mliberatore Sep 17, 2025
42ea0cd
Update api to be promise based
Twigz Sep 17, 2025
ef5f0b4
Fix up to use result
Twigz Sep 17, 2025
e7867e6
Merge branch 'beta/crypto-onramp' into twigz/crypto-onramp-payment-di…
Twigz Sep 17, 2025
3f8a072
Update CryptoOnrampFlow.tsx
Twigz Sep 17, 2025
dd84df6
Fix errors
Twigz Sep 17, 2025
d2ec9e9
Uses alternate method for getting card brand
mliberatore Sep 17, 2025
9c9dcd0
Cleanup
Twigz Sep 17, 2025
559cdc0
Update Onramp.ts
Twigz Sep 17, 2025
9249245
Update StripeSdkImpl.swift
Twigz Sep 17, 2025
82e5e58
Rename function
Twigz Sep 17, 2025
d77b870
Update typing information
Twigz Sep 17, 2025
a7a30d9
Refactors iOS `getCryptoTokenDisplayData` to return dictionary synchr…
mliberatore Sep 17, 2025
8aed51e
Fix compilation issue
Twigz Sep 17, 2025
d438455
Update RN apis and kotlin parsing
Twigz Sep 17, 2025
272a97a
Updates iOS for new payment token dictionary structure
mliberatore Sep 17, 2025
7748fc8
Update CryptoOnrampFlow.tsx
Twigz Sep 17, 2025
060178a
Merge branch 'twigz/crypto-onramp-payment-display-data' of https://gi…
Twigz Sep 17, 2025
d1f1d4f
Fix pr comments
Twigz Sep 17, 2025
4edb7e7
Re-implement promise API
Twigz Sep 18, 2025
1180dde
Fix objc name
Twigz Sep 18, 2025
6af923f
Update to latest iOS SDK version
Twigz Sep 18, 2025
d1dfea4
Update iOS version
Twigz Sep 22, 2025
dfbc2fc
update android sdk to 21.27.0
Twigz Sep 22, 2025
b13e0ea
Merge branch 'beta/crypto-onramp' into twigz/crypto-onramp-payment-di…
Twigz Sep 22, 2025
c5cefcb
Fix formatting
Twigz Sep 22, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
48 changes: 42 additions & 6 deletions example/src/screens/Onramp/CryptoOnrampFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function CryptoOnrampFlow() {
createCryptoPaymentToken,
performCheckout,
authorize,
getCryptoTokenDisplayData,
logOut,
isAuthError,
} = useOnramp();
Expand All @@ -75,7 +76,7 @@ export default function CryptoOnrampFlow() {
const [cardPaymentMethod] = useState('Card');
const [bankAccountPaymentMethod] = useState('BankAccount');

const [paymentDisplayData, setPaymentDisplayData] =
const [currentPaymentDisplayData, setCurrentPaymentDisplayData] =
useState<PaymentOptionData | null>(null);

const [cryptoPaymentToken, setCryptoPaymentToken] = useState<string | null>(
Expand Down Expand Up @@ -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.');
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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');

Expand All @@ -359,7 +388,7 @@ export default function CryptoOnrampFlow() {
customerId,
walletAddress,
walletNetwork,
paymentDisplayData,
currentPaymentDisplayData,
cryptoPaymentToken,
authToken,
]);
Expand Down Expand Up @@ -480,7 +509,7 @@ export default function CryptoOnrampFlow() {
setResponse(null);
setIsLinkUser(false);
setCustomerId(null);
setPaymentDisplayData(null);
setCurrentPaymentDisplayData(null);
setCryptoPaymentToken(null);
setAuthToken(null);
setWalletAddress(null);
Expand Down Expand Up @@ -528,7 +557,7 @@ export default function CryptoOnrampFlow() {
<OnrampResponseStatusSection
response={response}
customerId={customerId}
paymentDisplayData={paymentDisplayData}
paymentDisplayData={currentPaymentDisplayData}
cryptoPaymentToken={cryptoPaymentToken}
authToken={authToken}
walletAddress={walletAddress}
Expand All @@ -554,6 +583,13 @@ export default function CryptoOnrampFlow() {

{isLinkUser === true && customerId != null && (
<>
<View style={{ paddingHorizontal: 16 }}>
<Button
title="Display Static Payment Data"
onPress={showPaymentData}
variant="primary"
/>
</View>
<AttachKycInfoSection
userInfo={userInfo}
setUserInfo={setUserInfo}
Expand Down
7 changes: 7 additions & 0 deletions ios/StripeOnrampSdk.mm
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ - (instancetype)init
[StripeSdkImpl.shared onrampAuthorize:linkAuthIntentId resolver:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(getCryptoTokenDisplayData:(nonnull NSDictionary *)token
resolve:(nonnull RCTPromiseResolveBlock)resolve
reject:(nonnull RCTPromiseRejectBlock)reject)
{
[StripeSdkImpl.shared getCryptoTokenDisplayData:token resolver:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(initialise:(nonnull NSDictionary *)params
resolve:(nonnull RCTPromiseResolveBlock)resolve
reject:(nonnull RCTPromiseRejectBlock)reject)
Expand Down
69 changes: 69 additions & 0 deletions ios/StripeSdkImpl.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PassKit
@_spi(DashboardOnly) @_spi(STP) import Stripe
@_spi(EmbeddedPaymentElementPrivateBeta) import StripePaymentSheet
@_spi(AppearanceAPIAdditionsPreview) import StripePaymentSheet
@_spi(STP) import StripePaymentSheet
#if canImport(StripeCryptoOnramp)
@_spi(STP) import StripeCryptoOnramp
Expand Down Expand Up @@ -1556,6 +1557,49 @@ public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate {
}
}

@objc(getCryptoTokenDisplayData:resolver:rejecter:)
public func getCryptoTokenDisplayData(
token: NSDictionary,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) -> Void {
let label = STPPaymentMethodType.link.displayName

if let cardDetails = token["card"] as? [String: Any] {
let brand = cardDetails["brand"] as? String ?? ""
let funding = cardDetails["funding"] as? String ?? ""
let last4 = cardDetails["last4"] as? String ?? ""

let cardBrand = STPCard.brand(from: brand)
let icon = STPImageLibrary.cardBrandImage(for: cardBrand)
let brandName = STPCardBrandUtilities.stringFrom(cardBrand)

let mappedFunding = STPCardFundingType(funding)
let formattedBrandName = String(format: mappedFunding.displayNameWithBrand, brandName ?? "")
let sublabel = "\(formattedBrandName) •••• \(last4)"

let result = PaymentMethodDisplayData(icon: icon, label: label, sublabel: sublabel)
let displayData = Mappers.paymentMethodDisplayDataToMap(result)

resolve(["displayData": displayData])
} else if let bankDetails = token["us_bank_account"] as? [String: Any] {
let bankName = bankDetails["bank_name"] as? String ?? ""
let last4 = bankDetails["last4"] as? String ?? ""

let iconCode = PaymentSheetImageLibrary.bankIconCode(for: bankName)
let icon = PaymentSheetImageLibrary.bankIcon(for: iconCode, iconStyle: .filled)
let sublabel = "\(bankName) •••• \(last4)"

let result = PaymentMethodDisplayData(icon: icon, label: label, sublabel: sublabel)
let displayData = Mappers.paymentMethodDisplayDataToMap(result)

resolve(["displayData": displayData])
} else {
let errorResult = Errors.createError(ErrorType.Unknown, "'type' parameter not unknown.")
resolve(["error": errorResult["error"]!])
}
}

/// Checks for a `publishableKey`. Calls the resolve block with an error when one doesn’t exist.
/// - Parameter resolve: The resolve block that is called with an error if no `publishableKey` is found.
/// - Returns: `true` if a `publishableKey` was found. `false` otherwise.
Expand Down Expand Up @@ -1651,6 +1695,11 @@ public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate {
resolveWithCryptoOnrampNotAvailableError(resolve)
}

@objc(getCryptoTokenDisplayData:)
public func getCryptoTokenDisplayData(token: NSDictionary) -> [String: Any]? {
return nil
}

private func resolveWithCryptoOnrampNotAvailableError(_ resolver: @escaping RCTPromiseResolveBlock) {
resolver(Errors.createError(ErrorType.Failed, "StripeCryptoOnramp is not available. To enable, add the 'stripe-react-native/Onramp' subspec to your Podfile."))
}
Expand Down Expand Up @@ -1770,3 +1819,23 @@ extension FinancialConnectionsSheet.Configuration {
self.init(style: style)
}
}

private extension STPCardFundingType {
var displayNameWithBrand: String {
switch self {
case .credit: String.Localized.Funding.credit
case .debit: String.Localized.Funding.debit
case .prepaid: String.Localized.Funding.prepaid
case .other: String.Localized.Funding.default
}
}

init(_ typeString: String) {
self = switch typeString {
case "debit": .debit
case "credit": .credit
case "prepaid": .prepaid
default: .other
}
}
}
21 changes: 20 additions & 1 deletion src/hooks/useOnramp.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { EventSubscription } from 'react-native';
import NativeOnrampSdk from '../specs/NativeOnrampSdkModule';
import type { Onramp, OnrampError, StripeError } from '../types';
import { Onramp, OnrampError, StripeError } from '../types';
import type { PlatformPay } from '../types';
import { useCallback } from 'react';
import { addOnrampListener } from '../events';
import { CryptoPaymentToken } from '../types/Onramp';

let onCheckoutClientSecretRequestedSubscription: EventSubscription | null =
null;
Expand Down Expand Up @@ -140,6 +141,15 @@ export function useOnramp() {
[]
);

const _getCryptoTokenDisplayData = useCallback(
async (
token: CryptoPaymentToken
): Promise<Onramp.PaymentDisplayDataResult> => {
return NativeOnrampSdk.getCryptoTokenDisplayData(token);
},
[]
);

const _logOut = useCallback(async (): Promise<{
error?: StripeError<OnrampError>;
}> => {
Expand Down Expand Up @@ -259,6 +269,15 @@ export function useOnramp() {
*/
authorize: _authorize,

/**
* Retrieves display data (icon, label, sublabel) for the given payment method details.
* Suitable for rendering in the UI to summarize the selected payment method.
*
* @param token The token containing payment method details (card or bank account) to get display data for
* @returns Promise that resolves to an object with displayData or error
*/
getCryptoTokenDisplayData: _getCryptoTokenDisplayData,

/**
* Logs out the current user from their Link account.
*
Expand Down
Loading
Loading