@openreceive/browser 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.
@@ -950,8 +950,8 @@ function createSwapDisplayModel(invoice, options = {}) {
950
950
  const depositAmount = formatDepositAmount(swap.deposit_amount);
951
951
  const networkWarningEmphasis = `${depositAmount} ${asset.assetLabel} on the ${asset.networkLabel} network`;
952
952
  const depositRisk = swapDepositRisk(swap.pay_in_asset);
953
- const doubleSpendWarning = `Pay with one method only \u2014 if you already sent ${asset.assetLabel}, do not also pay the Lightning invoice.`;
954
953
  const settled = invoice.transaction_state === "settled";
954
+ const instructionsExpired = expiresInSeconds === 0 && swap.provider_state === "awaiting_deposit";
955
955
  const feeBreakdown = createSwapFeeBreakdown(swap.fee, swap);
956
956
  return {
957
957
  provider: swap.provider,
@@ -962,7 +962,8 @@ function createSwapDisplayModel(invoice, options = {}) {
962
962
  depositRisk,
963
963
  networkWarningTitle: depositRisk === "pinned" ? checkoutLabels.sendExactAmountTitle : checkoutLabels.wrongCurrencyOrNetworkTitle,
964
964
  networkWarningEmphasis,
965
- networkWarning: depositRisk === "pinned" ? `Send exactly ${networkWarningEmphasis}. ${doubleSpendWarning}` : `Be sure you are sending exactly ${networkWarningEmphasis}. If you send the wrong currency or send on the wrong network, your funds will be lost! ${doubleSpendWarning}`,
965
+ // The lost-funds sentence is claimed only where it is reachable.
966
+ networkWarning: depositRisk === "pinned" ? `Send exactly ${networkWarningEmphasis}.` : `Be sure you are sending exactly ${networkWarningEmphasis}. If you send the wrong currency or send on the wrong network, your funds will be lost!`,
966
967
  depositAddress: swap.deposit_address,
967
968
  ...swap.deposit_memo === void 0 ? {} : { depositMemo: swap.deposit_memo },
968
969
  depositAmount,
@@ -987,14 +988,14 @@ function createSwapDisplayModel(invoice, options = {}) {
987
988
  ],
988
989
  { label: checkoutLabels.swapCopyAmount, value: depositAmount, selectable: true }
989
990
  ],
990
- providerStateLabel: settled ? "Payment complete" : getSwapProviderStateLabel(swap.provider_state),
991
- providerStateDetail: settled ? "Your payment is confirmed and your order is complete." : getSwapProviderStateDetail(swap.provider_state, swap.pay_in_asset, {
991
+ providerStateLabel: instructionsExpired ? "Payment window closed" : settled ? "Payment complete" : getSwapProviderStateLabel(swap.provider_state),
992
+ providerStateDetail: instructionsExpired ? "Do not send another payment. We are checking for an existing deposit and any recovery options." : settled ? "Your payment is confirmed and your order is complete." : getSwapProviderStateDetail(swap.provider_state, swap.pay_in_asset, {
992
993
  refundReason: swap.refund_reason,
993
994
  depositAmount: swap.deposit_amount,
994
995
  depositReceivedAmount: swap.deposit_received_amount,
995
996
  refundAmount: swap.refund_amount
996
997
  }),
997
- state: settled ? "settled" : getSwapPanelState(swap.provider_state),
998
+ state: settled ? "settled" : instructionsExpired ? "progress" : getSwapPanelState(swap.provider_state),
998
999
  expiresInSeconds,
999
1000
  countdownLabel: formatCountdown(expiresInSeconds),
1000
1001
  qrPayload: createSwapQrPayload(swap),
@@ -1046,10 +1047,13 @@ function overlaySwapRefundStagingIntoSnapshot(snapshot, staged) {
1046
1047
  function selectCurrentSwapInvoice(snapshot, options = {}) {
1047
1048
  const dismissedInvoiceId = options.dismissedInvoiceId ?? null;
1048
1049
  const started = options.started ?? null;
1049
- const fromSnapshot = snapshot?.invoices.find(
1050
- (invoice) => invoice.rail === "swap" && invoice.swap !== void 0 && invoice.invoice_id !== dismissedInvoiceId
1051
- );
1052
- if (started === null || started.invoice_id === dismissedInvoiceId) return fromSnapshot;
1050
+ if (started === null) {
1051
+ if (dismissedInvoiceId !== null) return void 0;
1052
+ return snapshot?.invoices.find(
1053
+ (invoice) => invoice.rail === "swap" && invoice.swap !== void 0
1054
+ );
1055
+ }
1056
+ if (started.invoice_id === dismissedInvoiceId) return void 0;
1053
1057
  const matched = snapshot?.invoices.find((invoice) => invoice.invoice_id === started.invoice_id) ?? started;
1054
1058
  return overlaySwapRefundStaging(matched, started);
1055
1059
  }
@@ -1087,7 +1091,7 @@ function getSwapProviderStateDetail(state, payInAsset, refundContext = {}) {
1087
1091
  return "Your payment is confirmed and being converted. This usually finishes within a minute.";
1088
1092
  }
1089
1093
  if (state === "paying_invoice" || state === "completed") {
1090
- return "The provider is sending the Lightning payment. This usually takes a few seconds.";
1094
+ return "We are checking whether the wallet received the payment. Provider completion alone does not confirm payment.";
1091
1095
  }
1092
1096
  if (state === "expired") return "No payment was received before the payment window closed.";
1093
1097
  if (state === "refund_required" || state === "refund_pending" || state === "refunded") {
@@ -1361,6 +1365,127 @@ function isCheckoutStateSource(value) {
1361
1365
  return "checkout_id" in value && "reference" in value && "invoice_id" in value && "invoice" in value && "transaction_state" in value && "phase" in value;
1362
1366
  }
1363
1367
 
1368
+ // src/internal/console-logger.ts
1369
+ import { isSensitiveLogKey, sanitizeLogValue } from "@openreceive/core";
1370
+
1371
+ // src/internal/log-level.ts
1372
+ var BROWSER_LOG_LEVEL_ORDER = {
1373
+ debug: 10,
1374
+ info: 20,
1375
+ warn: 30,
1376
+ error: 40
1377
+ };
1378
+ var BROWSER_LOG_LEVEL_ALIASES = {
1379
+ debug: "debug",
1380
+ info: "info",
1381
+ warn: "warn",
1382
+ warning: "warn",
1383
+ error: "error"
1384
+ };
1385
+ function browserLogLevelOrder(level) {
1386
+ return BROWSER_LOG_LEVEL_ORDER[level];
1387
+ }
1388
+ function parseBrowserLogLevel(value) {
1389
+ if (value === void 0 || value === null) return void 0;
1390
+ const normalized = value.trim().toLowerCase();
1391
+ if (normalized.length === 0) return void 0;
1392
+ return BROWSER_LOG_LEVEL_ALIASES[normalized];
1393
+ }
1394
+ function resolveBrowserLogLevel(value, fallback = "info") {
1395
+ return parseBrowserLogLevel(value) ?? fallback;
1396
+ }
1397
+ function readBrowserLogLevelFromEnvironment(source) {
1398
+ if (source !== void 0) {
1399
+ return resolveBrowserLogLevel(
1400
+ source.LOG_LEVEL ?? source.VITE_LOG_LEVEL ?? source.NEXT_PUBLIC_LOG_LEVEL
1401
+ );
1402
+ }
1403
+ const fromGlobal = parseBrowserLogLevel(readGlobalLogLevel());
1404
+ if (fromGlobal !== void 0) return fromGlobal;
1405
+ const metaEnv = readImportMetaEnv();
1406
+ const parsedMeta = parseBrowserLogLevel(
1407
+ metaEnv?.LOG_LEVEL ?? metaEnv?.VITE_LOG_LEVEL ?? metaEnv?.NEXT_PUBLIC_LOG_LEVEL
1408
+ );
1409
+ if (parsedMeta !== void 0) return parsedMeta;
1410
+ const processEnv = typeof process !== "undefined" ? process.env : void 0;
1411
+ return resolveBrowserLogLevel(processEnv?.LOG_LEVEL);
1412
+ }
1413
+ function readGlobalLogLevel() {
1414
+ try {
1415
+ const value = globalThis.__OPENRECEIVE_LOG_LEVEL__;
1416
+ return typeof value === "string" ? value : void 0;
1417
+ } catch {
1418
+ return void 0;
1419
+ }
1420
+ }
1421
+ function readImportMetaEnv() {
1422
+ try {
1423
+ return import.meta.env;
1424
+ } catch {
1425
+ return void 0;
1426
+ }
1427
+ }
1428
+
1429
+ // src/internal/console-logger.ts
1430
+ function createConsoleWriter(options) {
1431
+ const target = options.console ?? console;
1432
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1433
+ return (level, prefix, event, message, fields) => {
1434
+ const minLevel = options.minLevel === void 0 ? readBrowserLogLevelFromEnvironment() : resolveBrowserLogLevel(options.minLevel);
1435
+ if (browserLogLevelOrder(level) < browserLogLevelOrder(minLevel)) {
1436
+ return;
1437
+ }
1438
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
1439
+ const sink = target[method] ?? target.log;
1440
+ const {
1441
+ level: _level,
1442
+ event: _event,
1443
+ message: cleanMessage,
1444
+ ...cleanFields
1445
+ } = sanitizeBrowserLogEntry({ level, event, message, ...fields });
1446
+ sink.call(
1447
+ target,
1448
+ `[${now().toISOString()}] ${level.toUpperCase()} [${prefix}] ${event}: ${cleanMessage}`,
1449
+ cleanFields
1450
+ );
1451
+ };
1452
+ }
1453
+ function sanitizeBrowserLogEntry(entry) {
1454
+ const clean = {};
1455
+ for (const [key, value] of Object.entries(entry)) {
1456
+ if (isSensitiveLogKey(key)) {
1457
+ clean[key] = "[REDACTED]";
1458
+ } else {
1459
+ clean[key] = sanitizeLogValue(value);
1460
+ }
1461
+ }
1462
+ return clean;
1463
+ }
1464
+ function createBrowserConsoleLogger(options = {}) {
1465
+ const prefix = options.prefix ?? "openreceive";
1466
+ const write = createConsoleWriter(options);
1467
+ return (entry) => {
1468
+ const { level, event, message, ...fields } = entry;
1469
+ write(level, prefix, event, message, fields);
1470
+ };
1471
+ }
1472
+ function createAppBrowserConsoleLogger(options) {
1473
+ const write = createConsoleWriter(options);
1474
+ return (event, message, fields = {}, level = "info") => {
1475
+ write(level, options.prefix, event, message, fields);
1476
+ };
1477
+ }
1478
+ var defaultBrowserConsoleLogger;
1479
+ function getDefaultBrowserConsoleLogger() {
1480
+ defaultBrowserConsoleLogger ??= createBrowserConsoleLogger();
1481
+ return defaultBrowserConsoleLogger;
1482
+ }
1483
+ function resolveBrowserLogger(logger) {
1484
+ if (logger === false) return void 0;
1485
+ if (logger !== void 0) return logger;
1486
+ return getDefaultBrowserConsoleLogger();
1487
+ }
1488
+
1364
1489
  // src/internal/wizard.ts
1365
1490
  import { getSwapRefundAddressError } from "@openreceive/core/swap-address";
1366
1491
  import {
@@ -1956,176 +2081,6 @@ function createPaymentWizardController(options = {}) {
1956
2081
  return new BrowserPaymentWizardController(options);
1957
2082
  }
1958
2083
 
1959
- // src/internal/log-level.ts
1960
- var BROWSER_LOG_LEVEL_ORDER = {
1961
- debug: 10,
1962
- info: 20,
1963
- warn: 30,
1964
- error: 40
1965
- };
1966
- var BROWSER_LOG_LEVEL_ALIASES = {
1967
- debug: "debug",
1968
- info: "info",
1969
- warn: "warn",
1970
- warning: "warn",
1971
- error: "error"
1972
- };
1973
- function browserLogLevelOrder(level) {
1974
- return BROWSER_LOG_LEVEL_ORDER[level];
1975
- }
1976
- function parseBrowserLogLevel(value) {
1977
- if (value === void 0 || value === null) return void 0;
1978
- const normalized = value.trim().toLowerCase();
1979
- if (normalized.length === 0) return void 0;
1980
- return BROWSER_LOG_LEVEL_ALIASES[normalized];
1981
- }
1982
- function resolveBrowserLogLevel(value, fallback = "info") {
1983
- return parseBrowserLogLevel(value) ?? fallback;
1984
- }
1985
- function readBrowserLogLevelFromEnvironment(source) {
1986
- if (source !== void 0) {
1987
- return resolveBrowserLogLevel(
1988
- source.LOG_LEVEL ?? source.VITE_LOG_LEVEL ?? source.NEXT_PUBLIC_LOG_LEVEL
1989
- );
1990
- }
1991
- const fromGlobal = parseBrowserLogLevel(readGlobalLogLevel());
1992
- if (fromGlobal !== void 0) return fromGlobal;
1993
- const metaEnv = readImportMetaEnv();
1994
- const parsedMeta = parseBrowserLogLevel(
1995
- metaEnv?.LOG_LEVEL ?? metaEnv?.VITE_LOG_LEVEL ?? metaEnv?.NEXT_PUBLIC_LOG_LEVEL
1996
- );
1997
- if (parsedMeta !== void 0) return parsedMeta;
1998
- const processEnv = typeof process !== "undefined" ? process.env : void 0;
1999
- return resolveBrowserLogLevel(processEnv?.LOG_LEVEL);
2000
- }
2001
- function readGlobalLogLevel() {
2002
- try {
2003
- const value = globalThis.__OPENRECEIVE_LOG_LEVEL__;
2004
- return typeof value === "string" ? value : void 0;
2005
- } catch {
2006
- return void 0;
2007
- }
2008
- }
2009
- function readImportMetaEnv() {
2010
- try {
2011
- return import.meta.env;
2012
- } catch {
2013
- return void 0;
2014
- }
2015
- }
2016
-
2017
- // src/internal/console-logger.ts
2018
- function createConsoleWriter(options) {
2019
- const target = options.console ?? console;
2020
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
2021
- return (level, prefix, event, message, fields) => {
2022
- const minLevel = options.minLevel === void 0 ? readBrowserLogLevelFromEnvironment() : resolveBrowserLogLevel(options.minLevel);
2023
- if (browserLogLevelOrder(level) < browserLogLevelOrder(minLevel)) {
2024
- return;
2025
- }
2026
- const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
2027
- const sink = target[method] ?? target.log;
2028
- const {
2029
- level: _level,
2030
- event: _event,
2031
- message: cleanMessage,
2032
- ...cleanFields
2033
- } = sanitizeBrowserLogEntry({ level, event, message, ...fields });
2034
- sink.call(
2035
- target,
2036
- `[${now().toISOString()}] ${level.toUpperCase()} [${prefix}] ${event}: ${cleanMessage}`,
2037
- cleanFields
2038
- );
2039
- };
2040
- }
2041
- function sanitizeBrowserLogEntry(entry) {
2042
- const clean = {};
2043
- for (const [key, value] of Object.entries(entry)) {
2044
- if (/secret|token|authorization|cookie|nwc/i.test(key)) {
2045
- clean[key] = "[REDACTED]";
2046
- } else {
2047
- clean[key] = sanitizeBrowserLogValue(value);
2048
- }
2049
- }
2050
- return clean;
2051
- }
2052
- function sanitizeBrowserLogValue(value) {
2053
- if (typeof value === "string") return redactBrowserSecrets(value);
2054
- if (Array.isArray(value)) return value.map(sanitizeBrowserLogValue);
2055
- if (typeof value !== "object" || value === null) return value;
2056
- const clean = {};
2057
- for (const [key, nested] of Object.entries(value)) {
2058
- if (/secret|token|authorization|cookie|nwc/i.test(key)) {
2059
- clean[key] = "[REDACTED]";
2060
- } else {
2061
- clean[key] = sanitizeBrowserLogValue(nested);
2062
- }
2063
- }
2064
- return clean;
2065
- }
2066
- function redactBrowserSecrets(value) {
2067
- return value.replace(/nostr\+walletconnect:\/\/[^\s"'`<>]+/g, "[REDACTED_NWC]").replace(/([?&](?:_or_evt|token|secret)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
2068
- }
2069
- function createBrowserConsoleLogger(options = {}) {
2070
- const prefix = options.prefix ?? "openreceive";
2071
- const write = createConsoleWriter(options);
2072
- return (entry) => {
2073
- const { level, event, message, ...fields } = entry;
2074
- write(level, prefix, event, message, fields);
2075
- };
2076
- }
2077
- function createAppBrowserConsoleLogger(options) {
2078
- const write = createConsoleWriter(options);
2079
- return (event, message, fields = {}, level = "info") => {
2080
- write(level, options.prefix, event, message, fields);
2081
- };
2082
- }
2083
- var defaultBrowserConsoleLogger;
2084
- function getDefaultBrowserConsoleLogger() {
2085
- defaultBrowserConsoleLogger ??= createBrowserConsoleLogger();
2086
- return defaultBrowserConsoleLogger;
2087
- }
2088
- function resolveBrowserLogger(logger) {
2089
- if (logger === false) return void 0;
2090
- if (logger !== void 0) return logger;
2091
- return getDefaultBrowserConsoleLogger();
2092
- }
2093
-
2094
- // src/internal/checkout-read.ts
2095
- import { nonEmptyString } from "@openreceive/core";
2096
- function optionalRecord(value) {
2097
- if (value === void 0) return void 0;
2098
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
2099
- throw new Error("OpenReceive metadata must be an object.");
2100
- }
2101
- return value;
2102
- }
2103
- function requiredInvoiceRail(value) {
2104
- if (value === "lightning" || value === "swap" || value === "checkout_lock") return value;
2105
- throw new TypeError("OpenReceive invoice rail must be lightning, swap, or checkout_lock.");
2106
- }
2107
- function requiredString(value, fieldName) {
2108
- const text = nonEmptyString(value);
2109
- if (text === void 0) {
2110
- throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2111
- }
2112
- return text;
2113
- }
2114
- function optionalSafeInteger(value, fieldName = "integer") {
2115
- if (value === void 0 || value === null) return void 0;
2116
- if (typeof value !== "number" || !Number.isSafeInteger(value)) {
2117
- throw new TypeError(`OpenReceive checkout response ${fieldName} must be a safe integer.`);
2118
- }
2119
- return value;
2120
- }
2121
- function requiredSafeInteger(value, fieldName) {
2122
- const integer = optionalSafeInteger(value, fieldName);
2123
- if (integer === void 0) {
2124
- throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2125
- }
2126
- return integer;
2127
- }
2128
-
2129
2084
  // src/internal/checkout-log.ts
2130
2085
  import { compact } from "@openreceive/core";
2131
2086
  function checkoutLogFields(state) {
@@ -2206,6 +2161,41 @@ function emitBrowserLog(logger, level, event, message, fields = {}) {
2206
2161
  }
2207
2162
  }
2208
2163
 
2164
+ // src/internal/checkout-read.ts
2165
+ import { nonEmptyString } from "@openreceive/core";
2166
+ function optionalRecord(value) {
2167
+ if (value === void 0) return void 0;
2168
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2169
+ throw new Error("OpenReceive metadata must be an object.");
2170
+ }
2171
+ return value;
2172
+ }
2173
+ function requiredInvoiceRail(value) {
2174
+ if (value === "lightning" || value === "swap" || value === "checkout_lock") return value;
2175
+ throw new TypeError("OpenReceive invoice rail must be lightning, swap, or checkout_lock.");
2176
+ }
2177
+ function requiredString(value, fieldName) {
2178
+ const text = nonEmptyString(value);
2179
+ if (text === void 0) {
2180
+ throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2181
+ }
2182
+ return text;
2183
+ }
2184
+ function optionalSafeInteger(value, fieldName = "integer") {
2185
+ if (value === void 0 || value === null) return void 0;
2186
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
2187
+ throw new TypeError(`OpenReceive checkout response ${fieldName} must be a safe integer.`);
2188
+ }
2189
+ return value;
2190
+ }
2191
+ function requiredSafeInteger(value, fieldName) {
2192
+ const integer = optionalSafeInteger(value, fieldName);
2193
+ if (integer === void 0) {
2194
+ throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2195
+ }
2196
+ return integer;
2197
+ }
2198
+
2209
2199
  // src/internal/checkout-state.ts
2210
2200
  function selectCheckoutDisplayInvoice(snapshot) {
2211
2201
  if (snapshot.active !== void 0 && snapshot.active.rail !== "checkout_lock") {
@@ -2380,15 +2370,14 @@ function normalizeCheckoutState(state, now) {
2380
2370
  const expiresInSeconds = base.expires_at === void 0 || now === void 0 ? void 0 : Math.max(0, base.expires_at - now);
2381
2371
  const phase = statePhase === "invoice_created" || statePhase === "verifying" ? expiresInSeconds === 0 ? "expired" : statePhase : statePhase;
2382
2372
  const settled = base.paid || base.transaction_state === "settled";
2373
+ const monitoring = checkoutMonitoring(base);
2383
2374
  return {
2384
2375
  ...base,
2385
2376
  ...deriveCheckoutStateLabels(base),
2386
2377
  phase,
2387
2378
  settled,
2388
- // A swap that expired, refunded, failed, or needs support review is over:
2389
- // the shadow invoice behind it will not be paid, so watchers stop polling
2390
- // both /payments/check and the provider instead of asking forever.
2391
- terminal: isTerminalPhase(phase) || !settled && isTerminalSwapProviderState(base.swap?.provider_state),
2379
+ // A local instruction deadline says nothing about wallet or refund finality.
2380
+ terminal: !monitoring.payment && !monitoring.provider,
2392
2381
  ...expiresInSeconds === void 0 ? {} : { expires_in_seconds: expiresInSeconds }
2393
2382
  };
2394
2383
  }
@@ -2409,8 +2398,13 @@ function getCheckoutPhase(transactionState, workflowState) {
2409
2398
  }
2410
2399
  return "invoice_created";
2411
2400
  }
2412
- function isTerminalPhase(phase) {
2413
- return phase === "expired" || phase === "failed" || phase === "cancelled";
2401
+ function checkoutMonitoring(state) {
2402
+ if (state.workflow_state === "cancelled") return { payment: false, provider: false };
2403
+ const payment = !state.paid && !["settled", "expired", "failed", "attention"].includes(state.transaction_state ?? "pending") && !["expired", "failed", "attention"].includes(state.workflow_state ?? "invoice_created");
2404
+ const providerState = state.swap?.provider_state;
2405
+ const recovery = providerState === "refund_required" || providerState === "refund_pending";
2406
+ const provider = providerState !== void 0 && !isTerminalSwapProviderState(providerState) && providerState !== "completed" && (recovery || !state.paid && state.transaction_state !== "settled");
2407
+ return { payment, provider };
2414
2408
  }
2415
2409
  function snapshotFromCheckoutState(state) {
2416
2410
  const invoice = {
@@ -2570,6 +2564,7 @@ function normalizeRequestCheckoutOptions(options) {
2570
2564
  const reference = nonEmptyString2(options.reference);
2571
2565
  const metadata = optionalRecord(options.metadata);
2572
2566
  return {
2567
+ signal: options.signal,
2573
2568
  routes: checkoutRoutes(options.prefix),
2574
2569
  reference: reference ?? "",
2575
2570
  fetch: options.fetch,
@@ -2599,6 +2594,7 @@ async function requestCheckout(options) {
2599
2594
  const headers = request.headers === void 0 ? {} : request.headers;
2600
2595
  const response = await fetcher(request.routes.checkouts, {
2601
2596
  method: "POST",
2597
+ signal: request.signal,
2602
2598
  headers: requestHeaders(headers, request.csrfHeader),
2603
2599
  body: JSON.stringify(requestBody)
2604
2600
  });
@@ -2622,6 +2618,7 @@ async function prepareCheckout(options) {
2622
2618
  const headers = request.headers === void 0 ? {} : request.headers;
2623
2619
  const response = await fetcher(request.routes.checkoutsPrepare, {
2624
2620
  method: "POST",
2621
+ signal: request.signal,
2625
2622
  headers: requestHeaders(headers, request.csrfHeader),
2626
2623
  body: JSON.stringify({ reference: request.reference })
2627
2624
  });
@@ -2645,50 +2642,57 @@ function createStatusFetcher(options) {
2645
2642
  if (activePaymentHash === void 0 || !/^[0-9a-f]{64}$/i.test(activePaymentHash)) {
2646
2643
  return snapshot;
2647
2644
  }
2648
- const response = await fetcher(routes.paymentsCheck, {
2649
- method: "POST",
2650
- headers: requestHeaders(headers, options.csrfHeader),
2651
- body: JSON.stringify({
2652
- reference,
2653
- payment_hash: activePaymentHash
2654
- })
2655
- });
2656
- const body = await readJsonResponse(response, "Could not refresh invoice status.");
2657
- const payment = recordOrEmpty(body);
2658
2645
  const next = structuredClone(snapshot);
2659
2646
  if (next.active === void 0) return next;
2660
- const state = nonEmptyString2(payment.status) ?? "pending";
2661
- const paidAt = optionalSafeInteger(payment.paid_at, "paid_at");
2662
- let active = {
2663
- ...next.active,
2664
- transaction_state: state === "not_found" ? "pending" : state,
2665
- ...paidAt === void 0 ? {} : { settled_at: paidAt }
2666
- };
2667
- if (active.rail === "swap" && active.swap !== void 0 && state !== "settled" && // A terminal provider state is final; re-reading it only adds provider load.
2668
- !isTerminalSwapProviderState(nonEmptyString2(active.swap.provider_state))) {
2647
+ let active = next.active;
2648
+ let payment = {};
2649
+ let paymentError;
2650
+ if (checkoutMonitoring(active).payment) {
2651
+ try {
2652
+ const response = await fetcher(routes.paymentsCheck, {
2653
+ method: "POST",
2654
+ headers: requestHeaders(headers, options.csrfHeader),
2655
+ body: JSON.stringify({ reference, payment_hash: activePaymentHash })
2656
+ });
2657
+ payment = recordOrEmpty(
2658
+ await readJsonResponse(response, "Could not refresh invoice status.")
2659
+ );
2660
+ const state2 = nonEmptyString2(payment.status) ?? "pending";
2661
+ const paidAt = optionalSafeInteger(payment.paid_at, "paid_at");
2662
+ active = {
2663
+ ...active,
2664
+ transaction_state: state2 === "not_found" ? "pending" : state2,
2665
+ ...paidAt === void 0 ? {} : { settled_at: paidAt }
2666
+ };
2667
+ } catch (error) {
2668
+ paymentError = error;
2669
+ }
2670
+ }
2671
+ if (active.rail === "swap" && checkoutMonitoring(active).provider) {
2669
2672
  try {
2670
2673
  const swapResponse = await fetcher(routes.swapsStatus, {
2671
2674
  method: "POST",
2672
2675
  headers: requestHeaders(headers, options.csrfHeader),
2673
2676
  body: JSON.stringify({ reference, payment_hash: activePaymentHash })
2674
2677
  });
2675
- const swapBody = recordOrEmpty(
2676
- await readJsonResponse(swapResponse, "Could not refresh swap status.")
2678
+ active = mergeSwapStatusIntoInvoice(
2679
+ active,
2680
+ recordOrEmpty(await readJsonResponse(swapResponse, "Could not refresh swap status."))
2677
2681
  );
2678
- active = mergeSwapStatusIntoInvoice(active, swapBody);
2679
2682
  swapStatusFailures = 0;
2680
2683
  } catch (error) {
2681
2684
  swapStatusFailures += 1;
2682
2685
  const definitive = error instanceof BrowserRequestError && error.retryable === false;
2683
- const swap = active.swap;
2684
- if (swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2686
+ if (active.swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2685
2687
  active = {
2686
2688
  ...active,
2687
- swap: { ...swap, attention: true, attention_reason: "swap_status_unavailable" }
2689
+ swap: { ...active.swap, attention: true, attention_reason: "swap_status_unavailable" }
2688
2690
  };
2689
2691
  }
2692
+ if (paymentError === void 0 && !checkoutMonitoring(active).payment) paymentError = error;
2690
2693
  }
2691
2694
  }
2695
+ const state = active.transaction_state;
2692
2696
  const paymentMethods2 = normalizePaymentMethods(payment.payment_methods);
2693
2697
  const others = next.invoices.filter(
2694
2698
  (entry) => entry.invoice_id !== active.invoice_id && entry.rail !== "checkout_lock"
@@ -2700,6 +2704,7 @@ function createStatusFetcher(options) {
2700
2704
  status: state === "settled" ? "paid" : state === "expired" || state === "failed" ? "expired" : "open",
2701
2705
  ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
2702
2706
  };
2707
+ if (paymentError !== void 0 && active.swap === void 0) throw paymentError;
2703
2708
  return snapshot;
2704
2709
  };
2705
2710
  }
@@ -3095,6 +3100,7 @@ async function requestJson(options, url, body) {
3095
3100
  const fetcher = options.fetch;
3096
3101
  const response = await fetcher(url, {
3097
3102
  method: "POST",
3103
+ signal: options.signal,
3098
3104
  headers: requestHeaders(options.headers, options.csrfHeader),
3099
3105
  body: JSON.stringify(body)
3100
3106
  });
@@ -3176,6 +3182,8 @@ var CheckoutWatcher = class {
3176
3182
  countdownTimer;
3177
3183
  pollTimer;
3178
3184
  running = false;
3185
+ generation = 0;
3186
+ pendingRefresh;
3179
3187
  pollInFlight = false;
3180
3188
  pollFailureCount = 0;
3181
3189
  pollBackoffUntil;
@@ -3242,6 +3250,9 @@ var CheckoutWatcher = class {
3242
3250
  }
3243
3251
  stop() {
3244
3252
  this.running = false;
3253
+ this.generation += 1;
3254
+ this.pollInFlight = false;
3255
+ this.pendingRefresh = void 0;
3245
3256
  this.stopCountdown();
3246
3257
  this.stopPolling();
3247
3258
  }
@@ -3280,8 +3291,10 @@ var CheckoutWatcher = class {
3280
3291
  if (refreshStatus === void 0 || current.reference.length === 0) {
3281
3292
  return current;
3282
3293
  }
3294
+ const generation = this.generation;
3283
3295
  try {
3284
- const fetched = await refreshStatus(current.reference);
3296
+ const fetched = await this.fetchSnapshot(current.reference);
3297
+ if (generation !== this.generation) return this.state ?? current;
3285
3298
  if (fetched === null) return current;
3286
3299
  const next = this.publishSnapshot(fetched);
3287
3300
  const nextState = createCheckoutState(next, {
@@ -3297,10 +3310,20 @@ var CheckoutWatcher = class {
3297
3310
  }
3298
3311
  return nextState;
3299
3312
  } catch (error) {
3313
+ if (generation !== this.generation) return this.state ?? current;
3300
3314
  this.options.onError?.(error);
3301
3315
  throw error;
3302
3316
  }
3303
3317
  }
3318
+ fetchSnapshot(reference) {
3319
+ if (this.pendingRefresh !== void 0) return this.pendingRefresh;
3320
+ const pending = Promise.resolve(this.options.refreshStatus?.(reference) ?? null);
3321
+ this.pendingRefresh = pending;
3322
+ void pending.finally(() => {
3323
+ if (this.pendingRefresh === pending) this.pendingRefresh = void 0;
3324
+ }).catch(() => void 0);
3325
+ return pending;
3326
+ }
3304
3327
  applyState(state) {
3305
3328
  if (!this.running) return;
3306
3329
  this.state = state;
@@ -3314,7 +3337,7 @@ var CheckoutWatcher = class {
3314
3337
  this.stop();
3315
3338
  return;
3316
3339
  }
3317
- if (state.settled || state.expires_at === void 0) {
3340
+ if (state.settled || state.expires_at === void 0 || state.expires_in_seconds === 0) {
3318
3341
  this.stopCountdown();
3319
3342
  } else if (this.countdownTimer === void 0) {
3320
3343
  this.countdownTimer = this.setInterval()(() => {
@@ -3330,7 +3353,7 @@ var CheckoutWatcher = class {
3330
3353
  }, 1e3);
3331
3354
  }
3332
3355
  const canPollAttempt = state.rail !== "checkout_lock" && typeof state.payment_hash === "string" && /^[0-9a-f]{64}$/i.test(state.payment_hash);
3333
- if (state.settled || this.options.refreshStatus === void 0 || state.reference.length === 0 || !canPollAttempt) {
3356
+ if (this.options.refreshStatus === void 0 || state.reference.length === 0 || !canPollAttempt) {
3334
3357
  this.stopPolling();
3335
3358
  } else if (this.pollTimer === void 0) {
3336
3359
  this.pollTimer = this.setInterval()(() => {
@@ -3342,20 +3365,22 @@ var CheckoutWatcher = class {
3342
3365
  const refreshStatus = this.options.refreshStatus;
3343
3366
  const current = this.state;
3344
3367
  if (!this.running || refreshStatus === void 0 || current === void 0) return;
3345
- if (current.terminal || current.settled) {
3368
+ if (current.terminal) {
3346
3369
  this.stopPolling();
3347
3370
  return;
3348
3371
  }
3349
3372
  if (this.pollInFlight) return;
3350
3373
  if (this.pollBackoffUntil !== void 0 && this.now() < this.pollBackoffUntil) return;
3351
3374
  this.pollInFlight = true;
3375
+ const generation = this.generation;
3352
3376
  try {
3353
- const fetched = await refreshStatus(current.reference);
3377
+ const fetched = await this.fetchSnapshot(current.reference);
3378
+ if (generation !== this.generation) return;
3354
3379
  this.pollFailureCount = 0;
3355
3380
  this.pollBackoffUntil = void 0;
3356
3381
  if (fetched === null) return;
3357
3382
  if (!this.running || this.state === void 0) return;
3358
- if (this.state.settled || this.state.terminal) return;
3383
+ if (this.state.terminal) return;
3359
3384
  const next = this.publishSnapshot(fetched);
3360
3385
  this.applyState(
3361
3386
  createCheckoutState(next, {
@@ -3366,13 +3391,14 @@ var CheckoutWatcher = class {
3366
3391
  })
3367
3392
  );
3368
3393
  } catch (error) {
3394
+ if (generation !== this.generation) return;
3369
3395
  this.pollFailureCount += 1;
3370
3396
  const retryAfterSeconds = error instanceof BrowserRequestError ? error.retryAfterSeconds : void 0;
3371
3397
  const backoffSeconds = retryAfterSeconds ?? Math.min(60, 2 ** Math.min(this.pollFailureCount, 6));
3372
3398
  this.pollBackoffUntil = this.now() + backoffSeconds;
3373
3399
  this.options.onError?.(error);
3374
3400
  } finally {
3375
- this.pollInFlight = false;
3401
+ if (generation === this.generation) this.pollInFlight = false;
3376
3402
  }
3377
3403
  }
3378
3404
  stopCountdown() {
@@ -3399,6 +3425,7 @@ var BrowserCheckoutController = class {
3399
3425
  options;
3400
3426
  watcher;
3401
3427
  state;
3428
+ generation = 0;
3402
3429
  constructor(options) {
3403
3430
  this.options = options;
3404
3431
  this.watcher = this.createWatcher(options);
@@ -3408,6 +3435,7 @@ var BrowserCheckoutController = class {
3408
3435
  return this.state;
3409
3436
  }
3410
3437
  stop() {
3438
+ this.generation += 1;
3411
3439
  this.watcher.stop();
3412
3440
  }
3413
3441
  getState() {
@@ -3469,6 +3497,7 @@ var BrowserCheckoutController = class {
3469
3497
  if (paymentHash === void 0) {
3470
3498
  throw new Error(`No swap attempt ${options.attemptId} in this checkout.`);
3471
3499
  }
3500
+ const generation = this.generation;
3472
3501
  const invoice = await requestSwapRefund({
3473
3502
  fetch: this.options.fetch ?? globalThis.fetch,
3474
3503
  prefix,
@@ -3480,10 +3509,12 @@ var BrowserCheckoutController = class {
3480
3509
  refundAddress: options.refundAddress,
3481
3510
  confirm
3482
3511
  });
3512
+ if (generation !== this.generation) throw new DOMException("Checkout changed.", "AbortError");
3483
3513
  this.watcher.stageSwapInvoice(invoice);
3484
3514
  return invoice;
3485
3515
  }
3486
3516
  cancel() {
3517
+ this.generation += 1;
3487
3518
  this.state = this.watcher.cancel();
3488
3519
  emitBrowserLog(
3489
3520
  this.options.logger,
@@ -3759,6 +3790,7 @@ export {
3759
3790
  createTransactionDetails,
3760
3791
  createTransactionDetailsFromState,
3761
3792
  resolveTransactionDetailRows,
3793
+ createAppBrowserConsoleLogger,
3762
3794
  getWizardEmptyMessage,
3763
3795
  getRouteNetworkLabel,
3764
3796
  createWizardRouteAssetDisplays,
@@ -3790,7 +3822,6 @@ export {
3790
3822
  createPaymentWizardModel,
3791
3823
  updatePaymentWizardSelection,
3792
3824
  createPaymentWizardController,
3793
- createAppBrowserConsoleLogger,
3794
3825
  selectCheckoutDisplayInvoice,
3795
3826
  isPaidCheckoutSnapshot,
3796
3827
  isReusableLightningInvoice,