UseDocumentation Index
Fetch the complete documentation index at: https://dev.moonpay.com/llms.txt
Use this file to discover all available pages before exploring further.
react-native-webview to embed frames in React Native applications. The WebView communicates with your app through the ReactNativeWebView JavaScript interface.
Read the manual integration
overview for core concepts
before you continue.
Setup
The
react-native-webview library automatically injects the
ReactNativeWebView JavaScript interface into the WebView. The frame detects
this interface and uses it for communication between the frame and your app.
You don’t need to inject a custom JavaScript bridge.Dependencies
Installreact-native-webview to render frames in your app.
- React Native CLI
- Expo
pnpm i react-native-webview
cd ios && pod install
npx expo install react-native-webview
getRandomValues (MDN) which is only available in browsers.
pnpm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
Base WebView component
Create a reusable base component for frame communication:MoonPayWebView.tsx
import React, {
useRef,
useCallback,
useImperativeHandle,
forwardRef,
} from "react";
import { View, ViewStyle, StyleSheet } from "react-native";
import { WebView, WebViewMessageEvent } from "react-native-webview";
type MoonPayWebViewProps = {
url: string;
channelId: string;
onMessage: (data: FrameMessage) => void;
onHandshake: () => void;
style?: ViewStyle;
};
export type MoonPayWebViewRef = {
sendMessage: (kind: string, payload?: object) => void;
};
export type FrameMessage = {
version: number;
meta: { channelId: string };
kind: string;
payload?: unknown;
};
export const MoonPayWebView = forwardRef<
MoonPayWebViewRef,
MoonPayWebViewProps
>(({ url, channelId, onMessage, onHandshake, style }, ref) => {
const webViewRef = useRef<WebView>(null);
const sendMessage = useCallback(
(kind: string, payload?: object) => {
const message = {
version: 2,
meta: { channelId },
kind,
...(payload && { payload }),
};
webViewRef.current?.postMessage(JSON.stringify(message));
},
[channelId],
);
useImperativeHandle(ref, () => ({ sendMessage }), [sendMessage]);
const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
try {
const data: FrameMessage = JSON.parse(event.nativeEvent.data);
if (data.meta?.channelId !== channelId) return;
if (data.kind === "handshake") {
sendMessage("ack");
onHandshake();
}
onMessage(data);
} catch {
// Ignore malformed messages
}
},
[channelId, sendMessage, onMessage, onHandshake],
);
return (
<View style={[styles.container, style]}>
<WebView
ref={webViewRef}
source={{ uri: url }}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
allowsInlineMediaPlayback
originWhitelist={["*"]}
style={styles.webview}
/>
</View>
);
});
const styles = StyleSheet.create({
container: { flex: 1 },
webview: { flex: 1 },
});
Encryption utility
When using the connect or check frame, you generate an X25519 keypair and provide the public key to the frame. The frame encrypts the returned client credentials with this key.Key generation
Generate an X25519 keypair for secure communication:crypto.ts
import { x25519 } from "@noble/curves/ed25519";
import { bytesToHex } from "@noble/hashes/utils";
import "react-native-get-random-values"; // Polyfill for crypto.getRandomValues
export function generateKeyPair() {
const { secretKey, publicKey } = x25519.keygen();
return {
privateKeyHex: bytesToHex(secretKey),
publicKeyHex: bytesToHex(publicKey),
};
}
export function generateChannelId() {
return `ch_${Date.now()}_${Math.random().toString(36).slice(2)}`;
}
Decryption utility
Decrypt the encrypted credentials returned from connect/check frames:decrypt.ts
import { x25519 } from "@noble/curves/ed25519";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { gcm } from "@noble/ciphers/aes";
import { hexToBytes } from "@noble/hashes/utils";
interface ClientCredentials {
accessToken: string;
clientToken: string;
}
export function decryptClientCredentials(
encryptedValue: string,
privateKeyHex: string,
): ClientCredentials {
const encrypted = JSON.parse(encryptedValue) as {
ephemeralPublicKey: string;
iv: string;
ciphertext: string;
};
const sharedSecret = x25519.getSharedSecret(
hexToBytes(privateKeyHex),
hexToBytes(encrypted.ephemeralPublicKey),
);
const derivedKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const aes = gcm(derivedKey, hexToBytes(encrypted.iv));
const decrypted = aes.decrypt(hexToBytes(encrypted.ciphertext));
const text = new TextDecoder().decode(decrypted);
return JSON.parse(text) as ClientCredentials;
}
Check frame
The check frame verifies whether a customer already has an active connection. It’s headless — no UI is rendered. See check frame reference for event details.Check component
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import {
generateKeyPair,
generateChannelId,
decryptClientCredentials,
} from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface CheckFrameProps {
sessionToken: string;
onActive: (credentials: { accessToken: string; clientToken: string }) => void;
onConnectionRequired: (credentials: {
accessToken: string;
clientToken: string;
}) => void;
onError: (error: { code: string; message: string }) => void;
onPending?: () => void;
onUnavailable?: () => void;
}
export function MoonPayCheckFrame({
sessionToken,
onActive,
onConnectionRequired,
onError,
onPending,
onUnavailable,
}: CheckFrameProps) {
const [channelId] = useState(generateChannelId);
const [keyPair] = useState(generateKeyPair);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/check-connection?${new URLSearchParams(
{
sessionToken,
publicKey: keyPair.publicKeyHex,
channelId,
},
).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
const payload = data.payload as {
status: string;
credentials?: string;
reason?: string;
};
switch (payload.status) {
case "active":
const credentials = decryptClientCredentials(
payload.credentials!,
keyPair.privateKeyHex,
);
// Check payload.capabilities.ramps.requirements.paymentDisclosures
// to determine if payment disclosures are required before transacting
onActive(credentials);
break;
case "connectionRequired":
const anonymousCredentials = decryptClientCredentials(
payload.credentials!,
keyPair.privateKeyHex,
);
onConnectionRequired(anonymousCredentials);
break;
case "pending":
onPending?.();
break;
case "unavailable":
onUnavailable?.();
break;
case "failed":
onError({
code: "failed",
message: payload.reason || "Check failed",
});
break;
}
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[
keyPair,
onActive,
onConnectionRequired,
onError,
onPending,
onUnavailable,
],
);
return (
<MoonPayWebView
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
/>
);
}
Usage
import { MoonPayCheckFrame } from "./MoonPayCheckFrame";
function SplashScreen({ navigation }) {
return (
<MoonPayCheckFrame
sessionToken="your-session-token"
onActive={({ accessToken, clientToken }) => {
// Customer is already connected — skip to payment
console.log("Already connected!", { accessToken, clientToken });
navigation.navigate("Payment");
}}
onConnectionRequired={({ accessToken, clientToken }) => {
// Store both tokens in memory, then pass clientToken to the connect frame
CredentialsStore.set({ accessToken, clientToken });
navigation.navigate("Connect", { clientToken });
}}
onError={(error) => {
console.error("Check failed:", error);
}}
/>
);
}
Connect frame
The connect frame establishes a customer connection to your application. See connect frame reference for event details.Connect component
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import {
generateKeyPair,
generateChannelId,
decryptClientCredentials,
} from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface ConnectFrameProps {
clientToken: string;
onComplete: (credentials: {
accessToken: string;
clientToken: string;
}) => void;
onError: (error: { code: string; message: string }) => void;
onPending?: () => void;
onUnavailable?: () => void;
}
export function MoonPayConnectFrame({
clientToken,
onComplete,
onError,
onPending,
onUnavailable,
}: ConnectFrameProps) {
const [channelId] = useState(generateChannelId);
const [keyPair] = useState(generateKeyPair);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/connect?${new URLSearchParams({
clientToken,
publicKey: keyPair.publicKeyHex,
channelId,
}).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
const payload = data.payload as {
status: string;
credentials?: string;
reason?: string;
};
switch (payload.status) {
case "active":
const credentials = decryptClientCredentials(
payload.credentials!,
keyPair.privateKeyHex,
);
// Check payload.capabilities.ramps.requirements.paymentDisclosures
// to determine if payment disclosures are required before transacting
onComplete(credentials);
break;
case "pending":
onPending?.();
break;
case "unavailable":
onUnavailable?.();
break;
case "failed":
onError({
code: "failed",
message: payload.reason || "Connection failed",
});
break;
}
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[keyPair, onComplete, onError, onPending, onUnavailable],
);
return (
<MoonPayWebView
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
/>
);
}
Usage
import { MoonPayConnectFrame } from "./MoonPayConnectFrame";
function ConnectScreen({ route, navigation }) {
// The clientToken is the anonymous token passed from the check frame's
// `connectionRequired` response.
const { clientToken } = route.params;
const handleComplete = ({ accessToken, clientToken }) => {
// Replace the anonymous credentials with the authenticated ones and store
// them in memory (e.g., React Context or state management).
console.log("Connected!", { accessToken, clientToken });
navigation.navigate("Home");
};
const handleError = (error) => {
console.error("Connection failed:", error);
// Show error UI
};
return (
<MoonPayConnectFrame
clientToken={clientToken}
onComplete={handleComplete}
onError={handleError}
onPending={() => console.log("Connection pending")}
onUnavailable={() => console.log("Connection unavailable")}
/>
);
}
Apple Pay frame
The Apple Pay frame renders the Apple Pay button and handles the payment flow. See Apple Pay frame reference for event details.Apple Pay component
import React, {
useState,
useCallback,
useRef,
useImperativeHandle,
forwardRef,
} from "react";
import {
MoonPayWebView,
MoonPayWebViewRef,
FrameMessage,
} from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface ApplePayFrameProps {
clientToken: string;
quoteSignature: string;
onComplete: (transaction: { id: string; status: string }) => void;
onError: (error: { code: string; message: string }) => void;
onQuoteExpired: () => void;
onReady?: () => void;
}
export type ApplePayFrameRef = {
updateQuote: (signature: string) => void;
};
export const MoonPayApplePayFrame = forwardRef<
ApplePayFrameRef,
ApplePayFrameProps
>(
(
{
clientToken,
quoteSignature,
onComplete,
onError,
onQuoteExpired,
onReady,
},
ref,
) => {
const frameRef = useRef<MoonPayWebViewRef>(null);
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/apple-pay?${new URLSearchParams(
{
clientToken,
channelId,
signature: quoteSignature,
},
).toString()}`;
useImperativeHandle(
ref,
() => ({
updateQuote: (signature: string) => {
frameRef.current?.sendMessage("setQuote", {
quote: { signature },
});
},
}),
[],
);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "complete":
const payload = data.payload as {
transaction:
| { id: string; status: string }
| { status: "failed"; failureReason: string };
};
if (payload.transaction.status === "failed") {
onError({
code: "transactionFailed",
message: (payload.transaction as { failureReason: string })
.failureReason,
});
} else {
onComplete(payload.transaction as { id: string; status: string });
}
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
onQuoteExpired();
} else {
onError(error);
}
break;
}
},
[onComplete, onError, onQuoteExpired, onReady],
);
return (
<MoonPayWebView
ref={frameRef}
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
style={{ height: 56 }}
/>
);
},
);
Usage
import { useRef } from "react";
import { MoonPayApplePayFrame, ApplePayFrameRef } from "./MoonPayApplePayFrame";
function PaymentScreen({ clientToken, quoteSignature }) {
const applePayRef = useRef<ApplePayFrameRef>(null);
const handleQuoteExpired = async () => {
// Fetch a new quote and send it to the frame
const newQuote = await fetchNewQuote();
applePayRef.current?.updateQuote(newQuote.signature);
};
return (
<MoonPayApplePayFrame
ref={applePayRef}
clientToken={clientToken}
quoteSignature={quoteSignature}
onComplete={(tx) => console.log("Transaction initiated:", tx.id)}
onError={(error) => console.error("Payment error:", error)}
onQuoteExpired={handleQuoteExpired}
onReady={() => console.log("Apple Pay button ready")}
/>
);
}
Add Card frame
The add card frame lets a customer save a new card to their account. See add card frame reference for event details.What you’ll need
Before you initialize the add card frame, you need:- A
clientTokenfrom a successful connect flow
Add Card component
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface CardResult {
id: string;
brand: string;
last4: string;
cardType: string;
expirationMonth: number;
expirationYear: number;
availability: { active: boolean };
}
interface AddCardFrameProps {
clientToken: string;
onComplete: (card: CardResult) => void;
onError: (error: { code: string; message: string }) => void;
onReady?: () => void;
}
export function MoonPayAddCardFrame({
clientToken,
onComplete,
onError,
onReady,
}: AddCardFrameProps) {
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/add-card?${new URLSearchParams({
clientToken,
channelId,
}).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "complete":
const payload = data.payload as { card: CardResult };
onComplete(payload.card);
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[onComplete, onError, onReady],
);
return (
<MoonPayWebView
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
style={{ flex: 1 }}
/>
);
}
Usage
import { MoonPayAddCardFrame } from "./MoonPayAddCardFrame";
function SaveCardScreen({ clientToken, navigation }) {
return (
<MoonPayAddCardFrame
clientToken={clientToken}
onComplete={(card) => {
console.log("Card saved:", card.id, card.brand, card.last4);
navigation.navigate("Payment");
}}
onError={(error) => {
console.error("Add card error:", error);
}}
onReady={() => console.log("Add card frame ready")}
/>
);
}
Buy frame
The buy frame processes a card payment for a quote. It is headless — rendered at zero size — while the customer completes payment. If 3-D Secure is required, the frame emits achallenge event with a URL you open in a separate challenge frame. See buy frame reference for event details.
What you’ll need
Before you initialize the buy frame, you need:- A
clientTokenfrom a successful connect flow - A valid quote signature for the transaction
Buy component
import React, {
useState,
useCallback,
useRef,
useImperativeHandle,
forwardRef,
} from "react";
import { StyleSheet } from "react-native";
import {
MoonPayWebView,
MoonPayWebViewRef,
FrameMessage,
} from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface BuyFrameProps {
clientToken: string;
quoteSignature: string;
onComplete: (transaction: { id: string; status: string }) => void;
onChallenge: (url: string) => void;
onError: (error: { code: string; message: string }) => void;
onQuoteExpired?: () => void;
}
export type BuyFrameRef = {
updateQuote: (signature: string) => void;
};
export const MoonPayBuyFrame = forwardRef<BuyFrameRef, BuyFrameProps>(
(
{
clientToken,
quoteSignature,
onComplete,
onChallenge,
onError,
onQuoteExpired,
},
ref,
) => {
const frameRef = useRef<MoonPayWebViewRef>(null);
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/buy?${new URLSearchParams({
clientToken,
channelId,
signature: quoteSignature,
}).toString()}`;
useImperativeHandle(
ref,
() => ({
updateQuote: (signature: string) => {
frameRef.current?.sendMessage("setQuote", {
quote: { signature },
});
},
}),
[],
);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
onComplete(
(data.payload as { transaction: { id: string; status: string } })
.transaction,
);
break;
case "challenge":
const challenge = data.payload as { kind: string; url: string };
onChallenge(challenge.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
onQuoteExpired?.();
} else {
onError(error);
}
break;
}
},
[onComplete, onChallenge, onError, onQuoteExpired],
);
return (
<MoonPayWebView
ref={frameRef}
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
style={StyleSheet.absoluteFillObject}
/>
);
},
);
Challenge component
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface ChallengeFrameProps {
challengeUrl: string;
onComplete: (transaction: { id: string; status: string }) => void;
onCancelled: () => void;
onError: (error: { code: string; message: string }) => void;
}
export function MoonPayChallengeFrame({
challengeUrl,
onComplete,
onCancelled,
onError,
}: ChallengeFrameProps) {
const [channelId] = useState(generateChannelId);
const frameUrl = useMemo(() => {
const url = new URL(challengeUrl);
url.searchParams.set("channelId", channelId);
return url.toString();
}, [challengeUrl, channelId]);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
const payload = data.payload as {
flow: "buy";
transaction: { id: string; status: string };
};
onComplete(payload.transaction);
break;
case "cancelled":
onCancelled();
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[onComplete, onCancelled, onError],
);
return (
<MoonPayWebView
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
/>
);
}
Usage
import { useRef, useState } from "react";
import { View, StyleSheet } from "react-native";
import { MoonPayBuyFrame, BuyFrameRef } from "./MoonPayBuyFrame";
import { MoonPayChallengeFrame } from "./MoonPayChallengeFrame";
function BuyPaymentScreen({ clientToken, quoteSignature, navigation }) {
const buyRef = useRef<BuyFrameRef>(null);
const [challengeUrl, setChallengeUrl] = useState<string | null>(null);
const handleComplete = (transaction: { id: string; status: string }) => {
setChallengeUrl(null);
console.log("Transaction initiated:", transaction.id);
navigation.navigate("TransactionStatus", { transactionId: transaction.id });
};
const handleChallengeResult = (transaction: {
id: string;
status: string;
}) => {
setChallengeUrl(null);
console.log("Transaction initiated:", transaction.id);
navigation.navigate("TransactionStatus", { transactionId: transaction.id });
};
const handleQuoteExpired = async () => {
const newQuote = await fetchNewQuote();
buyRef.current?.updateQuote(newQuote.signature);
};
return (
<View style={StyleSheet.absoluteFill}>
<MoonPayBuyFrame
ref={buyRef}
clientToken={clientToken}
quoteSignature={quoteSignature}
onComplete={handleComplete}
onChallenge={(url) => setChallengeUrl(url)}
onError={(error) => console.error("Buy error:", error)}
onQuoteExpired={handleQuoteExpired}
/>
{challengeUrl && (
<View style={StyleSheet.absoluteFill}>
<MoonPayChallengeFrame
challengeUrl={challengeUrl}
onComplete={handleChallengeResult}
onCancelled={() => setChallengeUrl(null)}
onError={(error) => {
console.error("Challenge error:", error);
setChallengeUrl(null);
}}
/>
</View>
)}
</View>
);
}
Widget frame
For payment methods beyond Apple Pay — including credit/debit cards, Google Pay, bank transfers, and more — use the widget frame. It renders the full MoonPay buy experience inside a WebView, including payment-method selection and transaction confirmation. See pay with widget for a full walkthrough.Widget component
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://blocks.moonpay.com";
interface WidgetFrameProps {
clientToken: string;
quoteSignature: string;
onTransactionCreated?: (transaction: { id: string; status: string }) => void;
onComplete: (transaction: { id: string; status: string }) => void;
onError: (error: { code: string; message: string }) => void;
onReady?: () => void;
}
export function MoonPayWidgetFrame({
clientToken,
quoteSignature,
onTransactionCreated,
onComplete,
onError,
onReady,
}: WidgetFrameProps) {
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/widget?${new URLSearchParams({
flow: "buy",
clientToken,
quoteSignature,
channelId,
}).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "transactionCreated":
const created = data.payload as {
transaction: { id: string; status: string };
};
onTransactionCreated?.(created.transaction);
break;
case "complete":
const payload = data.payload as {
transaction:
| { id: string; status: string }
| { status: "failed"; failureReason: string };
};
if (payload.transaction.status === "failed") {
onError({
code: "transactionFailed",
message: (payload.transaction as { failureReason: string })
.failureReason,
});
} else {
onComplete(payload.transaction as { id: string; status: string });
}
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[onComplete, onError, onTransactionCreated, onReady],
);
return (
<MoonPayWebView
url={frameUrl}
channelId={channelId}
onMessage={handleMessage}
onHandshake={() => {}}
/>
);
}
Usage
import { MoonPayWidgetFrame } from "./MoonPayWidgetFrame";
function WidgetPaymentScreen({ clientToken, quoteSignature }) {
return (
<MoonPayWidgetFrame
clientToken={clientToken}
quoteSignature={quoteSignature}
onReady={() => console.log("Widget loaded")}
onTransactionCreated={(tx) =>
console.log("Transaction created:", tx.id, tx.status)
}
onComplete={(tx) => console.log("Transaction complete:", tx.id)}
onError={(error) => console.error("Widget error:", error)}
/>
);
}