React Native SDK
OmiseSDKを使用してReact Nativeアプリケーションで支払いを受け付ける方法を学びます。このガイドでは、iOSとAndroidの両方をサポートするクロスプラットフォーム支払い統合についての完全な実装戦略を提供します。
概要
React NativeアプリケーションにおけるOmise支払い統合では、ネイティブSDK機能とJavaScript/TypeScriptコードの組み合わせが必要です。このガイドでは、プラットフォーム全体で支払いを受け付けるための完全な実装戦略を提供します。
主な機能
- クロスプラットフォームサポート - iOS/Android用の単一コードベース
- ネイティブパフォーマンス - 内部的にネイティブSDKを活用
- TypeScriptサポート - 完全な型定義を含める
- React Hooks - モダンなReactパターンとHooks
- 3D Secure対応 - シームレスな認証フロー
- Expo互換 - マネージドExpoワークフローに対応
前提条件
支払い受付を実装する前に:
- React Native環境をセットアップ(0.64以上)
- iOS/Androidのネイティブモジュール構成
- 請求作成用のバックエンドAPI
- API キー付きのOmiseアカウント
インストール
ネイティブ依存関係をインストール
# npmを使用
npm install omise-react-native
# yarnを使用
yarn add omise-react-native
# iOSの依存関係をインストール
cd ios && pod install && cd ..
Androidの設定
android/app/build.gradle に以下を追加:
dependencies {
implementation 'co.omise:omise-android:3.1.0'
}
iOSの設定
CocoaPods インストールは iOS 設定を自動的に処理します。ios/Podfile で最小iOSバージョンを確認してください:
platform :ios, '12.0'
ネイティブモジュールのリンク(React Native < 0.60)
react-native link omise-react-native
基本的な支払いフロー
Omiseプロバイダーのセットアップ
import React from 'react';
import { OmiseProvider } from 'omise-react-native';
export default function App() {
return (
<OmiseProvider publicKey="pkey_test_123">
<AppNavigator />
</OmiseProvider>
);
}
支払い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 {
// ステップ1: トークン作成
const token = await createToken({
card: {
name: cardData.name,
number: cardData.number,
expiration_month: cardData.expiryMonth,
expiration_year: cardData.expiryYear,
security_code: cardData.securityCode,
},
});
// ステップ2: バックエンドで請求作成
const charge = await createCharge({
token: token.id,
amount,
currency,
return_uri: 'myapp://payment/complete',
});
// ステップ3: 必要に応じて3D Secureを処理
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 };
}
支払いフォームコンポーネント
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 () => {
// 有効期限を解析
const [month, year] = expiry.split('/').map(s => parseInt(s.trim()));
// 検証
if (!validateForm()) {
return;
}
try {
await processPayment(
{
number: cardNumber.replace(/\s/g, ''),
name: cardholderName,
expiryMonth: month,
expiryYear: 2000 + year,
securityCode: cvv,
},
amount,
currency
);
onSuccess();
} catch (error) {
// エラーは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',
},
});
3D Secureの処理
WebView付き3D Secureフロー
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;
// 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,
},
});
3D Secureモーダルの使用
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);
// 請求ステータスを確認
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>
);
}
ディープリンク設定
iOS設定
ios/YourApp/Info.plist に以下を追加:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Android設定
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(() => {
// 初期URLを処理
Linking.getInitialURL().then(url => {
if (url) {
onLink(url);
}
});
// URLアップデートを処理
const subscription = Linking.addEventListener('url', event => {
onLink(event.url);
});
return () => {
subscription.remove();
};
}, [onLink]);
}
// 使用例
export default function App() {
useDeepLink(url => {
if (url.startsWith('myapp://payment/complete')) {
// 支払い戻りを処理
verifyPaymentStatus();
}
});
return <AppNavigator />;
}
保存されたカード支払い
保存されたカードリストコンポーネント
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,
},
});
保存されたカードの請求処理
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) {
// 3D Secureを処理
return { charge, requires3DS: true };
}
return { charge, requires3DS: false };
} catch (error) {
throw new Error(`Failed to charge card: ${error.message}`);
}
}
API統合
バックエンドサービスの作成
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();
エラーハンドリング
エラーハンドラーユーティリティ
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エラー
const { code, message } = error.response.data;
return new PaymentError(message, code, error.response.data);
} else if (error.request) {
// ネットワークエラー
return new PaymentError('Network error. Please check your connection.');
} else {
// その他のエラー
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;
}