> ## Documentation 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

> Reference implementation for React Native

This page covers the React Native-specific changes for integrating the MoonPay Developer Platform. Most code is shared with the [Web implementation](https://dev.moonpay.com/platform/sdk-reference/reference-implementation/web).

## Overview

For React Native, use `react-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

> [accordion] frames.tsx - Frame handling
>
> Replace the Web `frames.ts` with this React Native version using `react-native-webview`.
>
> ```tsx frames.tsx theme={null}
> 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/buy-button?${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 },
> });
> ```
