Skip to main content

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:

  1. React Native environment set up (0.64+)
  2. Native modules configured for iOS and Android
  3. Backend API for charge creation
  4. 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'
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>
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โ€‹

  1. 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);
  2. 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;
    }
  3. Use HTTPS

    // Enforce HTTPS in production
    const API_URL = __DEV__
    ? 'http://localhost:3000'
    : 'https://api.yourapp.com';

Performanceโ€‹

  1. Debounce Card Validation

    import { useMemo } from 'react';
    import debounce from 'lodash/debounce';

    const debouncedValidation = useMemo(
    () => debounce((number: string) => {
    setIsValid(validateCardNumber(number));
    }, 500),
    []
    );
  2. 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.

Next Stepsโ€‹