> ## 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](/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 title="frames.tsx - Frame handling" icon="mobile">
  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/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 },
  });
  ```
</Accordion>
