React Native Mobile Money
Integrate Mobile Money checkout in a React Native app with in-app browser, deep-linking return, and backend verification.
This recipe shows how to build a Mobile Money checkout flow in a React Native app.
You'll open Wajub's hosted payment page in a WebView, capture the payment result via
deep link, and verify the transaction server-side — a complete, production-ready flow.
Prerequisites
- A Wajub account with sandbox API keys
- React Native 0.71+ (Expo or bare workflow both work)
npx create-expo-app MyStore --template blank
cd MyStore
npx expo install react-native-webview expo-linking expo-web-browser1. Create the Wajub helper
Create services/wajub.ts:
⚠️ Never embed a private key (
sk./sk_test.) in a mobile app. Use a public key (pk./pk_test.) forinitializeCheckoutfrom the client — it's the only key type meant to be exposed client-side. Verification/refunds/transfers must be called from your own backend with the private key, never directly from the app.
const WAJUB_BASE = 'https://api.wajub.com';
const WAJUB_PUBLIC_KEY = 'pk_test.xxxxxxxxxxxxxxxx';
interface CheckoutParams {
amount: number;
email: string;
phone: string;
reference: string;
}
export async function initializeCheckout(params: CheckoutParams) {
const response = await fetch(`${WAJUB_BASE}/payments`, {
method: 'POST',
headers: {
Authorization: WAJUB_PUBLIC_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: params.amount,
currency: 'XAF',
email: params.email,
phone: params.phone,
reference: params.reference,
description: `Order ${params.reference}`,
callback: `mystore://payment/return`,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to initialize payment');
}
const data = await response.json();
return {
authorizationUrl: data.authorization_url,
id: data.transaction.id,
};
}
// Call this from your OWN backend (private key), not directly from the app
export async function verifyTransaction(id: string) {
const response = await fetch(`${WAJUB_BASE}/payments/${id}`, {
headers: {
Authorization: process.env.WAJUB_SECRET_KEY!,
},
});
if (!response.ok) {
throw new Error('Failed to verify transaction');
}
return response.json();
}2. Checkout screen with WebView
Create screens/CheckoutScreen.tsx:
import React, { useState, useCallback, useRef } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
import { WebView } from 'react-native-webview';
import { initializeCheckout } from '../services/wajub';
type Step = 'form' | 'checkout';
export default function CheckoutScreen() {
const [step, setStep] = useState<Step>('form');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null);
const [reference, setReference] = useState<string | null>(null);
const handleCheckout = useCallback(async () => {
if (!email || !phone || !amount) {
Alert.alert('Validation', 'Please fill in all fields.');
return;
}
setLoading(true);
try {
const ref = `ORDER-${Date.now()}`;
const { authorizationUrl } = await initializeCheckout({
amount: parseInt(amount, 10),
email,
phone,
reference: ref,
});
setReference(ref);
setAuthorizationUrl(authorizationUrl);
setStep('checkout');
} catch (err: any) {
Alert.alert('Error', err.message || 'Checkout failed. Please try again.');
} finally {
setLoading(false);
}
}, [email, phone, amount]);
const handleNavigationStateChange = useCallback((navState: any) => {
const { url } = navState;
if (url.includes('mystore://payment/return')) {
// Payment completed — navigate back to form with a success message
setStep('form');
setAuthorizationUrl(null);
Alert.alert(
'Payment submitted',
'Your payment is being processed. You’ll receive a confirmation shortly.',
);
}
if (url.includes('/payment/cancel')) {
setStep('form');
setAuthorizationUrl(null);
Alert.alert('Payment cancelled', 'You cancelled the payment.');
}
}, []);
if (step === 'checkout' && authorizationUrl) {
return (
<WebView
source={{ uri: authorizationUrl }}
onNavigationStateChange={handleNavigationStateChange}
startInLoadingState
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#6366f1" />
</View>
)}
style={styles.webview}
/>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Mobile Money Checkout</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mobile Money Number</Text>
<TextInput
style={styles.input}
value={phone}
onChangeText={setPhone}
placeholder="6XXXXXXXX"
keyboardType="phone-pad"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Amount (XAF)</Text>
<TextInput
style={styles.input}
value={amount}
onChangeText={setAmount}
placeholder="25000"
keyboardType="numeric"
/>
</View>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleCheckout}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Pay with Mobile Money</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f9fafb',
padding: 24,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: '700',
marginBottom: 32,
textAlign: 'center',
color: '#111827',
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '500',
color: '#374151',
marginBottom: 6,
},
input: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
color: '#111827',
},
button: {
backgroundColor: '#6366f1',
borderRadius: 10,
paddingVertical: 16,
alignItems: 'center',
marginTop: 12,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f9fafb',
},
webview: {
flex: 1,
},
});3. Handle return via deep link
Configure deep linking so the app can capture mystore://payment/return callbacks.
For Expo projects, update app.json:
{
"expo": {
"scheme": "mystore"
}
}Create hooks/useDeepLink.ts to listen for payment returns when the user is not on the WebView:
import { useEffect } from 'react';
import * as Linking from 'expo-linking';
export function useDeepLink(onPaymentReturn: (url: string) => void) {
useEffect(() => {
const handleDeepLink = ({ url }: { url: string }) => {
if (url.includes('payment/return')) {
onPaymentReturn(url);
}
};
const subscription = Linking.addEventListener('url', handleDeepLink);
Linking.getInitialURL().then((url) => {
if (url) handleDeepLink({ url });
});
return () => subscription.remove();
}, [onPaymentReturn]);
}In your root App.tsx, use the hook to verify the transaction when the deep link fires:
import { useDeepLink } from './hooks/useDeepLink';
import { verifyTransaction } from './services/wajub';
export default function App() {
useDeepLink(async (url) => {
// Parse the query string for the reference
const parsed = new URL(url);
const reference = parsed.searchParams.get('reference');
if (reference) {
try {
const result = await verifyTransaction(reference);
if (result.transaction?.status === 'succeeded') {
Alert.alert('Success', 'Payment confirmed!');
} else {
Alert.alert('Pending', 'Your payment is still processing.');
}
} catch {
Alert.alert('Error', 'Could not verify payment.');
}
}
});
return <CheckoutScreen />;
}Never verify from the client alone
The deep-link callback is a UX convenience. Always perform final verification and order
fulfillment from your backend webhook handler. The useDeepLink example above is for showing the
user a confirmation — not for granting access or fulfilling orders.
4. Backend verification endpoint
The WebView redirects to mystore://payment/return with the transaction reference as a query parameter.
Your backend should also listen for the payment.succeeded webhook to fulfill orders reliably.
Here's a minimal Node.js backend endpoint you can call from the app:
// backend/verify-payment.ts
import express from 'express';
const app = express();
app.post('/api/verify-payment', async (req, res) => {
const { reference } = req.body;
if (!reference) {
return res.status(400).json({ error: 'Missing reference' });
}
try {
const response = await fetch(`https://api.wajub.com/payments/${reference}`, {
headers: {
Authorization: process.env.WAJUB_SECRET_KEY,
},
});
const data = await response.json();
if (data.transaction?.status === 'succeeded') {
// Fulfill the order in your database
return res.json({ verified: true, transaction: data.transaction });
}
return res.json({ verified: false, status: data.transaction?.status });
} catch {
return res.status(500).json({ error: 'Verification failed' });
}
});
app.listen(3001);Was this page helpful?