Skip to main content

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.

Use 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

Install react-native-webview to render frames in your app.
pnpm i react-native-webview
If targeting iOS, you may also need to run:
cd ios && pod install
The connect and check frames require X25519 key exchange to encrypt client credentials. The examples below use noble-curves, but you can use any library that supports X25519 and AES-GCM. You also need a polyfill for 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:
  1. A clientToken from 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 a challenge 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:
  1. A clientToken from a successful connect flow
  2. 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)}
    />
  );
}