openreceive-rails 0.4.11 → 0.4.13

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.
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Laravel)
2
2
 
3
- These directions describe OpenReceive 0.4.11.
3
+ These directions describe OpenReceive 0.4.13.
4
4
 
5
5
  Add OpenReceive to a Laravel application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the package is on Packagist
@@ -35,71 +35,94 @@ The one required credential is a receive-only NWC code (Nostr Wallet Connect):
35
35
  a string from the merchant's wallet that can create invoices and read their
36
36
  status, and cannot spend. A swap provider (an "LSC" code) optionally lets the
37
37
  payer send USDT, USDC, ETH or SOL instead, converted into that same
38
- Lightning payment. You supply those credentials and three hooks — `authorize`,
38
+ Lightning payment. Step 0 collects those credentials from the user; you write three hooks — `authorize`,
39
39
  `amountFor`, `onPaid` on one `App\OpenReceive\Host` class;
40
40
  OpenReceive supplies invoices, polling, settlement and the checkout UI. It never
41
41
  owns orders, users, prices, or fulfillment.
42
42
 
43
- ## Step 0 — check the environment before you write code
44
-
45
- Do this before installing the package or editing files.
46
-
47
- 1. Look for `NWC_URI` in this app's server environment — `.env`, the deploy
48
- config, Forge/Vapor/Envoyer environment panels, whatever this app already
49
- uses. If the app runs in a container the value is in none of those: ask the
50
- running process (`docker exec <container> printenv NWC_URI`), because finding
51
- the NAME in a compose file or `.env.example` proves nothing about the value.
52
- Never print or echo the value itself; only report whether it is set. Check
53
- for `LSC_URI_PRIMARY` in the same pass. Remember `php artisan config:cache`:
54
- a cached config captured the values at cache time, and editing `.env`
55
- afterwards changes nothing until the cache is rebuilt.
56
-
57
- If OpenReceive is already installed here, `php artisan openreceive:doctor`
58
- answers this whole step in one command — every credential as set/unset, the
59
- host class and which hooks are still placeholders, the route mount, and the
60
- wallet preflight. It never prints a value.
61
- 2. If BOTH are already set — the common case in an existing app — say so and go
62
- straight to the quickstart. Steps 3 and 4 are for an environment that is
63
- missing one; do not stop to ask about altcoins that are already configured.
64
- If only `NWC_URI` is set, Bitcoin already works: continue, and raise the
65
- altcoin question at step 4 rather than blocking on it.
66
- 3. If `NWC_URI` is missing or empty, stop and tell the user exactly what to
67
- create:
68
-
69
- > OpenReceive cannot issue an invoice without a receive-only NWC code. Get
70
- > one at https://openreceive.org/get_a_nwc_code_to_receive_payments, then
71
- > put `NWC_URI=<the code>` in this app's server environment — for most apps
72
- > that is a `.env` file in the project root — and tell me when it's set.
73
-
74
- Wait for the user before wiring OpenReceive; do not invent a placeholder
75
- value. Waiting is not idleness: you may write `.env.example` with the
76
- variable NAMES only (`NWC_URI=`, `LSC_URI_PRIMARY=`) so the merchant has a
77
- file to copy, and keep building the parts of the host that do not touch
78
- OpenReceive — the order model, the cart, the routes. The stop guards the
79
- credential, not the rest of the app.
80
- 4. If `LSC_URI_PRIMARY` was not already set, ask the user: "Do you want to
81
- accept altcoins and stablecoins (USDT, USDC, ETH, SOL) as well as
82
- Bitcoin?"
83
-
84
- - Yes → send them to https://openreceive.org/set_up_swap_provider for a
85
- swap-provider (LSC) code, to set as `LSC_URI_PRIMARY` in the same server
86
- environment. Do NOT wait for it: no application code reads the value, so
87
- the integration is identical with or without it — the engine picks it up
88
- from the environment and swaps switch on. What a yes DOES change is the
89
- refund route back (the swap non-negotiable below): build it as part of
90
- this integration, not when the code arrives.
91
- - No → skip it. Bitcoin over Lightning works with `NWC_URI` alone, and you
92
- can add a swap provider later without changing application code.
93
- 5. Check the environment again and confirm `NWC_URI` is present.
94
- `LSC_URI_PRIMARY` may land later; swaps stay off until it does, and no code
95
- changes when it arrives.
96
- 6. If OpenReceive is ALREADY installed here, check the installed versions of
97
- `openreceive/laravel` (`composer show openreceive/laravel`) and
98
- `@openreceive/browser` against the release named at the top of this file.
99
- The headless display models below do not exist in older versions, and the
100
- first tile click throws with nothing saying why. Upgrade first — and if this
101
- app runs in containers, rebuild the images: `vendor/` is baked into the
102
- image, so an in-place `composer update` is undone by the next `compose up`.
43
+ ## Step 0 — ask for the two codes, one question at a time
44
+
45
+ Before anything else, look at one file: the project's `.env`, if it exists.
46
+ Read it only far enough to see whether `NWC_URI` and `LSC_URI_PRIMARY` are
47
+ there and non-empty, and never print the values. A code that is already
48
+ set is not asked for again; if both are set, skip to step 5
49
+ (making the server load the file), then the quickstart.
50
+
51
+ Otherwise your next action is a question to the user. Do not install the package,
52
+ edit the app, write `.env.example`, or search anywhere else before asking
53
+ it. Do not read deploy config (compose files, Forge/Vapor/Envoyer panels), Docker containers, or process environments, and never run
54
+ `printenv`. A new shop has neither code yet: hunting for one burns the turn and
55
+ can leak other secrets into the transcript.
56
+
57
+ Two server-only URIs are needed before the quickstart:
58
+
59
+ - `NWC_URI` — a receive-only Nostr Wallet Connect code,
60
+ `nostr+walletconnect://…`. Required for Bitcoin.
61
+ - `LSC_URI_PRIMARY` — a Lightning Swap Connect URI,
62
+ `lightning+swapconnect://…`. Required for USDT, USDC, ETH and SOL. Skip it
63
+ only when the user says they want Bitcoin alone.
64
+
65
+ The user never edits an environment file. They paste each code into the chat;
66
+ you store it. Ask one question per message.
67
+
68
+ 1. **First message — the NWC code, and nothing else.** Ask for it and walk them
69
+ through getting it:
70
+
71
+ > To receive payments I need a receive-only wallet code. In Rizful: open
72
+ > the menu, tap NWC, choose Receive-only NWC code, and tap Copy
73
+ > (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
74
+ > rather run your own wallet, Alby Hub works too: Connections → Add
75
+ > Connection → Read Only. Paste the code here and I will store it.
76
+
77
+ Do not mention `.env`, exports, or "tell me when it's set".
78
+ 2. **When they paste it.** If it does not start with `nostr+walletconnect://`,
79
+ ask them to copy the receive-only code again. Otherwise write
80
+ `NWC_URI=<paste>` into the project's `.env`, creating the file if needed.
81
+ Make sure `.gitignore` covers `.env` (and `.dockerignore`, if the app has
82
+ one). Never echo the value, commit it, or put it in client code. Reply only
83
+ that it is saved, then ask the next question.
84
+ 3. **Second message — swaps.** If the user already asked for stablecoins or
85
+ altcoins, skip the yes/no and go straight to the walkthrough. Otherwise ask
86
+ whether payers should also be able to pay with USDT, USDC, ETH or SOL. The
87
+ walkthrough:
88
+
89
+ > Go to https://lightning-swap.com, sign in for API keys, create a key, and
90
+ > copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste
91
+ > it here and I will store it — or say "Bitcoin only" and I will continue
92
+ > without it.
93
+
94
+ Mention FixedFloat only if they already use it.
95
+ 4. **When they paste it.** If it does not start with
96
+ `lightning+swapconnect://`, ask them to copy it again. Otherwise add
97
+ `LSC_URI_PRIMARY=<paste>` to the same `.env`, without echoing it. Swaps
98
+ are now on, so build the refund route back (the swap non-negotiable below) as
99
+ part of this integration. If they chose Bitcoin only, leave
100
+ `LSC_URI_PRIMARY` unset and skip that route.
101
+ 5. **Make the server load the file — yourself.** Laravel loads
102
+ `.env` itself. If the app runs `php artisan config:cache`, run
103
+ `php artisan config:clear` (or re-cache) after writing the file: a cached
104
+ config never reads `.env` again. When the app is started with Docker
105
+ Compose, give the service `env_file: .env`. Restart the server after
106
+ writing the file.
107
+
108
+ Do not invent placeholder URIs. Start the quickstart only once `NWC_URI` is
109
+ saved and `LSC_URI_PRIMARY` is saved or explicitly declined. The first boot
110
+ runs the receive-only preflight: if it reports spend methods such as
111
+ `pay_invoice`, remove `NWC_URI` from `.env` and ask for a receive-only code
112
+ again. Never set the spend-capable override to get past it.
113
+
114
+ ### Upgrading an existing install
115
+
116
+ This is not the opening move of a new integration. When OpenReceive is ALREADY
117
+ installed here, `php artisan openreceive:doctor` reports every credential as
118
+ set/unset (never the value), the host class and any placeholder hooks, the
119
+ route mount, and the wallet preflight. Check the installed
120
+ `openreceive/laravel` (`composer show openreceive/laravel`) and
121
+ `@openreceive/browser` against the release named at the top of this file: the
122
+ headless display models below do not exist in older versions, and the first
123
+ tile click throws with nothing saying why. Upgrade first — and if this app runs
124
+ in containers, rebuild the images: `vendor/` is baked into the image, so an
125
+ in-place `composer update` is undone by the next `compose up`.
103
126
 
104
127
  Only then start the quickstart.
105
128
 
@@ -302,12 +325,18 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-lara
302
325
 
303
326
  ## Laravel quickstart
304
327
 
305
- Requires PHP ≥ 8.2 (64-bit) and Laravel ≥ 11 (12 current). Extensions:
306
- `ext-gmp` is REQUIRED — the NWC transport's elliptic-curve math depends on it
307
- — plus `sodium`, `mbstring` and the `pdo_*` driver for your database
308
- (`pdo_pgsql`, `pdo_mysql` or `pdo_sqlite`). `php -m` lists what your build
309
- has; on Debian/Ubuntu it is `apt-get install php8.2-gmp`, in the official
310
- Docker image `docker-php-ext-install gmp`.
328
+ Requires PHP ≥ 8.2 (64-bit) and Laravel ≥ 11 (12 current). You also need these
329
+ PHP extensions:
330
+
331
+ - `ext-gmp`, which is REQUIRED. The NWC transport's elliptic-curve math depends
332
+ on it.
333
+ - `sodium` and `mbstring`.
334
+ - The `pdo_*` driver for your database (`pdo_pgsql`, `pdo_mysql` or
335
+ `pdo_sqlite`).
336
+
337
+ `php -m` lists what your build has. To add gmp on Debian/Ubuntu, run
338
+ `apt-get install php8.2-gmp`. In the official Docker image, run
339
+ `docker-php-ext-install gmp`.
311
340
 
312
341
  Add the Laravel package:
313
342
 
@@ -315,10 +344,10 @@ Add the Laravel package:
315
344
  composer require openreceive/laravel
316
345
  ```
317
346
 
318
- That is the whole install: `openreceive/laravel` depends on
319
- `openreceive/openreceive`, the engine, so the default wallet client — built from
320
- `NWC_URI` — works with nothing else added, and package discovery registers the
321
- service provider. Hosts that bring their own NWC client bind
347
+ That is the whole install. `openreceive/laravel` depends on
348
+ `openreceive/openreceive`, the engine. So the default wallet client works with
349
+ nothing else added. It is built from `NWC_URI`. Package discovery registers the
350
+ service provider. If your app brings its own NWC client, bind
322
351
  `OpenReceive\Nwc\ReceiveNwcClient` in the container instead.
323
352
 
324
353
  Then run:
@@ -330,35 +359,36 @@ php artisan migrate
330
359
 
331
360
  `openreceive:install` writes three files:
332
361
 
333
- - `config/openreceive.php` — the settings. The host hook is a CLASS NAME, not
334
- a closure, because `php artisan config:cache` serializes this file and a
335
- closure anywhere in it fails the cache;
336
- - `app/OpenReceive/Host.php` — the three hooks (`authorize`, `amountFor`,
337
- `onPaid`), with the two generated placeholders wired and the fulfillment
338
- note as comments;
339
- - `database/migrations/*_create_openreceive_tables.php` — one migration for
340
- both engine tables (`openreceive_payments` and `openreceive_meta`). Its DDL
341
- comes from the engine's `PaymentsSchema::statements()` for your connection's
342
- driver — PostgreSQL, MySQL/MariaDB and SQLite — and `php artisan migrate` runs
343
- it like any migration of your own; there is no second runner.
362
+ - `config/openreceive.php`: the settings. The host hook is a CLASS NAME, not a
363
+ closure. `php artisan config:cache` serializes this file, and a closure
364
+ anywhere in it makes the cache fail.
365
+ - `app/OpenReceive/Host.php`: the three hooks (`authorize`, `amountFor`,
366
+ `onPaid`), with the two generated placeholders wired in and the fulfillment
367
+ note as comments.
368
+ - `database/migrations/*_create_openreceive_tables.php`: one migration for both
369
+ engine tables (`openreceive_payments` and `openreceive_meta`). Its DDL comes
370
+ from the engine's `PaymentsSchema::statements()` for your connection's
371
+ driver: PostgreSQL, MySQL/MariaDB or SQLite. `php artisan migrate` runs it
372
+ like any of your own migrations. There is no second runner.
344
373
 
345
374
  → [OpenReceive\Storage](https://openreceive.org/guides/api-reference.md#openreceivestorage)
346
375
 
347
376
  There is no Eloquent model for the engine's tables, and you do not write one.
348
377
  The engine owns the table's commit locking, write-once settlement, and
349
- reconciliation state machine. `reference` is indexed but not unique (a
350
- reference may have many historical attempts); `payment_hash` is globally unique.
378
+ reconciliation state machine. `reference` is indexed but not unique, because
379
+ one reference may have many historical attempts. `payment_hash` is globally
380
+ unique.
351
381
 
352
382
  #### Fulfill exactly once
353
383
 
354
384
  Within OpenReceive's own settlement paths, `onPaid` runs at most once per
355
- reference: a second payment to a second invoice is recorded with
385
+ reference. A second payment to a second invoice is recorded with
356
386
  `status_reason = "duplicate_settlement"` and never fulfills again.
357
387
 
358
- The one thing you own: **if anything other than OpenReceive can also fulfill
359
- an order** — an admin action, a second payment processor, a replayed job —
360
- those paths race each other, and `onPaid` must be idempotent. The generated
361
- `Host.php` spells this out and shows the guarded transition:
388
+ One case is yours to handle. **If anything other than OpenReceive can also
389
+ fulfill an order**, such as an admin action, a second payment processor, or a
390
+ replayed job, those paths race each other. Then `onPaid` must be idempotent.
391
+ The generated `Host.php` explains this and shows the guarded transition:
362
392
 
363
393
  ```php
364
394
  public function onPaid(PaymentSettlement $settlement): void
@@ -374,23 +404,28 @@ public function onPaid(PaymentSettlement $settlement): void
374
404
  }
375
405
  ```
376
406
 
377
- Delivery is at-least-once: `onPaid` runs inside the settlement transaction,
378
- and an exception rolls it back for the next pass to retry. Keep it to database
379
- writes on the order — an email or webhook sent from here would survive the
380
- rollback and go out again. The `'paid'` transition above is the flag; let your
381
- own job drain it after commit, or implement `OpenReceive\Hosts\AfterPaid`:
382
- its `afterPaid(PaymentSettlement $settlement)` runs after COMMIT, best-effort,
383
- for the same first settled attempt — the place for `FulfillOrder::dispatch(…)`
384
- or an event. And open no `DB::transaction()` of your own inside `onPaid`: the
385
- engine already holds the transaction on this connection, and PDO refuses a
386
- nested `BEGIN`.
387
-
388
- **A query-builder `update()` fires no model events.** That is the point — it is
389
- one conditional `UPDATE`, so the claim is atomic and there is no model code
407
+ Delivery is at-least-once. `onPaid` runs inside the settlement transaction. If
408
+ it throws, the transaction rolls back and the next pass retries. So keep
409
+ `onPaid` to database writes on the order. An email or webhook sent from here
410
+ would survive the rollback and go out again. The `'paid'` transition above is
411
+ the flag. Drain it after commit in one of two ways:
412
+
413
+ - Let your own job drain it.
414
+ - Implement `OpenReceive\Hosts\AfterPaid`. Its
415
+ `afterPaid(PaymentSettlement $settlement)` runs after COMMIT, best-effort,
416
+ for the same first settled attempt. Put `FulfillOrder::dispatch(…)` or an
417
+ event there.
418
+
419
+ Do not open a `DB::transaction()` of your own inside `onPaid`. The engine
420
+ already holds the transaction on this connection, and PDO refuses a nested
421
+ `BEGIN`.
422
+
423
+ **A query-builder `update()` fires no model events.** That is intended. It
424
+ runs one conditional `UPDATE`, so the claim is atomic and no model code runs
390
425
  between the check and the write. It also means no observer, no `saved` event
391
- and no `Model::updated` listener runs, which is fine for a job draining the
392
- flag and useless for a model that owns the transition through events. If yours
393
- does, take a row lock for the duration instead:
426
+ and no `Model::updated` listener runs. That is fine for a job that drains the
427
+ flag. It does not work for a model that owns the transition through events. If
428
+ yours does, take a row lock for the duration instead:
394
429
 
395
430
  ```php
396
431
  public function onPaid(PaymentSettlement $settlement): void
@@ -403,26 +438,29 @@ public function onPaid(PaymentSettlement $settlement): void
403
438
  }
404
439
  ```
405
440
 
406
- **Unlocking a download works the same way.** If what the payer bought is a
407
- file, do not unlock it in the browser: gate the download route on the paid
408
- order row — `Order::where('id', $id)->where('user_id', $request->user()->id)->where('state', 'paid')->firstOrFail()`
409
- or a 404 — and serve the file only then. The `'paid'` written above is the
410
- unlock; the client never decides an order was fulfilled, it re-reads the row.
411
- Buy a Button's `ShopController::download` is this in twenty lines.
412
-
413
- Both shapes are idempotent, and both are correct. They differ only in whether
414
- your model layer gets to run: the query-builder `update()` skips it and is the
415
- right default; the row lock holds the row for the duration of the method and
416
- is what you want when the transition has to go through your model. The
417
- generated fulfillment note says the same thing — if your fulfillment is a
418
- read-modify-write that cannot be expressed as one conditional `UPDATE`, take
419
- the lock.
420
-
421
- A runnable illustration of this boundary — not a template to copy models from —
422
- is Buy a Button
423
- (`examples/buttons/server/laravel`).
424
- It has products, visitors, and orders, with the three hooks as the only bridge.
425
- Map that shape onto the models in THIS app.
441
+ **Unlocking a download works the same way.** If the payer bought a file, do not
442
+ unlock it in the browser. Gate the download route on the paid order row, and
443
+ serve the file only if that row exists:
444
+ `Order::where('id', $id)->where('user_id', $request->user()->id)->where('state', 'paid')->firstOrFail()`,
445
+ or a 404 otherwise. The `'paid'` written above is the unlock. The client never
446
+ decides that an order was fulfilled. It re-reads the row. Buy a Button's
447
+ `ShopController::download` does this in twenty lines.
448
+
449
+ Both shapes are idempotent and correct. They differ only in whether your model
450
+ layer runs:
451
+
452
+ - The query-builder `update()` skips the model layer. It is the right default.
453
+ - The row lock holds the row for the duration of the method. Use it when the
454
+ transition has to go through your model.
455
+
456
+ The generated fulfillment note says the same thing. If your fulfillment is a
457
+ read-modify-write that one conditional `UPDATE` cannot express, take the lock.
458
+
459
+ Buy a Button
460
+ (`examples/buttons/server/laravel`)
461
+ is a runnable illustration of this boundary. It is not a template to copy
462
+ models from. It has products, visitors, and orders, and the three hooks are the
463
+ only bridge. Map that shape onto the models in THIS app.
426
464
 
427
465
  ### Add wallet credentials
428
466
 
@@ -436,31 +474,37 @@ LSC_URI_BACKUP=
436
474
  ```
437
475
 
438
476
  1. Get a receive-only NWC code from a compatible wallet
439
- ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments))
440
- → `NWC_URI`.
441
- 2. Optionally set up a [swap provider](https://openreceive.org/set_up_swap_provider)
442
- → `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP` if you have one).
443
-
444
- Never put these values in browser code. Your application refuses to start if
445
- the NWC code also advertises spend methods such as `pay_invoice`; mint a
446
- receive-only code ([Security](https://openreceive.org/guides/security.md)).
447
-
448
- Under `php artisan config:cache` Laravel never reads `.env` at runtime — the
449
- values are captured when you cache, as for every other Laravel secret — so
477
+ ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
478
+ Put it in `NWC_URI`.
479
+ 2. Optional: set up a [swap provider](https://openreceive.org/set_up_swap_provider).
480
+ Put its connection string in `LSC_URI_PRIMARY`, and a second one in
481
+ `LSC_URI_BACKUP` if you have one.
482
+
483
+ Never put these values in browser code. Your app refuses to start if the NWC
484
+ code also advertises spend methods such as `pay_invoice`. Create a
485
+ receive-only code instead ([Security](https://openreceive.org/guides/security.md)).
486
+
487
+ Under `php artisan config:cache`, Laravel never reads `.env` at runtime. The
488
+ values are captured when you cache, as with every other Laravel secret. So
450
489
  re-run `config:cache` after changing one. The engine redacts connection
451
- strings in every log line, and `php artisan openreceive:doctor` prints each
452
- variable as set/unset only.
490
+ strings in every log line. `php artisan openreceive:doctor` prints each
491
+ variable only as set or unset.
453
492
  → [Environment variables](https://openreceive.org/guides/environment-variables.md).
454
493
 
455
494
  ### Configure the host hooks
456
495
 
457
496
  `app/OpenReceive/Host.php` needs three things: authorization, the trusted
458
- price, and fulfillment. All three receive the `reference` — a string you
459
- choose, and the fulfillment identity: your order id, one per thing you
460
- fulfill, created before checkout, kept across retries, never reused.
461
- OpenReceive never looks inside it, but `onPaid` commits fulfillment once per reference, a new
462
- checkout under a reference that already settled is refused with 409, and a
463
- fresh id per page load lets one order be paid twice.
497
+ price, and fulfillment. All three receive the `reference`. This is a string you
498
+ choose, and it is the fulfillment identity. Use your order id:
499
+
500
+ - one per thing you fulfill,
501
+ - created before checkout,
502
+ - kept across retries,
503
+ - never reused.
504
+
505
+ OpenReceive never looks inside it. But `onPaid` commits fulfillment once per
506
+ reference, and a new checkout under a reference that already settled is
507
+ refused with 409. A fresh id per page load would let one order be paid twice.
464
508
 
465
509
  ```php
466
510
  <?php
@@ -521,67 +565,74 @@ final class Host implements OpenReceiveHost
521
565
  }
522
566
  ```
523
567
 
524
- `config/openreceive.php` names that class (`'host' => App\OpenReceive\Host::class`)
525
- and the provider resolves it from the container, so it may take constructor
526
- dependencies. `onPaid` runs inside the settlement transaction, only for the
527
- first settled attempt for a reference.
568
+ `config/openreceive.php` names that class
569
+ (`'host' => App\OpenReceive\Host::class`). The provider resolves it from the
570
+ container, so it may take constructor dependencies. `onPaid` runs inside the
571
+ settlement transaction, only for the first settled attempt for a reference.
528
572
  → [OpenReceive\Host](https://openreceive.org/guides/api-reference.md#openreceivehost) ·
529
573
  [the authorize context](https://openreceive.org/guides/api-reference.md#the-authorize-context-php)
530
574
 
531
- The routes mount under `config('openreceive.route_prefix')` (`openreceive`) and
532
- `config('openreceive.middleware')` (`['web']`). The `web` group is how the
533
- engine picks up your application's session and CSRF protection: Laravel's
534
- `VerifyCsrfToken` reads `X-CSRF-TOKEN`, and the checkout client sends it from
535
- `<meta name="csrf-token" content="{{ csrf_token() }}">` automatically — keep
536
- that tag in the layout that renders the checkout. The same group brings every
537
- middleware it carries; a global `auth` redirect on the `web` group would send
538
- the engine's JSON routes to a login page too, so keep such guards on your own
539
- route groups, not on `web` itself.
540
-
541
- The generated `Host.php` ships `use LoggingOnPaid;` — a placeholder that only
542
- logs the settlement and fulfills nothing. Replace it with your real fulfillment
543
- (as above); the engine warns every time your application boots while the
544
- placeholder is still there, because orders would otherwise be recorded as
545
- settled without ever being fulfilled. The same applies to `use
546
- AllowAllAuthorize;`, the generated allow-all placeholder: it treats
547
- possession of the reference as authorization, which is safe only while
548
- references are unguessable, and the engine warns at boot until you replace it
549
- with your own ownership check (as above). Replace both, not just `onPaid`.
550
-
551
- The amount always comes from your own order record; payer-supplied amounts are
575
+ The routes mount under `config('openreceive.route_prefix')` (`openreceive`),
576
+ with the middleware in `config('openreceive.middleware')` (`['web']`). The
577
+ `web` group is how the engine picks up your application's session and CSRF
578
+ protection. Laravel's `VerifyCsrfToken` reads `X-CSRF-TOKEN`. The checkout
579
+ client sends it automatically from
580
+ `<meta name="csrf-token" content="{{ csrf_token() }}">`, so keep that tag in
581
+ the layout that renders the checkout.
582
+
583
+ The `web` group also brings every middleware it carries. A global `auth`
584
+ redirect on the `web` group would send the engine's JSON routes to a login page
585
+ too. Keep such guards on your own route groups, not on `web` itself.
586
+
587
+ The generated `Host.php` ships two placeholders. Replace both, not just
588
+ `onPaid`:
589
+
590
+ - `use LoggingOnPaid;` only logs the settlement and fulfills nothing. Replace
591
+ it with your real fulfillment (as above). Until you do, orders would be
592
+ recorded as settled without ever being fulfilled, so the engine warns every
593
+ time your application boots.
594
+ - `use AllowAllAuthorize;` allows everything. It treats possession of the
595
+ reference as authorization, which is safe only while references are
596
+ unguessable. The engine warns at boot until you replace it with your own
597
+ ownership check (as above).
598
+
599
+ The amount always comes from your own order record. Payer-supplied amounts are
552
600
  rejected. The engine's `PaymentRepository` interface remains the escape hatch
553
- for custom-storage applications and is not part of the quickstart.
554
-
555
- For public web shops, opt into the per-IP invoice cap with
556
- `'rate_limiting' => true`; leave it off (the default) when many payers share
557
- one IP. The client IP is `$request->ip()`, so Laravel's `TrustProxies` decides
558
- who the payer is behind a proxy. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
559
-
560
- In production the engine builds the wallet client — and runs its receive-only
561
- preflight — eagerly when your application boots, so a missing `NWC_URI`, a
562
- dead relay, or a spend-capable wallet stops the deploy instead of surfacing as
563
- customer-facing 500s on the first checkout. Outside production (tests,
564
- consoles) the client is built lazily so no live wallet is needed.
565
-
566
- PHP builds the whole engine again on every request, so that same preflight
567
- would run per request under PHP-FPM or Apache; the package remembers the
568
- wallet's info event in your default cache store for
569
- `config('openreceive.wallet_info_cache_seconds')` (600) so a checkout request
570
- costs one relay round trip, not two.
601
+ for apps with custom storage. It is not part of the quickstart.
602
+
603
+ For public web shops, turn on the per-IP invoice cap with
604
+ `'rate_limiting' => true`. Leave it off (the default) when many payers share
605
+ one IP. The client IP is `$request->ip()`, so behind a proxy Laravel's
606
+ `TrustProxies` decides who the payer is. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
607
+
608
+ In production, the engine builds the wallet client when your app boots. It
609
+ also runs the receive-only preflight right away: it reaches the wallet and
610
+ checks that the code cannot spend. A missing `NWC_URI`, a dead relay, or a
611
+ spend-capable wallet then stops the deploy. Otherwise those problems would
612
+ show up as 500 errors for customers on the first checkout. Outside production
613
+ (tests, consoles), the engine builds the client lazily, on first use, so no
614
+ live wallet is needed.
615
+
616
+ PHP builds the whole engine again on every request. Under PHP-FPM or Apache,
617
+ that same preflight would then run per request. To avoid that, the package
618
+ caches the wallet's info event in your default cache store for
619
+ `config('openreceive.wallet_info_cache_seconds')` (600). A checkout request
620
+ then costs one relay round trip, not two.
571
621
 
572
622
  ### Render the checkout
573
623
 
574
- Serve the compiled `styles.css` without Tailwind processing: import it from
575
- JavaScript (with a CSS-capable bundler) or use a plain `<link rel="stylesheet">`.
576
- Do not `@import` it into the host Tailwind entry. Its zero-specificity rules
577
- allow host styles to override checkout styles; scoping does not prevent that.
624
+ Serve the compiled `styles.css` without Tailwind processing. Either import it
625
+ from JavaScript (with a CSS-capable bundler) or use a plain
626
+ `<link rel="stylesheet">`. Do not `@import` it into your Tailwind entry. Its
627
+ rules have zero specificity, so your own styles can override checkout styles.
628
+ Scoping does not prevent that.
578
629
 
579
- The engine serves JSON checkout routes only — rendering is your view. Any
580
- OpenReceive frontend package works against the `/openreceive` mount; the
581
- smallest is the custom element (its default `prefix` is already
582
- `/openreceive`, and the package ships a self-contained `styles.css`; it is
583
- scoped to what OpenReceive renders). Laravel ships Vite, so the package installs like any
584
- other frontend dependency:
630
+ The engine serves JSON checkout routes only. Your view does the rendering. Any
631
+ OpenReceive frontend package works against the `/openreceive` mount. The
632
+ smallest is the custom element. Its default `prefix` is already
633
+ `/openreceive`. The package ships a self-contained `styles.css`, scoped to what
634
+ OpenReceive renders. Laravel ships Vite, so the package installs like any other
635
+ frontend dependency:
585
636
 
586
637
  ```sh
587
638
  npm install @openreceive/elements
@@ -605,32 +656,32 @@ defineElements();
605
656
  ```
606
657
 
607
658
  The element creates the checkout for `reference`, then renders and polls
608
- itself. React/Vue/Svelte/Angular apps use the matching wrapper package instead
609
- — same props and defaults ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a
610
- custom checkout only if this app cannot use a drop-in; then
611
- `@openreceive/browser/headless` is the API
612
- ([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
659
+ itself. React, Vue, Svelte, and Angular apps use the matching wrapper package
660
+ instead, with the same props and defaults
661
+ ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a custom checkout only if
662
+ this app cannot use a drop-in. In that case `@openreceive/browser/headless` is
663
+ the API ([Headless checkout](https://openreceive.org/guides/headless-checkout.md)).
613
664
 
614
665
  Everything the checkout draws ships inside the JavaScript: the payment-method
615
666
  icons, the wallet logos and the pay tutorials. There is no image file to copy
616
667
  or serve and no asset option to set. Deploy your normal JavaScript and CSS
617
668
  build output, including any generated JavaScript chunks. Bundlers with code
618
- splitting can defer tutorial screenshots until first open; single-file builds
619
- (including the standalone checkout) include them upfront. If your
620
- Content-Security-Policy has a strict `img-src`, allow `data:`
669
+ splitting can load tutorial screenshots only when a tutorial is first opened.
670
+ Single-file builds, including the standalone checkout, include them upfront. If
671
+ your Content-Security-Policy has a strict `img-src`, allow `data:`
621
672
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
622
673
 
623
- Then open the checkout in a browser, confirm the payment-method icons and
674
+ Then open the checkout in a browser. Confirm the payment-method icons and
624
675
  wallet logos render, and open a wallet's pay tutorial to check its screenshots.
625
- If an image is missing, inspect the console for CSP violations and the Network
676
+ If an image is missing, check the console for CSP violations and the Network
626
677
  panel for failed JavaScript chunks. Allow `data:` in `img-src` and deploy the
627
678
  complete build output. Do not add image routes, copy package source images, or
628
679
  use registry `icon_path` / tutorial `path` keys as browser URLs.
629
680
 
630
681
  **No npm in this project?** Every GitHub release attaches
631
- `standalone-checkout-<version>.tar.gz`: one self-registering ESM file, its
632
- stylesheet and a `MANIFEST.json` of hashes. Unpack it into
633
- `public/openreceive/` and the same element needs two tags:
682
+ `standalone-checkout-<version>.tar.gz`. It holds one self-registering ESM file,
683
+ its stylesheet and a `MANIFEST.json` of hashes. Unpack it into
684
+ `public/openreceive/`, and the same element needs just two tags:
634
685
 
635
686
  ```blade
636
687
  <link rel="stylesheet" href="/openreceive/openreceive-checkout.css" />
@@ -639,7 +690,7 @@ stylesheet and a `MANIFEST.json` of hashes. Unpack it into
639
690
  <openreceive-checkout reference="{{ $order->id }}"></openreceive-checkout>
640
691
  ```
641
692
 
642
- `MANIFEST.json` carries the version and a SHA-256 per file; keep the tarball
693
+ `MANIFEST.json` carries the version and a SHA-256 per file. Keep the tarball
643
694
  version in step with the Composer package.
644
695
 
645
696
  ### Reconciliation
@@ -655,33 +706,40 @@ load:
655
706
  php artisan openreceive:notifications
656
707
  ```
657
708
 
658
- It listens for the wallet's NWC-02 `payment_received` notifications and runs
659
- a periodic reconcile pass in the same process — one per deployment, not per
660
- web instance. `php artisan openreceive:reconcile` is the one-shot pass if you
661
- want to drive one yourself, and `php artisan openreceive:doctor` reports every
662
- credential as set/unset, the host and its placeholders, the route mount and
663
- the wallet preflight.
709
+ The worker listens for the wallet's NWC-02 `payment_received` notifications.
710
+ The same process also runs a periodic reconcile pass. Run one per deployment,
711
+ not one per web instance.
712
+
713
+ Two more commands help:
714
+
715
+ - `php artisan openreceive:reconcile` runs one pass, if you want to drive it
716
+ yourself.
717
+ - `php artisan openreceive:doctor` reports every credential as set or unset,
718
+ the host and its placeholders, the route mount, and the wallet preflight.
664
719
 
665
720
  ### Swap secrets
666
721
 
667
- The package recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP` using the
668
- shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors: setting either one
669
- auto-builds the matching provider, so an app that wants swaps only supplies the
670
- connection strings ([Environment variables](https://openreceive.org/guides/environment-variables.md)). The
671
- `openreceive.swap_providers` container binding is the override knob — bind a
672
- list of your own adapters to replace the auto-built set, or `[]` to disable
673
- swaps.
722
+ The package recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP`, using the
723
+ shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors. Setting
724
+ either one auto-builds the matching provider. So an app that wants swaps only
725
+ supplies the connection strings
726
+ ([Environment variables](https://openreceive.org/guides/environment-variables.md)). To override this, use the
727
+ `openreceive.swap_providers` container binding. Bind a list of your own
728
+ adapters to replace the auto-built set, or `[]` to disable swaps.
674
729
 
675
- One `openreceive_payments` row holds at most one provider order in its
730
+ One `openreceive_payments` row holds at most one provider order, in its
676
731
  server-only `swap_data`. The engine never returns `swap_data` from its routes
677
732
  or writes it to a log. Do not select it into your own API, serialize it, or
678
- log it; it may contain a provider credential.
733
+ log it. It may contain a provider credential.
679
734
 
680
735
  **Setting either connection string commits you to refunds.** A swap deposit can
681
- arrive short or late, which leaves it `refund_required` at the provider with
682
- only your UI able to claim it — and the payer claims it on a second visit,
683
- after leaving your page for an address in another wallet. That needs a
684
- per-order URL your app serves, a route that restores the order behind it, and
685
- something that restores the ATTEMPT, since `/checkouts/prepare` returns none.
686
- [Swap refunds](https://openreceive.org/guides/swap-refunds.md) is the whole of it; read it before you set
736
+ arrive short or late. The provider then marks it `refund_required`, and only
737
+ your UI can claim it. The payer claims it on a second visit, after leaving your
738
+ page to get an address in another wallet. That needs three things:
739
+
740
+ - a per-order URL your app serves,
741
+ - a route that restores the order behind it,
742
+ - something that restores the ATTEMPT, since `/checkouts/prepare` returns none.
743
+
744
+ [Swap refunds](https://openreceive.org/guides/swap-refunds.md) covers all of it. Read it before you set
687
745
  `LSC_URI_PRIMARY`.