@openreceive/react 0.4.10 → 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.
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  mergeAttemptIntoCheckout,
7
7
  mergeAttemptIntoSnapshot,
8
8
  OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES as OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES3,
9
- OPENRECEIVE_STYLE_ROOT_ATTRIBUTE as OPENRECEIVE_STYLE_ROOT_ATTRIBUTE2,
10
9
  OPENRECEIVE_DEFAULT_PREFIX,
10
+ OPENRECEIVE_STYLE_ROOT_ATTRIBUTE as OPENRECEIVE_STYLE_ROOT_ATTRIBUTE2,
11
11
  orClasses as orClasses7,
12
12
  prepareCheckout,
13
13
  requestCheckout,
@@ -30,7 +30,8 @@ function useCheckoutSession(options) {
30
30
  sessionRef.current = createCheckoutSession({
31
31
  snapshot: () => optionsRef.current.snapshot(),
32
32
  reference: () => optionsRef.current.reference(),
33
- requestCheckout: (reference) => optionsRef.current.requestCheckout?.(reference),
33
+ prefix: () => optionsRef.current.prefix?.(),
34
+ requestCheckout: (reference, signal) => optionsRef.current.requestCheckout?.(reference, signal),
34
35
  onSnapshot: (snapshot) => optionsRef.current.onSnapshot?.(snapshot),
35
36
  // The inner `swap` is always present because the session is built once,
36
37
  // on the first render, and the host may only gain swap options later.
@@ -57,7 +58,10 @@ function useCheckoutSession(options) {
57
58
  onChange: rerender
58
59
  });
59
60
  }
60
- return sessionRef.current;
61
+ const session = sessionRef.current;
62
+ session.syncIdentity();
63
+ React.useEffect(() => () => session.dispose(), [session]);
64
+ return session;
61
65
  }
62
66
 
63
67
  // src/components.ts
@@ -712,15 +716,15 @@ function TransactionDetailCopyButton(props) {
712
716
  }
713
717
 
714
718
  // src/use-checkout.ts
715
- import * as React6 from "react";
716
719
  import {
717
720
  copyInvoice as copyInvoiceHelper2,
718
721
  createCheckoutController,
719
722
  createCheckoutState,
720
723
  createCheckoutStatusModel as createCheckoutStatusModel2,
721
- openWallet as openWalletHelper2,
722
- deriveStatus
724
+ deriveStatus,
725
+ openWallet as openWalletHelper2
723
726
  } from "@openreceive/browser/headless";
727
+ import * as React6 from "react";
724
728
  function useCheckout(options) {
725
729
  const checkout = options.checkout;
726
730
  const [copied, showCopied] = useTransientValue(false);
@@ -857,8 +861,9 @@ function useCheckout(options) {
857
861
  }, [logContext, state.invoice, options.open, options.logger]);
858
862
  const reloadState = React6.useCallback(async () => {
859
863
  try {
860
- const next = await controllerRef.current?.reloadState();
861
- if (next !== void 0) setState(next);
864
+ const controller = controllerRef.current;
865
+ const next = await controller?.reloadState();
866
+ if (controllerRef.current === controller && next !== void 0) setState(next);
862
867
  } catch (error) {
863
868
  onErrorRef.current?.(error);
864
869
  throw error;
@@ -878,10 +883,11 @@ function useCheckout(options) {
878
883
  }, []);
879
884
  const stageSwapRefund = React6.useCallback(
880
885
  async (refund) => {
886
+ const controller = swapRefundController();
881
887
  try {
882
- return await swapRefundController().stageSwapRefund(refund);
888
+ return await controller.stageSwapRefund(refund);
883
889
  } catch (error) {
884
- onErrorRef.current?.(error);
890
+ if (controllerRef.current === controller) onErrorRef.current?.(error);
885
891
  throw error;
886
892
  }
887
893
  },
@@ -889,10 +895,11 @@ function useCheckout(options) {
889
895
  );
890
896
  const confirmSwapRefund = React6.useCallback(
891
897
  async (refund) => {
898
+ const controller = swapRefundController();
892
899
  try {
893
- return await swapRefundController().confirmSwapRefund(refund);
900
+ return await controller.confirmSwapRefund(refund);
894
901
  } catch (error) {
895
- onErrorRef.current?.(error);
902
+ if (controllerRef.current === controller) onErrorRef.current?.(error);
896
903
  throw error;
897
904
  }
898
905
  },
@@ -1861,24 +1868,20 @@ function renderKeepOrderNote(options) {
1861
1868
  )
1862
1869
  );
1863
1870
  }
1864
- var REFUND_ADDRESS_DRAFT_LIMIT = 8;
1865
- var refundAddressDraftByAttempt = /* @__PURE__ */ new Map();
1866
- function setRefundAddressDraft(attemptId, value) {
1867
- refundAddressDraftByAttempt.delete(attemptId);
1868
- refundAddressDraftByAttempt.set(attemptId, value);
1869
- while (refundAddressDraftByAttempt.size > REFUND_ADDRESS_DRAFT_LIMIT) {
1870
- const oldest = refundAddressDraftByAttempt.keys().next().value;
1871
- if (oldest === void 0) break;
1872
- refundAddressDraftByAttempt.delete(oldest);
1873
- }
1874
- }
1875
1871
  function SwapRefundForm(props) {
1876
- const [refundAddress, setRefundAddress] = React8.useState(
1877
- () => refundAddressDraftByAttempt.get(props.attemptId) ?? ""
1872
+ const [refundAddress, setRefundAddress] = React8.useState("");
1873
+ const [draftAttemptId, setDraftAttemptId] = React8.useState(props.attemptId);
1874
+ if (draftAttemptId !== props.attemptId) {
1875
+ setDraftAttemptId(props.attemptId);
1876
+ setRefundAddress("");
1877
+ }
1878
+ const generation = React8.useRef(0);
1879
+ React8.useEffect(
1880
+ () => () => {
1881
+ generation.current += 1;
1882
+ },
1883
+ [props.attemptId]
1878
1884
  );
1879
- React8.useEffect(() => {
1880
- setRefundAddress(refundAddressDraftByAttempt.get(props.attemptId) ?? "");
1881
- }, [props.attemptId]);
1882
1885
  const [submitting, setSubmitting] = React8.useState(false);
1883
1886
  const [showAddressError, setShowAddressError] = React8.useState(false);
1884
1887
  const address = refundAddress.trim();
@@ -1898,7 +1901,12 @@ function SwapRefundForm(props) {
1898
1901
  return;
1899
1902
  }
1900
1903
  setSubmitting(true);
1901
- void props.onRefund(props.attemptId, address, confirm).catch(props.onError).finally(() => setSubmitting(false));
1904
+ const captured = generation.current;
1905
+ void props.onRefund(props.attemptId, address, confirm).catch((error) => {
1906
+ if (captured === generation.current) props.onError?.(error);
1907
+ }).finally(() => {
1908
+ if (captured === generation.current) setSubmitting(false);
1909
+ });
1902
1910
  }
1903
1911
  },
1904
1912
  // The form says what it is and why it is here before it asks for anything:
@@ -1934,7 +1942,6 @@ function SwapRefundForm(props) {
1934
1942
  className: showError ? orClasses5.swapRefundInputInvalid : orClasses5.swapRefundInput,
1935
1943
  onChange: (event) => {
1936
1944
  const value = event.currentTarget.value;
1937
- setRefundAddressDraft(props.attemptId, value);
1938
1945
  setRefundAddress(value);
1939
1946
  },
1940
1947
  onBlur: () => {
@@ -1998,6 +2005,12 @@ function SwapPayloadQRCode(props) {
1998
2005
 
1999
2006
  // src/wizard.ts
2000
2007
  function PaymentWizard(props) {
2008
+ return React9.createElement(PaymentWizardSession, {
2009
+ ...props,
2010
+ key: JSON.stringify([props.checkout?.reference, props.prefix?.replace(/\/+$/, "")])
2011
+ });
2012
+ }
2013
+ function PaymentWizardSession(props) {
2001
2014
  const [selection, setSelection] = React9.useState(
2002
2015
  () => createPaymentWizardController().getSelection()
2003
2016
  );
@@ -2014,6 +2027,7 @@ function PaymentWizard(props) {
2014
2027
  const session = useCheckoutSession({
2015
2028
  snapshot: () => checkout,
2016
2029
  reference: () => reference,
2030
+ prefix: () => props.prefix,
2017
2031
  swap: {
2018
2032
  selection: {
2019
2033
  started: () => startedSwapInvoice ?? void 0,
@@ -2076,6 +2090,7 @@ function PaymentWizard(props) {
2076
2090
  if (prefix === void 0 || reference === void 0 || fetcher === void 0) {
2077
2091
  return;
2078
2092
  }
2093
+ const action = session.capture();
2079
2094
  try {
2080
2095
  const invoice = swapRefund !== void 0 ? await (confirm ? swapRefund.confirmSwapRefund : swapRefund.stageSwapRefund).call(
2081
2096
  swapRefund,
@@ -2093,10 +2108,11 @@ function PaymentWizard(props) {
2093
2108
  confirm,
2094
2109
  ...props.logger === void 0 ? {} : { logger: props.logger }
2095
2110
  });
2111
+ if (!action.isCurrent()) return;
2096
2112
  setStartedSwapInvoice(invoice);
2097
2113
  setDismissedSwapInvoiceId(null);
2098
2114
  } catch (error) {
2099
- props.onError?.(error);
2115
+ if (action.isCurrent()) props.onError?.(error);
2100
2116
  }
2101
2117
  },
2102
2118
  [
@@ -2108,7 +2124,8 @@ function PaymentWizard(props) {
2108
2124
  props.logger,
2109
2125
  swapRefund,
2110
2126
  startedSwapInvoice,
2111
- checkout?.invoices
2127
+ checkout?.invoices,
2128
+ session
2112
2129
  ]
2113
2130
  );
2114
2131
  const updateWizardSelection = React9.useCallback(
@@ -2917,11 +2934,21 @@ function Checkout(props) {
2917
2934
  if (checkout !== void 0) {
2918
2935
  return React10.createElement(CheckoutSnapshotMode, {
2919
2936
  ...props,
2937
+ key: JSON.stringify([
2938
+ checkout.reference,
2939
+ (props.prefix ?? OPENRECEIVE_DEFAULT_PREFIX).replace(/\/+$/, "")
2940
+ ]),
2920
2941
  checkout,
2921
2942
  prefix: props.prefix ?? OPENRECEIVE_DEFAULT_PREFIX
2922
2943
  });
2923
2944
  }
2924
- return React10.createElement(CheckoutCreate, props);
2945
+ return React10.createElement(CheckoutCreate, {
2946
+ ...props,
2947
+ key: JSON.stringify([
2948
+ props.reference,
2949
+ (props.prefix ?? OPENRECEIVE_DEFAULT_PREFIX).replace(/\/+$/, "")
2950
+ ])
2951
+ });
2925
2952
  }
2926
2953
  function CheckoutSnapshotMode(props) {
2927
2954
  const { checkout } = props;
@@ -2976,8 +3003,10 @@ function CheckoutCreate(props) {
2976
3003
  const session = useCheckoutSession({
2977
3004
  snapshot: () => createdCheckoutRef.current,
2978
3005
  reference: () => reference,
2979
- requestCheckout: (id) => requestCheckout({
3006
+ prefix: () => resolvedPrefix,
3007
+ requestCheckout: (id, signal) => requestCheckout({
2980
3008
  prefix: resolvedPrefix,
3009
+ signal,
2981
3010
  reference: id,
2982
3011
  ...csrfHeader === void 0 ? {} : { csrfHeader },
2983
3012
  ...metadataRef.current === void 0 ? {} : { metadata: metadataRef.current },
@@ -2998,14 +3027,17 @@ function CheckoutCreate(props) {
2998
3027
  }, [syncUrl, reference, resumePathPrefix, routeReference]);
2999
3028
  React10.useEffect(() => {
3000
3029
  let cancelled = false;
3030
+ const action = session.capture();
3001
3031
  setCreated({ status: "pending" });
3002
3032
  prepareCheckout({
3003
3033
  prefix: resolvedPrefix,
3034
+ signal: action.signal,
3004
3035
  reference,
3005
3036
  ...csrfHeader === void 0 ? {} : { csrfHeader },
3006
3037
  ...createFetchRef.current === void 0 ? {} : { fetch: createFetchRef.current }
3007
3038
  }).then(
3008
- (checkout) => resumePaymentHash === void 0 ? checkout : resumeSwapAttempt({
3039
+ (checkout) => cancelled || !action.isCurrent() || resumePaymentHash === void 0 ? checkout : resumeSwapAttempt({
3040
+ signal: action.signal,
3009
3041
  fetch: createFetchRef.current ?? globalThis.fetch,
3010
3042
  prefix: resolvedPrefix,
3011
3043
  ...csrfHeader === void 0 ? {} : { csrfHeader },
@@ -3014,9 +3046,9 @@ function CheckoutCreate(props) {
3014
3046
  snapshot: checkout
3015
3047
  })
3016
3048
  ).then((checkout) => {
3017
- if (!cancelled) setCreated({ status: "ready", checkout });
3049
+ if (!cancelled && action.isCurrent()) setCreated({ status: "ready", checkout });
3018
3050
  }).catch((error) => {
3019
- if (cancelled) return;
3051
+ if (cancelled || !action.isCurrent()) return;
3020
3052
  onErrorRef.current?.(error);
3021
3053
  setCreated({
3022
3054
  status: "error",
@@ -3025,6 +3057,7 @@ function CheckoutCreate(props) {
3025
3057
  });
3026
3058
  return () => {
3027
3059
  cancelled = true;
3060
+ session.reset();
3028
3061
  };
3029
3062
  }, [reference, resolvedPrefix, csrfHeader, resumePaymentHash, attempt]);
3030
3063
  const onSwapStarted = React10.useCallback(
@@ -3176,7 +3209,9 @@ function CheckoutView(props) {
3176
3209
  });
3177
3210
  const stampsTheme = !theme.fromScope;
3178
3211
  const ownsTheme = themeToggle && !theme.fromScope && lockedTheme === void 0;
3179
- const [swapFocused, setSwapFocused] = React10.useState(false);
3212
+ const [swapFocused, setSwapFocused] = React10.useState(
3213
+ () => checkout.invoices.some((invoice) => invoice.rail === "swap" && invoice.swap !== void 0)
3214
+ );
3180
3215
  const [lightningFocused, setLightningFocused] = React10.useState(false);
3181
3216
  const QRCodeComponent = components?.QRCode ?? QRCode;
3182
3217
  const InvoiceSummaryComponent = components?.InvoiceSummary ?? InvoiceSummary;
@@ -3188,7 +3223,7 @@ function CheckoutView(props) {
3188
3223
  const expired = checkoutModel.status === "expired";
3189
3224
  const settled = checkoutModel.status === "settled";
3190
3225
  const showLightning = !!checkoutModel.invoice && (!paymentWizard || lightningFocused) && !swapFocused && !expired;
3191
- const hideLightning = !showLightning && !expired && !settled;
3226
+ const hideLightning = !settled && (swapFocused || !showLightning && !expired);
3192
3227
  const showSummaryMeta = checkoutModel.status === "settled" || checkoutModel.status === "expired";
3193
3228
  const fiatCurrency = checkoutModel.fiat_quote?.fiat?.currency;
3194
3229
  const decodeInvoiceHref = createLightningInvoiceDecodeUrl2(checkoutModel.invoice, decodeLinkUrl);
@@ -3397,7 +3432,7 @@ function CheckoutView(props) {
3397
3432
  )
3398
3433
  )
3399
3434
  ),
3400
- paymentWizard && !settled && (!expired || swapFocused) ? React10.createElement(PaymentWizard, {
3435
+ paymentWizard && !settled && (!expired || swapFocused || checkoutModel.checkout?.invoices.some((invoice) => invoice.swap !== void 0)) ? React10.createElement(PaymentWizard, {
3401
3436
  key: "wizard",
3402
3437
  // Only pass invoice when it's a real bolt11 (non-empty, non-deferred).
3403
3438
  invoice: checkoutModel.invoice || void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openreceive/react",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "React components and hooks for Bitcoin Lightning checkout and optional USDT, USDC, SOL and ETH swaps.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -19,8 +19,8 @@
19
19
  "main": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts",
21
21
  "dependencies": {
22
- "@openreceive/browser": "0.4.10",
23
- "@openreceive/core": "0.4.10"
22
+ "@openreceive/browser": "0.4.11",
23
+ "@openreceive/core": "0.4.11"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": ">=18.0.0"
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (BTCPay Server)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
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
@@ -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
@@ -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.10.
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
@@ -307,6 +307,8 @@ enough; drop the `.md` for the same page a person would read.
307
307
  Questions, or a problem with the library itself:
308
308
  https://openreceive.org/contact
309
309
 
310
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
311
+
310
312
  ---
311
313
 
312
314
  ## The quickstart, in full
@@ -480,7 +482,7 @@ The host class needs three things: authorization, the trusted price, and
480
482
  fulfillment. All three receive the `reference` — a string you choose, and the
481
483
  fulfillment identity: your order id, one per thing you fulfill, created before
482
484
  checkout, kept across retries, never reused. OpenReceive never looks inside
483
- 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
484
486
  that already settled is refused with 409, and a fresh id per page load lets
485
487
  one order be paid twice.
486
488
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (FastAPI)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
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
@@ -301,6 +301,8 @@ enough; drop the `.md` for the same page a person would read.
301
301
  Questions, or a problem with the library itself:
302
302
  https://openreceive.org/contact
303
303
 
304
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
305
+
304
306
  ---
305
307
 
306
308
  ## The quickstart, in full
@@ -475,7 +477,7 @@ the `reference`. OpenReceive never prices from payer input.
475
477
  The `reference` is a string you choose, and it is the fulfillment identity:
476
478
  your order id — one per thing you fulfill, created before checkout, kept
477
479
  across retries, never reused. OpenReceive never looks inside it, but `on_paid`
478
- 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
479
481
  settled is refused with 409, and a fresh id per page load lets one order be
480
482
  paid twice.
481
483
 
@@ -524,7 +526,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
524
526
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
525
527
 
526
528
  That is the whole loop: your server owns the price and the order, the payer gets
527
- 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.
528
530
 
529
531
  A page without a bundler renders the same checkout as a custom element:
530
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.10.
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
@@ -284,6 +284,8 @@ enough; drop the `.md` for the same page a person would read.
284
284
  Questions, or a problem with the library itself:
285
285
  https://openreceive.org/contact
286
286
 
287
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
288
+
287
289
  ---
288
290
 
289
291
  ## The quickstart, in full
@@ -477,7 +479,7 @@ the `reference`. OpenReceive never prices from payer input.
477
479
  The `reference` is a string you choose, and it is the fulfillment identity:
478
480
  your order id — one per thing you fulfill, created before checkout, kept
479
481
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
480
- 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
481
483
  settled is refused with 409, and a fresh id per page load lets one order be
482
484
  paid twice.
483
485
 
@@ -526,7 +528,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
526
528
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
527
529
 
528
530
  That is the whole loop: your server owns the price and the order, the payer gets
529
- 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.
530
532
 
531
533
  A runnable illustration of this boundary — not a template to copy models from —
532
534
  is Buy a Button
@@ -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.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
@@ -291,6 +291,8 @@ enough; drop the `.md` for the same page a person would read.
291
291
  Questions, or a problem with the library itself:
292
292
  https://openreceive.org/contact
293
293
 
294
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
295
+
294
296
  ---
295
297
 
296
298
  ## The quickstart, in full
@@ -456,7 +458,7 @@ variable as set/unset only.
456
458
  price, and fulfillment. All three receive the `reference` — a string you
457
459
  choose, and the fulfillment identity: your order id, one per thing you
458
460
  fulfill, created before checkout, kept across retries, never reused.
459
- 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
460
462
  checkout under a reference that already settled is refused with 409, and a
461
463
  fresh id per page load lets one order be paid twice.
462
464
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Next.js)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
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
@@ -290,6 +290,8 @@ enough; drop the `.md` for the same page a person would read.
290
290
  Questions, or a problem with the library itself:
291
291
  https://openreceive.org/contact
292
292
 
293
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
294
+
293
295
  ---
294
296
 
295
297
  ## The quickstart, in full
@@ -496,7 +498,7 @@ the `reference`. OpenReceive never prices from payer input.
496
498
  The `reference` is a string you choose, and it is the fulfillment identity:
497
499
  your order id — one per thing you fulfill, created before checkout, kept
498
500
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
499
- 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
500
502
  settled is refused with 409, and a fresh id per page load lets one order be
501
503
  paid twice.
502
504
 
@@ -576,7 +578,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
576
578
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
577
579
 
578
580
  That is the whole loop: your server owns the price and the order, the payer gets
579
- 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.
580
582
 
581
583
  A runnable illustration of this boundary — not a template to copy models from —
582
584
  is Buy a Button
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Node.js)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Node application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the packages are on npm, and the
@@ -104,7 +104,7 @@ itself, and they hold for every integration.
104
104
  payer-supplied amounts.
105
105
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
106
106
  the payer made, not proof. Read a framework session; never trust a body field.
107
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
107
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
108
108
  per thing you fulfill, created before checkout, kept across retries, never
109
109
  reused. A fresh id per page load lets one order be paid twice.
110
110
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -276,6 +276,8 @@ enough; drop the `.md` for the same page a person would read.
276
276
  Questions, or a problem with the library itself:
277
277
  https://openreceive.org/contact
278
278
 
279
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
280
+
279
281
  ---
280
282
 
281
283
  ## The quickstart, in full
@@ -454,7 +456,7 @@ the `reference`. OpenReceive never prices from payer input.
454
456
  The `reference` is a string you choose, and it is the fulfillment identity:
455
457
  your order id — one per thing you fulfill, created before checkout, kept
456
458
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
457
- runs once per reference, a new checkout under a reference that already
459
+ commits fulfillment once per reference, a new checkout under a reference that already
458
460
  settled is refused with 409, and a fresh id per page load lets one order be
459
461
  paid twice.
460
462
 
@@ -503,7 +505,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
503
505
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
504
506
 
505
507
  That is the whole loop: your server owns the price and the order, the payer gets
506
- an invoice, and `onPaid` runs once inside the settlement transaction.
508
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
507
509
 
508
510
  A runnable illustration of this boundary — not a template to copy models from —
509
511
  is Buy a Button
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (PHP)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a PHP application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the engine is on Packagist
@@ -125,7 +125,7 @@ itself, and they hold for every integration.
125
125
  is a placeholder that allows everything (the engine warns at boot while a
126
126
  host uses it) — replace it with this app's real ownership check, same as
127
127
  `onPaid`'s `Hosts\LoggingOnPaid`.
128
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
128
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
129
129
  per thing you fulfill, created before checkout, kept across retries, never
130
130
  reused. A fresh id per page load lets one order be paid twice.
131
131
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -301,6 +301,8 @@ enough; drop the `.md` for the same page a person would read.
301
301
  Questions, or a problem with the library itself:
302
302
  https://openreceive.org/contact
303
303
 
304
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
305
+
304
306
  ---
305
307
 
306
308
  ## The quickstart, in full
@@ -512,7 +514,7 @@ prices with exact decimal math, and returns the order id the page will pass as
512
514
  the `reference`. OpenReceive never prices from payer input. The `reference` is
513
515
  a string you choose, and it is the fulfillment identity: your order id — one
514
516
  per thing you fulfill, created before checkout, kept across retries, never
515
- reused. `onPaid` runs once per reference, a new checkout under a reference
517
+ reused. `onPaid` commits fulfillment once per reference, a new checkout under a reference
516
518
  that already settled is refused with 409, and a fresh id per page load lets
517
519
  one order be paid twice.
518
520
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Rails)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Rails application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the gem is on RubyGems, the
@@ -116,7 +116,7 @@ itself, and they hold for every integration.
116
116
  body field. The generator installs `OpenReceive::ALLOW_ALL_AUTHORIZE`, a
117
117
  placeholder that allows everything (the engine warns at boot while it is
118
118
  set) — replace it with this app's real ownership check, same as `on_paid`.
119
- - `config.on_paid` must be idempotent. It runs once per `reference` — your order
119
+ - `config.on_paid` must be idempotent. Its database fulfillment commits once per `reference` — your order
120
120
  id, one per thing you fulfill, created before checkout, kept across retries,
121
121
  never 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
@@ -285,6 +285,8 @@ enough; drop the `.md` for the same page a person would read.
285
285
  Questions, or a problem with the library itself:
286
286
  https://openreceive.org/contact
287
287
 
288
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
289
+
288
290
  ---
289
291
 
290
292
  ## The quickstart, in full
@@ -432,7 +434,7 @@ The initializer needs three things: authorization, the trusted price, and
432
434
  fulfillment. All three receive the `reference` — a string you choose, and the
433
435
  fulfillment identity: your order id, one per thing you fulfill, created before
434
436
  checkout, kept across retries, never reused. OpenReceive never looks inside
435
- it, but `on_paid` runs once per reference, a new checkout under a reference
437
+ it, but `on_paid` commits fulfillment once per reference, a new checkout under a reference
436
438
  that already settled is refused with 409, and a fresh id per page load lets
437
439
  one order be paid twice.
438
440
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (WordPress + WooCommerce)
2
2
 
3
- These directions describe OpenReceive 0.4.10.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Install and configure the OpenReceive gateway in the existing WooCommerce
6
6
  store. Preserve its theme, checkout, customer accounts, order model and prices.
@@ -73,6 +73,8 @@ flows; a receive-only NWC wallet cannot send payments.
73
73
  - [Agent Directions: BTCPay Server](https://openreceive.org/guides/agent-directions-btcpay.md)
74
74
  - [WordPress + WooCommerce Quickstart](https://openreceive.org/guides/quickstart-woocommerce.md)
75
75
 
76
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
77
+
76
78
  ---
77
79
 
78
80
  ## The quickstart, in full