openreceive 0.4.28 → 0.4.30

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/lib/openreceive/version.rb +1 -1
  4. data/skills/integrate-openreceive/SKILL.md +10 -1
  5. data/skills/integrate-openreceive/references/astro.md +8 -9
  6. data/skills/integrate-openreceive/references/bolt.md +9 -7
  7. data/skills/integrate-openreceive/references/btcpay.md +3 -4
  8. data/skills/integrate-openreceive/references/django.md +2 -3
  9. data/skills/integrate-openreceive/references/edd.md +4 -5
  10. data/skills/integrate-openreceive/references/fastapi.md +4 -5
  11. data/skills/integrate-openreceive/references/fastify.md +4 -5
  12. data/skills/integrate-openreceive/references/flask.md +2 -3
  13. data/skills/integrate-openreceive/references/fossbilling.md +48 -16
  14. data/skills/integrate-openreceive/references/laravel.md +3 -4
  15. data/skills/integrate-openreceive/references/lovable.md +16 -12
  16. data/skills/integrate-openreceive/references/medusa.md +12 -5
  17. data/skills/integrate-openreceive/references/mobile.md +1 -1
  18. data/skills/integrate-openreceive/references/nestjs.md +2 -3
  19. data/skills/integrate-openreceive/references/next.md +4 -5
  20. data/skills/integrate-openreceive/references/node.md +4 -5
  21. data/skills/integrate-openreceive/references/nuxt.md +9 -10
  22. data/skills/integrate-openreceive/references/odoo.md +446 -0
  23. data/skills/integrate-openreceive/references/paymenter.md +5 -6
  24. data/skills/integrate-openreceive/references/php.md +2 -3
  25. data/skills/integrate-openreceive/references/prestashop.md +476 -0
  26. data/skills/integrate-openreceive/references/rails.md +2 -3
  27. data/skills/integrate-openreceive/references/react-router.md +8 -9
  28. data/skills/integrate-openreceive/references/sveltekit.md +9 -10
  29. data/skills/integrate-openreceive/references/tanstack-start.md +14 -11
  30. data/skills/integrate-openreceive/references/vendure.md +544 -0
  31. data/skills/integrate-openreceive/references/woocommerce.md +5 -6
  32. metadata +4 -1
@@ -17,7 +17,7 @@ curl -fsSL https://openreceive.org/agent-directions/tanstack-start/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.28.
20
+ These directions describe OpenReceive 0.4.30.
21
21
 
22
22
  Add OpenReceive to a TanStack Start application on Node — the app you are
23
23
  already working in, with its own Postgres. OpenReceive ships no TanStack
@@ -474,13 +474,12 @@ enough; drop the `.md` for the same page a person would read.
474
474
  - https://openreceive.org/guides/custom-checkout-route.md — advanced: replacing the shipped adapter's routes with your own
475
475
  - https://openreceive.org/guides/react-material-ui-recipe.md — a worked custom UI on a component library
476
476
  - https://openreceive.org/guides/quickstart-next.md — each hook explained step by step, on Next.js; they are the same here
477
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
477
478
  - https://openreceive.org/guides.md — the index, if what you need is not above
478
479
 
479
480
  Questions, or a problem with the library itself:
480
481
  https://openreceive.org/contact
481
482
 
482
- - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
483
-
484
483
  ---
485
484
 
486
485
  ## The quickstart, in full
@@ -600,12 +599,12 @@ export async function handleOpenReceive(request: Request): Promise<Response> {
600
599
  ```
601
600
 
602
601
  `authorize` sees the incoming request with its cookies, so check it with
603
- the session your app already has. The Next.js quickstart
602
+ the session your app already has. The [Next.js quickstart](https://openreceive.org/guides/quickstart-next.md)
604
603
  explains each hook. They are the same here.
605
604
 
606
605
  Create OpenReceive's two tables with your migrations, or run
607
606
  `paymentsSchemaSql("postgres")` from `@openreceive/http` once. It is
608
- idempotent. See Payment storage.
607
+ idempotent. See [Payment storage](https://openreceive.org/guides/storage.md).
609
608
 
610
609
  ### On Supabase (Lovable)
611
610
 
@@ -630,7 +629,7 @@ npm install @openreceive/http @openreceive/react
630
629
  ) returns void language plpgsql as $$
631
630
  begin
632
631
  update public.orders set status = 'paid'
633
- where id = p_reference::uuid and status = 'pending';
632
+ where id = p_reference::uuid and status = 'awaiting_payment'; -- your unpaid status
634
633
  end
635
634
  $$;
636
635
  ```
@@ -682,17 +681,21 @@ There is no `onPaid`: `openreceive_on_paid` is the fulfillment. `findOrder`
682
681
  reads the order with your server-side Supabase client; in a Lovable app that
683
682
  is `supabaseAdmin` from `@/integrations/supabase/client.server`.
684
683
 
685
- The checkout calls the payment routes from the browser with the page's
686
- cookies, and nothing else. A Lovable app keeps its Supabase sign-in in the
687
- browser, not in a cookie, so `authorize` cannot see who is signed in. Give
688
- the buyer a cookie of their own instead: create orders in a server function
684
+ By default the checkout calls the payment routes from the browser with the
685
+ page's cookies and nothing else. A Lovable app keeps its Supabase sign-in in
686
+ the browser, not in a cookie, so `authorize` cannot see who is signed in from
687
+ those cookies. Since 0.4.25 the React `<Checkout>` also takes a `fetch` that
688
+ sends the access token
689
+ ([Use Supabase Auth in `authorize`](https://openreceive.org/guides/supabase.md#4-use-supabase-auth-in-authorize)), but
690
+ this recipe keeps the simpler route that works with every checkout: give the
691
+ buyer a cookie of their own. Create orders in a server function
689
692
  that sets an HttpOnly `buyer` cookie (a random value, kept across orders) and
690
693
  stores the same value on the order as `buyer_token`. `currentBuyer` reads it
691
694
  back from the request's `cookie` header.
692
695
 
693
696
  Before it serves, the server checks the database, and the payment routes
694
697
  answer 503 until the migration is applied and `openreceive_on_paid` is
695
- yours; the log names the fix. More: Supabase over HTTPS.
698
+ yours; the log names the fix. More: [Supabase over HTTPS](https://openreceive.org/guides/supabase.md#supabase-over-https).
696
699
 
697
700
  ### The checkout page
698
701
 
@@ -0,0 +1,544 @@
1
+ This is the full file; follow it from Step 0.
2
+
3
+ # OpenReceive agent directions (Vendure)
4
+
5
+ ```sh
6
+ # READ THIS FIRST: this file is 25 KB and a summary drops required steps. Download it whole:
7
+ curl -fsSL https://openreceive.org/agent-directions/vendure/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
+ - Before installing or editing anything, ask the user for a receive-only NWC code. One question per message.
14
+ - Next ask for the LSC code. If the user wants stablecoins, USDT, USDC, ETH, SOL or crypto, do not ask yes/no first.
15
+ - The user only pastes codes into this chat. Never ask them to edit .env or run a command to save one.
16
+ - Write each code into .env yourself, as Step 0 says. Never echo it or put it in a shell command.
17
+ - Do not suggest rotating or revoking a code because it was pasted here.
18
+ - Start the quickstart only once the NWC code is saved, and the LSC code is saved or the user said "Bitcoin only".
19
+
20
+ These directions describe OpenReceive 0.4.30.
21
+
22
+ Add OpenReceive to the Vendure shop you are working in: a Vendure server with
23
+ its worker, and usually a storefront in its own project (Vendure's Next.js
24
+ starter or another). OpenReceive is a Vendure plugin on npm,
25
+ `@openreceive/vendure`, with a payment method handler. The quickstart
26
+ appended to this file has all the code: do not clone the OpenReceive
27
+ repository, and do not write your own payment handler, routes or settlement.
28
+
29
+ The one required credential is a receive-only NWC code (Nostr Wallet Connect):
30
+ a string from the merchant's wallet that can create invoices and read their
31
+ status, and cannot spend. A swap provider (an "LSC" code) optionally lets
32
+ buyers pay with USDT, USDC, ETH or SOL instead; the provider converts the
33
+ payment to BTC over Lightning in the merchant's connected wallet. Available
34
+ assets and networks depend on the provider.
35
+
36
+ Vendure keeps orders, prices, customers and fulfillment. The plugin keeps its
37
+ payment attempts in two tables of Vendure's database and adds the settled
38
+ payment to the order itself once the payment settles, so Vendure places it.
39
+ You write no hooks.
40
+
41
+ ## Step 0 — ask for the two codes, one question at a time
42
+
43
+ Before anything else, check one file: the Vendure server's `.env`.
44
+ `grep -E '^(NWC_URI|LSC_URI_PRIMARY)=.' .env | cut -d= -f1` names the codes it
45
+ sets without printing them. Check with that, here and after each write, not
46
+ by reading the file, and never print the values. A code that is already set is
47
+ not asked for again; if both are set, go to Step 1.
48
+
49
+ Otherwise your next action is a question to the user. Do not install packages,
50
+ edit the shop or search anywhere else before asking it. Do not read deploy
51
+ config, Docker containers or process environments looking for a code, and
52
+ never run `printenv`: a new shop has neither code yet.
53
+
54
+ The user never edits an environment file. They paste each code
55
+ into this chat; you store it. That is the supported path: do not ask them to
56
+ save it themselves, and do not tell them to revoke or replace a
57
+ code because it was pasted here. Ask one question per message.
58
+
59
+ 1. **First message — the NWC code, and nothing else.**
60
+
61
+ > To receive payments I need a receive-only wallet code. In Rizful: open
62
+ > the menu, tap NWC, choose Receive-only NWC code, and tap Copy
63
+ > (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
64
+ > rather run your own wallet, Alby Hub works too: Connections → Add
65
+ > Connection → Read Only. Paste the code here and I will store it.
66
+
67
+ 2. **When they paste it.** If it does not start with `nostr+walletconnect://`,
68
+ ask them to copy the receive-only code again. Otherwise write
69
+ `NWC_URI=<paste>` into the Vendure server's `.env` with your file-editing
70
+ tool. Never write it with a shell command (`echo`, `printf`, a heredoc): the
71
+ command line shows the code. Never put it in the storefront, in a
72
+ `NEXT_PUBLIC_` variable or in a commit. Reply only that it is saved, then
73
+ ask the next question.
74
+ 3. **Second message — swaps.** If the user asked for stablecoins, USDT, USDC,
75
+ ETH, SOL, altcoins or "crypto" (as in "Bitcoin and stablecoin payments"),
76
+ this message IS the walkthrough below: do not skip it, and do not ask yes
77
+ or no first. Otherwise ask whether buyers should also be able to pay with
78
+ USDT, USDC, ETH or SOL, then give the walkthrough. The walkthrough:
79
+
80
+ > Go to https://lightning-swap.com, sign in for API keys, create a key, and
81
+ > copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
82
+ > it here and I will store it — or say "Bitcoin only" and I will continue
83
+ > without it.
84
+
85
+ Mention FixedFloat only if they already use it.
86
+ 4. **When they paste it.** If it does not start with
87
+ `lightning+swapconnect://`, ask them to copy it again. Otherwise add
88
+ `LSC_URI_PRIMARY=<paste>` to the same `.env` with the file-editing tool.
89
+ Swaps are now on, so keep the route back (the swap non-negotiable below).
90
+
91
+ A new Vendure project loads `.env` itself. Under Docker Compose, give the
92
+ server, the worker and every other service that runs the Vendure config
93
+ `env_file` pointing at that `.env`, and recreate them with
94
+ `docker compose up -d`. Do not report setup as complete until the NWC code is
95
+ saved, and the LSC code is saved or the user said "Bitcoin only". Never
96
+ invent a placeholder code.
97
+
98
+ ## Step 1 — install the plugin in the Vendure server
99
+
100
+ In the Vendure server's project, run `npm install @openreceive/vendure`, then
101
+ add `OpenReceivePlugin` from `@openreceive/vendure/plugin` to the `plugins` in
102
+ `src/vendure-config.ts` exactly as the quickstart's "Install the plugin"
103
+ shows, keeping every setting already there. The plugin needs Vendure 3.3 or
104
+ later, Node 22.12 or later, and PostgreSQL, MySQL or MariaDB; it refuses
105
+ SQLite. Restart the server and the worker: the server's startup migrations
106
+ create the plugin's two tables, and the worker runs its scheduled task.
107
+
108
+ Restart them and the storefront the way this project already runs them: in
109
+ their own terminal, under Docker Compose with `docker compose up -d --build`,
110
+ or by the pid of the process you started. Never `pkill` or `killall` by name:
111
+ on a shared machine that stops other people's servers.
112
+
113
+ ## Step 2 — create the payment method and check the wallet
114
+
115
+ In the Vendure server's project, with the server up (under Docker Compose,
116
+ run both inside the server's container with `docker compose exec -T`):
117
+
118
+ ```sh
119
+ node node_modules/@openreceive/vendure/dist/enable-openreceive.js
120
+ npx openreceive doctor --url http://localhost:3000
121
+ ```
122
+
123
+ The first creates the payment method, code `openreceive`, in the default
124
+ channel, signing in with `SUPERADMIN_USERNAME` and `SUPERADMIN_PASSWORD` from
125
+ the environment; when those are missing, report that and ask the user to
126
+ create it in the dashboard (Settings → Payment methods, code `openreceive`,
127
+ handler OpenReceive). `doctor` reports each code as set or unset, asks the
128
+ wallet whether it is receive-only, and with `--url` (the server's own
129
+ address) checks that the plugin's routes answer. It names any failed check
130
+ and exits nonzero; fix it before going on. If it reports spend methods such
131
+ as `pay_invoice`, remove `NWC_URI` from `.env` and ask the user for a
132
+ receive-only code again. Never set the spend-capable override to get past it.
133
+
134
+ ## Step 3 — add the checkout to the storefront
135
+
136
+ For Vendure's Next.js starter, make the quickstart's storefront changes:
137
+ `npm install @openreceive/react`, the route file
138
+ `src/app/api/openreceive/[...path]/route.ts`, the server actions in
139
+ `src/features/checkout/openreceive.ts`, the checkout in the review step, and
140
+ the one-line change to `transitionToArrangingPayment`. Keep the rest of the
141
+ storefront as it is. Do not import `@openreceive/react/styles.css` from a
142
+ component or a CSS file: the route serves it, outside the starter's Tailwind
143
+ build.
144
+
145
+ For another storefront, follow the quickstart's "Another storefront": `POST
146
+ /openreceive/order` with the buyer's session where the buyer confirms, the
147
+ checkout with the `reference` it answers, and the checkout's calls sent to
148
+ `/openreceive` with the same session.
149
+
150
+ Rebuild and restart the storefront. Then prove the routes answer through it:
151
+
152
+ ```sh
153
+ curl -s -X POST <storefront url>/api/openreceive/checkouts \
154
+ -H 'content-type: application/json' \
155
+ -d '{"reference":"CHECK:USD:100"}'
156
+ ```
157
+
158
+ The answer is OpenReceive's JSON refusal, `"code":"FORBIDDEN"`: no session
159
+ owns such an order, and the plugin's routes are mounted behind the
160
+ storefront's route.
161
+
162
+ ## Step 4 — hand over, then stop
163
+
164
+ Setup is done when `doctor` is clean and the curl above answers FORBIDDEN.
165
+ The browser check is the user's: name it in one line of your closing message,
166
+ and do not run it yourself. You cannot pay an invoice: the code is
167
+ receive-only. Do not pay, settle or mark an order paid, and do not look for a
168
+ way to (a wallet control port, a test endpoint, another wallet, a payment
169
+ added through the Admin API).
170
+
171
+ Your last message starts "Setup is finished" and has at most five short
172
+ lines: the storefront's address, "add a product to the cart, check out, pick
173
+ Bitcoin & stablecoins at the payment step and place the order: the checkout
174
+ appears", and the methods it offers (Bitcoin, plus USDT, USDC, ETH and SOL
175
+ when the swap code is saved). Do not mention minimums, never say a coin will
176
+ not work, do not list what you changed, and send nothing after it. Do not add
177
+ containers, services, workers or cron jobs: the plugin's task runs in
178
+ Vendure's own worker.
179
+
180
+ ## Non-negotiables
181
+
182
+ - Never print, log or commit a code, never put one in a shell argument, and
183
+ never write one into the storefront, source files or browser code.
184
+ `doctor`'s set/unset is all you report.
185
+ - Do not suggest rotating, revoking or replacing a code because it was pasted
186
+ into this chat; that is the supported path.
187
+ - Work only in this shop's projects. Never read or run anything from another
188
+ project or directory on this machine (its `node_modules`, tools or source),
189
+ for any reason. A browser, Playwright and reading the plugin's source are
190
+ not part of setup: when `doctor`, the enable script or the curl fails,
191
+ report its output.
192
+ - Receive-only NWC is required. Never get past the preflight with a code that
193
+ can spend.
194
+ - The plugin owns only its two payment tables. Vendure owns orders, prices,
195
+ customers and email. Do not write your own payment handler, routes,
196
+ event subscribers or fulfillment code, and do not change Vendure's own
197
+ packages.
198
+ - IF SWAPS ARE ON, KEEP THE ROUTE BACK. A deposit that arrives short or late
199
+ becomes refundable, and the buyer claims it later from the same checkout.
200
+ Keep the plugin installed while swap payments may still need a refund.
201
+ https://openreceive.org/guides/swap-refunds.md
202
+ - A receive-only wallet cannot send refunds. A refund made in the Vendure
203
+ dashboard stays pending; the merchant sends it from their own wallet.
204
+
205
+ ## Further reading
206
+
207
+ - [Vendure Quickstart](https://openreceive.org/guides/quickstart-vendure.md)
208
+ - [Automated Swaps](https://openreceive.org/guides/automated-swaps.md)
209
+ - [Swap Refunds](https://openreceive.org/guides/swap-refunds.md)
210
+ - [Lightning Swap Connect URI](https://openreceive.org/guides/lightning-swap-connect.md)
211
+ - [Security](https://openreceive.org/guides/security.md)
212
+ - [Price Feeds](https://openreceive.org/guides/price-feeds.md)
213
+
214
+ ---
215
+
216
+ ## The quickstart, in full
217
+
218
+ Inlined verbatim so this file needs no network access. Steps 0–4 above are the setup and this is their reference: it has the code they name. Where the two differ, the steps win, and its dashboard screens are only for a shop whose Vendure server has no shell.
219
+ The page it comes from is https://openreceive.org/guides/quickstart-vendure.
220
+
221
+ ## Vendure quickstart
222
+
223
+ [Vendure](https://vendure.io) is an open-source headless commerce platform on
224
+ Node. The OpenReceive plugin adds a payment method to it: the buyer pays at
225
+ the storefront's checkout with Bitcoin over Lightning, straight into your own
226
+ wallet, and Vendure places the order once the payment settles. With a swap
227
+ provider, the same checkout also takes USDT, USDC, SOL and ETH, which the
228
+ provider converts to BTC over Lightning into the same wallet.
229
+
230
+ Vendure keeps owning orders, prices, customers and fulfillment. The plugin
231
+ keeps its payment attempts in two tables of Vendure's own database, over
232
+ Vendure's own connection. When a payment settles, the plugin adds a settled
233
+ payment to the order, with the Lightning payment hash as its transaction id,
234
+ and Vendure places the order as it does for any payment method: the order
235
+ reads PaymentSettled, stock is allocated and Vendure's emails go out.
236
+
237
+ Requirements: Vendure 3.3 or later on Node 22.12 or later, with PostgreSQL,
238
+ MySQL or MariaDB. SQLite and sql.js are not supported: Vendure shares their
239
+ one connection with the plugin. The worker must run, with
240
+ `DefaultSchedulerPlugin`, as a new Vendure project's does: the plugin's
241
+ scheduled task runs there.
242
+
243
+ ### Install the plugin
244
+
245
+ In the Vendure server's project:
246
+
247
+ ```sh
248
+ npm install @openreceive/vendure
249
+ ```
250
+
251
+ Add the plugin to `src/vendure-config.ts`, next to what is already there:
252
+
253
+ ```ts
254
+ import { OpenReceivePlugin } from "@openreceive/vendure/plugin";
255
+
256
+ export const config: VendureConfig = {
257
+ // …your existing settings
258
+ plugins: [
259
+ // …your existing plugins
260
+ OpenReceivePlugin,
261
+ ],
262
+ };
263
+ ```
264
+
265
+ The plugin registers the payment method handler `openreceive`, the
266
+ checkout's routes under `/openreceive` on the Vendure server, a scheduled
267
+ task that runs every minute on the worker, and a migration for its two
268
+ tables. Restart the server and the worker. A new Vendure project runs its
269
+ migrations when the server starts (`runMigrations` in `src/index.ts`), which
270
+ creates the tables; if yours does not, run its migrations the way it already
271
+ does.
272
+
273
+ ### Configure the wallet
274
+
275
+ Put the codes in the Vendure server's `.env`, never in the storefront:
276
+
277
+ ```sh
278
+ NWC_URI=nostr+walletconnect://…
279
+ LSC_URI_PRIMARY=lightning+swapconnect://…
280
+ ```
281
+
282
+ `NWC_URI` is a receive-only Nostr Wallet Connect code: it can create invoices
283
+ and read payments but cannot spend, and the plugin refuses a code that can
284
+ spend. `LSC_URI_PRIMARY` is optional; leave it out for Bitcoin-only checkout.
285
+ A new Vendure project loads `.env` itself (`import 'dotenv/config'` in
286
+ `vendure-config.ts`). Under Docker Compose, give the server and the worker
287
+ `env_file: .env`. Restart both after changing it.
288
+
289
+ Then create the payment method. In the dashboard, open **Settings → Payment
290
+ methods**, create one with the code `openreceive` and the handler
291
+ **OpenReceive**, and enable it. Or run, in the server's project while the
292
+ server is up:
293
+
294
+ ```sh
295
+ node node_modules/@openreceive/vendure/dist/enable-openreceive.js
296
+ ```
297
+
298
+ It signs in to the Admin API with `SUPERADMIN_USERNAME` and
299
+ `SUPERADMIN_PASSWORD` from `.env`, as a new Vendure project has them, and
300
+ creates the method in the default channel: "Bitcoin & stablecoins" with a
301
+ swap code, "Bitcoin Lightning" without. Run again, it enables the method.
302
+ `--admin-api <url>` names another Admin API and `--name "…"` another name.
303
+ Check the codes and the wallet with:
304
+
305
+ ```sh
306
+ npx openreceive doctor
307
+ ```
308
+
309
+ `doctor` reads `.env`, prints each code as set or unset (never its value)
310
+ and asks the wallet whether it is receive-only. With
311
+ `--url http://localhost:3000` it also checks that the plugin's routes answer.
312
+ It exits nonzero when a check fails.
313
+
314
+ ### Add the checkout to the storefront
315
+
316
+ The buyer pays at the checkout's review step, before the order is placed.
317
+ For Vendure's [Next.js starter](https://github.com/vendurehq/nextjs-starter-vendure),
318
+ install the React checkout:
319
+
320
+ ```sh
321
+ npm install @openreceive/react
322
+ ```
323
+
324
+ Add `src/app/api/openreceive/[...path]/route.ts`. It forwards the checkout's
325
+ calls to the Vendure server with the buyer's session token, which the starter
326
+ keeps in its `vendure-auth-token` cookie, and serves the checkout's
327
+ stylesheet as it is, outside the starter's Tailwind build:
328
+
329
+ ```ts
330
+ import { readFile } from "node:fs/promises";
331
+ import { createRequire } from "node:module";
332
+ import path from "node:path";
333
+ import { NextRequest } from "next/server";
334
+ import { getAuthToken } from "@/platform/vendure/auth-token";
335
+
336
+ const SHOP_API = process.env.VENDURE_SHOP_API_URL || process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL!;
337
+ const CHANNEL =
338
+ process.env.VENDURE_CHANNEL_TOKEN || process.env.NEXT_PUBLIC_VENDURE_CHANNEL_TOKEN || "__default_channel__";
339
+
340
+ async function stylesheet() {
341
+ const resolve = createRequire(path.join(process.cwd(), "package.json")).resolve;
342
+ const css = await readFile(resolve("@openreceive/react/styles.css"));
343
+ return new Response(css, {
344
+ headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=3600" },
345
+ });
346
+ }
347
+
348
+ async function forward(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
349
+ const { path: segments } = await params;
350
+ if (request.method === "GET" && segments.join("/") === "styles.css") return stylesheet();
351
+ const url = new URL(`/openreceive/${segments.join("/")}`, SHOP_API);
352
+ url.search = request.nextUrl.search;
353
+ const token = await getAuthToken();
354
+ const response = await fetch(url, {
355
+ method: request.method,
356
+ headers: {
357
+ "content-type": request.headers.get("content-type") ?? "application/json",
358
+ accept: "application/json",
359
+ "vendure-token": CHANNEL,
360
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
361
+ },
362
+ body: request.method === "GET" ? undefined : await request.text(),
363
+ cache: "no-store",
364
+ });
365
+ const headers = new Headers({ "cache-control": "no-store" });
366
+ for (const name of ["content-type", "retry-after", "x-request-id"]) {
367
+ const value = response.headers.get(name);
368
+ if (value) headers.set(name, value);
369
+ }
370
+ return new Response(await response.text(), { status: response.status, headers });
371
+ }
372
+
373
+ export { forward as GET, forward as POST };
374
+ ```
375
+
376
+ Add `src/features/checkout/openreceive.ts`, two server actions. The first
377
+ asks the plugin to start paying the active order: the plugin moves it to
378
+ ArrangingPayment, as Vendure's own checkout does before a payment, and
379
+ answers the reference the checkout pays. The second runs once the payment
380
+ settled, when the plugin has placed the order:
381
+
382
+ ```ts
383
+ "use server";
384
+
385
+ import { updateTag } from "next/cache";
386
+ import { getLocale } from "next-intl/server";
387
+ import { redirect } from "@/platform/i18n/navigation";
388
+ import { getAuthToken } from "@/platform/vendure/auth-token";
389
+
390
+ const SHOP_API = process.env.VENDURE_SHOP_API_URL || process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL!;
391
+ const CHANNEL =
392
+ process.env.VENDURE_CHANNEL_TOKEN || process.env.NEXT_PUBLIC_VENDURE_CHANNEL_TOKEN || "__default_channel__";
393
+
394
+ export async function startOpenReceivePayment(): Promise<{ reference: string; orderCode: string }> {
395
+ const token = await getAuthToken();
396
+ const response = await fetch(new URL("/openreceive/order", SHOP_API), {
397
+ method: "POST",
398
+ headers: {
399
+ "content-type": "application/json",
400
+ "vendure-token": CHANNEL,
401
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
402
+ },
403
+ body: "{}",
404
+ cache: "no-store",
405
+ });
406
+ const body = await response.json();
407
+ if (!response.ok) throw new Error(body.message ?? `OpenReceive answered ${response.status}`);
408
+ return { reference: body.reference, orderCode: body.order_code };
409
+ }
410
+
411
+ export async function finishOpenReceivePayment(orderCode: string) {
412
+ updateTag("cart");
413
+ updateTag("active-order");
414
+ const locale = await getLocale();
415
+ redirect({ href: `/order-confirmation/${orderCode}`, locale });
416
+ }
417
+ ```
418
+
419
+ In `src/features/checkout/routes/steps/review-step.tsx`, show the checkout
420
+ when the buyer places an order with OpenReceive:
421
+
422
+ ```tsx
423
+ import { finishOpenReceivePayment, startOpenReceivePayment } from "../../openreceive";
424
+ import { Checkout } from "@openreceive/react";
425
+
426
+ // The code of the OpenReceive payment method (enable-openreceive.js creates it).
427
+ const OPENRECEIVE_METHOD = "openreceive";
428
+
429
+ // in ReviewStep, beside the existing state:
430
+ const [openReceive, setOpenReceive] = useState<{ reference: string; orderCode: string } | null>(null);
431
+ const [paymentError, setPaymentError] = useState<string | null>(null);
432
+
433
+ // first in handlePlaceOrder, after setLoading(true):
434
+ if (selectedPaymentMethodCode === OPENRECEIVE_METHOD) {
435
+ // The buyer pays here; the plugin places the order once the payment settles.
436
+ try {
437
+ setOpenReceive(await startOpenReceivePayment());
438
+ } catch (error) {
439
+ setPaymentError(error instanceof Error ? error.message : String(error));
440
+ } finally {
441
+ setLoading(false);
442
+ }
443
+ return;
444
+ }
445
+
446
+ // in place of the Place Order <Button>:
447
+ {openReceive ? (
448
+ <>
449
+ <link rel="stylesheet" href="/api/openreceive/styles.css" precedence="default" />
450
+ <Checkout
451
+ reference={openReceive.reference}
452
+ prefix="/api/openreceive"
453
+ resumable
454
+ onSettled={() => {
455
+ void finishOpenReceivePayment(openReceive.orderCode);
456
+ }}
457
+ />
458
+ </>
459
+ ) : (
460
+ <Button onClick={handlePlaceOrder} /* …as it was */>…</Button>
461
+ )}
462
+ {paymentError && <p className="text-sm text-destructive text-center">{paymentError}</p>}
463
+ ```
464
+
465
+ The order now waits in ArrangingPayment while the buyer pays. In
466
+ `src/features/checkout/routes/actions.ts`, let `transitionToArrangingPayment`
467
+ accept an order that is already there, so a buyer who opened the Bitcoin
468
+ checkout can still pay with another method:
469
+
470
+ ```ts
471
+ if (result.data.transitionOrderToState?.__typename === "OrderStateTransitionError") {
472
+ const errorResult = result.data.transitionOrderToState;
473
+ // The OpenReceive checkout already moved it: it is ready to pay another way.
474
+ if (errorResult.fromState === "ArrangingPayment") return;
475
+ throw new Error(/* …as it was */);
476
+ }
477
+ ```
478
+
479
+ #### Another storefront
480
+
481
+ Where the buyer confirms the order, `POST /openreceive/order` on the Vendure
482
+ server with the buyer's session (the cookie or bearer token the storefront
483
+ already sends Vendure, plus `vendure-token` on a channel other than the
484
+ default) and an empty JSON body. Render React `<Checkout>`, or the
485
+ `<openreceive-checkout>` element from `@openreceive/elements`, with the
486
+ `reference` it answers, and send the checkout's calls to `/openreceive` on
487
+ the Vendure server with the same session: forward them from your
488
+ storefront's server as above, or call Vendure from the browser with a `fetch`
489
+ that sends the session. A browser call from another site needs the
490
+ storefront's exact origin in Vendure's `apiOptions.cors.origin`; a pattern or
491
+ `true` is not enough. When the checkout reports the payment settled, the
492
+ order is placed: show its confirmation.
493
+
494
+ ### How a payment works
495
+
496
+ 1. The buyer picks "Bitcoin & stablecoins" at the payment step and places
497
+ the order. The plugin moves the order to ArrangingPayment and answers its
498
+ reference: the order's code, currency and amount due.
499
+ 2. The checkout charges that amount in the order's currency, converted to
500
+ sats at the current BTC price. Only the buyer's session, or the signed-in
501
+ customer the order belongs to, may pay it. A Shop API call cannot add an
502
+ OpenReceive payment, so a buyer cannot mark an order paid.
503
+ 3. Once the wallet reports the payment, the plugin adds a settled payment to
504
+ the order, and Vendure places it.
505
+ 4. The checkout takes the buyer to the order's confirmation.
506
+
507
+ If the buyer closes the page after paying, the scheduled task finds the
508
+ payment within a minute or so and places the order anyway. If the cart
509
+ changes before the buyer pays, the amount due changes and so does the
510
+ reference: the checkout asks for a new invoice for the new total. Vendure
511
+ keeps an order in ArrangingPayment from changing; a storefront that lets the
512
+ buyer go back to the cart moves it back with
513
+ `transitionOrderToState(state: "AddingItems")`.
514
+
515
+ ### Refunds and removal
516
+
517
+ The receive-only wallet cannot send refunds. A refund made in the dashboard
518
+ stays pending in Vendure, and the plugin logs a warning naming the payment:
519
+ send the refund from your own wallet, then settle the refund in the
520
+ dashboard. A buyer whose swap deposit never converted reclaims it from the
521
+ same checkout, with the same session, while the order is still unpaid. If you
522
+ turn on swaps, keep that path available; see
523
+ [swap refunds](https://openreceive.org/guides/swap-refunds.md).
524
+
525
+ Removing the plugin keeps its two tables: they are a record of payments
526
+ received.
527
+
528
+ ### Tested, and not tested
529
+
530
+ Tested on 2026-10-19 with Vendure 3.8.0 on PostgreSQL 17 in Docker, its
531
+ worker and the Next.js starter, a test wallet and a test swap provider: the
532
+ migration, the enable script, `doctor`, and a guest paying an order by
533
+ Lightning and by USDT on Solana in Chromium, after which the order reads
534
+ PaymentSettled with the payment hash as its transaction id. A payment whose
535
+ buyer left was placed by the scheduled task. Another guest, a visitor with no
536
+ session, a changed amount and the Shop API's `addPaymentToOrder` were
537
+ refused, a reload resumed the same invoice, and a buyer who opened the
538
+ Bitcoin checkout then paid with Vendure's test method. CI runs the plugin
539
+ against a Vendure 3.8 server and PostgreSQL.
540
+
541
+ Not tested: MySQL and MariaDB, more than one channel, Vendure's other
542
+ storefront starters, and a storefront calling Vendure from the browser on
543
+ another site. Lightning cannot pull a payment, so OpenReceive saves no
544
+ payment methods.
@@ -17,7 +17,7 @@ curl -fsSL https://openreceive.org/agent-directions/woocommerce/full.md
17
17
  - Do not suggest rotating or revoking a code because it was pasted here.
18
18
  - Setup is done only when 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.28.
20
+ These directions describe OpenReceive 0.4.30.
21
21
 
22
22
  Install and configure the OpenReceive payment gateway in the WooCommerce store
23
23
  you are working in. Preserve its theme, checkout, customer accounts, order
@@ -95,7 +95,7 @@ is saved or the user said "Bitcoin only". Never invent a placeholder code.
95
95
  Install this release from the WordPress.org plugin directory:
96
96
 
97
97
  ```sh
98
- wp plugin install openreceive --version=0.4.28 --activate
98
+ wp plugin install openreceive --version=0.4.30 --activate
99
99
  ```
100
100
 
101
101
  If that fails because WordPress.org does not have this version yet, install
@@ -103,7 +103,7 @@ the ZIP attached to the same GitHub release. Never install the GitHub
103
103
  source-code ZIP or a ZIP from an older release:
104
104
 
105
105
  ```sh
106
- wp plugin install https://github.com/OpenReceive/openreceive/releases/download/v0.4.28/openreceive-wordpress-0.4.28.zip --activate
106
+ wp plugin install https://github.com/OpenReceive/openreceive/releases/download/v0.4.30/openreceive-wordpress-0.4.30.zip --activate
107
107
  ```
108
108
 
109
109
  It needs WooCommerce active, and PHP 8.2+ with GMP and sodium in BOTH the web
@@ -222,7 +222,6 @@ question.
222
222
  - [Lightning Swap Connect URI](https://openreceive.org/guides/lightning-swap-connect.md)
223
223
  - [Security](https://openreceive.org/guides/security.md)
224
224
  - [Price Feeds](https://openreceive.org/guides/price-feeds.md)
225
- - [Payment Safety Upgrade](https://openreceive.org/guides/payment-safety-upgrade.md)
226
225
 
227
226
  ---
228
227
 
@@ -254,7 +253,7 @@ database or application.
254
253
 
255
254
  Each GitHub release also attaches the same plugin as a ZIP, for a site that
256
255
  cannot reach WordPress.org or when WordPress.org does not list a release yet.
257
- Download [openreceive-wordpress-0.4.28.zip](https://github.com/OpenReceive/openreceive/releases/download/v0.4.28/openreceive-wordpress-0.4.28.zip)
256
+ Download [openreceive-wordpress-0.4.30.zip](https://github.com/OpenReceive/openreceive/releases/download/v0.4.30/openreceive-wordpress-0.4.30.zip)
258
257
  and install it through **Plugins → Add New → Upload Plugin**. Historical
259
258
  releases may lack this asset. If that exact URL returns 404, build the same tag
260
259
  below; never silently install an older ZIP. The GitHub source-code ZIP is not
@@ -264,7 +263,7 @@ GMP/sodium, Composer and WP-CLI:
264
263
  ```sh
265
264
  git clone https://github.com/OpenReceive/openreceive.git
266
265
  cd openreceive
267
- git checkout v0.4.28
266
+ git checkout v0.4.30
268
267
  npm ci
269
268
  npm run build:packages
270
269
  composer install --working-dir=packages/php/wordpress