Skip to main content

React Native SDK

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โ€‹

Is Omise React Native SDK officially supported?

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.

Can I use Expo?

Partial support. Managed Expo workflow has limitations with native modules. Use bare workflow or create custom native modules.

How do I test payments?

Use test card 4242 4242 4242 4242 with any future expiry and any 3-digit CVV. Use test public key (pkey_test_xxx).

What's the minimum React Native version?

React Native 0.64+ is recommended for best compatibility with modern libraries.

Can I use TypeScript?

Yes, TypeScript is fully supported and recommended for type safety.

How do I handle offline payments?

Store payment intent locally and process when connection is restored. Always verify status after reconnection.

Should I use WebView for 3D Secure?

Yes, react-native-webview is the recommended approach for handling 3D Secure authentication flows.

How do I handle different currencies?

Pass the currency code in the charge request. Format display amounts based on user's locale.

Can I customize the UI completely?

Yes, build your own UI components and use the SDK only for tokenization.

How do I handle Android back button during 3DS?

Implement back handler in your WebView modal to treat it as cancellation.

Next Stepsโ€‹