Search Overlay

Saved Cards - React Native SDK

The Saved Cards feature in the Paysafe React Native SDK enables customers to complete payments more quickly by using a previously stored card from their profile.

Saving a card to the customer profile

After successfully generating a single-use payment handle for a card payment via the mobile SDK, you can associate it with a customer profile depending on whether the customer is new or existing.

Paysafe recommends either completing a successful payment using the token or verifying that the token corresponds to a valid card before adding it to a customer profile.

Tokenize with optional fields

If a customer chooses to pay with a new card, you can display the Card Number and Expiry Date fields. These fields become mandatory if the singleUseCustomerToken and paymentTokenFrom parameters are not passed in the tokenize function call.

Depending on your implementation, it is also possible to display only the CVV field, if needed.

If the customer already has a profile created in the Payments API and has one or more saved cards, you can instruct Paysafe.js to tokenize a selected saved card by passing both a single-use customer token and a single-use payment handle to the tokenize function.

  • The single-use customer token is a temporary token that represents the customer profile. It helps prevent exposing the customer ID and multi-use payment token on the front end.
  • The single-use payment handle is a temporary token representing the card details. It is found in the paymentHandles object returned when the single-use customer token is created.

If the customer has multiple saved cards, you must ensure the correct single-use payment handle is passed, based on the selected card.

Prior to tokenization, complete these steps:

  1. Create a single-use customer token on your backend server.
  2. Retrieve the singleUseCustomerToken and the selected card’s paymentHandleToken from the response.
  3. Send both tokens to the frontend.
  4. Pass them to the tokenize function using the singleUseCustomerToken and paymentTokenFrom parameters in the options object.

Using the Demo app

To test the Saved Cards integration using the Demo app, you’ll need to modify some arguments representing the API key and the account ID associated with the card payment method.

  • open DemoAppExpo/android/app/src/main/java/com/DemoAppExpo/MainApplication.kt file

  • pass a valid API key to setupPaysafeSdk method
  • open DemoAppExpo/app/cardDetailScreen.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
  • open DemoAppExpo/android/app/src/main/java/com/DemoAppExpo/savedCards/api/Retrofit.kt file
  • extend addHeader(HEADER_AUTHORIZATION, "Basic ") with a valid merchant backend api key: addHeader(HEADER_AUTHORIZATION, "Basic <merachnt-backend-api-key>")
  • open DemoAppExpo/app/savedCardScreen.tsx 
  • add a valid profile id as an argument to the fetchSavedCards(profile-id) method
  • start DemoAppExpo with npx expo run:android runner from the DemoAppExpo directory

Usage example

The CardPaymentsNativeModule provides the repository and service needed to fetch saved cards.

import React, { useEffect, useState, useRef } from 'react';
import {
View,
Text,
TouchableOpacity,
ActivityIndicator,
Alert,
findNodeHandle,
NativeEventEmitter,
InteractionManager,
} from 'react-native';

const { CardPaymentsNativeModule } = NativeModules;

const SimpleSavedCardPayment = () => {
const [cards, setCards] = useState<any[]>([]);
const [selectedCard, setSelectedCard] = useState<any | null>(null);

const [isCardPaymentInitialized, setIsCardPaymentInitialized] = useState(false);
const [loadingCardPayments, setLoadingCardPayments] = useState(false);
const [isCardPaymentSubmitEnabled, setIsCardPaymentSubmitEnabled] = useState(false);
const [isCardTokenizing, setIsCardTokenizing] = useState(false);
const [layoutCount, setLayoutCount] = useState(0);

const cvvRef = useRef(null);

useEffect(() => {
const fetchCards = async () => {
try {
const saved = await CardPaymentsNativeModule.fetchSavedCards("profile-id");
setCards(saved);
} catch (e) {
Alert.alert('Error', 'Failed to fetch saved cards');
}
};
fetchCards();
}, []);

useEffect(() => {
if (layoutCount === 1 && selectedCard) {
InteractionManager.runAfterInteractions(() => {
if (!isCardPaymentInitialized) {
const cvvTag = findNodeHandle(cvvRef.current);
if (cvvTag) {
setLoadingCardPayments(true);
CardPayments.initialize(
'USD',
'1001234110',
null,
null,
null,
cvvTag
);
} else {
console.warn('CVV view not ready yet');
}
}
});
}
}, [layoutCount, isCardPaymentInitialized, selectedCard]);

const onCardViewLayout = () => {
setLayoutCount((count) => count + 1);
};

useEffect(() => {
const eventEmitter = new NativeEventEmitter(CardPayments);

const initSuccess = eventEmitter.addListener('CardPaymentInitialized', () => {
setIsCardPaymentInitialized(true);
setLoadingCardPayments(false);
});

const initFail = eventEmitter.addListener('CardFormInitError', (error) => {
setIsCardPaymentInitialized(false);
setLoadingCardPayments(false);
Alert.alert(error?.title ?? 'Init Failed', error?.message ?? 'Unknown error.');
});

const submitEnabled = eventEmitter.addListener('CardPaymentEnabled', () => {
setIsCardPaymentSubmitEnabled(true);
});

const tokenSuccess = eventEmitter.addListener('CardsTokenizationSuccessful', (res) => {
setIsCardTokenizing(false);
Alert.alert('Success', 'Payment Tokenized!\n' + JSON.stringify(res));
});

const tokenFail = eventEmitter.addListener('CardFormTokenizeError', (error) => {
setIsCardTokenizing(false);
Alert.alert('Payment Failed', error?.message ?? 'Tokenization error.');
});

return () => {
initSuccess.remove();
initFail.remove();
submitEnabled.remove();
tokenSuccess.remove();
tokenFail.remove();
};
}, []);

const handleSubmit = () => {
if (!selectedCard) return;
setIsCardTokenizing(true);

const opts = {
amount: 100,
currencyCode: 'USD',
transactionType: 'PAYMENT',
merchantRefNum: 'merchant_ref_' + Math.floor(Math.random() * 1000000),
billingDetails: {
nickName: "John Doe's card",
street: "5335 Gate Parkway Fourth Floor",
city: "Jacksonville",
state: "FL",
country: "US",
zip: "32256",
},
profile: {
firstName: "firstName",
lastName: "lastName",
locale: "EN_GB",
merchantCustomerId: "merchantCustomerId",
dateOfBirth: { day: 1, month: 1, year: 1990 },
email: "email@mail.com",
phone: "0123456789",
mobile: "0123456789",
gender: "MALE",
nationality: "nationality",
identityDocuments: [{ documentNumber: "SSN123456" }],
},
accountId: '1001234110',
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,
},
paymentHandleTokenFrom: selectedCard.paymentHandleTokenFrom,
singleUseCustomerToken: selectedCard.singleUseCustomerToken,
};

try {
CardPayments.tokenize(opts);
} catch (e) {
setIsCardTokenizing(false);
Alert.alert('Error', e?.message || 'Unknown tokenization error');
}
};

return (
<View style={{ flex: 1, padding: 20 }}>
{!selectedCard ? (
<>
<Text style={{ fontSize: 18, marginBottom: 10 }}>Select a Saved Card</Text>
{cards.map((card, i) => (
<TouchableOpacity
key={i}
onPress={() => {
setSelectedCard(card);
setIsCardPaymentInitialized(false);
setIsCardPaymentSubmitEnabled(false);
setLayoutCount(0);
}}
style={{
padding: 15,
marginVertical: 5,
backgroundColor: '#eee',
borderRadius: 8,
}}
>
<Text>{card.creditCardType} *{card.lastDigits}</Text>
<Text>{card.holderName}</Text>
</TouchableOpacity>
))}
</>
) : (
<>
<Text style={{ fontSize: 18, marginBottom: 10 }}>
Enter CVV for {selectedCard.creditCardType}
</Text>

<CardPayments.CvvView
ref={cvvRef}
style={{ width: '100%', height: 75, marginTop: 20 }}
cardType={selectedCard.creditCardType}
onLayout={onCardViewLayout}
/>

{isCardPaymentInitialized && !loadingCardPayments && (
<TouchableOpacity
onPress={handleSubmit}
disabled={!isCardPaymentSubmitEnabled || isCardTokenizing}
style={{
backgroundColor: (!isCardPaymentSubmitEnabled || isCardTokenizing) ? '#aaa' : '#007bff',
padding: 10,
borderRadius: 8,
alignItems: 'center',
marginTop: 20,
}}
>
{isCardTokenizing ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={{ color: '#fff', fontSize: 16 }}>Submit Payment</Text>
)}
</TouchableOpacity>
)}
</>
)}
</View>
);
};

export default SimpleSavedCardPayment;