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 secret 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 secret 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 (secret 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);