@openreceive/react 0.3.0 → 0.3.1

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.d.ts CHANGED
@@ -244,6 +244,14 @@ interface PaymentWizardProps {
244
244
  * back to posting the refund itself and carrying the staging locally.
245
245
  */
246
246
  readonly swapRefund?: SwapRefundStaging;
247
+ /**
248
+ * Whether this checkout has a URL the payer can come BACK to — `<Checkout
249
+ * syncUrl>`, or a host route of its own that carries the order. It decides
250
+ * which of the two refund-return warnings the deposit panel shows; see
251
+ * `SwapDisplayModel.refundReturnLabel`. Defaults to false, so a payer is
252
+ * never told to bookmark a page that will not bring them back.
253
+ */
254
+ readonly resumable?: boolean;
247
255
  readonly onError?: (error: unknown) => void;
248
256
  /**
249
257
  * Called when the payer enters or leaves the focused swap flow (a pay-in coin is
@@ -322,6 +330,8 @@ declare function renderSwapDepositPanel(options: {
322
330
  readonly onError?: (error: unknown) => void;
323
331
  readonly onRefund: (attemptId: string, refundAddress: string, confirm: boolean) => Promise<void>;
324
332
  readonly onBackToLightning: () => void;
333
+ /** Does this checkout have a URL to return to? See `refundReturnLabel`. */
334
+ readonly resumable?: boolean;
325
335
  }): React.ReactElement | null;
326
336
 
327
337
  declare function useTheme(options?: UseThemeOptions): UseThemeResult;
package/dist/index.js CHANGED
@@ -30,16 +30,23 @@ function useCheckoutSession(options) {
30
30
  reference: () => optionsRef.current.reference(),
31
31
  requestCheckout: (reference) => optionsRef.current.requestCheckout?.(reference),
32
32
  onSnapshot: (snapshot) => optionsRef.current.onSnapshot?.(snapshot),
33
- swapPrefix: () => optionsRef.current.swapPrefix?.(),
34
- fetch: () => optionsRef.current.fetch?.(),
35
- swapSelection: {
36
- started: () => optionsRef.current.swapSelection?.started(),
37
- setStarted: (invoice) => optionsRef.current.swapSelection?.setStarted(invoice),
38
- dismissedInvoiceId: () => optionsRef.current.swapSelection?.dismissedInvoiceId() ?? null,
39
- setDismissedInvoiceId: (invoiceId) => optionsRef.current.swapSelection?.setDismissedInvoiceId(invoiceId),
40
- setSelectedAsset: (payInAsset) => optionsRef.current.swapSelection?.setSelectedAsset(payInAsset)
33
+ // The inner `swap` is always present because the session is built once,
34
+ // on the first render, and the host may only gain swap options later.
35
+ // Every accessor reads through `optionsRef`, so a host with no `swap`
36
+ // makes `prefix()` answer undefined and the session reports the missing
37
+ // wiring instead of silently doing nothing.
38
+ swap: {
39
+ selection: {
40
+ started: () => optionsRef.current.swap?.selection.started(),
41
+ setStarted: (invoice) => optionsRef.current.swap?.selection.setStarted(invoice),
42
+ dismissedInvoiceId: () => optionsRef.current.swap?.selection.dismissedInvoiceId() ?? null,
43
+ setDismissedInvoiceId: (invoiceId) => optionsRef.current.swap?.selection.setDismissedInvoiceId(invoiceId),
44
+ setSelectedAsset: (payInAsset) => optionsRef.current.swap?.selection.setSelectedAsset(payInAsset)
45
+ },
46
+ prefix: () => optionsRef.current.swap?.prefix(),
47
+ fetch: () => optionsRef.current.swap?.fetch(),
48
+ onStarted: (invoice) => optionsRef.current.swap?.onStarted?.(invoice)
41
49
  },
42
- onSwapStarted: (invoice) => optionsRef.current.onSwapStarted?.(invoice),
43
50
  get logger() {
44
51
  return optionsRef.current.logger;
45
52
  },
@@ -56,7 +63,7 @@ import {
56
63
  OPENRECEIVE_CHECKOUT_DATA_ATTRIBUTES,
57
64
  copyInvoice as copyInvoiceHelper,
58
65
  createCheckoutStatusModel,
59
- createQrSvg,
66
+ createQrSvgController,
60
67
  formatAmountCaption,
61
68
  checkoutLabels,
62
69
  openWallet as openWalletHelper,
@@ -185,14 +192,15 @@ function QRCode(props) {
185
192
  const onErrorRef = React3.useRef(onError);
186
193
  onErrorRef.current = onError;
187
194
  React3.useEffect(() => {
188
- let cancelled = false;
189
- createQrSvg(invoice, { encoder, width }).then((nextSvg) => {
190
- if (!cancelled) setSvg(nextSvg);
191
- }).catch((error) => {
192
- if (!cancelled) onErrorRef.current?.(error);
195
+ const controller = createQrSvgController({
196
+ onValue: setSvg,
197
+ onError: (error) => onErrorRef.current?.(error),
198
+ ...encoder === void 0 ? {} : { encoder },
199
+ width
193
200
  });
201
+ controller.show(invoice);
194
202
  return () => {
195
- cancelled = true;
203
+ controller.stop();
196
204
  };
197
205
  }, [invoice, encoder, width]);
198
206
  return React3.createElement("div", {
@@ -1194,7 +1202,7 @@ import {
1194
1202
  createDetailExternalLink,
1195
1203
  createSwapDisplayModel,
1196
1204
  createTransactionDetails,
1197
- createQrPayloadSvg,
1205
+ createQrSvgController as createQrSvgController2,
1198
1206
  getSwapRefundFormError,
1199
1207
  checkoutLabels as checkoutLabels4,
1200
1208
  createSwapUnavailableModel,
@@ -1301,10 +1309,10 @@ function renderSwapUnavailable(quote, checkout) {
1301
1309
  );
1302
1310
  }
1303
1311
  function renderSwapDepositPanel(options) {
1304
- const display = createSwapDisplayModel(
1305
- options.invoice,
1306
- options.now === void 0 ? {} : { now: options.now }
1307
- );
1312
+ const display = createSwapDisplayModel(options.invoice, {
1313
+ ...options.now === void 0 ? {} : { now: options.now },
1314
+ ...options.resumable === void 0 ? {} : { resumable: options.resumable }
1315
+ });
1308
1316
  if (display === void 0) return null;
1309
1317
  const backButton = React8.createElement(
1310
1318
  "button",
@@ -1454,7 +1462,7 @@ function renderSwapDepositPanel(options) {
1454
1462
  onError: options.onError
1455
1463
  }),
1456
1464
  renderSwapSupportDetails(display, options),
1457
- renderSwapRefundReturnWarning()
1465
+ renderSwapRefundReturnWarning(display)
1458
1466
  );
1459
1467
  }
1460
1468
  if (display.state === "refund_pending" || display.state === "refunded") {
@@ -1482,7 +1490,7 @@ function renderSwapDepositPanel(options) {
1482
1490
  })
1483
1491
  ),
1484
1492
  renderSwapSupportDetails(display, options),
1485
- renderSwapRefundReturnWarning()
1493
+ renderSwapRefundReturnWarning(display)
1486
1494
  );
1487
1495
  }
1488
1496
  if (display.state === "deposit") {
@@ -1742,13 +1750,13 @@ function renderSwapSupportDetails(display, options) {
1742
1750
  )
1743
1751
  );
1744
1752
  }
1745
- function renderSwapRefundReturnWarning() {
1753
+ function renderSwapRefundReturnWarning(display) {
1746
1754
  return React8.createElement(
1747
1755
  "p",
1748
1756
  {
1749
1757
  className: orClasses5.swapWarning
1750
1758
  },
1751
- checkoutLabels4.refundReturnWarning
1759
+ display.refundReturnLabel
1752
1760
  );
1753
1761
  }
1754
1762
  var REFUND_ADDRESS_DRAFT_LIMIT = 8;
@@ -1845,17 +1853,19 @@ function SwapPayloadQRCode(props) {
1845
1853
  const [svg, setSvg] = React8.useState("");
1846
1854
  const onErrorRef = React8.useRef(props.onError);
1847
1855
  onErrorRef.current = props.onError;
1856
+ const { payload, encoder } = props;
1848
1857
  React8.useEffect(() => {
1849
- let cancelled = false;
1850
- createQrPayloadSvg(props.payload, { encoder: props.encoder, width: 220 }).then((nextSvg) => {
1851
- if (!cancelled) setSvg(nextSvg);
1852
- }).catch((error) => {
1853
- if (!cancelled) onErrorRef.current?.(error);
1858
+ const controller = createQrSvgController2({
1859
+ onValue: setSvg,
1860
+ onError: (error) => onErrorRef.current?.(error),
1861
+ ...encoder === void 0 ? {} : { encoder },
1862
+ width: 220
1854
1863
  });
1864
+ controller.showPayload(payload);
1855
1865
  return () => {
1856
- cancelled = true;
1866
+ controller.stop();
1857
1867
  };
1858
- }, [props.payload, props.encoder]);
1868
+ }, [payload, encoder]);
1859
1869
  const imageSource = svg.length === 0 ? void 0 : `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
1860
1870
  return React8.createElement("img", {
1861
1871
  alt: "",
@@ -1873,9 +1883,7 @@ function PaymentWizard(props) {
1873
1883
  const [startedSwapInvoice, setStartedSwapInvoice] = React9.useState(null);
1874
1884
  const [dismissedSwapInvoiceId, setDismissedSwapInvoiceId] = React9.useState(null);
1875
1885
  const [selectedSwapAsset, setSelectedSwapAsset] = React9.useState(null);
1876
- const [selectedSwapNetworks, setSelectedSwapNetworks] = React9.useState(
1877
- {}
1878
- );
1886
+ const [selectedSwapAssetByGroup, setSelectedSwapAssetByGroup] = React9.useState({});
1879
1887
  const [selectedPickerKey, setSelectedPickerKey] = React9.useState(null);
1880
1888
  const autoSwapAttemptedRef = React9.useRef(/* @__PURE__ */ new Set());
1881
1889
  const fetcher = props.fetch ?? globalThis.fetch;
@@ -1884,16 +1892,18 @@ function PaymentWizard(props) {
1884
1892
  const session = useCheckoutSession({
1885
1893
  snapshot: () => checkout,
1886
1894
  reference: () => reference,
1887
- swapPrefix: () => props.prefix,
1888
- fetch: () => fetcher,
1889
- swapSelection: {
1890
- started: () => startedSwapInvoice ?? void 0,
1891
- setStarted: setStartedSwapInvoice,
1892
- dismissedInvoiceId: () => dismissedSwapInvoiceId,
1893
- setDismissedInvoiceId: setDismissedSwapInvoiceId,
1894
- setSelectedAsset: setSelectedSwapAsset
1895
+ swap: {
1896
+ selection: {
1897
+ started: () => startedSwapInvoice ?? void 0,
1898
+ setStarted: setStartedSwapInvoice,
1899
+ dismissedInvoiceId: () => dismissedSwapInvoiceId,
1900
+ setDismissedInvoiceId: setDismissedSwapInvoiceId,
1901
+ setSelectedAsset: setSelectedSwapAsset
1902
+ },
1903
+ prefix: () => props.prefix,
1904
+ fetch: () => fetcher,
1905
+ onStarted: (invoice) => props.onSwapStarted?.(invoice)
1895
1906
  },
1896
- onSwapStarted: (invoice) => props.onSwapStarted?.(invoice),
1897
1907
  ...props.logger === void 0 ? {} : { logger: props.logger },
1898
1908
  onError: (error) => props.onError?.(error)
1899
1909
  });
@@ -1901,7 +1911,7 @@ function PaymentWizard(props) {
1901
1911
  const clearSwapFocus = React9.useCallback(() => {
1902
1912
  setSelectedSwapAsset(null);
1903
1913
  setSelectedPickerKey(null);
1904
- setSelectedSwapNetworks({});
1914
+ setSelectedSwapAssetByGroup({});
1905
1915
  session.clearSwapStartError();
1906
1916
  props.swapRefund?.clearSwapRefundStaging();
1907
1917
  }, [session, props.swapRefund]);
@@ -2038,6 +2048,7 @@ function PaymentWizard(props) {
2038
2048
  logger: props.logger,
2039
2049
  onError: props.onError,
2040
2050
  onRefund: refundSwap,
2051
+ ...props.resumable === void 0 ? {} : { resumable: props.resumable },
2041
2052
  onBackToLightning: () => {
2042
2053
  setDismissedSwapInvoiceId(activeSwapForAsset.invoice_id);
2043
2054
  clearSwapFocus();
@@ -2061,7 +2072,7 @@ function PaymentWizard(props) {
2061
2072
  checkout: checkout ?? void 0,
2062
2073
  selectedPickerKey,
2063
2074
  startingAsset: swapStartingAsset,
2064
- selectedSwapNetworks,
2075
+ selectedSwapAssetByGroup,
2065
2076
  // One resolver decides what a tile click MEANS, so the "ask the
2066
2077
  // network question only when it is a real question" rule is not
2067
2078
  // re-derived here: a single-network group comes back as start_swap
@@ -2071,7 +2082,7 @@ function PaymentWizard(props) {
2071
2082
  pickerKey: key,
2072
2083
  previousKey,
2073
2084
  entries: buildMethodGridEntries(paymentMethods, swapAssetOptions),
2074
- selectedNetworks: selectedSwapNetworks
2085
+ selectedAssetByGroup: selectedSwapAssetByGroup
2075
2086
  });
2076
2087
  if (resolved.kind === "none") return;
2077
2088
  if (resolved.kind === "select_method") {
@@ -2088,10 +2099,10 @@ function PaymentWizard(props) {
2088
2099
  return;
2089
2100
  }
2090
2101
  setSelectedPickerKey(key);
2091
- setSelectedSwapNetworks(resolved.selectedNetworks);
2102
+ setSelectedSwapAssetByGroup(resolved.selectedAssetByGroup);
2092
2103
  },
2093
2104
  onSelectNetwork: (groupKey, payInAsset) => {
2094
- setSelectedSwapNetworks((current) => ({
2105
+ setSelectedSwapAssetByGroup((current) => ({
2095
2106
  ...current,
2096
2107
  [groupKey]: payInAsset
2097
2108
  }));
@@ -2174,6 +2185,7 @@ function PaymentWizard(props) {
2174
2185
  logger: props.logger,
2175
2186
  onError: props.onError,
2176
2187
  onRefund: refundSwap,
2188
+ ...props.resumable === void 0 ? {} : { resumable: props.resumable },
2177
2189
  onBackToLightning: () => {
2178
2190
  setDismissedSwapInvoiceId(activeSwapForRoute.invoice_id);
2179
2191
  clearSwapFocus();
@@ -2275,7 +2287,7 @@ function renderCompactPaymentMethodSelector(options) {
2275
2287
  const display = createMethodGridDisplay({
2276
2288
  entries: buildMethodGridEntries(paymentMethods, options.swapAssetOptions),
2277
2289
  selectedPickerKey: options.selectedPickerKey,
2278
- selectedNetworks: options.selectedSwapNetworks,
2290
+ selectedAssetByGroup: options.selectedSwapAssetByGroup,
2279
2291
  startingAsset: options.startingAsset,
2280
2292
  ...options.checkout === void 0 ? {} : { checkout: options.checkout }
2281
2293
  });
@@ -2890,9 +2902,13 @@ function CheckoutView(props) {
2890
2902
  prefix = OPENRECEIVE_DEFAULT_PREFIX,
2891
2903
  metadata: _metadata,
2892
2904
  createFetch: _createFetch,
2893
- syncUrl: _syncUrl,
2905
+ // Not `_`-prefixed like its neighbours: the view reads it, to infer
2906
+ // `resumable` for the refund screen. Still destructured out so it never
2907
+ // reaches the rendered <section>.
2908
+ syncUrl,
2894
2909
  resumePathPrefix: _resumePathPrefix,
2895
- routeReference: _routeReference,
2910
+ routeReference,
2911
+ resumable,
2896
2912
  onRequestLightning,
2897
2913
  onSwapStarted,
2898
2914
  mintingLightning = false,
@@ -3175,6 +3191,11 @@ function CheckoutView(props) {
3175
3191
  onSwapStarted,
3176
3192
  resolveAssetUrl,
3177
3193
  assetBaseUrl,
3194
+ // Whether a payer who closes the tab has a URL to come back
3195
+ // to. Explicit wins — only the host knows about a per-order
3196
+ // route of its own; otherwise infer it from the two props that
3197
+ // put the reference in the URL.
3198
+ resumable: resumable ?? (syncUrl === true || routeReference !== void 0),
3178
3199
  // The engine's staged refund address rides the controller this
3179
3200
  // model owns, so a poll cannot wipe a review in progress.
3180
3201
  swapRefund: checkoutModel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openreceive/react",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "React checkout components and hooks for OpenReceive (<Checkout>, useCheckout, PaymentWizard).",
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.3.0",
23
- "@openreceive/core": "0.3.0"
22
+ "@openreceive/browser": "0.3.1",
23
+ "@openreceive/core": "0.3.1"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": ">=18.0.0"