Search Overlay

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:

  1. 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.
  2. 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.
  3. 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:

  1. 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).
  2. 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.
  3. 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:

  • The merchant has no account or payment methods configured.
  • The account that is provided on initialization is not configured for the merchant.

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.