React Native - Accept Payments
Learn how to accept payments in your React Native application using Omise. This guide covers cross-platform payment integration with both iOS and Android support.
Overviewโ
Integrating Omise payments in React Native applications requires combining native SDK capabilities with JavaScript/TypeScript code. This guide provides a complete implementation strategy for accepting payments across platforms.
Key Featuresโ
- Cross-Platform Support - Single codebase for iOS and Android
- Native Performance - Leverages native SDKs under the hood
- TypeScript Support - Full type definitions included
- React Hooks - Modern React patterns and hooks
- 3D Secure Handling - Seamless authentication flows
- Expo Compatible - Works with managed Expo workflow
Prerequisitesโ
Before implementing payment acceptance:
- React Native environment set up (0.64+)
- Native modules configured for iOS and Android
- Backend API for charge creation
- Omise account with API keys
Installationโ
Install Native Dependenciesโ
# Using npm
npm install omise-react-native
# Using yarn
yarn add omise-react-native
# Install iOS dependencies
cd ios && pod install && cd ..
Configure Androidโ
Add to android/app/build.gradle:
dependencies {
implementation 'co.omise:omise-android:3.1.0'
}
Configure iOSโ
The CocoaPods installation handles iOS configuration automatically. Ensure minimum iOS version in ios/Podfile:
platform :ios, '12.0'
Link Native Modules (React Native < 0.60)โ
react-native link omise-react-native
Basic Payment Flowโ
Setup Omise Providerโ
import React from 'react';
import { OmiseProvider } from 'omise-react-native';
export default function App() {
return (
<OmiseProvider publicKey="pkey_test_123">
<AppNavigator />
</OmiseProvider>
);
}
Create Payment Hookโ
import { useState, useCallback } from 'react';
import { createToken } from 'omise-react-native';
import { Alert } from 'react-native';
interface CardData {
number: string;
name: string;
expiryMonth: number;
expiryYear: number;
securityCode: string;
}
export function usePayment() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const processPayment = useCallback(
async (cardData: CardData, amount: number, currency: string) => {
setLoading(true);
setError(null);
try {
// Step 1: Create token
const token = await createToken({
card: {
name: cardData.name,
number: cardData.number,
expiration_month: cardData.expiryMonth,
expiration_year: cardData.expiryYear,
security_code: cardData.securityCode,
},
});
// Step 2: Create charge on backend
const charge = await createCharge({
token: token.id,
amount,
currency,
return_uri: 'myapp://payment/complete',
});
// Step 3: Handle 3D Secure if needed
if (charge.authorize_uri) {
await handle3DSecure(charge.authorize_uri);
}
return charge;
} catch (err: any) {
setError(err.message);
Alert.alert('Payment Error', err.message);
throw err;
} finally {
setLoading(false);
}
},
[]
);
return { processPayment, loading, error };
}
Payment Form Componentโ
import React, { useState } from 'react';
import {
View,
TextInput,
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { usePayment } from './usePayment';
interface PaymentFormProps {
amount: number;
currency: string;
onSuccess: () => void;
}
export default function PaymentForm({
amount,
currency,
onSuccess,
}: PaymentFormProps) {
const [cardNumber, setCardNumber] = useState('');
const [cardholderName, setCardholderName] = useState('');
const [expiry, setExpiry] = useState('');
const [cvv, setCvv] = useState('');
const { processPayment, loading } = usePayment();
const handleSubmit = async () => {
// Parse expiry
const [month, year] = expiry.split('/').map(s => parseInt(s.trim()));
// Validate
if (!validateForm()) {
return;
}
try {
await processPayment(
{
number: cardNumber.replace(/\s/g, ''),
name: cardholderName,
expiryMonth: month,
expiryYear: 2000 + year, // Convert YY to YYYY
securityCode: cvv,
},
amount,
currency
);
onSuccess();
} catch (error) {
// Error already handled in hook
}
};
const validateForm = (): boolean => {
if (cardNumber.replace(/\s/g, '').length < 13) {
Alert.alert('Error', 'Invalid card number');
return false;
}
if (!cardholderName.trim()) {
Alert.alert('Error', 'Cardholder name required');
return false;
}
if (!expiry.match(/^\d{2}\/\d{2}$/)) {
Alert.alert('Error', 'Invalid expiry date (MM/YY)');
return false;
}
if (cvv.length < 3) {
Alert.alert('Error', 'Invalid CVV');
return false;
}
return true;
};
const formatCardNumber = (text: string) => {
const cleaned = text.replace(/\s/g, '');
const formatted = cleaned.match(/.{1,4}/g)?.join(' ') || cleaned;
setCardNumber(formatted);
};
const formatExpiry = (text: string) => {
const cleaned = text.replace(/\D/g, '');
if (cleaned.length >= 2) {
setExpiry(`${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}`);
} else {
setExpiry(cleaned);
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Card Number"
value={cardNumber}
onChangeText={formatCardNumber}
keyboardType="number-pad"
maxLength={19}
editable={!loading}
/>
<TextInput
style={styles.input}
placeholder="Cardholder Name"
value={cardholderName}
onChangeText={setCardholderName}
autoCapitalize="words"
editable={!loading}
/>
<View style={styles.row}>
<TextInput
style={[styles.input, styles.halfInput]}
placeholder="MM/YY"
value={expiry}
onChangeText={formatExpiry}
keyboardType="number-pad"
maxLength={5}
editable={!loading}
/>
<TextInput
style={[styles.input, styles.halfInput]}
placeholder="CVV"
value={cvv}
onChangeText={setCvv}
keyboardType="number-pad"
maxLength={4}
secureTextEntry
editable={!loading}
/>
</View>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSubmit}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>
Pay {currency} {(amount / 100).toFixed(2)}
</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
marginBottom: 12,
fontSize: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
halfInput: {
width: '48%',
},
button: {
backgroundColor: '#4CAF50',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonDisabled: {
backgroundColor: '#ccc',
},
buttonText: {
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
},
});
Handling 3D Secureโ
3D Secure Flow with WebViewโ
import React, { useRef } from 'react';
import { Modal, View, StyleSheet, TouchableOpacity, Text } from 'react-native';
import { WebView } from 'react-native-webview';
interface ThreeDSecureModalProps {
visible: boolean;
authorizeUri: string;
onComplete: () => void;
onCancel: () => void;
}
export default function ThreeDSecureModal({
visible,
authorizeUri,
onComplete,
onCancel,
}: ThreeDSecureModalProps) {
const webViewRef = useRef<WebView>(null);
const handleNavigationStateChange = (navState: any) => {
const { url } = navState;
// Check if user returned from 3DS
if (url.startsWith('myapp://payment/complete')) {
onComplete();
}
};
return (
<Modal visible={visible} animationType="slide">
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Secure Authentication</Text>
<TouchableOpacity onPress={onCancel} style={styles.closeButton}>
<Text style={styles.closeText}>Cancel</Text>
</TouchableOpacity>
</View>
<WebView
ref={webViewRef}
source={{ uri: authorizeUri }}
onNavigationStateChange={handleNavigationStateChange}
style={styles.webview}
startInLoadingState
/>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ddd',
},
title: {
fontSize: 18,
fontWeight: 'bold',
},
closeButton: {
padding: 8,
},
closeText: {
color: '#007AFF',
fontSize: 16,
},
webview: {
flex: 1,
},
});
Using 3D Secure Modalโ
import React, { useState } from 'react';
import { View } from 'react-native';
import PaymentForm from './PaymentForm';
import ThreeDSecureModal from './ThreeDSecureModal';
export default function CheckoutScreen() {
const [show3DS, setShow3DS] = useState(false);
const [authorizeUri, setAuthorizeUri] = useState('');
const [chargeId, setChargeId] = useState('');
const handlePaymentInitiated = async (charge: any) => {
if (charge.authorize_uri) {
setAuthorizeUri(charge.authorize_uri);
setChargeId(charge.id);
setShow3DS(true);
} else {
handlePaymentSuccess();
}
};
const handle3DSComplete = async () => {
setShow3DS(false);
// Verify charge status
try {
const charge = await verifyCharge(chargeId);
if (charge.status === 'successful') {
handlePaymentSuccess();
} else {
handlePaymentFailure('Payment verification failed');
}
} catch (error) {
handlePaymentFailure(error.message);
}
};
const handle3DSCancel = () => {
setShow3DS(false);
handlePaymentFailure('Authentication cancelled');
};
return (
<View style={{ flex: 1 }}>
<PaymentForm
amount={100000}
currency="THB"
onSuccess={handlePaymentInitiated}
/>
<ThreeDSecureModal
visible={show3DS}
authorizeUri={authorizeUri}
onComplete={handle3DSComplete}
onCancel={handle3DSCancel}
/>
</View>
);
}
Deep Linking Configurationโ
iOS Configurationโ
Add to ios/YourApp/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Android Configurationโ
Add to android/app/src/main/AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
Handling Deep Linksโ
import { useEffect } from 'react';
import { Linking } from 'react-native';
export function useDeepLink(onLink: (url: string) => void) {
useEffect(() => {
// Handle initial URL
Linking.getInitialURL().then(url => {
if (url) {
onLink(url);
}
});
// Handle URL updates
const subscription = Linking.addEventListener('url', event => {
onLink(event.url);
});
return () => {
subscription.remove();
};
}, [onLink]);
}
// Usage
export default function App() {
useDeepLink(url => {
if (url.startsWith('myapp://payment/complete')) {
// Handle payment return
verifyPaymentStatus();
}
});
return <AppNavigator />;
}
Saved Card Paymentsโ
Saved Cards List Componentโ
import React, { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
Image,
} from 'react-native';
interface Card {
id: string;
brand: string;
last_digits: string;
expiration_month: number;
expiration_year: number;
}
interface SavedCardsListProps {
customerId: string;
onCardSelect: (card: Card) => void;
}
export default function SavedCardsList({
customerId,
onCardSelect,
}: SavedCardsListProps) {
const [cards, setCards] = useState<Card[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadCards();
}, [customerId]);
const loadCards = async () => {
try {
const response = await fetch(
`https://api.yourapp.com/customers/${customerId}/cards`
);
const data = await response.json();
setCards(data.data);
} catch (error) {
console.error('Failed to load cards:', error);
} finally {
setLoading(false);
}
};
const getCardIcon = (brand: string) => {
const icons = {
Visa: require('./assets/visa.png'),
MasterCard: require('./assets/mastercard.png'),
'American Express': require('./assets/amex.png'),
};
return icons[brand] || require('./assets/card-default.png');
};
const renderCard = ({ item }: { item: Card }) => (
<TouchableOpacity
style={styles.cardItem}
onPress={() => onCardSelect(item)}
>
<Image source={getCardIcon(item.brand)} style={styles.cardIcon} />
<View style={styles.cardInfo}>
<Text style={styles.cardBrand}>{item.brand}</Text>
<Text style={styles.cardNumber}>โขโขโขโข {item.last_digits}</Text>
</View>
<Text style={styles.cardExpiry}>
{item.expiration_month}/{item.expiration_year % 100}
</Text>
</TouchableOpacity>
);
if (loading) {
return <ActivityIndicator />;
}
return (
<View style={styles.container}>
<Text style={styles.title}>Saved Cards</Text>
<FlatList
data={cards}
renderItem={renderCard}
keyExtractor={item => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
},
cardItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: '#f5f5f5',
borderRadius: 8,
},
cardIcon: {
width: 40,
height: 25,
marginRight: 12,
},
cardInfo: {
flex: 1,
},
cardBrand: {
fontSize: 16,
fontWeight: '600',
},
cardNumber: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
cardExpiry: {
fontSize: 14,
color: '#666',
},
separator: {
height: 12,
},
});
Charging Saved Cardโ
async function chargeWithSavedCard(
customerId: string,
cardId: string,
amount: number,
currency: string
) {
try {
const response = await fetch('https://api.yourapp.com/charges', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: customerId,
card: cardId,
amount,
currency,
return_uri: 'myapp://payment/complete',
}),
});
const charge = await response.json();
if (charge.authorize_uri) {
// Handle 3D Secure
return { charge, requires3DS: true };
}
return { charge, requires3DS: false };
} catch (error) {
throw new Error(`Failed to charge card: ${error.message}`);
}
}
API Integrationโ
Create Backend Serviceโ
class OmiseService {
private baseUrl = 'https://api.yourapp.com';
async createCharge(params: {
token: string;
amount: number;
currency: string;
return_uri: string;
metadata?: Record<string, any>;
}) {
const response = await fetch(`${this.baseUrl}/charges`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await getAuthToken()}`,
},
body: JSON.stringify(params),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create charge');
}
return response.json();
}
async verifyCharge(chargeId: string) {
const response = await fetch(`${this.baseUrl}/charges/${chargeId}`, {
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
},
});
if (!response.ok) {
throw new Error('Failed to verify charge');
}
return response.json();
}
async createCustomer(params: {
email: string;
description?: string;
metadata?: Record<string, any>;
}) {
const response = await fetch(`${this.baseUrl}/customers`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await getAuthToken()}`,
},
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error('Failed to create customer');
}
return response.json();
}
async attachCard(customerId: string, tokenId: string) {
const response = await fetch(
`${this.baseUrl}/customers/${customerId}/cards`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await getAuthToken()}`,
},
body: JSON.stringify({ card: tokenId }),
}
);
if (!response.ok) {
throw new Error('Failed to attach card');
}
return response.json();
}
}
export const omiseService = new OmiseService();
Error Handlingโ
Error Handler Utilityโ
export class PaymentError extends Error {
constructor(
message: string,
public code?: string,
public details?: any
) {
super(message);
this.name = 'PaymentError';
}
}
export function handlePaymentError(error: any): PaymentError {
if (error.response) {
// API error
const { code, message } = error.response.data;
return new PaymentError(message, code, error.response.data);
} else if (error.request) {
// Network error
return new PaymentError('Network error. Please check your connection.');
} else {
// Other errors
return new PaymentError(error.message || 'An unexpected error occurred');
}
}
export function getErrorMessage(error: PaymentError): string {
const errorMessages: Record<string, string> = {
invalid_card: 'Invalid card information',
insufficient_funds: 'Insufficient funds on card',
stolen_or_lost_card: 'Card reported as lost or stolen',
failed_processing: 'Payment processing failed',
};
return errorMessages[error.code || ''] || error.message;
}
Common Use Casesโ
One-Time Purchaseโ
async function handleOneTimePurchase(
items: CartItem[],
cardData: CardData
) {
try {
// Calculate total
const total = items.reduce((sum, item) => sum + item.price, 0);
// Create token
const token = await createToken({ card: cardData });
// Create charge
const charge = await omiseService.createCharge({
token: token.id,
amount: total,
currency: 'THB',
return_uri: 'myapp://payment/complete',
metadata: {
items: items.map(i => i.id),
},
});
return charge;
} catch (error) {
throw handlePaymentError(error);
}
}
Subscription Setupโ
async function setupSubscription(
plan: string,
cardData: CardData,
email: string
) {
try {
// Create customer
const customer = await omiseService.createCustomer({
email,
description: `${plan} subscription`,
});
// Create token and attach card
const token = await createToken({ card: cardData });
await omiseService.attachCard(customer.id, token.id);
// Create first charge
const charge = await omiseService.createCharge({
token: token.id,
amount: getPlanAmount(plan),
currency: 'THB',
return_uri: 'myapp://payment/complete',
metadata: {
plan,
type: 'subscription',
},
});
// Create schedule on backend
await createSchedule(customer.id, plan);
return { customer, charge };
} catch (error) {
throw handlePaymentError(error);
}
}
Best Practicesโ
Securityโ
-
Never Store Sensitive Data
// โ Good - Use tokens
const token = await createToken({ card: cardData });
await sendToBackend(token.id);
// โ Bad - Don't store card data
await AsyncStorage.setItem('cardNumber', cardNumber); -
Validate Input
function validateCardNumber(number: string): boolean {
const cleaned = number.replace(/\s/g, '');
return /^\d{13,19}$/.test(cleaned) && luhnCheck(cleaned);
}
function luhnCheck(cardNumber: string): boolean {
// Implement Luhn algorithm
let sum = 0;
let isEven = false;
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber[i]);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
} -
Use HTTPS
// Enforce HTTPS in production
const API_URL = __DEV__
? 'http://localhost:3000'
: 'https://api.yourapp.com';
Performanceโ
-
Debounce Card Validation
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
const debouncedValidation = useMemo(
() => debounce((number: string) => {
setIsValid(validateCardNumber(number));
}, 500),
[]
); -
Cache Payment Methods
import { useQuery } from 'react-query';
function useSavedCards(customerId: string) {
return useQuery(
['cards', customerId],
() => omiseService.getCards(customerId),
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
}
);
}
Troubleshootingโ
Common Issuesโ
Token Creation Fails
// Check public key configuration
console.log('Public Key:', publicKey);
// Verify card data
console.log('Card Number Valid:', validateCardNumber(cardNumber));
console.log('CVV Valid:', /^\d{3,4}$/.test(cvv));
3D Secure Not Working
// Verify WebView is properly configured
import { WebView } from 'react-native-webview';
// Check deep link configuration
Linking.canOpenURL('myapp://payment/complete').then(supported => {
console.log('Deep linking supported:', supported);
});
Build Issues
# Clear caches
npm start -- --reset-cache
# Rebuild iOS
cd ios && pod install && cd ..
npx react-native run-ios
# Rebuild Android
cd android && ./gradlew clean && cd ..
npx react-native run-android
FAQโ
General Questionsโ
Q: Is Omise React Native SDK officially supported?
A: While there's no official Omise React Native SDK, you can use native modules to bridge iOS and Android SDKs or use the REST API directly.
Q: Can I use Expo?
A: Partial support. Managed Expo workflow has limitations with native modules. Use bare workflow or create custom native modules.
Q: How do I test payments?
A: Use test card 4242 4242 4242 4242 with any future expiry and any 3-digit CVV. Use test public key (pkey_test_xxx).
Q: What's the minimum React Native version?
A: React Native 0.64+ is recommended for best compatibility with modern libraries.
Q: Can I use TypeScript?
A: Yes, TypeScript is fully supported and recommended for type safety.
Q: How do I handle offline payments?
A: Store payment intent locally and process when connection is restored. Always verify status after reconnection.
Implementation Questionsโ
Q: Should I use WebView for 3D Secure?
A: Yes, react-native-webview is the recommended approach for handling 3D Secure authentication flows.
Q: How do I handle different currencies?
A: Pass the currency code in the charge request. Format display amounts based on user's locale.
Q: Can I customize the UI completely?
A: Yes, build your own UI components and use the SDK only for tokenization.
Q: How do I handle Android back button during 3DS?
A: Implement back handler in your WebView modal to treat it as cancellation.
Related Resourcesโ
- Developer Tools - Mobile SDKs
- iOS SDK Payments
- Android SDK Payments
- Charges API Reference
- Tokens API Reference
- 3D Secure Guide
- Testing Guide