Card Payments - React Native SDK
@paysafe/paysafe-card-payments is part of the Paysafe PH Mobile React Native SDK — a suite of packages that provide seamless payment integrations for React Native applications.
This package enables card payments by bridging the existing Paysafe native SDK into the React Native environment. It allows developers to integrate card payment checkout flows, handle payment results, and interact with card payments from TypeScript, while benefiting from the reliability and performance of Paysafe’s native SDK.
It resides within a monorepo alongside other React Native payment modules and uses React Native bridges to communicate with the underlying native Android SDK.
Prerequisites
API key
For information on obtaining your API key, refer to the React Native SDK Overview.
Using the Demo app
To test the card payments integration using the Demo app, you need to modify some arguments representing the API key and the account id associated with the card payments method.
- open DemoAppExpo/android/app/src/main/java/com/DemoAppExpo/MainApplication.kt file
- pass a valid API key to setupPaysafeSdk method
- open DemoAppExpo/app/(tabs)/payments.tsx file
- pass a valid account-id as a second argument to CardPayments.initialize(<currencyCode>, <accountId>, view, view, view, view) method
- pass a valid account-id in cardPaymentsTokenizeOptions
- start DemoAppExpo with npx expo run:android runner from DemoAppExpo directory
Integrating the card payments package
To integrate the card payments package into your React Native application, install it via npm:
npm install @paysafe/paysafe-card-payments@current-version
NOTE: Replace current-version with the correct version number from the GitHub version catalog.
Card form views
The card payments package does not create new Compose or XML views, nor does it render the card form UI in JavaScript. Instead, it leverages native views from our Android SDK to deliver a high-performance, fully native customer experience.
These native views are exposed to React Native using a bridge layer, allowing your React Native app to initialize, display, and interact with them just like standard React components — while still benefiting from the performance and features of the native SDK.
This bridge handles:
-
Initializing the PSCardFormController with the correct input views (card number, expiry date, CVV, etc.).
-
Listening for native events such as form initialization status and Continue button activation.
-
Calling native methods like tokenization and initialization, and returning the results back to the React Native layer.
Since the heavy lifting is done by the native SDK, the React Native code remains lightweight. UI rendering, validation, and payment logic are handled natively for optimal performance and security.
To learn more about how the native Compose views work and explore available options, refer to our Android Native SDK Documentation.
Securely collect card information from the client using PSCardFormController, a predefined cards component provided by the native SDK that collects the card number, expiration date and CVV/CVC.
STEP 1: Add hosted field views to your TypeScript code.
import { NativeEventEmitter, Alert, Button, findNodeHandle, InteractionManager, TouchableOpacity, ActivityIndicator, NativeModules } from 'react-native';
import React, { useEffect, useState, useRef } from 'react';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { useRouter } from 'expo-router';
import * as CardPayments from 'paysafe-card-payments';
export default function screen() {
const cardNumberRef = useRef(null);
const cardHolderRef = useRef(null);
const expiryRef = useRef(null);
const cvvRef = useRef(null);
useEffect(() => {
if (layoutCount === 4) {
InteractionManager.runAfterInteractions(() => {
const cardNumberViewTag = findNodeHandle(cardNumberRef.current);
const cardHolderViewTag = findNodeHandle(cardHolderRef.current);
const cvvViewTag = findNodeHandle(cvvRef.current);
const expiryViewTag = findNodeHandle(expiryRef.current);
if (cardNumberViewTag && cardHolderViewTag && cvvViewTag && expiryViewTag) {
console.log("You can proceed with initialisation!")
} else {
console.warn('Some CardPayment tags still null');
}
});
}
}, [layoutCount]);
const onCardViewLayout = () => {
setLayoutCount((count) => count + 1);
};
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={paymentsStyles.headerImage}
/>
}>
<ThemedView style={paymentsStyles.titleContainer}>
<ThemedText type="title">Explore</ThemedText>
</ThemedView>
<CardPayments.CardNumberView
ref={cardNumberRef}
style={{ width: '100%', height: 75 }}
onLayout={onCardViewLayout}
/>
<CardPayments.CardholderNameView
ref={cardHolderRef}
style={{ width: '100%', height: 75 }}
onLayout={onCardViewLayout}
/>
<CardPayments.CvvView
ref={cvvRef}
style={{ width: '100%', height: 75 }}
onLayout={onCardViewLayout}
/>
<CardPayments.ExpiryDatePickerView
ref={expiryRef}
style={{ width: '100%', height: 75 }}
onLayout={onCardViewLayout}
/>
</ParallaxScrollView>
);
}
Initialization
STEP 2: Initialize the card form controller.
The initialize method ensures that the card form controller is properly initialized with any combination of optional card input views. It also handles saved cards and communicates success or failure events back to the React Native layer.
NOTE: The initialize method can only be invoked from TypeScript code.
Initialize method signature
function initialize(
currencyCode: string,
accountId: string,
cardNumberViewTag?: number,
cardHolderNameViewTag?: number,
expiryDateViewTag?: number,
cvvViewTag?: number
)
Parameters:
- currencyCode: string – The currency code used for transactions (e.g. "USD").
- accountId: string – The account identifier associated with the card form.
- cardNumberViewTag?: number – Optional view tag for the card number input.
- cardHolderNameViewTag?: number – Optional view tag for the cardholder name input.
- expiryDateViewTag?: number – Optional view tag for the expiry date input.
- cvvViewTag?: number – Optional view tag for the CVV input. (At least CVV should be passed for successful initialization).
Behavior:
- Retrieves the current activity from reactApplicationContext.
- If the activity is null, emits a CardFormInitError event.
- If the activity does not implement LifecycleOwner, emits a CardFormInitError event.
- For each optional view tag (cardNumberViewTag, cardHolderNameViewTag, expiryDateViewTag, cvvViewTag), it tries to find the corresponding view in the activity and retrieve its ComposeView using the activity's lifecycle.
- Calls the native Paysafe SDK method PSCardFormController.initialize with:
- A PSCardFormConfig containing the currency code and account ID.
- The views retrieved (if available).
- A callback to handle success or failure.
Callback behavior:
-
onSuccess:
- If only the CVV view is present (other views are null), calls handleSavedCardSuccessfulInit for saved card initialization.
- Otherwise, calls handleSuccess to complete standard initialization (adding a new card).
- Emits a CardPaymentInitialized event.
-
onFailure:
- Emits a CardFormInitError event with the exception.
Error handling:
Any exception during the initialization process triggers a CardFormInitError with thrown exception event to notify the React Native layer.
Usage example
useEffect(() => {
if (layoutCount === 4) {
InteractionManager.runAfterInteractions(() => {
const cardNumberViewTag = findNodeHandle(cardNumberRef.current);
const cardHolderViewTag = findNodeHandle(cardHolderRef.current);
const cvvViewTag = findNodeHandle(cvvRef.current);
const expiryViewTag = findNodeHandle(expiryRef.current);
if (cardNumberViewTag && cardHolderViewTag && cvvViewTag && expiryViewTag) {
CardPayments.initialize(
'USD',
'account-id',
cardNumberViewTag,
cardHolderViewTag,
expiryViewTag,
cvvViewTag
);
} else {
console.warn('Some CardPayment tags still null');
}
});
}
}, [layoutCount]);
STEP 3: Configure options for tokenizing card data and creating a card payment handle token.
/// cardPaymentsTokenizeOptions is object of type ReadableMap
/// Payment amount in minor units
const cardPaymentsTokenizeOptions = {
amount: 10000,
currencyCode: 'USD',
transactionType: 'PAYMENT',
merchantRefNum: 'merchant_ref_' + Math.floor(Math.random() * 1000000),
billingDetails: {
nickName: 'NickName',
street: 'Street',
city: 'City',
state: 'AL',
country: 'US',
zip: '12345',
},
profile: {
firstName: 'John',
lastName: 'Doe',
locale: 'EN_GB',
merchantCustomerId: 'customer_123',
dateOfBirth: {
day: 1,
month: 1,
year: 1990,
},
email: 'email@mail.com',
phone: '0123456789',
mobile: '0123456789',
gender: 'MALE',
nationality: 'US',
},
accountId: 'account-id',
merchantDescriptor: {
dynamicDescriptor: 'dynamicDescriptor',
phone: '0123456789',
},
shippingDetails: {
shipMethod: 'NEXT_DAY_OR_OVERNIGHT',
street: 'Street',
street2: 'Street2',
city: 'Marbury',
state: 'AL',
countryCode: 'US',
zip: '36051',
},
renderType: 'BOTH',
threeDs: {
merchantUrl: 'https://api.qa.paysafe.com/checkout/v2/index.html#/desktop',
process: true
},
};
Tokenization
STEP 4: Trigger tokenization on button click.
Call CardPayments.tokenize when the customer clicks a button. (Note that this button should be implemented by you).
This function handles card tokenization by sending the card details to the cardController and reporting the results back to the React Native layer.
Tokenize method signature
/**
Tokenization using Card Payments from native android code.
@param readableCardTokenizeOptions containing various parameters for tokenization of type ReadableMap
fun tokenize(
readableCardTokenizeOptions: ReadableMap
)
/**
Tokenization using Card Payments from typescript.
@param readableCardPaymentsTokenizeOptions containing various parameters for tokenization of type ReadableMap
function tokenize(readableCardPaymentsTokenizeOptions: unknown)
Parameters:
- readableCardTokenizeOptions: ReadableMap – A map containing the card data and options required for tokenization.
These options are detailed on the Paysafe Android SDK Tokenize page.
Behavior:
-
Check for cardController existence:
- If cardController is null, a CardFormTokenizeError event is immediately sent with a relevant exception, and the function returns. (The initialize method must be successfully completed before calling tokenize).
-
Handle tokenization results:
- If the result is PSResult.Success, a CardTokenizationSuccessful event is sent with the resulting paymentResult (the response value returned from the tokenize method).
- If the result is PSResult.Failure, a CardTokenizationFailed event is sent with the exception.
- If the result is of an unsupported type (neither success nor failure), a CardTokenizationFailed event is sent with a generic exception.
-
Error handling:
- Any exceptions thrown during tokenization are caught and reported via the CardTokenizationFailed event.
Usage example
const handleSubmitPayment = () => {
setIsCardTokenizing(true);
try {
CardPayments.tokenize(cardPaymentsTokenizeOptions);
} catch (error: unknown) {
setIsCardTokenizing(false);
Alert.alert('Error', `CardPayments tokenization failed: ${error?.message || error}`);
}
};
This tokenize method returns a PaymentHandle result, which resolves to a single-use payment handle. This handle can be used with the Payments API to process a payment. Single-use handles are valid for only 5 minutes and are not consumed by verification.
For more details, see Payments with a Handle.
STEP 5: Complete payment with PaymentHandleToken.
Send the payment handle token received from PSCardFormController.tokenize() to your merchant server.
From there, use the standard Payments API payments endpoint to process the payment.
curl -location -request POST 'https://api.test.paysafe.com/paymenthub/v1/payments' \
- u apiKeyId:apiKeyPassword
}'
- header 'Content-Type: application/json' \
- data-raw '{
"merchantRefNum": "{{paymentHandleToken}}",
"amount": 5000,
"currencyCode": "USD",
"settleWithAuth": true,
"paymentHandleToken": "CARDS PAYMENT TOKEN RECEIVED FROM CLIENT BROWSER"
}'
Test the integration
By this point you should have a basic card integration that collects card details and processes payments.
There are several test cards you can use in test mode to make sure this integration is ready. Use them with any CVC, postal code, and future expiration date.
| Card number | Description |
|---|---|
| 4835641100110000 | Succeeds and immediately processes the payment. |
| 5100400000000000 | Requires authentication. Paysafe will trigger a modal asking for the customer to authenticate. |
| 5573560100022200 | Always fails with a decline code of insufficient_funds. |
For the full list of test cards, see our testing guide.
Card payment exceptions
| Error code | Display message | Detailed message | Comments |
|---|---|---|---|
9061 | There was an error (9061), please contact our support. | Invalid account id for ${paymentMethod} | AccountId is present, but not configured for the intended payment context. |
9101 | There was an error (9101), please contact our support. | Invalid accountId parameter | The SDK checks if the accountId string contains only numbers. |
9055 | There was an error (9055), please contact our support. | Invalid currency parameter | The SDK checks if the currency parameter has only 3 letters. This exception is thrown when the server responds with code 5001. |
9085 | There was an error (9085), please contact our support. | There are no available payment methods for this API key. |
|
9031 | There was an error (9031), please contact our support. | Invalid card number separator | ['-', ' ', '']; these are the allowed card number separators. |
9084 | There was an error (9084), please contact our support. | Failed to load available payment methods | Triggered by an internal server error during the payment methods API call. |
9127 | There was an error (9127), please contact our support. | resetCardDetails is available only when card fields are initialized |
|
9073 | There was an error (9073), please contact our support. | Account not configured correctly. | Caused by an improperly created merchant account configuration:
|
9131 | There was an error (9131), please contact our support. | Status of the payment handle is ${status} | Thrown when the payment handle status is checked and is FAILED. |