Overview
For React Native, usereact-native-webview instead of iframes. The @noble/* cryptography packages work in React Native without modification.
| File | Difference from Web |
|---|---|
frames.tsx | Uses react-native-webview instead of iframes |
crypto.ts | Same as Web |
api.ts | Same as Web |
client.ts | Same as Web |
types.ts | Same as Web |
index.ts | Same as Web |
React Native-Specific Files
frames.tsx - Frame handling
frames.tsx - Frame handling
Replace the Web
frames.ts with this React Native version using react-native-webview.frames.tsx
import React, { useRef, useCallback, useState } from "react";
import { View, StyleSheet } from "react-native";
import { WebView, WebViewMessageEvent } from "react-native-webview";
import { decrypt, generateKeyPair, type KeyPair } from "./crypto";
import type {
Connection,
Credentials,
ConnectEvent,
ApplePayEvent,
} from "./types";
const FRAME_ORIGIN = "https://platform.moonpay.com";
type FrameMessage = {
version: 2;
meta: { channelId: string };
kind: string;
payload?: unknown;
};
function parseMessage(data: string, channelId: string): FrameMessage | null {
try {
const message: FrameMessage = JSON.parse(data);
return message.version === 2 && message.meta?.channelId === channelId
? message
: null;
} catch {
return null;
}
}
function decryptCredentials(
encrypted: string,
privateKey: string,
): Credentials {
return JSON.parse(decrypt(encrypted, privateKey)) as Credentials;
}
// Connect Frame Component
interface ConnectFrameProps {
sessionToken: string;
theme?: "dark" | "light";
onEvent: (event: ConnectEvent) => void;
}
export function ConnectFrame({
sessionToken,
theme,
onEvent,
}: ConnectFrameProps) {
const webViewRef = useRef<WebView>(null);
const [keyPair] = useState(() => generateKeyPair());
const [channelId] = useState(
() => `ch_${Math.random().toString(36).substr(2, 9)}`,
);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/connect?${new URLSearchParams({
sessionToken,
publicKey: keyPair.publicKey,
channelId,
...(theme && { theme }),
}).toString()}`;
const sendMessage = useCallback((data: object) => {
webViewRef.current?.postMessage(JSON.stringify(data));
}, []);
const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
const message = parseMessage(event.nativeEvent.data, channelId);
if (!message) return;
switch (message.kind) {
case "handshake":
sendMessage({ version: 2, meta: { channelId }, kind: "ack" });
break;
case "ready":
onEvent({ kind: "ready" });
break;
case "complete": {
const payload = message.payload as {
status: string;
credentials?: string;
reason?: string;
};
if (payload.status === "connectionRequired") return;
if (payload.status === "active" && payload.credentials) {
try {
const credentials = decryptCredentials(
payload.credentials,
keyPair.privateKey,
);
onEvent({
kind: "complete",
payload: { frame: { dispose: () => {} } },
connection: { status: "active", credentials },
});
} catch {
onEvent({
kind: "error",
payload: {
kind: "genericError",
message: "Failed to decrypt credentials",
},
});
}
} else if (payload.status === "failed") {
onEvent({
kind: "complete",
payload: { frame: { dispose: () => {} } },
connection: {
status: "failed",
reason: payload.reason || "Connection failed",
},
});
}
break;
}
case "error": {
const payload = message.payload as { code: string; message: string };
onEvent({
kind: "error",
payload: { kind: "genericError", message: payload.message },
});
break;
}
}
},
[channelId, keyPair, onEvent, sendMessage],
);
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: frameUrl }}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
mediaPlaybackRequiresUserAction={false}
allowsInlineMediaPlayback
/>
</View>
);
}
// Apple Pay Frame Component
interface ApplePayFrameProps {
clientToken: string;
quoteSignature: string;
onEvent: (event: ApplePayEvent) => void;
}
export function ApplePayFrame({
clientToken,
quoteSignature,
onEvent,
}: ApplePayFrameProps) {
const webViewRef = useRef<WebView>(null);
const [channelId] = useState(() => Math.random().toString(36).substr(2, 9));
const frameUrl = `${FRAME_ORIGIN}/platform/v1/apple-pay?${new URLSearchParams(
{
clientToken,
signature: quoteSignature,
channelId,
},
).toString()}`;
const sendSetQuote = useCallback(
(signature: string) => {
const data = {
version: 2,
meta: { channelId },
kind: "setQuote",
payload: { quote: { signature } },
};
webViewRef.current?.postMessage(JSON.stringify(data));
},
[channelId],
);
const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
const message = parseMessage(event.nativeEvent.data, channelId);
if (!message) return;
switch (message.kind) {
case "handshake":
webViewRef.current?.postMessage(
JSON.stringify({ version: 2, meta: { channelId }, kind: "ack" }),
);
break;
case "ready":
onEvent({ kind: "ready" });
break;
case "complete": {
const payload = message.payload as {
transaction: { id: string; status: string; failureReason?: string };
};
onEvent({
kind: "complete",
payload: { transaction: payload.transaction as any },
});
break;
}
case "error": {
const payload = message.payload as {
kind?: string;
code?: string;
message: string;
};
const errorKind = payload.kind || payload.code;
if (errorKind === "quoteExpired") {
onEvent({
kind: "quoteExpired",
payload: { setQuote: sendSetQuote },
});
} else if (errorKind === "applePayUnavailable") {
onEvent({ kind: "unsupported" });
} else {
onEvent({ kind: "error", payload: payload as any });
}
break;
}
}
},
[channelId, onEvent, sendSetQuote],
);
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: frameUrl }}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});