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

# Web

> Reference implementation for Web (TypeScript/JavaScript)

This page contains a complete reference implementation for integrating the MoonPay Developer Platform on Web. The implementation is in TypeScript and uses iframes to render MoonPay frames.

## Overview

The Web SDK consists of the following files:

| File | Description |
| - | - |
| `types.ts` | Type definitions imported from generated OpenAPI types, plus SDK-specific types like `Result`, `Connection`, and error types |
| `api.ts` | API client functions for fetching payment methods and quotes |
| `client.ts` | Main `createClient` function that manages credentials and provides a unified interface |
| `crypto.ts` | X25519 key generation and AES-GCM decryption for secure credential handling |
| `frames.ts` | Iframe management for check, connect, and Apple Pay frames with postMessage communication |
| `index.ts` | Public API exports |

## Project Setup

> [accordion] package.json
>
> ```json package.json theme={null}
> {
>   "name": "@moonpay/platform",
>   "version": "0.0.1",
>   "type": "module",
>   "main": "./src/index.ts",
>   "types": "./src/index.ts",
>   "scripts": {
>     "generate": "openapi-ts",
>     "typecheck": "tsc --noEmit"
>   },
>   "dependencies": {
>     "@noble/ciphers": "^2.1.1",
>     "@noble/curves": "^2.0.1",
>     "@noble/hashes": "^2.0.1"
>   },
>   "devDependencies": {
>     "@hey-api/openapi-ts": "^0.92.3",
>     "typescript": "~5.9.3"
>   }
> }
> ```

> [accordion] openapi-ts.config.ts - Type generation config
>
> Configuration for generating TypeScript types from the [OpenAPI spec](https://dev.moonpay.com/platform/guides/openapi-codegen.md). Download the `openapi.json` file to your project root, then run `npm run generate`.
>
> ```ts openapi-ts.config.ts theme={null}
> import { defineConfig } from "@hey-api/openapi-ts";
>
> export default defineConfig({
>   input: "./openapi.json",
>   output: {
>     clean: true,
>     lint: false,
>     path: "./src/gen",
>   },
>   plugins: [
>     {
>       name: "@hey-api/typescript",
>       enums: false, // Use union types instead of enums for better tree-shaking
>     },
>   ],
> });
> ```

## Source Files

> [accordion] index.ts - Public API exports
>
> ```ts index.ts theme={null}
> // Client
> export { createClient } from "./client.js";
> export type { Client, CreateClientOptions } from "./client.js";
>
> // Types
> export type {
>   Result,
>   Connection,
>   Credentials,
>   CreateClientError,
>   GetConnectionError,
>   ConnectError,
>   ConnectEventError,
>   GetPaymentMethodsError,
>   GetQuoteError,
>   SetupApplePayError,
>   ConnectEvent,
>   ApplePayEvent,
>   ApplePayEventError,
>   ConnectFrame,
>   ApplePayFrame,
>   ConnectOptions,
>   GetQuoteParams,
>   PaymentMethod,
>   Quote,
>   Transaction,
>   TransactionStatus,
>   Fees,
>   QuoteLimits,
>   Asset,
>   Wallet,
> } from "./types.js";
> ```

> [accordion] types.ts - Type definitions
>
> Types are imported from the generated OpenAPI types where possible, with SDK-specific types defined separately.
>
> ```ts types.ts theme={null}
> // Import generated types from OpenAPI spec
> import type {
>   AuthorizeReturnType,
>   GetQuoteResponse,
>   MoonPayPaymentsPaymentMethod,
>   TransactionStatus as GeneratedTransactionStatus,
>   Fees as GeneratedFees,
>   QuoteLimits as GeneratedQuoteLimits,
>   Asset as GeneratedAsset,
>   Wallet as GeneratedWallet,
> } from "./gen";
>
> // Re-export generated types
> export type {
>   GetQuoteResponse,
>   Fees,
>   QuoteLimits,
>   Asset,
>   Wallet,
>   TransactionStatus,
> } from "./gen";
>
> // Alias GetQuoteResponse as Quote for convenience
> export type Quote = GetQuoteResponse;
>
> // Result type for SDK operations
> export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
>
> // Credentials derived from AuthorizeReturnType
> export type Credentials = Pick<
>   AuthorizeReturnType,
>   "accessToken" | "clientToken"
> >;
>
> // Connection status
> export type Connection =
>   | { status: "connectionRequired" }
>   | { status: "active"; credentials: Credentials }
>   | { status: "unavailable" }
>   | { status: "pending" }
>   | { status: "failed"; reason: string };
>
> // Error types (SDK-specific)
> export type CreateClientError = {
>   kind: "validationError" | "genericError";
>   message: string;
> };
>
> export type GetConnectionError = {
>   kind:
>     | "ipAddressMismatch"
>     | "invalidSessionToken"
>     | "frameTimeout"
>     | "genericError";
>   message: string;
> };
>
> export type ConnectError = {
>   kind:
>     | "ipAddressMismatch"
>     | "invalidSessionToken"
>     | "frameTimeout"
>     | "genericError";
>   message: string;
> };
>
> export type ConnectEventError = {
>   kind: "genericError";
>   message: string;
> };
>
> export type GetPaymentMethodsError = {
>   kind: "validationError" | "genericError";
>   message: string;
> };
>
> export type GetQuoteError = {
>   kind: "validationError" | "genericError";
>   message: string;
> };
>
> export type SetupApplePayError = {
>   kind:
>     | "invalidQuote"
>     | "quoteExpired"
>     | "applePayUnavailable"
>     | "genericError";
>   message: string;
> };
>
> // Event types (SDK-specific)
> export type ConnectEvent =
>   | { kind: "ready" }
>   | {
>       kind: "complete";
>       payload: { frame: { dispose: () => void } };
>       connection: Connection;
>     }
>   | { kind: "error"; payload: ConnectEventError };
>
> // Frame types (SDK-specific)
> export type ConnectFrame = {
>   dispose: () => void;
> };
>
> // Connect options (SDK-specific)
> export type ConnectOptions = {
>   container: HTMLElement;
>   theme?: { appearance: "dark" | "light" };
>   onEvent: (event: ConnectEvent) => void;
> };
>
> // Payment method - re-export from generated types
> export type PaymentMethod = MoonPayPaymentsPaymentMethod;
>
> // Quote params (SDK-specific for convenience)
> export type GetQuoteParams = {
>   source: string;
>   destination: string;
>   sourceAmount: string;
>   walletAddress: string;
>   paymentMethod: string;
> };
>
> // Transaction - uses generated TransactionStatus with discriminated union
> export type TransactionStatus = GeneratedTransactionStatus;
>
> export type Transaction =
>   | { id: string; status: Exclude<TransactionStatus, "failed"> }
>   | { id: string; status: "failed"; failureReason: string };
>
> // Apple Pay types (SDK-specific)
> export type ApplePayEventError = {
>   kind:
>     | "invalidQuote"
>     | "quoteExpired"
>     | "applePayUnavailable"
>     | "genericError";
>   message: string;
> };
>
> export type ApplePayEvent =
>   | { kind: "ready" }
>   | { kind: "complete"; payload: { transaction: Transaction } }
>   | {
>       kind: "quoteExpired";
>       payload: { setQuote: (quoteSignature: string) => void };
>     }
>   | { kind: "error"; payload: ApplePayEventError }
>   | { kind: "unsupported" };
>
> export type ApplePayFrame = {
>   setQuote: (quoteSignature: string) => void;
>   dispose: () => void;
> };
>
> export type SetupApplePayOptions = {
>   quote: string;
>   container: HTMLElement;
>   onEvent: (event: ApplePayEvent) => void;
> };
> ```

> [accordion] client.ts - Main client
>
> ```ts client.ts theme={null}
> import {
>   runCheckFrame,
>   setupConnectFrame,
>   setupApplePay as setupApplePayFrame,
>   type KeyPair,
> } from "./frames.js";
> import {
>   getPaymentMethods as getPaymentMethodsApi,
>   getQuote as getQuoteApi,
> } from "./api.js";
> import type {
>   Result,
>   Connection,
>   Credentials,
>   CreateClientError,
>   GetConnectionError,
>   ConnectError,
>   ConnectOptions,
>   ConnectFrame,
>   PaymentMethod,
>   GetPaymentMethodsError,
>   Quote,
>   GetQuoteParams,
>   GetQuoteError,
>   ApplePayFrame,
>   ApplePayEvent,
>   SetupApplePayError,
> } from "./types.js";
>
> export type MessageDirection = "inbound" | "outbound";
>
> export type SetupApplePayClientOptions = {
>   quote: string;
>   container: HTMLElement;
>   onEvent: (event: ApplePayEvent) => void;
> };
>
> export type Client = {
>   getConnection: () => Promise<Result<Connection, GetConnectionError>>;
>   connect: (
>     options: ConnectOptions,
>   ) => Promise<Result<ConnectFrame, ConnectError>>;
>   getPaymentMethods: () => Promise<
>     Result<PaymentMethod[], GetPaymentMethodsError>
>   >;
>   getQuote: (params: GetQuoteParams) => Promise<Result<Quote, GetQuoteError>>;
>   setupApplePay: (
>     options: SetupApplePayClientOptions,
>   ) => Promise<Result<ApplePayFrame, SetupApplePayError>>;
> };
>
> export type CreateClientOptions = {
>   sessionToken: string;
>   onMessage?: (direction: MessageDirection, data: unknown) => void;
> };
>
> export function createClient(
>   options: CreateClientOptions,
> ): Result<Client, CreateClientError> {
>   const { sessionToken, onMessage } = options;
>
>   if (!sessionToken || typeof sessionToken !== "string") {
>     return {
>       ok: false,
>       error: { kind: "validationError", message: "Invalid sessionToken" },
>     };
>   }
>
>   let storedKeyPair: KeyPair | null = null;
>   let storedCredentials: Credentials | null = null;
>
>   const client: Client = {
>     async getConnection() {
>       const result = await runCheckFrame({ sessionToken, onMessage });
>       if (result.ok) {
>         storedKeyPair = result.keyPair;
>         if (result.value.status === "active")
>           storedCredentials = result.value.credentials;
>         return { ok: true, value: result.value };
>       }
>       return {
>         ok: false,
>         error: {
>           kind: result.error.kind as GetConnectionError["kind"],
>           message: result.error.message,
>         },
>       };
>     },
>
>     async connect(connectOptions) {
>       const { container, theme, onEvent } = connectOptions;
>
>       if (!storedKeyPair) {
>         const checkResult = await runCheckFrame({ sessionToken, onMessage });
>         if (!checkResult.ok) {
>           return {
>             ok: false,
>             error: {
>               kind: checkResult.error.kind as ConnectError["kind"],
>               message: checkResult.error.message,
>             },
>           };
>         }
>         storedKeyPair = checkResult.keyPair;
>       }
>
>       const wrappedOnEvent = (event: Parameters<typeof onEvent>[0]) => {
>         if (
>           event.kind === "complete" &&
>           event.connection?.status === "active"
>         ) {
>           storedCredentials = event.connection.credentials;
>         }
>         onEvent(event);
>       };
>
>       const result = await setupConnectFrame({
>         sessionToken,
>         keyPair: storedKeyPair,
>         container,
>         theme: theme?.appearance,
>         onEvent: wrappedOnEvent,
>         onMessage,
>       });
>
>       return result.ok
>         ? { ok: true, value: result.value }
>         : {
>             ok: false,
>             error: {
>               kind: result.error.kind as ConnectError["kind"],
>               message: result.error.message,
>             },
>           };
>     },
>
>     async getPaymentMethods() {
>       if (!storedCredentials) {
>         return {
>           ok: false,
>           error: {
>             kind: "validationError",
>             message:
>               "No active connection. Call getConnection() or connect() first.",
>           },
>         };
>       }
>       return getPaymentMethodsApi({
>         accessToken: storedCredentials.accessToken,
>       });
>     },
>
>     async getQuote(params) {
>       if (!storedCredentials) {
>         return {
>           ok: false,
>           error: {
>             kind: "validationError",
>             message:
>               "No active connection. Call getConnection() or connect() first.",
>           },
>         };
>       }
>       return getQuoteApi({
>         accessToken: storedCredentials.accessToken,
>         ...params,
>       });
>     },
>
>     async setupApplePay(applePayOptions) {
>       if (!storedCredentials) {
>         return {
>           ok: false,
>           error: {
>             kind: "genericError",
>             message:
>               "No active connection. Call getConnection() or connect() first.",
>           },
>         };
>       }
>       return setupApplePayFrame({
>         clientToken: storedCredentials.clientToken,
>         quote: applePayOptions.quote,
>         container: applePayOptions.container,
>         onEvent: applePayOptions.onEvent,
>       });
>     },
>   };
>
>   return { ok: true, value: client };
> }
> ```

> [accordion] api.ts - API client functions
>
> ```ts api.ts theme={null}
> import type {
>   Result,
>   PaymentMethod,
>   GetPaymentMethodsError,
>   Quote,
>   GetQuoteParams,
>   GetQuoteError,
> } from "./types.js";
>
> const MOONPAY_API_URL = "https://api.moonpay.com";
>
> export type GetPaymentMethodsOptions = { accessToken: string };
> export type GetQuoteOptions = GetQuoteParams & { accessToken: string };
>
> export async function getPaymentMethods(
>   options: GetPaymentMethodsOptions,
> ): Promise<Result<PaymentMethod[], GetPaymentMethodsError>> {
>   try {
>     const response = await fetch(
>       `${MOONPAY_API_URL}/platform/v1/payment-methods`,
>       {
>         method: "GET",
>         headers: {
>           Authorization: `Bearer ${options.accessToken}`,
>           "Content-Type": "application/json",
>         },
>       },
>     );
>     const data = await response.json();
>     if (response.ok && Array.isArray(data)) return { ok: true, value: data };
>     return {
>       ok: false,
>       error: {
>         kind: "genericError",
>         message: data?.message || "Failed to fetch payment methods",
>       },
>     };
>   } catch (err) {
>     return {
>       ok: false,
>       error: {
>         kind: "genericError",
>         message: err instanceof Error ? err.message : "Request failed",
>       },
>     };
>   }
> }
>
> export async function getQuote(
>   options: GetQuoteOptions,
> ): Promise<Result<Quote, GetQuoteError>> {
>   const {
>     accessToken,
>     source,
>     destination,
>     sourceAmount,
>     walletAddress,
>     paymentMethod,
>   } = options;
>
>   try {
>     const response = await fetch(`${MOONPAY_API_URL}/platform/v1/quotes/buy`, {
>       method: "POST",
>       headers: {
>         Authorization: `Bearer ${accessToken}`,
>         "Content-Type": "application/json",
>       },
>       body: JSON.stringify({
>         source: { asset: { code: source }, amount: sourceAmount },
>         destination: { asset: { code: destination } },
>         wallet: { address: walletAddress },
>         paymentMethod: { type: paymentMethod },
>       }),
>     });
>
>     const data = await response.json();
>     if (response.ok && data.data) {
>       const quote: Quote = data.data;
>       if (!quote.signature) {
>         return {
>           ok: false,
>           error: {
>             kind: "validationError",
>             message: "Quote is not executable - signature missing",
>           },
>         };
>       }
>       return { ok: true, value: quote };
>     }
>     return {
>       ok: false,
>       error: {
>         kind: "genericError",
>         message: data?.message || "Failed to get quote",
>       },
>     };
>   } catch (err) {
>     return {
>       ok: false,
>       error: {
>         kind: "genericError",
>         message: err instanceof Error ? err.message : "Request failed",
>       },
>     };
>   }
> }
> ```

> [accordion] crypto.ts - Encryption utilities
>
> ```ts crypto.ts theme={null}
> import { gcm } from "@noble/ciphers/aes.js";
> import { x25519 } from "@noble/curves/ed25519.js";
> import { hkdf } from "@noble/hashes/hkdf.js";
> import { sha256 } from "@noble/hashes/sha2.js";
>
> export type KeyPair = { privateKey: string; publicKey: string };
>
> const hexToBytes = (hex: string): Uint8Array => {
>   const bytes = new Uint8Array(hex.length / 2);
>   for (let i = 0; i < bytes.length; i++)
>     bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
>   return bytes;
> };
>
> const bytesToHex = (bytes: Uint8Array): string =>
>   bytes.reduce((hex, b) => hex + b.toString(16).padStart(2, "0"), "");
>
> export const decrypt = (
>   encryptedData: string,
>   privateKeyHex: string,
> ): string => {
>   const parsed = JSON.parse(atob(encryptedData)) as {
>     ephemeralPublicKey: string;
>     iv: string;
>     ciphertext: string;
>   };
>
>   const sharedSecret = x25519.getSharedSecret(
>     hexToBytes(privateKeyHex),
>     hexToBytes(parsed.ephemeralPublicKey),
>   );
>   const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
>   const plainTextBytes = gcm(encryptionKey, hexToBytes(parsed.iv)).decrypt(
>     hexToBytes(parsed.ciphertext),
>   );
>
>   return new TextDecoder().decode(plainTextBytes);
> };
>
> export const generateKeyPair = (): KeyPair => {
>   const { secretKey, publicKey } = x25519.keygen();
>   return {
>     privateKey: bytesToHex(secretKey),
>     publicKey: bytesToHex(publicKey),
>   };
> };
> ```

> [accordion] frames.ts - Frame handling
>
> Iframe management for check, connect, and Apple Pay frames.
>
> ```ts frames.ts theme={null}
> import { decrypt, generateKeyPair, type KeyPair } from "./crypto.js";
> import type {
>   Connection,
>   Credentials,
>   Result,
>   ApplePayFrame,
>   ApplePayEvent,
>   ApplePayEventError,
>   Transaction,
>   SetupApplePayError,
>   SetupApplePayOptions,
>   ConnectEvent,
> } from "./types.js";
>
> const FRAME_ORIGIN = "https://platform.moonpay.com";
>
> type FrameMessage = {
>   version: 2;
>   meta: { channelId: string };
>   kind: string;
>   payload?: unknown;
> };
> type CompletePayload = {
>   status: Connection["status"];
>   credentials?: string;
>   reason?: string;
> };
> type ErrorPayload = { code: string; message: string };
> type MessageDirection = "inbound" | "outbound";
>
> function parseMessage(data: unknown, channelId: string): FrameMessage | null {
>   try {
>     const message: FrameMessage =
>       typeof data === "string" ? JSON.parse(data) : 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;
> }
>
> function buildFrameUrl(path: string, params: Record<string, string>): string {
>   const url = new URL(path, FRAME_ORIGIN);
>   Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
>   return url.toString();
> }
>
> const FRAME_TIMEOUT_MS = 10000;
> const generateChannelId = () => `ch_${crypto.randomUUID()}`;
>
> // Check Frame
> export type RunCheckFrameResult =
>   | { ok: true; value: Connection; keyPair: KeyPair }
>   | { ok: false; error: { kind: string; message: string } };
>
> export function runCheckFrame(options: {
>   sessionToken: string;
>   onMessage?: (dir: MessageDirection, data: unknown) => void;
> }): Promise<RunCheckFrameResult> {
>   return new Promise((resolve) => {
>     const channelId = generateChannelId();
>     const keyPair = generateKeyPair();
>     let timeoutId: ReturnType<typeof setTimeout>;
>
>     const iframe = document.createElement("iframe");
>     iframe.style.display = "none";
>     iframe.src = buildFrameUrl("/platform/v1/check-connection", {
>       sessionToken: options.sessionToken,
>       publicKey: keyPair.publicKey,
>       channelId,
>     });
>
>     const cleanup = () => {
>       window.removeEventListener("message", handleMessage);
>       iframe.remove();
>     };
>
>     const handleMessage = (event: MessageEvent) => {
>       if (!event.origin.includes("moonpay.com")) return;
>       const message = parseMessage(event.data, channelId);
>       if (!message) return;
>       options.onMessage?.("inbound", message);
>
>       if (message.kind === "handshake") {
>         const ack = { version: 2, meta: { channelId }, kind: "ack" };
>         options.onMessage?.("outbound", ack);
>         iframe.contentWindow?.postMessage(JSON.stringify(ack), event.origin);
>       } else if (message.kind === "complete") {
>         clearTimeout(timeoutId);
>         cleanup();
>         const payload = message.payload as CompletePayload;
>         if (payload.status === "active" && payload.credentials) {
>           try {
>             resolve({
>               ok: true,
>               value: {
>                 status: "active",
>                 credentials: decryptCredentials(
>                   payload.credentials,
>                   keyPair.privateKey,
>                 ),
>               },
>               keyPair,
>             });
>           } catch {
>             resolve({
>               ok: false,
>               error: {
>                 kind: "genericError",
>                 message: "Failed to decrypt credentials",
>               },
>             });
>           }
>         } else if (payload.status === "failed") {
>           resolve({
>             ok: true,
>             value: {
>               status: "failed",
>               reason: payload.reason || "Connection failed",
>             },
>             keyPair,
>           });
>         } else {
>           resolve({
>             ok: true,
>             value: { status: payload.status } as Connection,
>             keyPair,
>           });
>         }
>       } else if (message.kind === "error") {
>         clearTimeout(timeoutId);
>         cleanup();
>         const payload = message.payload as ErrorPayload;
>         resolve({
>           ok: false,
>           error: { kind: payload.code, message: payload.message },
>         });
>       }
>     };
>
>     window.addEventListener("message", handleMessage);
>     document.body.appendChild(iframe);
>     timeoutId = setTimeout(() => {
>       cleanup();
>       resolve({
>         ok: false,
>         error: {
>           kind: "frameTimeout",
>           message: "Check frame did not respond in time",
>         },
>       });
>     }, FRAME_TIMEOUT_MS);
>   });
> }
>
> // Connect Frame
> export type SetupConnectFrameResult =
>   | { ok: true; value: { dispose: () => void } }
>   | { ok: false; error: { kind: string; message: string } };
>
> export function setupConnectFrame(options: {
>   sessionToken: string;
>   keyPair: KeyPair;
>   container: HTMLElement;
>   theme?: "dark" | "light";
>   onEvent: (event: ConnectEvent) => void;
>   onMessage?: (dir: MessageDirection, data: unknown) => void;
> }): Promise<SetupConnectFrameResult> {
>   return new Promise((resolve) => {
>     const channelId = generateChannelId();
>     let timeoutId: ReturnType<typeof setTimeout>;
>
>     const iframe = document.createElement("iframe");
>     iframe.style.cssText = "width:100%;height:100%;border:none";
>     iframe.allow = "camera; microphone";
>     iframe.src = buildFrameUrl("/platform/v1/connect", {
>       sessionToken: options.sessionToken,
>       publicKey: options.keyPair.publicKey,
>       channelId,
>       ...(options.theme && { theme: options.theme }),
>     });
>
>     const dispose = () => {
>       window.removeEventListener("message", handleMessage);
>       iframe.remove();
>     };
>
>     const handleMessage = (event: MessageEvent) => {
>       if (!event.origin.includes("moonpay.com")) return;
>       const message = parseMessage(event.data, channelId);
>       if (!message) return;
>       options.onMessage?.("inbound", message);
>
>       if (message.kind === "handshake") {
>         const ack = { version: 2, meta: { channelId }, kind: "ack" };
>         options.onMessage?.("outbound", ack);
>         iframe.contentWindow?.postMessage(JSON.stringify(ack), event.origin);
>       } else if (message.kind === "ready") {
>         clearTimeout(timeoutId);
>         options.onEvent({ kind: "ready" });
>       } else if (message.kind === "complete") {
>         clearTimeout(timeoutId);
>         const payload = message.payload as CompletePayload;
>         if (payload.status === "connectionRequired") return;
>         if (payload.status === "active" && payload.credentials) {
>           try {
>             options.onEvent({
>               kind: "complete",
>               payload: { frame: { dispose } },
>               connection: {
>                 status: "active",
>                 credentials: decryptCredentials(
>                   payload.credentials,
>                   options.keyPair.privateKey,
>                 ),
>               },
>             });
>           } catch {
>             options.onEvent({
>               kind: "error",
>               payload: {
>                 kind: "genericError",
>                 message: "Failed to decrypt credentials",
>               },
>             });
>           }
>         } else if (payload.status === "failed") {
>           options.onEvent({
>             kind: "complete",
>             payload: { frame: { dispose } },
>             connection: {
>               status: "failed",
>               reason: payload.reason || "Connection failed",
>             },
>           });
>         } else {
>           options.onEvent({
>             kind: "complete",
>             payload: { frame: { dispose } },
>             connection: { status: payload.status } as Connection,
>           });
>         }
>       } else if (message.kind === "error") {
>         clearTimeout(timeoutId);
>         options.onEvent({
>           kind: "error",
>           payload: {
>             kind: "genericError",
>             message: (message.payload as ErrorPayload).message,
>           },
>         });
>       }
>     };
>
>     window.addEventListener("message", handleMessage);
>     options.container.appendChild(iframe);
>     timeoutId = setTimeout(() => {
>       dispose();
>       resolve({
>         ok: false,
>         error: {
>           kind: "frameTimeout",
>           message: "Connect frame did not respond in time",
>         },
>       });
>     }, FRAME_TIMEOUT_MS);
>     resolve({ ok: true, value: { dispose } });
>   });
> }
>
> // Apple Pay Frame
> export function setupApplePay(
>   options: SetupApplePayOptions & { clientToken: string },
> ): Promise<Result<ApplePayFrame, SetupApplePayError>> {
>   return new Promise((resolve) => {
>     const channelId = crypto.randomUUID();
>     let frameOrigin: string | null = null;
>
>     const iframe = document.createElement("iframe");
>     iframe.style.cssText = "width:100%;height:100%;border:none";
>     iframe.allow = "payment";
>     const url = new URL("/platform/v1/buy-button", FRAME_ORIGIN);
>     url.searchParams.set("clientToken", options.clientToken);
>     url.searchParams.set("signature", options.quote);
>     url.searchParams.set("channelId", channelId);
>     iframe.src = url.toString();
>
>     const sendSetQuote = (signature: string) => {
>       if (!iframe.contentWindow || !frameOrigin) return;
>       iframe.contentWindow.postMessage(
>         JSON.stringify({
>           version: 2,
>           meta: { channelId },
>           kind: "setQuote",
>           payload: { quote: { signature } },
>         }),
>         frameOrigin,
>       );
>     };
>
>     const dispose = () => {
>       window.removeEventListener("message", handleMessage);
>       iframe.remove();
>     };
>
>     const handleMessage = (event: MessageEvent) => {
>       if (!event.origin.includes("moonpay.com")) return;
>       const message = parseMessage(event.data, channelId);
>       if (!message) return;
>
>       if (message.kind === "handshake") {
>         frameOrigin = event.origin;
>         iframe.contentWindow?.postMessage(
>           JSON.stringify({ version: 2, meta: { channelId }, kind: "ack" }),
>           event.origin,
>         );
>       } else if (message.kind === "ready") {
>         options.onEvent({ kind: "ready" });
>       } else if (message.kind === "complete") {
>         options.onEvent({
>           kind: "complete",
>           payload: {
>             transaction: (message.payload as { transaction: Transaction })
>               .transaction,
>           },
>         });
>       } else if (message.kind === "error") {
>         const payload = message.payload as ApplePayEventError;
>         const errorKind =
>           payload.kind || (payload as unknown as { code?: string }).code;
>         if (errorKind === "quoteExpired")
>           options.onEvent({
>             kind: "quoteExpired",
>             payload: { setQuote: sendSetQuote },
>           });
>         else if (errorKind === "applePayUnavailable")
>           options.onEvent({ kind: "unsupported" });
>         else options.onEvent({ kind: "error", payload });
>       }
>     };
>
>     window.addEventListener("message", handleMessage);
>     options.container.appendChild(iframe);
>     resolve({ ok: true, value: { setQuote: sendSetQuote, dispose } });
>   });
> }
>
> export { generateKeyPair, type KeyPair };
> ```
