@moonpay/platform-sdk-react-native 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+ # @moonpay/platform-sdk-react-native
2
+
3
+ React Native SDK for the [MoonPay Developer Platform](https://dev.moonpay.com). Connect customers to MoonPay, fetch quotes and payment methods, and render MoonPay frames (widget, Apple Pay, Google Pay, buy, add-card, challenge) as WebViews in your React Native app.
4
+
5
+ > React Native guides are not yet published on [dev.moonpay.com](https://dev.moonpay.com) — this README is the current starting point. The platform concepts (sessions, connections, quotes, frames) are the same as for the [web SDK](https://www.npmjs.com/package/@moonpay/platform-sdk-web), so the existing guides still apply.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @moonpay/platform-sdk-react-native react-native-webview
11
+ ```
12
+
13
+ For Expo projects:
14
+
15
+ ```bash
16
+ npx expo install react-native-webview
17
+ npm install @moonpay/platform-sdk-react-native
18
+ ```
19
+
20
+ Peer dependencies:
21
+
22
+ - `react` >= 18
23
+ - `react-native` >= 0.73 (Hermes supported)
24
+ - `react-native-webview` >= 13
25
+
26
+ The SDK's crypto layer is pure JavaScript (`@noble` libraries) — no native modules or polyfills required.
27
+
28
+ ## Quick start
29
+
30
+ **1. Create a session server-side** with [`@moonpay/platform-sdk-node`](https://www.npmjs.com/package/@moonpay/platform-sdk-node) and pass the session token to your app:
31
+
32
+ ```typescript
33
+ // On your server — never ship your API key in the app
34
+ import { createServerClient } from '@moonpay/platform-sdk-node';
35
+
36
+ const server = createServerClient({ apiKey: process.env.MOONPAY_API_KEY });
37
+
38
+ const result = await server.createSession({
39
+ externalCustomerId: 'customer-123',
40
+ deviceIp: customerIp,
41
+ });
42
+
43
+ if (result.ok) {
44
+ // Send result.value.sessionToken to your app
45
+ }
46
+ ```
47
+
48
+ **2. Wrap your app in `MoonPayProvider`** with the session token:
49
+
50
+ ```tsx
51
+ import { MoonPayProvider } from '@moonpay/platform-sdk-react-native';
52
+
53
+ export function App() {
54
+ return (
55
+ <MoonPayProvider sessionToken={sessionToken}>
56
+ <YourApp />
57
+ </MoonPayProvider>
58
+ );
59
+ }
60
+ ```
61
+
62
+ `MoonPayProvider` also accepts optional `apiBaseUrl` and `frameBaseUrl` props that point the SDK at a different MoonPay environment — useful if MoonPay gives you test endpoints during onboarding. Leave them unset to use production.
63
+
64
+ **3. Check the connection and connect the customer:**
65
+
66
+ ```tsx
67
+ import { Button } from 'react-native';
68
+ import { useMoonPay } from '@moonpay/platform-sdk-react-native';
69
+
70
+ function BuyScreen() {
71
+ const { client } = useMoonPay();
72
+
73
+ const startConnect = async () => {
74
+ const connection = await client.getConnection();
75
+ if (!connection.ok) {
76
+ console.error(connection.error);
77
+ return;
78
+ }
79
+
80
+ if (connection.value.status === 'connectionRequired') {
81
+ // Presents the connect flow full-screen so the customer can sign in to MoonPay
82
+ await client.connect({
83
+ onEvent: (event) => console.log(event.kind),
84
+ });
85
+ }
86
+ };
87
+
88
+ return <Button title="Get started" onPress={startConnect} />;
89
+ }
90
+ ```
91
+
92
+ All client methods return a `Result<T, E>` discriminated union instead of throwing — check `result.ok` before using `result.value`.
93
+
94
+ ## Connection statuses
95
+
96
+ `getConnection()` resolves with one of six statuses. Handle `connectionRequired` and `active` at minimum:
97
+
98
+ | Status | Meaning | What to do |
99
+ |--------|---------|------------|
100
+ | `connectionRequired` | New or expired customer | Run the connect flow — `client.connect()` or `<MoonPayConnect>`. Headless integrations: `<MoonPayAuth>` |
101
+ | `active` | Customer is connected | Proceed — quotes, payment methods, and payment frames are ready to use |
102
+ | `pending` | KYC decision is delayed | Show a waiting state; the status can resolve to `active` on a later visit |
103
+ | `failed` | Terminal failure, such as a KYC rejection | Do not retry the connect flow; the `reason` field describes the failure |
104
+ | `unavailable` | Customer is in a restricted location | Hide MoonPay functionality |
105
+ | `termsAcceptanceRequired` | Headless integrations only — the customer has no valid Terms of Use attestation on file | Show the Terms of Use in your UI, capture acceptance via `POST /platform/v1/terms/attestations`, then check the connection again |
106
+
107
+ ## Two ways to render frames
108
+
109
+ The SDK supports both a declarative and an imperative style:
110
+
111
+ - **Components** (`<MoonPayWidget>`, `<MoonPayApplePayButton>`, …) render inline wherever you place them in your layout. Prefer these — you keep full control over placement, styling, and navigation.
112
+ - **Client methods** (`client.setupWidget()`, `client.connect()`, …) present the frame in a full-screen modal managed by the SDK. Useful when you want a flow to take over the screen without layout work.
113
+
114
+ Headless frames (`<MoonPayConnectionCheck>`, `<MoonPayBuyFrame>`, `<MoonPayConnectionReset>`) render nothing visible — they run a flow in a hidden WebView and report results through `onEvent`.
115
+
116
+ ## Examples
117
+
118
+ ### Connect a customer inline
119
+
120
+ Render the connect flow as part of your own screen instead of a modal:
121
+
122
+ ```tsx
123
+ import { MoonPayConnect } from '@moonpay/platform-sdk-react-native';
124
+
125
+ function ConnectScreen({ onConnected }: { onConnected: () => void }) {
126
+ return (
127
+ <MoonPayConnect
128
+ theme={{ appearance: 'dark' }}
129
+ onEvent={(event) => {
130
+ if (event.kind === 'complete') onConnected();
131
+ }}
132
+ />
133
+ );
134
+ }
135
+ ```
136
+
137
+ On completion, credentials are applied to the shared client automatically — you can call payment methods right away.
138
+
139
+ ### Authenticate with email and OTP (headless)
140
+
141
+ Identity API partners can use `<MoonPayAuth>` instead of the full connect flow — it drives the customer through email and OTP authentication only. The auth frame needs a `clientToken`, which the SDK stores automatically when `getConnection()` resolves with `connectionRequired`, so always check the connection first:
142
+
143
+ ```tsx
144
+ import { useState } from 'react';
145
+ import { Button } from 'react-native';
146
+ import { MoonPayAuth, useMoonPay } from '@moonpay/platform-sdk-react-native';
147
+
148
+ function SignInScreen({ onAuthenticated }: { onAuthenticated: () => void }) {
149
+ const { client } = useMoonPay();
150
+ const [showAuth, setShowAuth] = useState(false);
151
+
152
+ const start = async () => {
153
+ const connection = await client.getConnection();
154
+ if (connection.ok && connection.value.status === 'connectionRequired') {
155
+ setShowAuth(true); // the clientToken is now stored on the client
156
+ }
157
+ };
158
+
159
+ if (!showAuth) {
160
+ return <Button title="Sign in" onPress={start} />;
161
+ }
162
+
163
+ return (
164
+ <MoonPayAuth
165
+ onEvent={(event) => {
166
+ if (event.kind === 'complete') onAuthenticated();
167
+ }}
168
+ />
169
+ );
170
+ }
171
+ ```
172
+
173
+ Mounting `<MoonPayAuth>` — or calling `setupAuth()` — without that prior `getConnection()` call emits an `error` event. On completion, credentials are applied to the shared client automatically, and `event.payload.status` is either `active` or `termsAcceptanceRequired` (see [Connection statuses](#connection-statuses)).
174
+
175
+ ### Get a quote and render the widget
176
+
177
+ ```tsx
178
+ import { useState } from 'react';
179
+ import { Button } from 'react-native';
180
+ import { MoonPayWidget, useMoonPay } from '@moonpay/platform-sdk-react-native';
181
+
182
+ function WidgetScreen() {
183
+ const { client } = useMoonPay();
184
+ const [quote, setQuote] = useState<string | null>(null);
185
+
186
+ const loadQuote = async () => {
187
+ const result = await client.getQuote({
188
+ source: { asset: { code: 'USD' }, amount: '100' },
189
+ destination: { asset: { code: 'BTC' } },
190
+ });
191
+ if (result.ok) {
192
+ setQuote(result.value.data.signature);
193
+ }
194
+ };
195
+
196
+ if (!quote) {
197
+ return <Button title="Get quote" onPress={loadQuote} />;
198
+ }
199
+
200
+ return (
201
+ <MoonPayWidget
202
+ quote={quote}
203
+ onEvent={(event) => {
204
+ if (event.kind === 'complete') {
205
+ console.log('Transaction complete', event.payload.transaction.id);
206
+ }
207
+ }}
208
+ />
209
+ );
210
+ }
211
+ ```
212
+
213
+ ### Render an Apple Pay button
214
+
215
+ The button mounts inline at its natural size (height 48 by default). Quote updates are pushed into the frame without remounting:
216
+
217
+ ```tsx
218
+ import { MoonPayApplePayButton } from '@moonpay/platform-sdk-react-native';
219
+
220
+ <MoonPayApplePayButton
221
+ quote={quoteSignature}
222
+ onEvent={(event) => {
223
+ switch (event.kind) {
224
+ case 'complete':
225
+ console.log('Paid', event.payload.transaction.id);
226
+ break;
227
+ case 'challenge':
228
+ // Card issuer requires 3DS — see "Handle a 3DS challenge" below
229
+ setChallengeUrl(event.payload.url);
230
+ break;
231
+ case 'quoteExpired':
232
+ // Fetch a fresh quote and push it without remounting
233
+ refreshQuote().then((signature) => event.payload.setQuote(signature));
234
+ break;
235
+ case 'unsupported':
236
+ // Device or region does not support Apple Pay — render a fallback
237
+ break;
238
+ }
239
+ }}
240
+ />
241
+ ```
242
+
243
+ `<MoonPayGooglePayButton>` and `<MoonPayBuyButton>` follow the same pattern.
244
+
245
+ ### Execute a buy headlessly
246
+
247
+ `<MoonPayBuyFrame>` executes a buy in a hidden WebView — your own UI stays in control the whole time. It needs an executable quote: one created with a `wallet` and `paymentMethod`. Render the frame when the customer confirms the purchase:
248
+
249
+ ```tsx
250
+ import { MoonPayBuyFrame } from '@moonpay/platform-sdk-react-native';
251
+
252
+ <MoonPayBuyFrame
253
+ quote={quoteSignature}
254
+ externalTransactionId="order-123"
255
+ onEvent={(event) => {
256
+ switch (event.kind) {
257
+ case 'complete':
258
+ console.log('Transaction complete', event.payload.transaction.id);
259
+ break;
260
+ case 'challenge':
261
+ // Card issuer requires 3DS — see "Handle a 3DS challenge" below
262
+ setChallengeUrl(event.payload.url);
263
+ break;
264
+ case 'error':
265
+ console.error(event.payload.code, event.payload.message);
266
+ break;
267
+ }
268
+ }}
269
+ />
270
+ ```
271
+
272
+ Like the payment buttons, `quote` is reactive — pushing a fresh signature does not remount the frame.
273
+
274
+ ### Handle a 3DS challenge
275
+
276
+ Payment frames (buy, buy button, Apple Pay, Google Pay) emit a `challenge` event when the card issuer requires verification. The event payload carries the full challenge URL:
277
+
278
+ ```tsx
279
+ // In the payment frame's onEvent handler:
280
+ case 'challenge':
281
+ setChallengeUrl(event.payload.url);
282
+ break;
283
+ ```
284
+
285
+ Mount `<MoonPayChallenge>` with that URL. The challenge frame reports the final outcome itself — `complete` carries the finished transaction, and `cancelled` means the customer dismissed the verification. Unmount it on either event:
286
+
287
+ ```tsx
288
+ import { MoonPayChallenge } from '@moonpay/platform-sdk-react-native';
289
+
290
+ {challengeUrl && (
291
+ <MoonPayChallenge
292
+ url={challengeUrl}
293
+ onEvent={(event) => {
294
+ if (event.kind === 'complete' && event.payload.flow === 'buy') {
295
+ console.log('Transaction complete', event.payload.transaction.id);
296
+ }
297
+ if (event.kind === 'complete' || event.kind === 'cancelled') {
298
+ setChallengeUrl(null);
299
+ }
300
+ }}
301
+ />
302
+ )}
303
+ ```
304
+
305
+ ## Error handling
306
+
307
+ Methods never throw — they return `Result<T, E>`:
308
+
309
+ ```typescript
310
+ const result = await client.getPaymentMethods();
311
+
312
+ if (result.ok) {
313
+ console.log(result.value.data);
314
+ } else {
315
+ console.error(result.error.code, result.error.message);
316
+ }
317
+ ```
318
+
319
+ ## What the SDK provides
320
+
321
+ **Hook**
322
+
323
+ - `useMoonPay()` — returns the `client` from the nearest `MoonPayProvider`
324
+
325
+ **Client methods**
326
+
327
+ - **Connection** — `getConnection()`, `connect()`, `setupAuth()` (requires a prior `getConnection()` call), `resetConnection()`
328
+ - **Payments data** — `getQuote()`, `getPaymentMethods()`, `deletePaymentMethod()`, `listTransactions()`, `getTransaction()`
329
+ - **Frames** — `setupWidget()`, `setupBuy()`, `setupChallenge()`, `setupAddCard()`
330
+ - **Identity** — `createIdentity()`, `getIdentity()`, `updateIdentity()`, `verifyIdentity()` and file upload helpers for headless integrations
331
+
332
+ **Components**
333
+
334
+ | Component | Renders |
335
+ |-----------|---------|
336
+ | `<MoonPayConnect>` | Connect flow, inline |
337
+ | `<MoonPayConnectionCheck>` | Headless connection check (nothing visible) |
338
+ | `<MoonPayConnectionReset>` | Headless connection reset (nothing visible) |
339
+ | `<MoonPayAuth>` | Email/OTP auth for headless / Identity API integrations — check the connection first |
340
+ | `<MoonPayWidget>` | Full buy widget, inline |
341
+ | `<MoonPayApplePayButton>` | Apple Pay button, inline |
342
+ | `<MoonPayGooglePayButton>` | Google Pay button, inline |
343
+ | `<MoonPayBuyButton>` | Card buy button, inline |
344
+ | `<MoonPayBuyFrame>` | Headless buy execution (nothing visible) |
345
+ | `<MoonPayAddCard>` | Card entry form, inline |
346
+ | `<MoonPayChallenge>` | 3DS challenge, inline |
347
+
348
+ All visible components accept a `style` prop and an `onEvent` callback typed to that frame's events.
349
+
350
+ ## Related packages
351
+
352
+ | Package | Use it for |
353
+ |---------|------------|
354
+ | `@moonpay/platform-sdk-node` | Server-side session creation |
355
+ | `@moonpay/platform-sdk-web` | Web apps |
356
+ | `@moonpay/platform-protocol` | Shared protocol and API types |
357
+
358
+ ## Documentation
359
+
360
+ Platform guides and API reference: [dev.moonpay.com](https://dev.moonpay.com)
361
+
362
+ - [Introduction](https://dev.moonpay.com/platform/overview/introduction)
363
+ - [API & SDK credentials](https://dev.moonpay.com/platform/guides/api-and-sdk-credentials)
364
+ - [Connect a customer](https://dev.moonpay.com/platform/guides/connect-a-customer)
365
+ - [Pay with Widget](https://dev.moonpay.com/platform/guides/pay-with-widget)
366
+ - [Pay with Apple Pay](https://dev.moonpay.com/platform/guides/pay-with-apple-pay)
367
+
368
+ ## License
369
+
370
+ MIT
package/dist/index.cjs CHANGED
@@ -180,7 +180,10 @@ function applyCredentialsOnComplete(msg, ctx) {
180
180
  if (msg.kind !== "complete" || !ctx.privateKey)
181
181
  return;
182
182
  const connection = msg.payload;
183
- if (connection.status !== import_connection.ConnectionStatus.active)
183
+ if (connection.status !== import_connection.ConnectionStatus.active && connection.status !== import_connection.ConnectionStatus.connectionRequired) {
184
+ return;
185
+ }
186
+ if (!connection.credentials)
184
187
  return;
185
188
  const creds = (0, import_platform_sdk_core.decryptCredentials)(connection.credentials, ctx.privateKey);
186
189
  ctx.core.context.setAccessToken(creds.accessToken);
@@ -555,8 +558,13 @@ function createFrameSession(spec, ctx, props, overrides = {}) {
555
558
  }
556
559
  };
557
560
  handle.onMessage((msg) => {
558
- spec.onMessageEffect?.(msg, { ...ctx, privateKey: keyPair?.privateKey });
559
- const event = spec.mapMessage(msg, commands);
561
+ let event;
562
+ try {
563
+ spec.onMessageEffect?.(msg, { ...ctx, privateKey: keyPair?.privateKey });
564
+ event = spec.mapMessage(msg, commands);
565
+ } catch (e) {
566
+ event = spec.errorEvent(e instanceof Error ? e.message : "Failed to process frame message");
567
+ }
560
568
  if (event !== null) {
561
569
  for (const handler of [...eventHandlers]) {
562
570
  handler(event);