@openreceive/react 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.
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,13 +2005,21 @@ 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
  );
2004
2017
  const [activeTutorial, setActiveTutorial] = React9.useState(null);
2005
2018
  const [startedSwapInvoice, setStartedSwapInvoice] = React9.useState(null);
2006
2019
  const [dismissedSwapInvoiceId, setDismissedSwapInvoiceId] = React9.useState(null);
2007
- const [selectedSwapAsset, setSelectedSwapAsset] = React9.useState(null);
2020
+ const [selectedSwapAsset, setSelectedSwapAsset] = React9.useState(
2021
+ () => selectCurrentSwapInvoice(props.checkout)?.swap?.pay_in_asset ?? null
2022
+ );
2008
2023
  const [selectedSwapAssetByGroup, setSelectedSwapAssetByGroup] = React9.useState({});
2009
2024
  const [selectedPickerKey, setSelectedPickerKey] = React9.useState(null);
2010
2025
  const autoSwapAttemptedRef = React9.useRef(/* @__PURE__ */ new Set());
@@ -2014,6 +2029,7 @@ function PaymentWizard(props) {
2014
2029
  const session = useCheckoutSession({
2015
2030
  snapshot: () => checkout,
2016
2031
  reference: () => reference,
2032
+ prefix: () => props.prefix,
2017
2033
  swap: {
2018
2034
  selection: {
2019
2035
  started: () => startedSwapInvoice ?? void 0,
@@ -2076,6 +2092,7 @@ function PaymentWizard(props) {
2076
2092
  if (prefix === void 0 || reference === void 0 || fetcher === void 0) {
2077
2093
  return;
2078
2094
  }
2095
+ const action = session.capture();
2079
2096
  try {
2080
2097
  const invoice = swapRefund !== void 0 ? await (confirm ? swapRefund.confirmSwapRefund : swapRefund.stageSwapRefund).call(
2081
2098
  swapRefund,
@@ -2093,10 +2110,11 @@ function PaymentWizard(props) {
2093
2110
  confirm,
2094
2111
  ...props.logger === void 0 ? {} : { logger: props.logger }
2095
2112
  });
2113
+ if (!action.isCurrent()) return;
2096
2114
  setStartedSwapInvoice(invoice);
2097
2115
  setDismissedSwapInvoiceId(null);
2098
2116
  } catch (error) {
2099
- props.onError?.(error);
2117
+ if (action.isCurrent()) props.onError?.(error);
2100
2118
  }
2101
2119
  },
2102
2120
  [
@@ -2108,7 +2126,8 @@ function PaymentWizard(props) {
2108
2126
  props.logger,
2109
2127
  swapRefund,
2110
2128
  startedSwapInvoice,
2111
- checkout?.invoices
2129
+ checkout?.invoices,
2130
+ session
2112
2131
  ]
2113
2132
  );
2114
2133
  const updateWizardSelection = React9.useCallback(
@@ -2148,7 +2167,7 @@ function PaymentWizard(props) {
2148
2167
  stickySwapInvoiceRef.current = void 0;
2149
2168
  }
2150
2169
  const activeSwapForAsset = selectedSwapAsset === null ? void 0 : currentSwapInvoice !== void 0 && currentSwapInvoice.swap?.pay_in_asset === selectedSwapAsset ? currentSwapInvoice : stickySwapInvoiceRef.current?.swap?.pay_in_asset === selectedSwapAsset ? stickySwapInvoiceRef.current : void 0;
2151
- const resumeAdoptedRef = React9.useRef(false);
2170
+ const resumeAdoptedRef = React9.useRef(selectedSwapAsset !== null);
2152
2171
  React9.useEffect(() => {
2153
2172
  if (resumeAdoptedRef.current) return;
2154
2173
  if (selectedSwapAsset !== null) {
@@ -2917,11 +2936,21 @@ function Checkout(props) {
2917
2936
  if (checkout !== void 0) {
2918
2937
  return React10.createElement(CheckoutSnapshotMode, {
2919
2938
  ...props,
2939
+ key: JSON.stringify([
2940
+ checkout.reference,
2941
+ (props.prefix ?? OPENRECEIVE_DEFAULT_PREFIX).replace(/\/+$/, "")
2942
+ ]),
2920
2943
  checkout,
2921
2944
  prefix: props.prefix ?? OPENRECEIVE_DEFAULT_PREFIX
2922
2945
  });
2923
2946
  }
2924
- return React10.createElement(CheckoutCreate, props);
2947
+ return React10.createElement(CheckoutCreate, {
2948
+ ...props,
2949
+ key: JSON.stringify([
2950
+ props.reference,
2951
+ (props.prefix ?? OPENRECEIVE_DEFAULT_PREFIX).replace(/\/+$/, "")
2952
+ ])
2953
+ });
2925
2954
  }
2926
2955
  function CheckoutSnapshotMode(props) {
2927
2956
  const { checkout } = props;
@@ -2976,8 +3005,10 @@ function CheckoutCreate(props) {
2976
3005
  const session = useCheckoutSession({
2977
3006
  snapshot: () => createdCheckoutRef.current,
2978
3007
  reference: () => reference,
2979
- requestCheckout: (id) => requestCheckout({
3008
+ prefix: () => resolvedPrefix,
3009
+ requestCheckout: (id, signal) => requestCheckout({
2980
3010
  prefix: resolvedPrefix,
3011
+ signal,
2981
3012
  reference: id,
2982
3013
  ...csrfHeader === void 0 ? {} : { csrfHeader },
2983
3014
  ...metadataRef.current === void 0 ? {} : { metadata: metadataRef.current },
@@ -2998,14 +3029,17 @@ function CheckoutCreate(props) {
2998
3029
  }, [syncUrl, reference, resumePathPrefix, routeReference]);
2999
3030
  React10.useEffect(() => {
3000
3031
  let cancelled = false;
3032
+ const action = session.capture();
3001
3033
  setCreated({ status: "pending" });
3002
3034
  prepareCheckout({
3003
3035
  prefix: resolvedPrefix,
3036
+ signal: action.signal,
3004
3037
  reference,
3005
3038
  ...csrfHeader === void 0 ? {} : { csrfHeader },
3006
3039
  ...createFetchRef.current === void 0 ? {} : { fetch: createFetchRef.current }
3007
3040
  }).then(
3008
- (checkout) => resumePaymentHash === void 0 ? checkout : resumeSwapAttempt({
3041
+ (checkout) => cancelled || !action.isCurrent() || resumePaymentHash === void 0 ? checkout : resumeSwapAttempt({
3042
+ signal: action.signal,
3009
3043
  fetch: createFetchRef.current ?? globalThis.fetch,
3010
3044
  prefix: resolvedPrefix,
3011
3045
  ...csrfHeader === void 0 ? {} : { csrfHeader },
@@ -3014,9 +3048,9 @@ function CheckoutCreate(props) {
3014
3048
  snapshot: checkout
3015
3049
  })
3016
3050
  ).then((checkout) => {
3017
- if (!cancelled) setCreated({ status: "ready", checkout });
3051
+ if (!cancelled && action.isCurrent()) setCreated({ status: "ready", checkout });
3018
3052
  }).catch((error) => {
3019
- if (cancelled) return;
3053
+ if (cancelled || !action.isCurrent()) return;
3020
3054
  onErrorRef.current?.(error);
3021
3055
  setCreated({
3022
3056
  status: "error",
@@ -3025,6 +3059,7 @@ function CheckoutCreate(props) {
3025
3059
  });
3026
3060
  return () => {
3027
3061
  cancelled = true;
3062
+ session.reset();
3028
3063
  };
3029
3064
  }, [reference, resolvedPrefix, csrfHeader, resumePaymentHash, attempt]);
3030
3065
  const onSwapStarted = React10.useCallback(
@@ -3176,7 +3211,9 @@ function CheckoutView(props) {
3176
3211
  });
3177
3212
  const stampsTheme = !theme.fromScope;
3178
3213
  const ownsTheme = themeToggle && !theme.fromScope && lockedTheme === void 0;
3179
- const [swapFocused, setSwapFocused] = React10.useState(false);
3214
+ const [swapFocused, setSwapFocused] = React10.useState(
3215
+ () => checkout.invoices.some((invoice) => invoice.rail === "swap" && invoice.swap !== void 0)
3216
+ );
3180
3217
  const [lightningFocused, setLightningFocused] = React10.useState(false);
3181
3218
  const QRCodeComponent = components?.QRCode ?? QRCode;
3182
3219
  const InvoiceSummaryComponent = components?.InvoiceSummary ?? InvoiceSummary;
@@ -3188,7 +3225,7 @@ function CheckoutView(props) {
3188
3225
  const expired = checkoutModel.status === "expired";
3189
3226
  const settled = checkoutModel.status === "settled";
3190
3227
  const showLightning = !!checkoutModel.invoice && (!paymentWizard || lightningFocused) && !swapFocused && !expired;
3191
- const hideLightning = !showLightning && !expired && !settled;
3228
+ const hideLightning = !settled && (swapFocused || !showLightning && !expired);
3192
3229
  const showSummaryMeta = checkoutModel.status === "settled" || checkoutModel.status === "expired";
3193
3230
  const fiatCurrency = checkoutModel.fiat_quote?.fiat?.currency;
3194
3231
  const decodeInvoiceHref = createLightningInvoiceDecodeUrl2(checkoutModel.invoice, decodeLinkUrl);
@@ -3397,7 +3434,7 @@ function CheckoutView(props) {
3397
3434
  )
3398
3435
  )
3399
3436
  ),
3400
- paymentWizard && !settled && (!expired || swapFocused) ? React10.createElement(PaymentWizard, {
3437
+ paymentWizard && !settled && (!expired || swapFocused || checkoutModel.checkout?.invoices.some((invoice) => invoice.swap !== void 0)) ? React10.createElement(PaymentWizard, {
3401
3438
  key: "wizard",
3402
3439
  // Only pass invoice when it's a real bolt11 (non-empty, non-deferred).
3403
3440
  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.12",
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.12",
23
+ "@openreceive/core": "0.4.12"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": ">=18.0.0"
@@ -34,9 +34,10 @@ code** (`NWC_URI`).
34
34
  - BTCPay Server: [references/btcpay.md](references/btcpay.md) — a plugin,
35
35
  configured in BTCPay's store UI or Greenfield API; no application code,
36
36
  no npm packages, no gem. The rest of this file is about the library.
37
- 3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
38
- environment before writing code. Never print the value; never invent a
39
- placeholder.
37
+ 3. Follow its **Step 0** first: before writing code or searching the machine,
38
+ ask the user for the receive-only NWC code (then the swap URI), one question
39
+ per message, and store each pasted code in the project's env file yourself.
40
+ Never print the value; never invent a placeholder.
40
41
 
41
42
  Install, per adapter — Express: `npm install @openreceive/express @openreceive/react`;
42
43
  Fastify: `npm install @openreceive/fastify @openreceive/react`; Next.js:
@@ -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.12.
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
@@ -35,10 +35,12 @@ refund path on the same checkout screen.
35
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
- 2. Check whether the plugin is installed (Server Settings → Plugins, or the
39
- store navigation shows an "OpenReceive" entry). If not, install it from the
40
- BTCPay plugin directory (Server Settings → Plugins, search "OpenReceive"),
41
- as the quickstart says; do not invent an installer command.
38
+ 2. Check whether the plugin is installed (the Plugins menu — the plug icon in
39
+ the top-right corner — under Installed Plugins, or the store navigation
40
+ shows an "OpenReceive" entry). If not, install it from the BTCPay plugin
41
+ directory (the same Plugins menu → Plugin Directory, search "openreceive",
42
+ then Install and Restart now), as the quickstart says; do not invent an
43
+ installer command.
42
44
  3. Check whether the store already has an OpenReceive connection:
43
45
  `GET /api/v1/stores/{storeId}/openreceive/settings` returns
44
46
  `lightningNodeIsOpenReceive`. If true, the wallet step is done — go to
@@ -119,6 +121,8 @@ enough; drop the `.md` for the same page a person would read.
119
121
  Questions, or a problem with the plugin itself:
120
122
  https://openreceive.org/contact
121
123
 
124
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
125
+
122
126
  ---
123
127
 
124
128
  ## The quickstart, in full
@@ -131,15 +135,15 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-btcp
131
135
  Requires BTCPay Server ≥ 2.4.4.
132
136
 
133
137
  The OpenReceive plugin makes a receive-only NWC wallet the Lightning node of a
134
- BTCPay store. BTCPay mints every Lightning invoice in that wallet and records
135
- payments through its own settlement machinery. Optionally, payers can pay a
136
- BTCPay invoice with USDT, USDC, ETH or SOL through a Lightning Swap Connect
137
- provider; the swap settles into the same wallet. The store's internal node is
138
+ BTCPay store. BTCPay creates every Lightning invoice in that wallet. It records
139
+ payments the same way it records any other payment. You can also let payers pay
140
+ a BTCPay invoice with USDT, USDC, ETH or SOL through a Lightning Swap Connect
141
+ provider. The swap pays into the same wallet. The store's internal node is
138
142
  never used.
139
143
 
140
144
  This is not the Node or Rails library. There are no hooks, no
141
- `openreceive_payments` table and no OpenReceive HTTP routes: BTCPay's
142
- invoices, checkout, webhooks and Greenfield API are the host.
145
+ `openreceive_payments` table and no OpenReceive HTTP routes. BTCPay's own
146
+ invoices, checkout, webhooks and Greenfield API do that work.
143
147
 
144
148
  ### 1. Prerequisites
145
149
 
@@ -155,39 +159,66 @@ invoices, checkout, webhooks and Greenfield API are the host.
155
159
 
156
160
  ### 2. Install the plugin
157
161
 
158
- In BTCPay, open **Server Settings → Plugins**, search the plugin directory
159
- 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
+ Sign in as a **server administrator**. If someone else hosts your server, ask
163
+ them to install the plugin for you.
164
+
165
+ **1. Open the Plugins menu.** It is the plug icon in the top-right corner.
166
+
167
+ **2. Click Plugin Directory.**
168
+
169
+ **3. Search for `openreceive`** and click the **OpenReceive** result.
170
+
171
+ **4. Click Install in BTCPay Server.** Confirm when prompted, then click
172
+ **Restart now** and wait for BTCPay to come back.
173
+
174
+ At startup, BTCPay creates the plugin's two tables in its own Postgres
175
+ database: `openreceive_invoices` and `openreceive_swaps`, in the schema
176
+ `BTCPayServer.Plugins.OpenReceive`. Nothing else is created.
163
177
 
164
178
  To build the plugin from source instead, follow
165
179
  [the .NET workspace README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/README.md).
166
180
 
167
181
  ### 3. Connect the wallet
168
182
 
169
- Follow the plugin README's illustrated walkthrough:
170
- [OpenReceive for BTCPay Server](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/BTCPayServer.Plugins.OpenReceive/README.md).
171
- It opens the **OpenReceive** page in the store's sidebar, saves the
172
- receive-only NWC code, optionally saves the LSC code to turn swaps on, and
173
- creates a first test invoice. There is nothing else to configure: you never
174
- open BTCPay's Lightning node screen, and the plugin never reads the internal
175
- node.
183
+ 1. Select your store and open **OpenReceive** in its sidebar, under Wallets.
184
+ 2. Paste your receive-only NWC code. To see what the wallet supports first,
185
+ click **Test connection**.
186
+ 3. Click **Save NWC Code**.
187
+ 4. To turn swaps on, paste a Lightning Swap Connect code and click **Save swap
188
+ settings**.
176
189
 
177
- Saving fails closed if the wallet advertises a spend method such as
178
- `pay_invoice`. Mint a receive-only code instead; the override for a wallet
179
- that cannot is a deliberate, logged choice. Swaps raise the store's invoice
180
- expiration to 60 minutes when it is shorter, because a swap needs at least 45
181
- minutes of invoice life.
190
+ The page then shows **Wallet connected**. If you set up a provider, it also
191
+ shows **Swaps on**. There is nothing else to configure. You never open BTCPay's
192
+ Lightning node screen, and the plugin never reads the internal node.
193
+
194
+ Screenshots for each of those steps, and for creating a first test invoice,
195
+ are in the plugin's
196
+ [README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/BTCPayServer.Plugins.OpenReceive/README.md).
197
+
198
+ The plugin refuses to save a code whose wallet advertises a spend method such
199
+ as `pay_invoice`. Create a receive-only code instead. If your wallet cannot
200
+ make one, there is an override, but using it is a deliberate choice and the
201
+ plugin logs it.
202
+
203
+ Turning swaps on raises the store's invoice expiration to 60 minutes if it is
204
+ shorter. A swap needs the invoice to stay open for at least 45 minutes.
182
205
 
183
206
  ### 4. Check it
184
207
 
185
- **Run a health check** on the OpenReceive page runs every probe in place:
186
- the connection, the wallet preflight, payment notifications, the last wallet
187
- scan, the swap provider and its assets, the invoice expiration, and swaps
188
- that need a human. Each failing probe carries a fix link.
208
+ Click **Run a health check** on the OpenReceive page. It runs every check
209
+ right there:
210
+
211
+ - the connection
212
+ - the wallet preflight
213
+ - payment notifications
214
+ - the last wallet scan
215
+ - the swap provider and its assets
216
+ - the invoice expiration
217
+ - swaps that need a human
218
+
219
+ Each failing check comes with a link to the fix.
189
220
 
190
- Every setting, Greenfield route, swap state, log event and probe is in the
191
- [BTCPay plugin reference](https://openreceive.org/guides/btcpay-reference.md), including what is
192
- unsupported by design: every send-side feature, top-up invoices, and a bare
193
- `nostr+walletconnect://` string in BTCPay's Lightning node screen.
221
+ The [BTCPay plugin reference](https://openreceive.org/guides/btcpay-reference.md) lists every setting,
222
+ Greenfield route, swap state, log event and check. It also lists what the
223
+ plugin does not support by design: every send-side feature, top-up invoices,
224
+ and a bare `nostr+walletconnect://` string in BTCPay's Lightning node screen.