openreceive 0.4.26 → 0.4.27

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.
@@ -0,0 +1,492 @@
1
+ This is the full file; follow it from Step 0.
2
+
3
+ # OpenReceive agent directions (mobile apps)
4
+
5
+ ```sh
6
+ # READ THIS FIRST: this file is 26 KB and a summary drops required steps. Download it whole:
7
+ curl -fsSL https://openreceive.org/agent-directions/mobile/full.md
8
+ # Skip the download only if you already have all of it: pasted, read from disk or fetched raw.
9
+ ```
10
+
11
+ **Step 0 in brief** (Step 0 below has the details):
12
+
13
+ - Find the app's server first: OpenReceive runs there, and the app only opens its checkout page.
14
+ - Before installing or editing anything, ask the user for a receive-only NWC code. One question per message.
15
+ - Next ask for the LSC code. If the user wants stablecoins, USDT, USDC, ETH, SOL or crypto, do not ask yes/no first.
16
+ - Write each code into the server's env file yourself. Never into the app, its config or an EXPO_PUBLIC_ name.
17
+ - The app never decides paid: it asks the server. A link back to the app is navigation only.
18
+ - Do not suggest rotating or revoking a code because it was pasted here.
19
+
20
+ These directions describe OpenReceive 0.4.27.
21
+
22
+ Add Bitcoin checkout, with optional USDT, USDC, ETH and SOL, to a mobile app:
23
+ React Native or Expo, Flutter, Swift or Kotlin. OpenReceive has no mobile SDK
24
+ and an app does not need one. OpenReceive runs on the app's server, which keeps
25
+ the wallet code and serves a checkout page. The app opens that page in a
26
+ browser tab and asks its server whether the order is paid. The mobile apps
27
+ guide is appended to this file in full.
28
+
29
+ So this is two jobs: add OpenReceive to the app's server with that server's
30
+ own agent directions, then make the app open the checkout and read the result.
31
+ Do not install `@openreceive/*` packages into the app itself, and do not build
32
+ a native payment screen.
33
+
34
+ ## Step 0 — what the app sells, where its server is, and the two codes
35
+
36
+ 1. **What does the app sell?** Read the app's product, cart and order code.
37
+ Physical goods, and services used outside the app (food, rides, tickets,
38
+ lodging), may be paid this way in every region. Digital goods, subscriptions, unlocks, and
39
+ tips tied to content must use the store's in-app purchase, except through a
40
+ link-out program (the guide's "What the app stores allow" below). If the app
41
+ sells digital goods, stop and tell the user that, with that section's table,
42
+ before you build anything. Go on only if they say a link-out program covers
43
+ them.
44
+ 2. **Find the app's server.** The app's API base URL (in its config, its env,
45
+ or its API client) names it. It may be in this workspace (a `server/`,
46
+ `api/` or `backend/` folder) or in another project. Name its stack, and the
47
+ directions that go with it. For Express they are
48
+ https://openreceive.org/agent-directions/node/full.md, and for another stack
49
+ put its name in place of `node`: `fastify`, `next`, `nestjs`, `fastapi`,
50
+ `django`, `flask`, `rails`, `php`, `laravel`, `sveltekit`, `nuxt`,
51
+ `astro`, `react-router` or `tanstack-start`.
52
+ - **The server is not in this workspace.** OpenReceive is added there, by
53
+ whoever edits it. Tell the user so, give them that URL for the agent that
54
+ edits the server, and stop. Come back for Step 2 once the server serves
55
+ the checkout link.
56
+ - **The app has no server of its own,** only Supabase or Firebase. OpenReceive
57
+ needs one, with a Postgres database: Supabase's, or a new one for a
58
+ Firestore app. Ask the user which stack to add, suggest one in a language
59
+ the team already uses, and add it in this workspace.
60
+
61
+ Do not edit the app or the server yet.
62
+
63
+ Then your next action is a question to the user. Do not search the app, the
64
+ server, deploy config, Docker containers or process environments for a code,
65
+ and never run `printenv`: a new shop has none, and hunting for one can leak
66
+ other secrets into the transcript. If the server's env file already sets
67
+ them, `grep -E '^(NWC_URI|LSC_URI_PRIMARY)=.' <file> | cut -d= -f1` names the
68
+ ones it sets without printing them; a code that is set is not asked for again.
69
+
70
+ Two codes are needed, and both live only in the server's environment:
71
+
72
+ - `NWC_URI`: a receive-only Nostr Wallet Connect code,
73
+ `nostr+walletconnect://…`. Required for Bitcoin.
74
+ - `LSC_URI_PRIMARY`: a Lightning Swap Connect URI,
75
+ `lightning+swapconnect://…`. Required for USDT, USDC, ETH and SOL. Skip it
76
+ only when the user says they want Bitcoin alone.
77
+
78
+ Never put either in the app: not in its code, `app.json`, `Info.plist`, a
79
+ `BuildConfig` field, a bundled `.env`, or any `EXPO_PUBLIC_` name. Anyone who
80
+ downloads an app can read what it ships with.
81
+
82
+ The user never edits an environment file. They paste each code into this chat;
83
+ you store it. Ask one question per message.
84
+
85
+ 1. **First message — the NWC code, and nothing else.** Ask for it and walk them
86
+ through getting it:
87
+
88
+ > To receive payments I need a receive-only wallet code. In Rizful: open
89
+ > the menu, tap NWC, choose Receive-only NWC code, and tap Copy
90
+ > (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
91
+ > rather run your own wallet, Alby Hub works too: Connections → Add
92
+ > Connection → Read Only. Paste the code here and I will store it.
93
+
94
+ 2. **When they paste it.** If it does not start with `nostr+walletconnect://`,
95
+ ask them to copy the receive-only code again. Otherwise write
96
+ `NWC_URI=<paste>` into the server's env file with your file-editing tool:
97
+ `.env.local` in a Next.js server, `.env` in the server's root for every
98
+ other stack. Never write it with a shell command, which shows the code.
99
+ Make sure `.gitignore` covers the file. Never echo the value. Reply only
100
+ that it is saved, then ask the next question.
101
+ 3. **Second message — swaps.** If the user asked for stablecoins, USDT, USDC,
102
+ ETH, SOL, altcoins or "crypto", this message IS the walkthrough below: send
103
+ it as it is, and do not ask yes or no first. Otherwise ask whether payers
104
+ should also be able to pay with USDT, USDC, ETH or SOL, then give the
105
+ walkthrough:
106
+
107
+ > Go to https://lightning-swap.com, sign in for API keys, create a key, and
108
+ > copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
109
+ > it here and I will store it — or say "Bitcoin only" and I will continue
110
+ > without it.
111
+
112
+ 4. **When they paste it.** If it does not start with
113
+ `lightning+swapconnect://`, ask them to copy it again. Otherwise add
114
+ `LSC_URI_PRIMARY=<paste>` to the same file, the same way. Swaps run on the
115
+ checkout page, so the app needs nothing more for them.
116
+
117
+ ## Step 1 — the server
118
+
119
+ Download the server's directions with your shell and follow them, from the
120
+ rest of their Step 0 (loading the env file, restarting) through their
121
+ quickstart and hand-over checks. The two codes are already saved, so skip
122
+ their questions. Their checkout page, in a desktop browser, comes first.
123
+
124
+ Then add the mobile half, from the guide's "The checkout link" below, in that
125
+ stack's own idiom. The guide's code is Express; translate it:
126
+
127
+ - A route the app calls with its own sign-in, which returns a one-time link to
128
+ the checkout page for one of that user's orders. Store only a hash of the
129
+ link's token, for ten minutes and one use.
130
+ - The page route: it consumes the token, sets an HttpOnly cookie for that
131
+ order's payer, redirects to itself without the token, and serves the
132
+ checkout, plus a link back to the app once it settles.
133
+ - `authorize` also lets in the browser that holds that order's cookie. If the
134
+ server serves a website too, keep its own check: either one passes.
135
+
136
+ ## Step 2 — the app
137
+
138
+ Add the guide's snippet for the app's platform, from "Open the checkout from
139
+ the app" below: ask the server for the link, open it in a browser tab
140
+ (`expo-web-browser`, `url_launcher` in `inAppBrowserView`,
141
+ `SFSafariViewController`, or Custom Tabs), and ask the server for the order
142
+ when the payer comes back: by the link back, by closing the tab, or by
143
+ returning the app to the foreground. If the app has no URL scheme for the link
144
+ back, add one.
145
+
146
+ ## After the integration — hand over, then stop
147
+
148
+ The server side is done when its directions' checks pass, the checkout-link
149
+ route answers for a signed-in user's order, the link opens the checkout in a
150
+ desktop browser, and the same link answers 404 the second time. The app side
151
+ is done when it builds and its pay action opens the page.
152
+
153
+ You cannot pay the invoice: the code is receive-only. Do not pay, settle or
154
+ mark an order paid, and do not look for a way to. In one line of your closing
155
+ message, give the user the real check: on a phone, pay a small order from
156
+ their own wallet, come back to the app, and see the order shown as paid. Then
157
+ stop.
158
+
159
+ ## Non-negotiables
160
+
161
+ - Do not suggest rotating, revoking or replacing a code because it was pasted
162
+ into this chat; that is the supported path.
163
+ - The codes live only in the server's environment, never in the app or its
164
+ build config.
165
+ - The app never decides "paid", from a link back, a timer or the page's
166
+ contents. It asks the server, which `onPaid` updates when the payment
167
+ settles.
168
+ - No native checkout screen, swap picker or wallet list in the app. The
169
+ checkout, its swap screens and its refund way back belong on the page.
170
+ - A new one-time link each time the app opens the page; never reuse one.
171
+ - No digital goods through the checkout outside a store's link-out program.
172
+
173
+ ## More documentation
174
+
175
+ Fetch one when the moment comes. Each is raw markdown, so a plain GET is
176
+ enough; drop the `.md` for the same page a person would read.
177
+
178
+ - https://openreceive.org/guides/authorization.md — how `authorize` sees the request, before you add the cookie check
179
+ - https://openreceive.org/guides/frontend-checkout.md — the checkout page's element and React component, and their settled event
180
+ - https://openreceive.org/guides/supabase.md — when the database is Supabase
181
+ - https://openreceive.org/guides/serverless.md — when the server is a function host: Vercel, Netlify, AWS Lambda, Cloud Run
182
+ - https://openreceive.org/guides/swap-refunds.md — the refund flow on the page, before you turn swaps on
183
+ - https://openreceive.org/guides/lightning-swap-connect.md — what an `LSC_URI_*` code actually is
184
+ - https://openreceive.org/guides/security.md and https://openreceive.org/guides/deploying.md — before this goes anywhere real
185
+ - https://openreceive.org/guides.md — the index, if what you need is not above
186
+
187
+ Questions, or a problem with the library itself:
188
+ https://openreceive.org/contact
189
+
190
+ ---
191
+
192
+ ## The quickstart, in full
193
+
194
+ Inlined verbatim so this file needs no network access. Steps 1 and 2 above say when to use each part: its server code is an example in Express, for the server's own stack.
195
+ The page it comes from is https://openreceive.org/guides/mobile-apps.
196
+
197
+ ## Bitcoin checkout in mobile apps
198
+
199
+ OpenReceive has no mobile SDK, and an app does not need one. Your server keeps
200
+ the wallet code and creates each Lightning invoice. The app opens a checkout
201
+ page that your server serves, and the payer pays from their own wallet app. The
202
+ payment goes straight to your wallet. There is no OpenReceive account, no API
203
+ key and no processor in between.
204
+
205
+ Optional swaps let customers pay with **USDT, USDC, SOL, and ETH**. A swap
206
+ provider you configure converts the payment to **BTC over Lightning**, and it
207
+ settles into the same wallet. Available assets and networks depend on the
208
+ provider. Swaps run on the checkout page, so the app needs nothing extra for
209
+ them.
210
+
211
+ This guide covers React Native and Expo, Flutter, Swift and Kotlin apps. Before
212
+ you build, read [What the app stores allow](#what-the-app-stores-allow): what
213
+ you sell decides whether the app may take the payment at all.
214
+
215
+ ### How it works
216
+
217
+ 1. The app asks your server for a checkout link for one order, with the app's
218
+ own sign-in.
219
+ 2. Your server makes a one-time link to its checkout page for that order.
220
+ 3. The app opens the link in a browser tab: Safari View Controller on iOS,
221
+ Custom Tabs on Android.
222
+ 4. The page swaps the one-time link for a cookie that lets this browser pay
223
+ this order, and shows the checkout. The payer pays with Lightning, or with a
224
+ swap.
225
+ 5. Your server's `onPaid` hook marks the order paid when the wallet confirms the
226
+ payment.
227
+ 6. The payer comes back to the app, by closing the tab or with a link back. The
228
+ app asks your server whether the order is paid.
229
+
230
+ Two rules hold throughout:
231
+
232
+ - **The wallet code never ships in the app.** `NWC_URI` and `LSC_URI_PRIMARY`
233
+ live only in your server's environment. Anything inside an app package can be
234
+ read by anyone who downloads it.
235
+ - **The app trusts only your server.** A link back to the app says the payer
236
+ is done looking at the page, not that they paid. The order's paid state comes
237
+ from your server, which `onPaid` updates inside the settlement transaction.
238
+
239
+ ### What the app stores allow
240
+
241
+ Both stores make digital purchases go through their own billing, and both
242
+ exempt physical goods and real-world services. Bitcoin is no exception: Apple's
243
+ guideline 3.1.1 names "cryptocurrencies and cryptocurrency wallets" among the
244
+ mechanisms an app may not use to unlock content or features.
245
+
246
+ | What the payment is for | Apple (iOS) | Google Play | Checkout in the app |
247
+ | --- | --- | --- | --- |
248
+ | Physical goods, and services used outside the app (food, rides, tickets, lodging, bill pay) | Must not use in-app purchase (3.1.3(e)) | Must not use Play billing | Yes, every region |
249
+ | Real-time one-to-one services (tutoring, consultations, fitness training) | Other payment methods allowed (3.1.3(d)). One-to-few and one-to-many services must use in-app purchase | Not on Google's list of exceptions; check the policy | iOS yes; check Google first |
250
+ | A gift from one person to another | Allowed without in-app purchase if it is optional and 100% goes to the receiver. A gift tied to digital content must use in-app purchase (3.2.1(vii)) | Peer-to-peer payments must not use Play billing | Yes, with care |
251
+ | Donations | In the app only for Apple-approved nonprofits that also offer Apple Pay (3.2.1(vi)). Otherwise collect outside the app, such as in Safari (3.2.2(iv)) | Tax-exempt donations must not use Play billing | Android yes; iOS in Safari |
252
+ | Digital goods, subscriptions, unlocks, tips tied to content | In-app purchase, except through a link-out program below | Play billing, except through a program below | Only through a link-out program |
253
+
254
+ Sources: [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/),
255
+ [Google Play Payments policy](https://support.google.com/googleplay/android-developer/answer/9858738).
256
+
257
+ #### Digital goods: the link-out programs
258
+
259
+ Some storefronts let an app send the buyer to a website to pay for digital
260
+ goods. The store still takes a commission on those sales, and that commission
261
+ applies when the buyer pays in bitcoin too.
262
+
263
+ | Storefront | Apple | Google Play |
264
+ | --- | --- | --- |
265
+ | United States | No entitlement needed for buttons and links to the web (3.1.1(a)). A court barred Apple from charging a commission on these purchases in 2025. The appeals court has since said Apple may charge some commission, and the case is still before the courts ([Apple's 10-Q, June 2026](https://www.sec.gov/Archives/edgar/data/0000320193/000032019326000020/aapl-20260627.htm)) | [External content links program](https://support.google.com/googleplay/android-developer/answer/16470497): enroll first, then pay service fees; reporting and fees start October 1, 2026 |
266
+ | European Union | [Out-of-app offers](https://developer.apple.com/support/communication-and-promotion-of-offers-on-the-app-store-in-the-eu/): 15% commission (10% for small businesses and for subscriptions after their first year) from October 1, 2026 | [External offers program](https://support.google.com/googleplay/android-developer/answer/14372887): enroll first, then pay service fees |
267
+ | Japan | [Out-of-app offers](https://developer.apple.com/support/payment-options-on-the-app-store-in-japan/), iOS 26.2 or later: 15% (10%) | Not covered here |
268
+ | Brazil | [Out-of-app offers](https://developer.apple.com/support/payment-options-on-the-app-store-in-brazil), iOS 26.5 or later, buyers 18 or older: 15% (10%) | Not covered here |
269
+ | Everywhere else | In-app purchase only | Play billing only |
270
+
271
+ In the EU, Japan and Brazil, Apple's commission covers sales made within 7 days
272
+ of the link tap. The app must also offer in-app purchase on the same screen, at
273
+ least as prominently as the link, and show Apple's disclosure sheet before it
274
+ opens the browser. Open the link in the browser app, not a tab inside the app:
275
+ the Japan and Brazil terms say so. Kids-category apps may not link out to a
276
+ website at all.
277
+
278
+ The table is a summary of the stores' pages as of October 2026, and both
279
+ stores change these terms often. It is not legal advice. Read the linked pages
280
+ before you submit a build. We have not seen an App Store or Google Play review
281
+ decision about a Lightning checkout for physical goods, either way.
282
+
283
+ ### Your server
284
+
285
+ The checkout page and the payment routes run on your server, from any
286
+ OpenReceive quickstart: [Express](https://openreceive.org/guides/quickstart-node.md),
287
+ [Fastify](https://openreceive.org/guides/quickstart-fastify.md), [Next.js](https://openreceive.org/guides/quickstart-next.md),
288
+ [NestJS](https://openreceive.org/guides/quickstart-nestjs.md), [Rails](https://openreceive.org/guides/quickstart-rails.md),
289
+ [FastAPI](https://openreceive.org/guides/quickstart-fastapi.md), [Django](https://openreceive.org/guides/quickstart-django.md),
290
+ [Flask](https://openreceive.org/guides/quickstart-flask.md), [PHP](https://openreceive.org/guides/quickstart-php.md) or
291
+ [Laravel](https://openreceive.org/guides/quickstart-laravel.md).
292
+
293
+ An app built on a backend-as-a-service has functions instead of a server.
294
+ On Supabase, see [Bitcoin checkout with Supabase](https://openreceive.org/guides/supabase.md). For function
295
+ hosts such as Vercel, Netlify, AWS Lambda and Cloud Run, see
296
+ [Bitcoin checkout on serverless hosts](https://openreceive.org/guides/serverless.md). An app whose only
297
+ database is Firestore needs a Postgres database for its payment records
298
+ (Supabase or Neon both have free plans), because OpenReceive stores them in
299
+ SQL.
300
+
301
+ ### The checkout link
302
+
303
+ The app is signed in to your API, usually with a token. The browser tab the app
304
+ opens is not: it has no access to the app's token. So the server hands the app
305
+ a one-time link, and the page turns it into a cookie that only this order's
306
+ payer holds. OpenReceive mints no tokens. This code is yours.
307
+
308
+ With Express, after the [Express quickstart](https://openreceive.org/guides/quickstart-node.md), and
309
+ `cookie-parser` mounted:
310
+
311
+ ```ts
312
+ import { createHash, randomBytes } from "node:crypto";
313
+
314
+ const hash = (value: string) => createHash("sha256").update(value).digest("hex");
315
+
316
+ // The app asks for a link to pay one of its user's orders.
317
+ app.post("/api/orders/:id/checkout-link", requireAppUser, async (req, res) => {
318
+ const order = await orders.find(req.params.id);
319
+ if (!order || order.userId !== req.user.id) return res.sendStatus(404);
320
+ const once = randomBytes(32).toString("base64url");
321
+ // Store only a hash, valid for ten minutes and for one use.
322
+ await orders.saveLinkToken(order.id, hash(once), Date.now() + 10 * 60_000);
323
+ res.json({ url: `https://shop.example.com/pay/${order.id}?t=${once}` });
324
+ });
325
+
326
+ // The browser tab opens the link. The one-time token becomes a cookie.
327
+ app.get("/pay/:id", async (req, res) => {
328
+ const order = await orders.find(req.params.id);
329
+ if (!order) return res.sendStatus(404);
330
+ if (typeof req.query.t === "string") {
331
+ // Consumes the token: a second visit with the same link gets nothing.
332
+ if (!(await orders.useLinkToken(order.id, hash(req.query.t)))) return res.sendStatus(404);
333
+ const buyer = randomBytes(32).toString("base64url");
334
+ await orders.saveBuyer(order.id, hash(buyer));
335
+ res.cookie(`pay_${order.id}`, buyer, { httpOnly: true, secure: true, sameSite: "lax" });
336
+ return res.redirect(303, `/pay/${order.id}`); // the token leaves the address bar
337
+ }
338
+ res.send(checkoutPage(order.id)); // the HTML below
339
+ });
340
+ ```
341
+
342
+ Then `authorize`, in the options you pass `openReceiveExpress`, lets in only
343
+ the browser that holds that order's cookie:
344
+
345
+ ```ts
346
+ authorize: async ({ native, resource }) => {
347
+ const buyer = native.cookies[`pay_${resource.reference}`];
348
+ return typeof buyer === "string" && (await orders.buyerMatches(resource.reference, hash(buyer)));
349
+ },
350
+ ```
351
+
352
+ The page is the [standalone checkout](https://openreceive.org/guides/frontend-checkout.md#without-a-bundler)
353
+ plus a way back to the app once the order is paid:
354
+
355
+ ```html
356
+ <link rel="stylesheet" href="/static/openreceive/openreceive-checkout.css" />
357
+ <script type="module" src="/static/openreceive/openreceive-checkout.js"></script>
358
+
359
+ <openreceive-checkout id="checkout" reference="ord_123" prefix="/openreceive">
360
+ </openreceive-checkout>
361
+ <p id="done" hidden>Paid. <a href="myshop://orders/ord_123">Back to the app</a></p>
362
+ <script>
363
+ document.getElementById("checkout").addEventListener("openreceive-settled", () => {
364
+ document.getElementById("done").hidden = false;
365
+ });
366
+ </script>
367
+ ```
368
+
369
+ `myshop://` is the app's own URL scheme. A universal link (iOS) or App Link
370
+ (Android) works too, but only on a different domain from the checkout page:
371
+ iOS keeps a link to the page's own domain in the browser.
372
+
373
+ A React page works the same way with `<Checkout>` and its `onSettled` prop.
374
+ See [Frontend checkout](https://openreceive.org/guides/frontend-checkout.md).
375
+
376
+ ### Open the checkout from the app
377
+
378
+ Each snippet asks the server for the link, opens it, and checks the order with
379
+ the server when the payer comes back. `api`, `token` and `fetchOrder` stand for
380
+ the app's own client code.
381
+
382
+ React Native with Expo (`expo-web-browser`):
383
+
384
+ ```ts
385
+ import * as WebBrowser from "expo-web-browser";
386
+ import * as Linking from "expo-linking";
387
+
388
+ export async function pay(orderId: string) {
389
+ const res = await fetch(`${api}/api/orders/${orderId}/checkout-link`, {
390
+ method: "POST",
391
+ headers: { Authorization: `Bearer ${token}` },
392
+ });
393
+ const { url } = await res.json();
394
+ // Safari View Controller on iOS, Custom Tabs on Android. On iOS this
395
+ // resolves when the payer closes it; on Android, as soon as it opens.
396
+ await WebBrowser.openBrowserAsync(url);
397
+ }
398
+
399
+ // The "Back to the app" link: close the tab (iOS) and check the order.
400
+ Linking.addEventListener("url", ({ url }) => {
401
+ const { path } = Linking.parse(url);
402
+ if (path?.startsWith("orders/")) {
403
+ WebBrowser.dismissBrowser();
404
+ fetchOrder(path.slice("orders/".length));
405
+ }
406
+ });
407
+ // Also check the order when the app returns to the foreground
408
+ // (AppState "active"), for a payer who closed the tab instead.
409
+ ```
410
+
411
+ Flutter (`url_launcher`):
412
+
413
+ ```dart
414
+ import 'dart:convert';
415
+ import 'package:http/http.dart' as http;
416
+ import 'package:url_launcher/url_launcher.dart';
417
+
418
+ Future<void> pay(String orderId) async {
419
+ final res = await http.post(
420
+ Uri.parse('$api/api/orders/$orderId/checkout-link'),
421
+ headers: {'Authorization': 'Bearer $token'},
422
+ );
423
+ final url = Uri.parse(jsonDecode(res.body)['url'] as String);
424
+ // Custom Tabs on Android, Safari View Controller on iOS.
425
+ await launchUrl(url, mode: LaunchMode.inAppBrowserView);
426
+ }
427
+ // Check the order when the app resumes (a WidgetsBindingObserver's
428
+ // didChangeAppLifecycleState) and when myshop://orders/<id> arrives.
429
+ ```
430
+
431
+ Swift (`SafariServices`):
432
+
433
+ ```swift
434
+ import SafariServices
435
+
436
+ final class PayController: NSObject, SFSafariViewControllerDelegate {
437
+ func pay(orderId: String, from presenter: UIViewController) async throws {
438
+ var request = URLRequest(url: api.appending(path: "api/orders/\(orderId)/checkout-link"))
439
+ request.httpMethod = "POST"
440
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
441
+ let (data, _) = try await URLSession.shared.data(for: request)
442
+ let link = try JSONDecoder().decode(CheckoutLink.self, from: data)
443
+ let safari = SFSafariViewController(url: link.url)
444
+ safari.delegate = self
445
+ presenter.present(safari, animated: true)
446
+ }
447
+
448
+ // The payer closed the tab: ask the server whether the order is paid.
449
+ func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
450
+ Task { await fetchOrder() }
451
+ }
452
+ }
453
+
454
+ struct CheckoutLink: Decodable { let url: URL }
455
+ ```
456
+
457
+ Kotlin (`androidx.browser`):
458
+
459
+ ```kotlin
460
+ import androidx.browser.customtabs.CustomTabsIntent
461
+
462
+ suspend fun pay(activity: Activity, orderId: String) {
463
+ val url = api.checkoutLink(orderId) // POST /api/orders/{id}/checkout-link with the app's token
464
+ CustomTabsIntent.Builder().build().launchUrl(activity, Uri.parse(url))
465
+ }
466
+
467
+ // In the activity: the payer is back, so ask the server.
468
+ override fun onResume() {
469
+ super.onResume()
470
+ viewModel.refreshOrder()
471
+ }
472
+ ```
473
+
474
+ For a digital purchase in a link-out program, open the link in the browser app
475
+ instead, through the store's own API: Apple's `ExternalPurchaseCustomLink` in the
476
+ EU, Japan and Brazil, a plain link in the US storefront, and Google's external
477
+ links APIs once enrolled. See the links in
478
+ [the program table](#digital-goods-the-link-out-programs).
479
+
480
+ ### What not to do
481
+
482
+ - Do not build the payment into a native screen with a swap picker. The swap
483
+ flow, its refund screens and its way back belong on the checkout page.
484
+ - Do not decide "paid" in the app, from a link back, a timer or the page's
485
+ contents. Ask your server.
486
+ - Do not reuse a checkout link. Make a new one each time the app opens the
487
+ page; the cookie keeps a payer who reloads the page in.
488
+ - Do not sell digital goods through the checkout outside the link-out
489
+ programs. A build that does is likely to be rejected.
490
+
491
+ To test the server side before you ship the app, see
492
+ [Testing your OpenReceive integration](https://openreceive.org/guides/host-testing.md).
@@ -3,7 +3,7 @@ This is the full file; follow it from Step 0.
3
3
  # OpenReceive agent directions (NestJS)
4
4
 
5
5
  ```sh
6
- # READ THIS FIRST: this file is 42 KB and a summary drops required steps. Download it whole:
6
+ # READ THIS FIRST: this file is 43 KB and a summary drops required steps. Download it whole:
7
7
  curl -fsSL https://openreceive.org/agent-directions/nestjs/full.md
8
8
  # Skip the download only if you already have all of it: pasted, read from disk or fetched raw.
9
9
  ```
@@ -17,7 +17,7 @@ curl -fsSL https://openreceive.org/agent-directions/nestjs/full.md
17
17
  - Do not suggest rotating or revoking a code because it was pasted here.
18
18
  - Start the quickstart only once the NWC code is saved, and the LSC code is saved or the user said "Bitcoin only".
19
19
 
20
- These directions describe OpenReceive 0.4.26.
20
+ These directions describe OpenReceive 0.4.27.
21
21
 
22
22
  Add OpenReceive to a NestJS application — the app you are already working in,
23
23
  on Nest's default Express platform. You do not need a copy of the OpenReceive
@@ -68,7 +68,9 @@ server load the file), then the quickstart.
68
68
  On a hosted builder (v0, Vercel, Replit, Lovable), the user may say instead
69
69
  that both codes are already set as the project's environment variables or
70
70
  secrets. Believe them: do not ask for the codes, do not write `.env`, and
71
- do not read or print the variables. Skip step 5's file loading: the platform already puts them in `process.env`.
71
+ do not read or print the variables. Skip step 5's file loading: the platform already puts them in `process.env`. Do not add them to a compose file or any other deploy config
72
+ either: they already reach the app, and a line such as `NWC_URI: ${NWC_URI}`
73
+ replaces them with an empty value.
72
74
 
73
75
  Otherwise your next action is a question to the user. Do not install packages,
74
76
  edit the app, write `.env.example`, or search anywhere else before asking
@@ -188,10 +190,9 @@ The quickstart below has the code. These are the rules it cannot state for
188
190
  itself, and they hold for every integration.
189
191
 
190
192
  - Mount OpenReceive with `app.use(openReceiveExpress(...))` in `src/main.ts`:
191
- after the app's own session, cookie-parser or Passport middleware, before
192
- `app.listen()`, with `app.useBodyParser("json")` right before it. Nest adds
193
- its own JSON parser only at listen, after the mount, so without that line
194
- every checkout request fails with a 500.
193
+ after the app's own session, cookie-parser or Passport middleware, and
194
+ before `app.listen()`. The adapter reads the JSON body itself, so it needs
195
+ no `app.useBodyParser("json")`.
195
196
  - Nest's guards, interceptors and pipes do not run for middleware. `authorize`
196
197
  makes the same ownership check the app's order page makes, by calling the
197
198
  service or helper its guard uses. Do not wrap OpenReceive in a controller to
@@ -331,6 +332,30 @@ products, and you do not join them.
331
332
  only in the hook; emails, jobs, and pushes after commit. Placeholder style is
332
333
  the dialect you declared: `?` on sqlite, `$1` on postgres.
333
334
 
335
+ ### If the database is Supabase
336
+
337
+ Supabase is Postgres, so the tables above go there like any others. Three
338
+ things differ; https://openreceive.org/guides/supabase.md has each in full.
339
+
340
+ - Connect through its transaction pooler (port `6543`, user
341
+ `postgres.<project-ref>`). Run migrations through the session pooler
342
+ (port `5432` on the same host).
343
+ - Verify Supabase's certificate authority: delete `sslmode` from the URL and
344
+ give `pg` `ssl: { ca }`, the text of the project's `prod-ca-2021.crt` (an
345
+ environment variable such as `SUPABASE_CA_CERT`; it is not a secret). Never
346
+ `rejectUnauthorized: false`.
347
+ - After the migration, close both tables to Supabase's Data API, or anyone
348
+ with the project's anon key can read `swap_data`:
349
+
350
+ ```sql
351
+ alter table openreceive_payments enable row level security;
352
+ alter table openreceive_meta enable row level security;
353
+ revoke all on table openreceive_payments, openreceive_meta from anon, authenticated;
354
+ ```
355
+
356
+ `npx openreceive scaffold payments --supabase` writes the HTTPS-API route,
357
+ which is for Cloudflare Workers only. A Node server uses the connection above.
358
+
334
359
  ## If you build your own checkout UI
335
360
 
336
361
  The drop-ins (`<Checkout>`, `<openreceive-checkout>`) already obey all of this.
@@ -442,17 +467,20 @@ NestJS on its default Express platform. Requires Node ≥ 22 and NestJS ≥ 10.
442
467
 
443
468
  OpenReceive mounts in `main.ts` as Express middleware, with
444
469
  `@openreceive/express`, the adapter the [Express quickstart](https://openreceive.org/guides/quickstart-node.md)
445
- uses. There is no Nest module to import. Three things differ from a plain
470
+ uses. There is no Nest module to import. Two things differ from a plain
446
471
  Express app, and this page is about them:
447
472
 
448
- - Nest adds its JSON body parser only when the app starts listening, after
449
- anything `main.ts` mounts. Add it before the mount with
450
- `app.useBodyParser("json")`.
451
473
  - The hooks reach the app's own services through `app.get()`: the order
452
474
  service, and the TypeORM `DataSource` or the Prisma service.
453
475
  - Guards, interceptors and pipes run for controllers, not for middleware. So
454
476
  `authorize` makes the same check your guard makes.
455
477
 
478
+ Nest adds its JSON body parser only when the app starts listening, after
479
+ anything `main.ts` mounts. From 0.4.27 the adapter reads the body itself, so
480
+ the mount needs nothing for it. With 0.4.26 or older, add
481
+ `app.useBodyParser("json")` right before the mount, or every checkout answers
482
+ 500.
483
+
456
484
  ### 1. Install
457
485
 
458
486
  ```sh
@@ -556,9 +584,6 @@ async function bootstrap() {
556
584
  // stay above the mount, so the session is on the request when authorize runs.
557
585
  const orders = app.get(OrdersService);
558
586
  const auth = app.get(AuthService);
559
- // Nest adds its JSON body parser only at listen, after this mount. Without
560
- // this line every checkout request fails with a 500.
561
- app.useBodyParser("json");
562
587
  app.use(
563
588
  openReceiveExpress({
564
589
  wallet: { nwc: process.env.NWC_URI! }, // receive-only NWC code; your app refuses to start otherwise
@@ -3,7 +3,7 @@ This is the full file; follow it from Step 0.
3
3
  # OpenReceive agent directions (Next.js)
4
4
 
5
5
  ```sh
6
- # READ THIS FIRST: this file is 42 KB and a summary drops required steps. Download it whole:
6
+ # READ THIS FIRST: this file is 43 KB and a summary drops required steps. Download it whole:
7
7
  curl -fsSL https://openreceive.org/agent-directions/next/full.md
8
8
  # Skip the download only if you already have all of it: pasted, read from disk or fetched raw.
9
9
  ```
@@ -17,7 +17,7 @@ curl -fsSL https://openreceive.org/agent-directions/next/full.md
17
17
  - Do not suggest rotating or revoking a code because it was pasted here.
18
18
  - Start the quickstart only once the NWC code is saved, and the LSC code is saved or the user said "Bitcoin only".
19
19
 
20
- These directions describe OpenReceive 0.4.26.
20
+ These directions describe OpenReceive 0.4.27.
21
21
 
22
22
  Add OpenReceive to a Next.js App Router application — the app you are already
23
23
  working in. You do not need a copy of the OpenReceive source: the packages are
@@ -68,7 +68,9 @@ server load the file), then the quickstart.
68
68
  On a hosted builder (v0, Vercel, Replit, Lovable), the user may say instead
69
69
  that both codes are already set as the project's environment variables or
70
70
  secrets. Believe them: do not ask for the codes, do not write `.env.local`, and
71
- do not read or print the variables. Next reads them from the environment.
71
+ do not read or print the variables. Next reads them from the environment. Do not add them to a compose file or any other deploy config
72
+ either: they already reach the app, and a line such as `NWC_URI: ${NWC_URI}`
73
+ replaces them with an empty value.
72
74
 
73
75
  Otherwise your next action is a question to the user. Do not install packages,
74
76
  edit the app, write `.env.example`, or search anywhere else before asking
@@ -319,6 +321,30 @@ products, and you do not join them.
319
321
  only in the hook; emails, jobs, and pushes after commit. Placeholder style is
320
322
  the dialect you declared: `?` on sqlite, `$1` on postgres.
321
323
 
324
+ ### If the database is Supabase
325
+
326
+ Supabase is Postgres, so the tables above go there like any others. Three
327
+ things differ; https://openreceive.org/guides/supabase.md has each in full.
328
+
329
+ - Connect through its transaction pooler (port `6543`, user
330
+ `postgres.<project-ref>`). Run migrations through the session pooler
331
+ (port `5432` on the same host).
332
+ - Verify Supabase's certificate authority: delete `sslmode` from the URL and
333
+ give `pg` `ssl: { ca }`, the text of the project's `prod-ca-2021.crt` (an
334
+ environment variable such as `SUPABASE_CA_CERT`; it is not a secret). Never
335
+ `rejectUnauthorized: false`.
336
+ - After the migration, close both tables to Supabase's Data API, or anyone
337
+ with the project's anon key can read `swap_data`:
338
+
339
+ ```sql
340
+ alter table openreceive_payments enable row level security;
341
+ alter table openreceive_meta enable row level security;
342
+ revoke all on table openreceive_payments, openreceive_meta from anon, authenticated;
343
+ ```
344
+
345
+ `npx openreceive scaffold payments --supabase` writes the HTTPS-API route,
346
+ which is for Cloudflare Workers only. A Node server uses the connection above.
347
+
322
348
  ## If you build your own checkout UI
323
349
 
324
350
  The drop-ins (`<Checkout>`, `<openreceive-checkout>`) already obey all of this.