@openreceive/browser 0.4.9 → 0.4.11

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,7 +1,7 @@
1
1
  import { AssetIndexEntry, PaymentWizardRoute } from '@openreceive/provider-data';
2
2
  export { PaymentWizardRoute, PaymentWizardRouteRequest, getPaymentWizardRoutes, loadPayTutorialImages, payTutorialImage } from '@openreceive/provider-data';
3
- import { P as PaymentIconId, a as ParseOpenReceiveOptionalIntegerOptions, b as PaymentMethod, R as ResolvedTheme, T as ThemePreference, c as PaymentMethodOption, d as TickingValueOptions, e as TickingValueController, f as TransientFeedbackOptions, g as TransientFeedbackController, C as CheckoutInvoiceSnapshot, U as UnixSeconds, S as SwapDisplayModel, h as CheckoutSnapshot, i as SwapDepositRisk, j as CheckoutState, k as TransactionDetailsInput, l as TransactionDetailRow, m as CreateCheckoutStateOptions, n as CheckoutStatusModelInput, o as CheckoutStatusModel, p as CheckoutPaymentMethod, B as BrowserLoggerOption, q as ThemeAttributeTarget, r as CheckoutElementAttributes, s as CheckoutElementAttributeOptions, t as CheckoutElementEventHandlers, u as CheckoutElementListeners, v as CreateCheckoutShellOptions, w as CheckoutShellElements, x as CheckoutShellOptions, y as CheckoutShellModel, z as CreateOpenReceiveThemeToggleElementOptions, A as ThemeToggleElementAttributeOptions, D as ThemeToggleElementAttributes, E as StoredThemeModelOptions, F as ThemeModel, G as ThemeModelOptions, H as ReadThemePreferenceOptions, I as ThemeControlTargets, J as ThemeStorageOptions, K as PaymentWizardControllerOptions, L as PaymentWizardController, M as PaymentWizardSelection, N as PaymentWizardModel, W as WizardRouteAssetDisplay, O as WizardRouteDisplay, Q as PaymentWizardSelectionAction } from './status-Doh8Wz4D.js';
4
- export { V as BrowserLogContext, X as BrowserLogger, Y as BrowserRequestError, Z as CheckoutController, _ as CheckoutControllerOptions, $ as CheckoutPhase, a0 as CheckoutShellRootAttributes, a1 as CheckoutStatusRefresh, a2 as OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES, a3 as OPENRECEIVE_CHECKOUT_DATA_SELECTORS, a4 as OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES, a5 as OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS, a6 as OPENRECEIVE_CHECKOUT_ELEMENT_PARTS, a7 as OPENRECEIVE_CHECKOUT_ELEMENT_PART_SELECTORS, a8 as OPENRECEIVE_CHECKOUT_ELEMENT_SLOTS, a9 as OPENRECEIVE_CHECKOUT_ELEMENT_TAG_NAME, aa as OPENRECEIVE_COPY_FEEDBACK_MS, ab as OPENRECEIVE_DEFAULT_POLL_INTERVAL_MS, ac as OPENRECEIVE_DEFAULT_PREFIX, ad as OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES, ae as OPENRECEIVE_PAYMENT_WIZARD_SELECTORS, af as OPENRECEIVE_PROVIDER_PREVIEW_LIMIT, ag as OPENRECEIVE_STYLE_ROOT_ATTRIBUTE, ah as OPENRECEIVE_STYLE_ROOT_SELECTOR, ai as OPENRECEIVE_THEME_STORAGE_KEY, aj as OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES, ak as OPENRECEIVE_THEME_TOGGLE_ELEMENT_EVENTS, al as OPENRECEIVE_THEME_TOGGLE_ELEMENT_PARTS, am as OPENRECEIVE_THEME_TOGGLE_ELEMENT_PART_SELECTORS, an as OPENRECEIVE_THEME_TOGGLE_ELEMENT_TAG_NAME, ao as QrEncoder, ap as QrSvgController, aq as QrSvgControllerOptions, ar as Status, as as SwapCopyRow, at as SwapRefundStaging, au as WizardProviderDisplay, av as assertDisplayInvoice, aw as copyInvoice, ax as createCheckoutActionEvent, ay as createCheckoutController, az as createCheckoutErrorEvent, aA as createCheckoutProviderCopyEvent, aB as createCheckoutStateEvent, aC as createQrPayloadSvg, aD as createQrSvg, aE as createQrSvgController, aF as createStatusFetcher, aG as createThemeChangeEvent, aH as currentCheckoutUrl, aI as deriveCheckoutStateLabels, aJ as deriveStatus, aK as enterCheckoutResumePath, aL as escapeHtml, aM as formatAmountCaption, aN as formatDepositAmount, aO as formatFiatAmount, aP as formatMsats, aQ as formatSwapLimit, aR as formatUnixTime, aS as openWallet, aT as paymentIconSvgs, aU as prepareCheckout, aV as requestCheckout } from './status-Doh8Wz4D.js';
3
+ import { P as PaymentIconId, a as ParseOpenReceiveOptionalIntegerOptions, b as PaymentMethod, R as ResolvedTheme, T as ThemePreference, c as PaymentMethodOption, d as TickingValueOptions, e as TickingValueController, f as TransientFeedbackOptions, g as TransientFeedbackController, C as CheckoutInvoiceSnapshot, U as UnixSeconds, S as SwapDisplayModel, h as CheckoutSnapshot, i as SwapDepositRisk, j as CheckoutState, k as TransactionDetailsInput, l as TransactionDetailRow, m as CreateCheckoutStateOptions, n as CheckoutStatusModelInput, o as CheckoutStatusModel, p as CheckoutPaymentMethod, B as BrowserLoggerOption, q as ThemeAttributeTarget, r as CheckoutElementAttributes, s as CheckoutElementAttributeOptions, t as CheckoutElementEventHandlers, u as CheckoutElementListeners, v as CreateCheckoutShellOptions, w as CheckoutShellElements, x as CheckoutShellOptions, y as CheckoutShellModel, z as CreateOpenReceiveThemeToggleElementOptions, A as ThemeToggleElementAttributeOptions, D as ThemeToggleElementAttributes, E as StoredThemeModelOptions, F as ThemeModel, G as ThemeModelOptions, H as ReadThemePreferenceOptions, I as ThemeControlTargets, J as ThemeStorageOptions, K as PaymentWizardControllerOptions, L as PaymentWizardController, M as PaymentWizardSelection, N as PaymentWizardModel, W as WizardRouteAssetDisplay, O as WizardRouteDisplay, Q as PaymentWizardSelectionAction } from './status-hEzap3jk.js';
4
+ export { V as BrowserLogContext, X as BrowserLogger, Y as BrowserRequestError, Z as CheckoutController, _ as CheckoutControllerOptions, $ as CheckoutPhase, a0 as CheckoutShellRootAttributes, a1 as CheckoutStatusRefresh, a2 as OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES, a3 as OPENRECEIVE_CHECKOUT_DATA_SELECTORS, a4 as OPENRECEIVE_CHECKOUT_ELEMENT_ATTRIBUTES, a5 as OPENRECEIVE_CHECKOUT_ELEMENT_EVENTS, a6 as OPENRECEIVE_CHECKOUT_ELEMENT_PARTS, a7 as OPENRECEIVE_CHECKOUT_ELEMENT_PART_SELECTORS, a8 as OPENRECEIVE_CHECKOUT_ELEMENT_SLOTS, a9 as OPENRECEIVE_CHECKOUT_ELEMENT_TAG_NAME, aa as OPENRECEIVE_COPY_FEEDBACK_MS, ab as OPENRECEIVE_DEFAULT_POLL_INTERVAL_MS, ac as OPENRECEIVE_DEFAULT_PREFIX, ad as OPENRECEIVE_PAYMENT_WIZARD_ATTRIBUTES, ae as OPENRECEIVE_PAYMENT_WIZARD_SELECTORS, af as OPENRECEIVE_PROVIDER_PREVIEW_LIMIT, ag as OPENRECEIVE_STYLE_ROOT_ATTRIBUTE, ah as OPENRECEIVE_STYLE_ROOT_SELECTOR, ai as OPENRECEIVE_THEME_STORAGE_KEY, aj as OPENRECEIVE_THEME_TOGGLE_ELEMENT_ATTRIBUTES, ak as OPENRECEIVE_THEME_TOGGLE_ELEMENT_EVENTS, al as OPENRECEIVE_THEME_TOGGLE_ELEMENT_PARTS, am as OPENRECEIVE_THEME_TOGGLE_ELEMENT_PART_SELECTORS, an as OPENRECEIVE_THEME_TOGGLE_ELEMENT_TAG_NAME, ao as QrEncoder, ap as QrSvgController, aq as QrSvgControllerOptions, ar as Status, as as SwapCopyRow, at as SwapRefundStaging, au as WizardProviderDisplay, av as assertDisplayInvoice, aw as copyInvoice, ax as createCheckoutActionEvent, ay as createCheckoutController, az as createCheckoutErrorEvent, aA as createCheckoutProviderCopyEvent, aB as createCheckoutStateEvent, aC as createQrPayloadSvg, aD as createQrSvg, aE as createQrSvgController, aF as createStatusFetcher, aG as createThemeChangeEvent, aH as currentCheckoutUrl, aI as deriveCheckoutStateLabels, aJ as deriveStatus, aK as enterCheckoutResumePath, aL as escapeHtml, aM as formatAmountCaption, aN as formatDepositAmount, aO as formatFiatAmount, aP as formatMsats, aQ as formatSwapLimit, aR as formatUnixTime, aS as openWallet, aT as paymentIconSvgs, aU as prepareCheckout, aV as requestCheckout } from './status-hEzap3jk.js';
5
5
  import '@openreceive/core';
6
6
 
7
7
  /** Each packaged payment icon as a `data:image/svg+xml` URI, keyed by id. */
@@ -747,13 +747,15 @@ interface CheckoutSessionOptions {
747
747
  snapshot(): CheckoutSnapshot | undefined;
748
748
  /** The order being paid, read at call time (the element's is an attribute). */
749
749
  reference(): string | undefined;
750
+ /** Endpoint is part of checkout identity; trailing slashes are equivalent. */
751
+ prefix?(): string | undefined;
750
752
  /**
751
753
  * Mint a Lightning invoice for this reference (POST `${prefix}/checkouts`).
752
754
  * The host closes over its own prefix, metadata and fetch. Answer `undefined`
753
755
  * to say "this host cannot mint" — React's payment wizard is such a host: it
754
756
  * asks its parent for Lightning through `onRequestLightning` instead.
755
757
  */
756
- requestCheckout?(reference: string): Promise<CheckoutSnapshot> | undefined;
758
+ requestCheckout?(reference: string, signal?: AbortSignal): Promise<CheckoutSnapshot> | undefined;
757
759
  /** Publish a snapshot that now carries the Lightning attempt. */
758
760
  onSnapshot?(snapshot: CheckoutSnapshot): void;
759
761
  /**
@@ -785,6 +787,14 @@ interface CheckoutSession {
785
787
  * carries the accepted range the host renders in its unavailable panel.
786
788
  */
787
789
  readonly swapQuotes: Readonly<Record<string, CheckoutPaymentMethod>>;
790
+ /** Invalidate every outstanding action when identity changes. */
791
+ syncIdentity(): void;
792
+ reset(): void;
793
+ dispose(): void;
794
+ capture(): {
795
+ readonly signal: AbortSignal;
796
+ isCurrent(): boolean;
797
+ };
788
798
  ensureLightning(): Promise<void>;
789
799
  /**
790
800
  * Quote the pay-in asset, then start the swap when the quote confirms the
@@ -825,6 +835,7 @@ declare function createCheckoutShell(snapshot: CheckoutSnapshot, options?: Creat
825
835
  * URL input — no call in this module takes a route of its own.
826
836
  */
827
837
  interface SwapRequestOptions {
838
+ readonly signal?: AbortSignal;
828
839
  readonly fetch: typeof globalThis.fetch;
829
840
  readonly prefix: string;
830
841
  readonly logger?: BrowserLoggerOption;
package/dist/headless.js CHANGED
@@ -119,7 +119,7 @@ import {
119
119
  swapOptionLimitSentence,
120
120
  swapPickerKey,
121
121
  updatePaymentWizardSelection
122
- } from "./chunk-CPVYEWVM.js";
122
+ } from "./chunk-5F7F2ZAM.js";
123
123
 
124
124
  // src/headless.ts
125
125
  import {
@@ -177,7 +177,43 @@ function createCheckoutSession(options) {
177
177
  let mintingLightning = false;
178
178
  let startingSwapAsset = null;
179
179
  let swapQuotes = {};
180
+ let generation = 0;
181
+ let abort = new AbortController();
182
+ const identityKey = () => JSON.stringify([
183
+ options.reference(),
184
+ (options.prefix?.() ?? options.swap?.prefix() ?? "").replace(/\/+$/, "")
185
+ ]);
186
+ let identity = identityKey();
187
+ function invalidateActions() {
188
+ abort.abort();
189
+ abort = new AbortController();
190
+ generation += 1;
191
+ mintingLightning = false;
192
+ startingSwapAsset = null;
193
+ }
194
+ function reset() {
195
+ invalidateActions();
196
+ identity = identityKey();
197
+ wizardError = void 0;
198
+ swapStartError = void 0;
199
+ lightningRequested = false;
200
+ mintingLightning = false;
201
+ startingSwapAsset = null;
202
+ swapQuotes = {};
203
+ }
204
+ function syncIdentity() {
205
+ if (identityKey() !== identity) reset();
206
+ }
207
+ function capture() {
208
+ syncIdentity();
209
+ const captured = generation;
210
+ return {
211
+ signal: abort.signal,
212
+ isCurrent: () => captured === generation && identityKey() === identity
213
+ };
214
+ }
180
215
  async function ensureLightning() {
216
+ const action = capture();
181
217
  if (mintingLightning) return;
182
218
  const reference = options.reference();
183
219
  if (reference === void 0 || reference.length === 0) return;
@@ -195,20 +231,25 @@ function createCheckoutSession(options) {
195
231
  wizardError = void 0;
196
232
  options.onChange();
197
233
  try {
198
- const pending = options.requestCheckout?.(reference);
234
+ const pending = options.requestCheckout?.(reference, action.signal);
199
235
  if (pending === void 0) return;
200
236
  const checkout = await pending;
237
+ if (!action.isCurrent()) return;
201
238
  lightningRequested = true;
202
239
  options.onSnapshot?.(mergeMintedCheckout(checkout, options.snapshot()));
203
240
  } catch (error) {
241
+ if (!action.isCurrent()) return;
204
242
  wizardError = payerFacingMessage(error, MINT_FAILED);
205
243
  options.onError(error);
206
244
  } finally {
207
- mintingLightning = false;
208
- options.onChange();
245
+ if (action.isCurrent()) {
246
+ mintingLightning = false;
247
+ options.onChange();
248
+ }
209
249
  }
210
250
  }
211
251
  async function startSwap(payInAsset) {
252
+ const action = capture();
212
253
  if (startingSwapAsset !== null) return;
213
254
  const swap = options.swap;
214
255
  if (swap === void 0) {
@@ -239,9 +280,18 @@ function createCheckoutSession(options) {
239
280
  swapStartError = void 0;
240
281
  options.onChange();
241
282
  try {
242
- const quote = await quoteSwapAsset(payInAsset, prefix, fetcher, reference, csrfHeader);
283
+ const quote = await quoteSwapAsset(
284
+ payInAsset,
285
+ prefix,
286
+ fetcher,
287
+ reference,
288
+ csrfHeader,
289
+ action
290
+ );
291
+ if (!action.isCurrent()) return;
243
292
  if (quote !== void 0 && quote.available === false) return;
244
293
  const started = await startSwapRequest({
294
+ signal: action.signal,
245
295
  fetch: fetcher,
246
296
  prefix,
247
297
  ...csrfHeader === void 0 ? {} : { csrfHeader },
@@ -249,12 +299,14 @@ function createCheckoutSession(options) {
249
299
  payInAsset,
250
300
  ...options.logger === void 0 ? {} : { logger: options.logger }
251
301
  });
302
+ if (!action.isCurrent()) return;
252
303
  selection.setStarted(started);
253
304
  selection.setDismissedInvoiceId(null);
254
305
  swap.onStarted?.(started);
255
306
  selection.setSelectedAsset(payInAsset);
256
307
  options.onChange();
257
308
  } catch (error) {
309
+ if (!action.isCurrent()) return;
258
310
  const landed = selection.started();
259
311
  if (landed?.swap?.pay_in_asset === payInAsset && landed.invoice_id !== selection.dismissedInvoiceId()) {
260
312
  selection.setSelectedAsset(payInAsset);
@@ -264,17 +316,22 @@ function createCheckoutSession(options) {
264
316
  selection.setSelectedAsset(payInAsset);
265
317
  failSwapStart(error);
266
318
  } finally {
267
- startingSwapAsset = null;
319
+ if (action.isCurrent()) {
320
+ startingSwapAsset = null;
321
+ options.onChange();
322
+ }
268
323
  }
269
324
  }
270
- async function quoteSwapAsset(payInAsset, prefix, fetcher, reference, csrfHeader) {
325
+ async function quoteSwapAsset(payInAsset, prefix, fetcher, reference, csrfHeader, action) {
271
326
  const body = await postJson({
327
+ signal: action.signal,
272
328
  fetch: fetcher,
273
329
  prefix,
274
330
  ...csrfHeader === void 0 ? {} : { csrfHeader },
275
331
  ...options.logger === void 0 ? {} : { logger: options.logger },
276
332
  body: { reference, action: "swap_quote", pay_in_asset: payInAsset }
277
333
  });
334
+ if (!action.isCurrent()) return void 0;
278
335
  const quote = normalizeSwapQuote(body);
279
336
  if (quote === void 0) return void 0;
280
337
  swapQuotes = { ...swapQuotes, [quote.pay_in_asset]: quote };
@@ -304,12 +361,17 @@ function createCheckoutSession(options) {
304
361
  get swapQuotes() {
305
362
  return swapQuotes;
306
363
  },
364
+ syncIdentity,
365
+ reset,
366
+ dispose: reset,
367
+ capture,
307
368
  ensureLightning,
308
369
  startSwap,
309
370
  resetLightningRequest() {
310
- lightningRequested = false;
371
+ reset();
311
372
  },
312
373
  clearSwapStartError() {
374
+ invalidateActions();
313
375
  swapStartError = void 0;
314
376
  }
315
377
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { aW as BrowserLogLevel } from './status-Doh8Wz4D.js';
2
- export { aX as BrowserLogEntry, X as BrowserLogger, B as BrowserLoggerOption, Y as BrowserRequestError, Z as CheckoutController, _ as CheckoutControllerOptions, h as CheckoutSnapshot, j as CheckoutState, aY as CopyInvoiceOptions, aZ as GuestCheckoutResumeController, a_ as GuestCheckoutResumeOptions, a$ as OpenWalletOptions, b0 as PrepareCheckoutOptions, b1 as QrOptions, b2 as RequestCheckoutOptions, ar as Status, b3 as StatusInvoiceLike, U as UnixSeconds, aw as copyInvoice, ay as createCheckoutController, b4 as createGuestCheckoutResume, b5 as createGuestOrderFetcher, b6 as createLightningUri, b7 as createQrPngDataUrl, aD as createQrSvg, aJ as deriveStatus, aK as enterCheckoutResumePath, aS as openWallet, aU as prepareCheckout, aV as requestCheckout } from './status-Doh8Wz4D.js';
1
+ import { aW as BrowserLogLevel } from './status-hEzap3jk.js';
2
+ export { aX as BrowserLogEntry, X as BrowserLogger, B as BrowserLoggerOption, Y as BrowserRequestError, Z as CheckoutController, _ as CheckoutControllerOptions, h as CheckoutSnapshot, j as CheckoutState, aY as CopyInvoiceOptions, aZ as GuestCheckoutResumeController, a_ as GuestCheckoutResumeOptions, a$ as OpenWalletOptions, b0 as PrepareCheckoutOptions, b1 as QrOptions, b2 as RequestCheckoutOptions, ar as Status, b3 as StatusInvoiceLike, U as UnixSeconds, aw as copyInvoice, ay as createCheckoutController, b4 as createGuestCheckoutResume, b5 as createGuestOrderFetcher, b6 as createLightningUri, b7 as createQrPngDataUrl, aD as createQrSvg, aJ as deriveStatus, aK as enterCheckoutResumePath, aS as openWallet, aU as prepareCheckout, aV as requestCheckout } from './status-hEzap3jk.js';
3
3
  import '@openreceive/provider-data';
4
4
  import '@openreceive/core';
5
5
 
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  openWallet,
14
14
  prepareCheckout,
15
15
  requestCheckout
16
- } from "./chunk-CPVYEWVM.js";
16
+ } from "./chunk-5F7F2ZAM.js";
17
17
  export {
18
18
  BrowserRequestError,
19
19
  copyInvoice,
@@ -431,14 +431,17 @@ interface CheckoutInvoiceSwapSnapshot {
431
431
  }
432
432
  /**
433
433
  * Formatted fee breakout for the deposit panel, explaining why the payer sends more
434
- * than the cart total. All figures are display-ready fiat strings.
434
+ * than the cart total. All figures are display-ready strings. `cartTotal` is
435
+ * always fiat; `youSend` and `fee` are fiat for a floating asset and token
436
+ * amounts ("50.05 USDC") for a stablecoin pegged to the fee currency, where a
437
+ * fiat valuation would read as the deposit amount with a typo.
435
438
  */
436
439
  interface SwapFeeBreakdown {
437
440
  /** Cart total delivered to the merchant, e.g. "$10.00". */
438
441
  readonly cartTotal: string;
439
- /** Fiat value of the crypto the payer sends, e.g. "$10.59". */
442
+ /** What the payer sends: its fiat value ("$10.59") or, for a pegged stablecoin, the deposit amount itself ("10.59 USDC"). */
440
443
  readonly youSend: string;
441
- /** The swap fee absorbed by the payer (exchange spread + network fees), e.g. "$0.59". */
444
+ /** The swap fee absorbed by the payer (exchange spread + network fees), e.g. "$0.59" or "0.59 USDC". */
442
445
  readonly fee: string;
443
446
  /** The fee as a percentage of the cart total, e.g. "5.9%", when computable. */
444
447
  readonly feePercent?: string;
@@ -817,8 +820,9 @@ interface RequestCheckoutOptions extends RequestCheckoutBaseOptions {
817
820
  * `prepareCheckout` posts only the order id — it locks the amount and lists
818
821
  * payment methods without minting — so there is no memo or metadata to carry.
819
822
  */
820
- type PrepareCheckoutOptions = Pick<RequestCheckoutBaseOptions, "prefix" | "reference" | "fetch" | "headers" | "csrfHeader">;
823
+ type PrepareCheckoutOptions = Pick<RequestCheckoutBaseOptions, "prefix" | "reference" | "fetch" | "headers" | "csrfHeader" | "signal">;
821
824
  interface RequestCheckoutBaseOptions {
825
+ readonly signal?: AbortSignal;
822
826
  /**
823
827
  * Base path the shipped router is mounted at (e.g. `/openreceive`). The create and
824
828
  * prepare routes are derived from it — see {@link checkoutRoutes}. It is required
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openreceive/browser",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Browser helpers and a headless UI engine for Bitcoin Lightning checkout and optional USDT, USDC, SOL and ETH swaps.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -30,8 +30,8 @@
30
30
  "./styles.css": "./dist/styles.css"
31
31
  },
32
32
  "dependencies": {
33
- "@openreceive/core": "0.4.9",
34
- "@openreceive/provider-data": "0.4.9",
33
+ "@openreceive/core": "0.4.11",
34
+ "@openreceive/provider-data": "0.4.11",
35
35
  "qrcode": "^1.5.4"
36
36
  },
37
37
  "devDependencies": {
@@ -70,6 +70,14 @@ same diagnostics redacted, always exit 0 — safe to share.
70
70
  - Refunds exist only for swap deposits from `refund_required`. There is **no
71
71
  Lightning refund** — the wallet cannot spend. Do not chase one.
72
72
  https://openreceive.org/guides/swap-refunds.md
73
+ - "Payer reports two different amounts on a stablecoin checkout" (50.05 or
74
+ 50.03?): the deposit amount is a token quantity, `fee.pay_in_fiat` is its
75
+ fiat valuation. Only `swap.deposit_amount` is an instruction. From 0.4.10 the
76
+ packaged checkout renders a USD stablecoin's breakdown in the token and never
77
+ shows `pay_in_fiat`; on an older bundle, upgrade `@openreceive/*`. To verify,
78
+ read the row's `deposit_amount` and `fee` and confirm the UI shows only the
79
+ deposit amount. A custom UI must call `createSwapFeeBreakdown(fee, swap)`
80
+ with the swap, not the fee alone.
73
81
 
74
82
  ## 6. Checkout UI shows nothing
75
83
 
@@ -86,6 +86,17 @@ state; do not retry-loop it, and do not build an idempotency store around it —
86
86
  that serialization is the library's job. (A hook failure while persisting an
87
87
  attempt is a **503 retryable**, deliberately distinct.)
88
88
 
89
+ ## Amounts on the deposit panel
90
+
91
+ `swap.deposit_amount` is the ONLY amount a payer is ever told to send, in the
92
+ pay-in token. `swap.fee.pay_in_fiat` / `payout_fiat` are fiat valuations that
93
+ explain the spread (why the deposit exceeds the cart total); they are not
94
+ instructions. For a stablecoin pegged to the fee currency (USDT, USDC) the
95
+ packaged checkout expresses the breakdown in the token and never renders
96
+ `pay_in_fiat` — "$50.03" under "50.05 USDC" reads as the same number with a
97
+ typo. A custom UI gets the same rule from `createSwapFeeBreakdown(fee, swap)`;
98
+ pass the swap, not just the fee.
99
+
89
100
  ## Secrets
90
101
 
91
102
  `NWC_URI` and `LSC_URI_*` are server-only. Never put them in browser code,
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (BTCPay Server)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Connect a BTCPay Server store to a receive-only NWC wallet with the OpenReceive
6
6
  plugin, and optionally let payers pay BTCPay invoices with USDT, USDC, ETH or
@@ -32,7 +32,7 @@ refund path on the same checkout screen.
32
32
 
33
33
  ## Step 0 — check the deployment before you change anything
34
34
 
35
- 1. Confirm the BTCPay Server version is 2.4.2 or later (Server Settings →
35
+ 1. Confirm the BTCPay Server version is 2.4.4 or later (Server Settings →
36
36
  About, or `GET /api/v1/server/info`). The plugin declares that minimum and
37
37
  BTCPay refuses to load it below.
38
38
  2. Check whether the plugin is installed (Server Settings → Plugins, or the
@@ -119,6 +119,8 @@ enough; drop the `.md` for the same page a person would read.
119
119
  Questions, or a problem with the plugin itself:
120
120
  https://openreceive.org/contact
121
121
 
122
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
123
+
122
124
  ---
123
125
 
124
126
  ## The quickstart, in full
@@ -128,7 +130,7 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-btcp
128
130
 
129
131
  ## BTCPay Server quickstart
130
132
 
131
- Requires BTCPay Server ≥ 2.4.2.
133
+ Requires BTCPay Server ≥ 2.4.4.
132
134
 
133
135
  The OpenReceive plugin makes a receive-only NWC wallet the Lightning node of a
134
136
  BTCPay store. BTCPay mints every Lightning invoice in that wallet and records
@@ -143,7 +145,7 @@ invoices, checkout, webhooks and Greenfield API are the host.
143
145
 
144
146
  ### 1. Prerequisites
145
147
 
146
- - A BTCPay Server, version 2.4.2 or later, on any network (mainnet, testnet,
148
+ - A BTCPay Server, version 2.4.4 or later, on any network (mainnet, testnet,
147
149
  signet, regtest). The wallet must be on the same network.
148
150
  - A receive-only NWC code for the wallet you want to receive into
149
151
  ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
@@ -157,9 +159,9 @@ invoices, checkout, webhooks and Greenfield API are the host.
157
159
 
158
160
  In BTCPay, open **Server Settings → Plugins**, search the plugin directory
159
161
  for **OpenReceive**, click **Install**, and restart BTCPay when prompted.
160
- BTCPay creates the plugin's one table (`openreceive_swaps`, schema
161
- `BTCPayServer.Plugins.OpenReceive`) in its own Postgres at startup; nothing
162
- else is created.
162
+ BTCPay creates the plugin's two tables (`openreceive_invoices` and
163
+ `openreceive_swaps`, schema `BTCPayServer.Plugins.OpenReceive`) in its own
164
+ Postgres at startup; nothing else is created.
163
165
 
164
166
  To build the plugin from source instead, follow
165
167
  [the .NET workspace README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/README.md).
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Django)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Django project — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the Python package is on PyPI
@@ -130,7 +130,7 @@ itself, and they hold for every integration.
130
130
  a placeholder that allows everything (`manage.py check` warns
131
131
  `openreceive.W002` while it is set) — replace it with this app's real
132
132
  ownership check, same as `on_paid`.
133
- - `on_paid` must be idempotent. It runs once per `reference` — your order
133
+ - `on_paid` must be idempotent. Its database fulfillment commits once per `reference` — your order
134
134
  id, one per thing you fulfill, created before checkout, kept across retries,
135
135
  never reused. A fresh id per page load lets one order be paid twice.
136
136
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -231,6 +231,13 @@ built on `@openreceive/browser/headless`. Read that before writing components.
231
231
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
232
232
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
233
233
  the model gives it.
234
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
235
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
236
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
237
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
238
+ (`payout_fiat`); express "you send" and the fee in the token. Use
239
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
240
+ it applies this for you; SOL and ETH keep a fiat breakdown.
234
241
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
235
242
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
236
243
  `startSwap` reports through `onError`.
@@ -300,6 +307,8 @@ enough; drop the `.md` for the same page a person would read.
300
307
  Questions, or a problem with the library itself:
301
308
  https://openreceive.org/contact
302
309
 
310
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
311
+
303
312
  ---
304
313
 
305
314
  ## The quickstart, in full
@@ -473,7 +482,7 @@ The host class needs three things: authorization, the trusted price, and
473
482
  fulfillment. All three receive the `reference` — a string you choose, and the
474
483
  fulfillment identity: your order id, one per thing you fulfill, created before
475
484
  checkout, kept across retries, never reused. OpenReceive never looks inside
476
- it, but `on_paid` runs once per reference, a new checkout under a reference
485
+ it, but `on_paid` commits fulfillment once per reference, a new checkout under a reference
477
486
  that already settled is refused with 409, and a fresh id per page load lets
478
487
  one order be paid twice.
479
488
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (FastAPI)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a FastAPI application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the engine is on PyPI
@@ -116,7 +116,7 @@ itself, and they hold for every integration.
116
116
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
117
117
  the payer made, not proof. Read the Starlette request's session, cookie or
118
118
  auth dependency; never trust a body field.
119
- - `on_paid` must be idempotent. It runs once per `reference` — your order id, one
119
+ - `on_paid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
120
120
  per thing you fulfill, created before checkout, kept across retries, never
121
121
  reused. A fresh id per page load lets one order be paid twice.
122
122
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -228,6 +228,13 @@ components.
228
228
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
229
229
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
230
230
  the model gives it.
231
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
232
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
233
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
234
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
235
+ (`payout_fiat`); express "you send" and the fee in the token. Use
236
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
237
+ it applies this for you; SOL and ETH keep a fiat breakdown.
231
238
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
232
239
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
233
240
  `startSwap` reports through `onError`.
@@ -294,6 +301,8 @@ enough; drop the `.md` for the same page a person would read.
294
301
  Questions, or a problem with the library itself:
295
302
  https://openreceive.org/contact
296
303
 
304
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
305
+
297
306
  ---
298
307
 
299
308
  ## The quickstart, in full
@@ -468,7 +477,7 @@ the `reference`. OpenReceive never prices from payer input.
468
477
  The `reference` is a string you choose, and it is the fulfillment identity:
469
478
  your order id — one per thing you fulfill, created before checkout, kept
470
479
  across retries, never reused. OpenReceive never looks inside it, but `on_paid`
471
- runs once per reference, a new checkout under a reference that already
480
+ commits fulfillment once per reference, a new checkout under a reference that already
472
481
  settled is refused with 409, and a fresh id per page load lets one order be
473
482
  paid twice.
474
483
 
@@ -517,7 +526,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
517
526
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
518
527
 
519
528
  That is the whole loop: your server owns the price and the order, the payer gets
520
- an invoice, and `onPaid` runs once inside the settlement transaction.
529
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
521
530
 
522
531
  A page without a bundler renders the same checkout as a custom element:
523
532
  `<openreceive-checkout reference="…" prefix="/openreceive">` from
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Fastify)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Fastify application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the packages are on npm, and
@@ -107,7 +107,7 @@ itself, and they hold for every integration.
107
107
  payer-supplied amounts.
108
108
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
109
109
  the payer made, not proof. Read a framework session; never trust a body field.
110
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
110
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
111
111
  per thing you fulfill, created before checkout, kept across retries, never
112
112
  reused. A fresh id per page load lets one order be paid twice.
113
113
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -211,6 +211,13 @@ components.
211
211
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
212
212
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
213
213
  the model gives it.
214
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
215
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
216
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
217
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
218
+ (`payout_fiat`); express "you send" and the fee in the token. Use
219
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
220
+ it applies this for you; SOL and ETH keep a fiat breakdown.
214
221
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
215
222
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
216
223
  `startSwap` reports through `onError`.
@@ -277,6 +284,8 @@ enough; drop the `.md` for the same page a person would read.
277
284
  Questions, or a problem with the library itself:
278
285
  https://openreceive.org/contact
279
286
 
287
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
288
+
280
289
  ---
281
290
 
282
291
  ## The quickstart, in full
@@ -470,7 +479,7 @@ the `reference`. OpenReceive never prices from payer input.
470
479
  The `reference` is a string you choose, and it is the fulfillment identity:
471
480
  your order id — one per thing you fulfill, created before checkout, kept
472
481
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
473
- runs once per reference, a new checkout under a reference that already
482
+ commits fulfillment once per reference, a new checkout under a reference that already
474
483
  settled is refused with 409, and a fresh id per page load lets one order be
475
484
  paid twice.
476
485
 
@@ -519,7 +528,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
519
528
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
520
529
 
521
530
  That is the whole loop: your server owns the price and the order, the payer gets
522
- an invoice, and `onPaid` runs once inside the settlement transaction.
531
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
523
532
 
524
533
  A runnable illustration of this boundary — not a template to copy models from —
525
534
  is Buy a Button
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Laravel)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
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
@@ -121,7 +121,7 @@ itself, and they hold for every integration.
121
121
  scaffolds `use AllowAllAuthorize;`, a placeholder trait that allows
122
122
  everything (the engine warns at boot while it is there) — replace it with
123
123
  this app's real ownership check, same as `onPaid`.
124
- - `onPaid` must be idempotent. It runs once per `reference` — your order
124
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order
125
125
  id, one per thing you fulfill, created before checkout, kept across retries,
126
126
  never reused. A fresh id per page load lets one order be paid twice.
127
127
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -216,6 +216,13 @@ built on `@openreceive/browser/headless`. Read that before writing components.
216
216
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
217
217
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
218
218
  the model gives it.
219
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
220
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
221
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
222
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
223
+ (`payout_fiat`); express "you send" and the fee in the token. Use
224
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
225
+ it applies this for you; SOL and ETH keep a fiat breakdown.
219
226
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
220
227
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
221
228
  `startSwap` reports through `onError`.
@@ -284,6 +291,8 @@ enough; drop the `.md` for the same page a person would read.
284
291
  Questions, or a problem with the library itself:
285
292
  https://openreceive.org/contact
286
293
 
294
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
295
+
287
296
  ---
288
297
 
289
298
  ## The quickstart, in full
@@ -449,7 +458,7 @@ variable as set/unset only.
449
458
  price, and fulfillment. All three receive the `reference` — a string you
450
459
  choose, and the fulfillment identity: your order id, one per thing you
451
460
  fulfill, created before checkout, kept across retries, never reused.
452
- OpenReceive never looks inside it, but `onPaid` runs once per reference, a new
461
+ OpenReceive never looks inside it, but `onPaid` commits fulfillment once per reference, a new
453
462
  checkout under a reference that already settled is refused with 409, and a
454
463
  fresh id per page load lets one order be paid twice.
455
464
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Next.js)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Next.js App Router application — the app you are already
6
6
  working in. You do not need a copy of the OpenReceive source: the packages are
@@ -109,7 +109,7 @@ itself, and they hold for every integration.
109
109
  payer-supplied amounts.
110
110
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
111
111
  the payer made, not proof. Read a framework session; never trust a body field.
112
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
112
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
113
113
  per thing you fulfill, created before checkout, kept across retries, never
114
114
  reused. A fresh id per page load lets one order be paid twice.
115
115
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -217,6 +217,13 @@ components.
217
217
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
218
218
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
219
219
  the model gives it.
220
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
221
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
222
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
223
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
224
+ (`payout_fiat`); express "you send" and the fee in the token. Use
225
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
226
+ it applies this for you; SOL and ETH keep a fiat breakdown.
220
227
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
221
228
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
222
229
  `startSwap` reports through `onError`.
@@ -283,6 +290,8 @@ enough; drop the `.md` for the same page a person would read.
283
290
  Questions, or a problem with the library itself:
284
291
  https://openreceive.org/contact
285
292
 
293
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
294
+
286
295
  ---
287
296
 
288
297
  ## The quickstart, in full
@@ -489,7 +498,7 @@ the `reference`. OpenReceive never prices from payer input.
489
498
  The `reference` is a string you choose, and it is the fulfillment identity:
490
499
  your order id — one per thing you fulfill, created before checkout, kept
491
500
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
492
- runs once per reference, a new checkout under a reference that already
501
+ commits fulfillment once per reference, a new checkout under a reference that already
493
502
  settled is refused with 409, and a fresh id per page load lets one order be
494
503
  paid twice.
495
504
 
@@ -569,7 +578,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
569
578
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
570
579
 
571
580
  That is the whole loop: your server owns the price and the order, the payer gets
572
- an invoice, and `onPaid` runs once inside the settlement transaction.
581
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
573
582
 
574
583
  A runnable illustration of this boundary — not a template to copy models from —
575
584
  is Buy a Button