@openreceive/vue 0.4.10 → 0.4.12

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.10.
3
+ These directions describe OpenReceive 0.4.12.
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
 
@@ -121,7 +144,7 @@ itself, and they hold for every integration.
121
144
  scaffolds `use AllowAllAuthorize;`, a placeholder trait that allows
122
145
  everything (the engine warns at boot while it is there) — replace it with
123
146
  this app's real ownership check, same as `onPaid`.
124
- - `onPaid` must be idempotent. It runs once per `reference` — your order
147
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order
125
148
  id, one per thing you fulfill, created before checkout, kept across retries,
126
149
  never reused. A fresh id per page load lets one order be paid twice.
127
150
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -291,6 +314,8 @@ enough; drop the `.md` for the same page a person would read.
291
314
  Questions, or a problem with the library itself:
292
315
  https://openreceive.org/contact
293
316
 
317
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
318
+
294
319
  ---
295
320
 
296
321
  ## The quickstart, in full
@@ -300,12 +325,18 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-lara
300
325
 
301
326
  ## Laravel quickstart
302
327
 
303
- Requires PHP ≥ 8.2 (64-bit) and Laravel ≥ 11 (12 current). Extensions:
304
- `ext-gmp` is REQUIRED — the NWC transport's elliptic-curve math depends on it
305
- — plus `sodium`, `mbstring` and the `pdo_*` driver for your database
306
- (`pdo_pgsql`, `pdo_mysql` or `pdo_sqlite`). `php -m` lists what your build
307
- has; on Debian/Ubuntu it is `apt-get install php8.2-gmp`, in the official
308
- 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`.
309
340
 
310
341
  Add the Laravel package:
311
342
 
@@ -313,10 +344,10 @@ Add the Laravel package:
313
344
  composer require openreceive/laravel
314
345
  ```
315
346
 
316
- That is the whole install: `openreceive/laravel` depends on
317
- `openreceive/openreceive`, the engine, so the default wallet client — built from
318
- `NWC_URI` — works with nothing else added, and package discovery registers the
319
- 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
320
351
  `OpenReceive\Nwc\ReceiveNwcClient` in the container instead.
321
352
 
322
353
  Then run:
@@ -328,35 +359,36 @@ php artisan migrate
328
359
 
329
360
  `openreceive:install` writes three files:
330
361
 
331
- - `config/openreceive.php` — the settings. The host hook is a CLASS NAME, not
332
- a closure, because `php artisan config:cache` serializes this file and a
333
- closure anywhere in it fails the cache;
334
- - `app/OpenReceive/Host.php` — the three hooks (`authorize`, `amountFor`,
335
- `onPaid`), with the two generated placeholders wired and the fulfillment
336
- note as comments;
337
- - `database/migrations/*_create_openreceive_tables.php` — one migration for
338
- both engine tables (`openreceive_payments` and `openreceive_meta`). Its DDL
339
- comes from the engine's `PaymentsSchema::statements()` for your connection's
340
- driver — PostgreSQL, MySQL/MariaDB and SQLite — and `php artisan migrate` runs
341
- 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.
342
373
 
343
374
  → [OpenReceive\Storage](https://openreceive.org/guides/api-reference.md#openreceivestorage)
344
375
 
345
376
  There is no Eloquent model for the engine's tables, and you do not write one.
346
377
  The engine owns the table's commit locking, write-once settlement, and
347
- reconciliation state machine. `reference` is indexed but not unique (a
348
- 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.
349
381
 
350
382
  #### Fulfill exactly once
351
383
 
352
384
  Within OpenReceive's own settlement paths, `onPaid` runs at most once per
353
- reference: a second payment to a second invoice is recorded with
385
+ reference. A second payment to a second invoice is recorded with
354
386
  `status_reason = "duplicate_settlement"` and never fulfills again.
355
387
 
356
- The one thing you own: **if anything other than OpenReceive can also fulfill
357
- an order** — an admin action, a second payment processor, a replayed job —
358
- those paths race each other, and `onPaid` must be idempotent. The generated
359
- `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:
360
392
 
361
393
  ```php
362
394
  public function onPaid(PaymentSettlement $settlement): void
@@ -372,23 +404,28 @@ public function onPaid(PaymentSettlement $settlement): void
372
404
  }
373
405
  ```
374
406
 
375
- Delivery is at-least-once: `onPaid` runs inside the settlement transaction,
376
- and an exception rolls it back for the next pass to retry. Keep it to database
377
- writes on the order — an email or webhook sent from here would survive the
378
- rollback and go out again. The `'paid'` transition above is the flag; let your
379
- own job drain it after commit, or implement `OpenReceive\Hosts\AfterPaid`:
380
- its `afterPaid(PaymentSettlement $settlement)` runs after COMMIT, best-effort,
381
- for the same first settled attempt — the place for `FulfillOrder::dispatch(…)`
382
- or an event. And open no `DB::transaction()` of your own inside `onPaid`: the
383
- engine already holds the transaction on this connection, and PDO refuses a
384
- nested `BEGIN`.
385
-
386
- **A query-builder `update()` fires no model events.** That is the point — it is
387
- 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
388
425
  between the check and the write. It also means no observer, no `saved` event
389
- and no `Model::updated` listener runs, which is fine for a job draining the
390
- flag and useless for a model that owns the transition through events. If yours
391
- 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:
392
429
 
393
430
  ```php
394
431
  public function onPaid(PaymentSettlement $settlement): void
@@ -401,26 +438,29 @@ public function onPaid(PaymentSettlement $settlement): void
401
438
  }
402
439
  ```
403
440
 
404
- **Unlocking a download works the same way.** If what the payer bought is a
405
- file, do not unlock it in the browser: gate the download route on the paid
406
- order row — `Order::where('id', $id)->where('user_id', $request->user()->id)->where('state', 'paid')->firstOrFail()`
407
- or a 404 — and serve the file only then. The `'paid'` written above is the
408
- unlock; the client never decides an order was fulfilled, it re-reads the row.
409
- Buy a Button's `ShopController::download` is this in twenty lines.
410
-
411
- Both shapes are idempotent, and both are correct. They differ only in whether
412
- your model layer gets to run: the query-builder `update()` skips it and is the
413
- right default; the row lock holds the row for the duration of the method and
414
- is what you want when the transition has to go through your model. The
415
- generated fulfillment note says the same thing — if your fulfillment is a
416
- read-modify-write that cannot be expressed as one conditional `UPDATE`, take
417
- the lock.
418
-
419
- A runnable illustration of this boundary — not a template to copy models from —
420
- is Buy a Button
421
- (`examples/buttons/server/laravel`).
422
- It has products, visitors, and orders, with the three hooks as the only bridge.
423
- 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.
424
464
 
425
465
  ### Add wallet credentials
426
466
 
@@ -434,31 +474,37 @@ LSC_URI_BACKUP=
434
474
  ```
435
475
 
436
476
  1. Get a receive-only NWC code from a compatible wallet
437
- ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments))
438
- → `NWC_URI`.
439
- 2. Optionally set up a [swap provider](https://openreceive.org/set_up_swap_provider)
440
- → `LSC_URI_PRIMARY` (and `LSC_URI_BACKUP` if you have one).
441
-
442
- Never put these values in browser code. Your application refuses to start if
443
- the NWC code also advertises spend methods such as `pay_invoice`; mint a
444
- receive-only code ([Security](https://openreceive.org/guides/security.md)).
445
-
446
- Under `php artisan config:cache` Laravel never reads `.env` at runtime — the
447
- 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
448
489
  re-run `config:cache` after changing one. The engine redacts connection
449
- strings in every log line, and `php artisan openreceive:doctor` prints each
450
- variable as set/unset only.
490
+ strings in every log line. `php artisan openreceive:doctor` prints each
491
+ variable only as set or unset.
451
492
  → [Environment variables](https://openreceive.org/guides/environment-variables.md).
452
493
 
453
494
  ### Configure the host hooks
454
495
 
455
496
  `app/OpenReceive/Host.php` needs three things: authorization, the trusted
456
- price, and fulfillment. All three receive the `reference` — a string you
457
- choose, and the fulfillment identity: your order id, one per thing you
458
- fulfill, created before checkout, kept across retries, never reused.
459
- OpenReceive never looks inside it, but `onPaid` runs once per reference, a new
460
- checkout under a reference that already settled is refused with 409, and a
461
- 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.
462
508
 
463
509
  ```php
464
510
  <?php
@@ -519,67 +565,74 @@ final class Host implements OpenReceiveHost
519
565
  }
520
566
  ```
521
567
 
522
- `config/openreceive.php` names that class (`'host' => App\OpenReceive\Host::class`)
523
- and the provider resolves it from the container, so it may take constructor
524
- dependencies. `onPaid` runs inside the settlement transaction, only for the
525
- 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.
526
572
  → [OpenReceive\Host](https://openreceive.org/guides/api-reference.md#openreceivehost) ·
527
573
  [the authorize context](https://openreceive.org/guides/api-reference.md#the-authorize-context-php)
528
574
 
529
- The routes mount under `config('openreceive.route_prefix')` (`openreceive`) and
530
- `config('openreceive.middleware')` (`['web']`). The `web` group is how the
531
- engine picks up your application's session and CSRF protection: Laravel's
532
- `VerifyCsrfToken` reads `X-CSRF-TOKEN`, and the checkout client sends it from
533
- `<meta name="csrf-token" content="{{ csrf_token() }}">` automatically — keep
534
- that tag in the layout that renders the checkout. The same group brings every
535
- middleware it carries; a global `auth` redirect on the `web` group would send
536
- the engine's JSON routes to a login page too, so keep such guards on your own
537
- route groups, not on `web` itself.
538
-
539
- The generated `Host.php` ships `use LoggingOnPaid;` — a placeholder that only
540
- logs the settlement and fulfills nothing. Replace it with your real fulfillment
541
- (as above); the engine warns every time your application boots while the
542
- placeholder is still there, because orders would otherwise be recorded as
543
- settled without ever being fulfilled. The same applies to `use
544
- AllowAllAuthorize;`, the generated allow-all placeholder: it treats
545
- possession of the reference as authorization, which is safe only while
546
- references are unguessable, and the engine warns at boot until you replace it
547
- with your own ownership check (as above). Replace both, not just `onPaid`.
548
-
549
- 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
550
600
  rejected. The engine's `PaymentRepository` interface remains the escape hatch
551
- for custom-storage applications and is not part of the quickstart.
552
-
553
- For public web shops, opt into the per-IP invoice cap with
554
- `'rate_limiting' => true`; leave it off (the default) when many payers share
555
- one IP. The client IP is `$request->ip()`, so Laravel's `TrustProxies` decides
556
- who the payer is behind a proxy. → [Rate limiting](https://openreceive.org/guides/rate-limiting.md)
557
-
558
- In production the engine builds the wallet client — and runs its receive-only
559
- preflight — eagerly when your application boots, so a missing `NWC_URI`, a
560
- dead relay, or a spend-capable wallet stops the deploy instead of surfacing as
561
- customer-facing 500s on the first checkout. Outside production (tests,
562
- consoles) the client is built lazily so no live wallet is needed.
563
-
564
- PHP builds the whole engine again on every request, so that same preflight
565
- would run per request under PHP-FPM or Apache; the package remembers the
566
- wallet's info event in your default cache store for
567
- `config('openreceive.wallet_info_cache_seconds')` (600) so a checkout request
568
- 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.
569
621
 
570
622
  ### Render the checkout
571
623
 
572
- Serve the compiled `styles.css` without Tailwind processing: import it from
573
- JavaScript (with a CSS-capable bundler) or use a plain `<link rel="stylesheet">`.
574
- Do not `@import` it into the host Tailwind entry. Its zero-specificity rules
575
- 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.
576
629
 
577
- The engine serves JSON checkout routes only — rendering is your view. Any
578
- OpenReceive frontend package works against the `/openreceive` mount; the
579
- smallest is the custom element (its default `prefix` is already
580
- `/openreceive`, and the package ships a self-contained `styles.css`; it is
581
- scoped to what OpenReceive renders). Laravel ships Vite, so the package installs like any
582
- 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:
583
636
 
584
637
  ```sh
585
638
  npm install @openreceive/elements
@@ -603,32 +656,32 @@ defineElements();
603
656
  ```
604
657
 
605
658
  The element creates the checkout for `reference`, then renders and polls
606
- itself. React/Vue/Svelte/Angular apps use the matching wrapper package instead
607
- — same props and defaults ([Frontend checkout](https://openreceive.org/guides/frontend-checkout.md)). Build a
608
- custom checkout only if this app cannot use a drop-in; then
609
- `@openreceive/browser/headless` is the API
610
- ([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)).
611
664
 
612
665
  Everything the checkout draws ships inside the JavaScript: the payment-method
613
666
  icons, the wallet logos and the pay tutorials. There is no image file to copy
614
667
  or serve and no asset option to set. Deploy your normal JavaScript and CSS
615
668
  build output, including any generated JavaScript chunks. Bundlers with code
616
- splitting can defer tutorial screenshots until first open; single-file builds
617
- (including the standalone checkout) include them upfront. If your
618
- 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:`
619
672
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
620
673
 
621
- 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
622
675
  wallet logos render, and open a wallet's pay tutorial to check its screenshots.
623
- 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
624
677
  panel for failed JavaScript chunks. Allow `data:` in `img-src` and deploy the
625
678
  complete build output. Do not add image routes, copy package source images, or
626
679
  use registry `icon_path` / tutorial `path` keys as browser URLs.
627
680
 
628
681
  **No npm in this project?** Every GitHub release attaches
629
- `standalone-checkout-<version>.tar.gz`: one self-registering ESM file, its
630
- stylesheet and a `MANIFEST.json` of hashes. Unpack it into
631
- `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:
632
685
 
633
686
  ```blade
634
687
  <link rel="stylesheet" href="/openreceive/openreceive-checkout.css" />
@@ -637,7 +690,7 @@ stylesheet and a `MANIFEST.json` of hashes. Unpack it into
637
690
  <openreceive-checkout reference="{{ $order->id }}"></openreceive-checkout>
638
691
  ```
639
692
 
640
- `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
641
694
  version in step with the Composer package.
642
695
 
643
696
  ### Reconciliation
@@ -653,33 +706,40 @@ load:
653
706
  php artisan openreceive:notifications
654
707
  ```
655
708
 
656
- It listens for the wallet's NWC-02 `payment_received` notifications and runs
657
- a periodic reconcile pass in the same process — one per deployment, not per
658
- web instance. `php artisan openreceive:reconcile` is the one-shot pass if you
659
- want to drive one yourself, and `php artisan openreceive:doctor` reports every
660
- credential as set/unset, the host and its placeholders, the route mount and
661
- 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.
662
719
 
663
720
  ### Swap secrets
664
721
 
665
- The package recognizes `LSC_URI_PRIMARY` and `LSC_URI_BACKUP` using the
666
- shared [Lightning Swap Connect](https://openreceive.org/guides/lightning-swap-connect.md) vectors: setting either one
667
- auto-builds the matching provider, so an app that wants swaps only supplies the
668
- connection strings ([Environment variables](https://openreceive.org/guides/environment-variables.md)). The
669
- `openreceive.swap_providers` container binding is the override knob — bind a
670
- list of your own adapters to replace the auto-built set, or `[]` to disable
671
- 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.
672
729
 
673
- 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
674
731
  server-only `swap_data`. The engine never returns `swap_data` from its routes
675
732
  or writes it to a log. Do not select it into your own API, serialize it, or
676
- log it; it may contain a provider credential.
733
+ log it. It may contain a provider credential.
677
734
 
678
735
  **Setting either connection string commits you to refunds.** A swap deposit can
679
- arrive short or late, which leaves it `refund_required` at the provider with
680
- only your UI able to claim it — and the payer claims it on a second visit,
681
- after leaving your page for an address in another wallet. That needs a
682
- per-order URL your app serves, a route that restores the order behind it, and
683
- something that restores the ATTEMPT, since `/checkouts/prepare` returns none.
684
- [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
685
745
  `LSC_URI_PRIMARY`.