@openreceive/browser 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.
@@ -952,6 +952,7 @@ function createSwapDisplayModel(invoice, options = {}) {
952
952
  const depositRisk = swapDepositRisk(swap.pay_in_asset);
953
953
  const doubleSpendWarning = `Pay with one method only \u2014 if you already sent ${asset.assetLabel}, do not also pay the Lightning invoice.`;
954
954
  const settled = invoice.transaction_state === "settled";
955
+ const instructionsExpired = expiresInSeconds === 0 && swap.provider_state === "awaiting_deposit";
955
956
  const feeBreakdown = createSwapFeeBreakdown(swap.fee, swap);
956
957
  return {
957
958
  provider: swap.provider,
@@ -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),
@@ -1087,7 +1088,7 @@ function getSwapProviderStateDetail(state, payInAsset, refundContext = {}) {
1087
1088
  return "Your payment is confirmed and being converted. This usually finishes within a minute.";
1088
1089
  }
1089
1090
  if (state === "paying_invoice" || state === "completed") {
1090
- return "The provider is sending the Lightning payment. This usually takes a few seconds.";
1091
+ return "We are checking whether the wallet received the payment. Provider completion alone does not confirm payment.";
1091
1092
  }
1092
1093
  if (state === "expired") return "No payment was received before the payment window closed.";
1093
1094
  if (state === "refund_required" || state === "refund_pending" || state === "refunded") {
@@ -1361,6 +1362,127 @@ function isCheckoutStateSource(value) {
1361
1362
  return "checkout_id" in value && "reference" in value && "invoice_id" in value && "invoice" in value && "transaction_state" in value && "phase" in value;
1362
1363
  }
1363
1364
 
1365
+ // src/internal/console-logger.ts
1366
+ import { isSensitiveLogKey, sanitizeLogValue } from "@openreceive/core";
1367
+
1368
+ // src/internal/log-level.ts
1369
+ var BROWSER_LOG_LEVEL_ORDER = {
1370
+ debug: 10,
1371
+ info: 20,
1372
+ warn: 30,
1373
+ error: 40
1374
+ };
1375
+ var BROWSER_LOG_LEVEL_ALIASES = {
1376
+ debug: "debug",
1377
+ info: "info",
1378
+ warn: "warn",
1379
+ warning: "warn",
1380
+ error: "error"
1381
+ };
1382
+ function browserLogLevelOrder(level) {
1383
+ return BROWSER_LOG_LEVEL_ORDER[level];
1384
+ }
1385
+ function parseBrowserLogLevel(value) {
1386
+ if (value === void 0 || value === null) return void 0;
1387
+ const normalized = value.trim().toLowerCase();
1388
+ if (normalized.length === 0) return void 0;
1389
+ return BROWSER_LOG_LEVEL_ALIASES[normalized];
1390
+ }
1391
+ function resolveBrowserLogLevel(value, fallback = "info") {
1392
+ return parseBrowserLogLevel(value) ?? fallback;
1393
+ }
1394
+ function readBrowserLogLevelFromEnvironment(source) {
1395
+ if (source !== void 0) {
1396
+ return resolveBrowserLogLevel(
1397
+ source.LOG_LEVEL ?? source.VITE_LOG_LEVEL ?? source.NEXT_PUBLIC_LOG_LEVEL
1398
+ );
1399
+ }
1400
+ const fromGlobal = parseBrowserLogLevel(readGlobalLogLevel());
1401
+ if (fromGlobal !== void 0) return fromGlobal;
1402
+ const metaEnv = readImportMetaEnv();
1403
+ const parsedMeta = parseBrowserLogLevel(
1404
+ metaEnv?.LOG_LEVEL ?? metaEnv?.VITE_LOG_LEVEL ?? metaEnv?.NEXT_PUBLIC_LOG_LEVEL
1405
+ );
1406
+ if (parsedMeta !== void 0) return parsedMeta;
1407
+ const processEnv = typeof process !== "undefined" ? process.env : void 0;
1408
+ return resolveBrowserLogLevel(processEnv?.LOG_LEVEL);
1409
+ }
1410
+ function readGlobalLogLevel() {
1411
+ try {
1412
+ const value = globalThis.__OPENRECEIVE_LOG_LEVEL__;
1413
+ return typeof value === "string" ? value : void 0;
1414
+ } catch {
1415
+ return void 0;
1416
+ }
1417
+ }
1418
+ function readImportMetaEnv() {
1419
+ try {
1420
+ return import.meta.env;
1421
+ } catch {
1422
+ return void 0;
1423
+ }
1424
+ }
1425
+
1426
+ // src/internal/console-logger.ts
1427
+ function createConsoleWriter(options) {
1428
+ const target = options.console ?? console;
1429
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1430
+ return (level, prefix, event, message, fields) => {
1431
+ const minLevel = options.minLevel === void 0 ? readBrowserLogLevelFromEnvironment() : resolveBrowserLogLevel(options.minLevel);
1432
+ if (browserLogLevelOrder(level) < browserLogLevelOrder(minLevel)) {
1433
+ return;
1434
+ }
1435
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
1436
+ const sink = target[method] ?? target.log;
1437
+ const {
1438
+ level: _level,
1439
+ event: _event,
1440
+ message: cleanMessage,
1441
+ ...cleanFields
1442
+ } = sanitizeBrowserLogEntry({ level, event, message, ...fields });
1443
+ sink.call(
1444
+ target,
1445
+ `[${now().toISOString()}] ${level.toUpperCase()} [${prefix}] ${event}: ${cleanMessage}`,
1446
+ cleanFields
1447
+ );
1448
+ };
1449
+ }
1450
+ function sanitizeBrowserLogEntry(entry) {
1451
+ const clean = {};
1452
+ for (const [key, value] of Object.entries(entry)) {
1453
+ if (isSensitiveLogKey(key)) {
1454
+ clean[key] = "[REDACTED]";
1455
+ } else {
1456
+ clean[key] = sanitizeLogValue(value);
1457
+ }
1458
+ }
1459
+ return clean;
1460
+ }
1461
+ function createBrowserConsoleLogger(options = {}) {
1462
+ const prefix = options.prefix ?? "openreceive";
1463
+ const write = createConsoleWriter(options);
1464
+ return (entry) => {
1465
+ const { level, event, message, ...fields } = entry;
1466
+ write(level, prefix, event, message, fields);
1467
+ };
1468
+ }
1469
+ function createAppBrowserConsoleLogger(options) {
1470
+ const write = createConsoleWriter(options);
1471
+ return (event, message, fields = {}, level = "info") => {
1472
+ write(level, options.prefix, event, message, fields);
1473
+ };
1474
+ }
1475
+ var defaultBrowserConsoleLogger;
1476
+ function getDefaultBrowserConsoleLogger() {
1477
+ defaultBrowserConsoleLogger ??= createBrowserConsoleLogger();
1478
+ return defaultBrowserConsoleLogger;
1479
+ }
1480
+ function resolveBrowserLogger(logger) {
1481
+ if (logger === false) return void 0;
1482
+ if (logger !== void 0) return logger;
1483
+ return getDefaultBrowserConsoleLogger();
1484
+ }
1485
+
1364
1486
  // src/internal/wizard.ts
1365
1487
  import { getSwapRefundAddressError } from "@openreceive/core/swap-address";
1366
1488
  import {
@@ -1956,176 +2078,6 @@ function createPaymentWizardController(options = {}) {
1956
2078
  return new BrowserPaymentWizardController(options);
1957
2079
  }
1958
2080
 
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
2081
  // src/internal/checkout-log.ts
2130
2082
  import { compact } from "@openreceive/core";
2131
2083
  function checkoutLogFields(state) {
@@ -2206,6 +2158,41 @@ function emitBrowserLog(logger, level, event, message, fields = {}) {
2206
2158
  }
2207
2159
  }
2208
2160
 
2161
+ // src/internal/checkout-read.ts
2162
+ import { nonEmptyString } from "@openreceive/core";
2163
+ function optionalRecord(value) {
2164
+ if (value === void 0) return void 0;
2165
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2166
+ throw new Error("OpenReceive metadata must be an object.");
2167
+ }
2168
+ return value;
2169
+ }
2170
+ function requiredInvoiceRail(value) {
2171
+ if (value === "lightning" || value === "swap" || value === "checkout_lock") return value;
2172
+ throw new TypeError("OpenReceive invoice rail must be lightning, swap, or checkout_lock.");
2173
+ }
2174
+ function requiredString(value, fieldName) {
2175
+ const text = nonEmptyString(value);
2176
+ if (text === void 0) {
2177
+ throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2178
+ }
2179
+ return text;
2180
+ }
2181
+ function optionalSafeInteger(value, fieldName = "integer") {
2182
+ if (value === void 0 || value === null) return void 0;
2183
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
2184
+ throw new TypeError(`OpenReceive checkout response ${fieldName} must be a safe integer.`);
2185
+ }
2186
+ return value;
2187
+ }
2188
+ function requiredSafeInteger(value, fieldName) {
2189
+ const integer = optionalSafeInteger(value, fieldName);
2190
+ if (integer === void 0) {
2191
+ throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2192
+ }
2193
+ return integer;
2194
+ }
2195
+
2209
2196
  // src/internal/checkout-state.ts
2210
2197
  function selectCheckoutDisplayInvoice(snapshot) {
2211
2198
  if (snapshot.active !== void 0 && snapshot.active.rail !== "checkout_lock") {
@@ -2380,15 +2367,14 @@ function normalizeCheckoutState(state, now) {
2380
2367
  const expiresInSeconds = base.expires_at === void 0 || now === void 0 ? void 0 : Math.max(0, base.expires_at - now);
2381
2368
  const phase = statePhase === "invoice_created" || statePhase === "verifying" ? expiresInSeconds === 0 ? "expired" : statePhase : statePhase;
2382
2369
  const settled = base.paid || base.transaction_state === "settled";
2370
+ const monitoring = checkoutMonitoring(base);
2383
2371
  return {
2384
2372
  ...base,
2385
2373
  ...deriveCheckoutStateLabels(base),
2386
2374
  phase,
2387
2375
  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),
2376
+ // A local instruction deadline says nothing about wallet or refund finality.
2377
+ terminal: !monitoring.payment && !monitoring.provider,
2392
2378
  ...expiresInSeconds === void 0 ? {} : { expires_in_seconds: expiresInSeconds }
2393
2379
  };
2394
2380
  }
@@ -2409,8 +2395,13 @@ function getCheckoutPhase(transactionState, workflowState) {
2409
2395
  }
2410
2396
  return "invoice_created";
2411
2397
  }
2412
- function isTerminalPhase(phase) {
2413
- return phase === "expired" || phase === "failed" || phase === "cancelled";
2398
+ function checkoutMonitoring(state) {
2399
+ if (state.workflow_state === "cancelled") return { payment: false, provider: false };
2400
+ const payment = !state.paid && !["settled", "expired", "failed", "attention"].includes(state.transaction_state ?? "pending") && !["expired", "failed", "attention"].includes(state.workflow_state ?? "invoice_created");
2401
+ const providerState = state.swap?.provider_state;
2402
+ const recovery = providerState === "refund_required" || providerState === "refund_pending";
2403
+ const provider = providerState !== void 0 && !isTerminalSwapProviderState(providerState) && providerState !== "completed" && (recovery || !state.paid && state.transaction_state !== "settled");
2404
+ return { payment, provider };
2414
2405
  }
2415
2406
  function snapshotFromCheckoutState(state) {
2416
2407
  const invoice = {
@@ -2570,6 +2561,7 @@ function normalizeRequestCheckoutOptions(options) {
2570
2561
  const reference = nonEmptyString2(options.reference);
2571
2562
  const metadata = optionalRecord(options.metadata);
2572
2563
  return {
2564
+ signal: options.signal,
2573
2565
  routes: checkoutRoutes(options.prefix),
2574
2566
  reference: reference ?? "",
2575
2567
  fetch: options.fetch,
@@ -2599,6 +2591,7 @@ async function requestCheckout(options) {
2599
2591
  const headers = request.headers === void 0 ? {} : request.headers;
2600
2592
  const response = await fetcher(request.routes.checkouts, {
2601
2593
  method: "POST",
2594
+ signal: request.signal,
2602
2595
  headers: requestHeaders(headers, request.csrfHeader),
2603
2596
  body: JSON.stringify(requestBody)
2604
2597
  });
@@ -2622,6 +2615,7 @@ async function prepareCheckout(options) {
2622
2615
  const headers = request.headers === void 0 ? {} : request.headers;
2623
2616
  const response = await fetcher(request.routes.checkoutsPrepare, {
2624
2617
  method: "POST",
2618
+ signal: request.signal,
2625
2619
  headers: requestHeaders(headers, request.csrfHeader),
2626
2620
  body: JSON.stringify({ reference: request.reference })
2627
2621
  });
@@ -2645,50 +2639,57 @@ function createStatusFetcher(options) {
2645
2639
  if (activePaymentHash === void 0 || !/^[0-9a-f]{64}$/i.test(activePaymentHash)) {
2646
2640
  return snapshot;
2647
2641
  }
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
2642
  const next = structuredClone(snapshot);
2659
2643
  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))) {
2644
+ let active = next.active;
2645
+ let payment = {};
2646
+ let paymentError;
2647
+ if (checkoutMonitoring(active).payment) {
2648
+ try {
2649
+ const response = await fetcher(routes.paymentsCheck, {
2650
+ method: "POST",
2651
+ headers: requestHeaders(headers, options.csrfHeader),
2652
+ body: JSON.stringify({ reference, payment_hash: activePaymentHash })
2653
+ });
2654
+ payment = recordOrEmpty(
2655
+ await readJsonResponse(response, "Could not refresh invoice status.")
2656
+ );
2657
+ const state2 = nonEmptyString2(payment.status) ?? "pending";
2658
+ const paidAt = optionalSafeInteger(payment.paid_at, "paid_at");
2659
+ active = {
2660
+ ...active,
2661
+ transaction_state: state2 === "not_found" ? "pending" : state2,
2662
+ ...paidAt === void 0 ? {} : { settled_at: paidAt }
2663
+ };
2664
+ } catch (error) {
2665
+ paymentError = error;
2666
+ }
2667
+ }
2668
+ if (active.rail === "swap" && checkoutMonitoring(active).provider) {
2669
2669
  try {
2670
2670
  const swapResponse = await fetcher(routes.swapsStatus, {
2671
2671
  method: "POST",
2672
2672
  headers: requestHeaders(headers, options.csrfHeader),
2673
2673
  body: JSON.stringify({ reference, payment_hash: activePaymentHash })
2674
2674
  });
2675
- const swapBody = recordOrEmpty(
2676
- await readJsonResponse(swapResponse, "Could not refresh swap status.")
2675
+ active = mergeSwapStatusIntoInvoice(
2676
+ active,
2677
+ recordOrEmpty(await readJsonResponse(swapResponse, "Could not refresh swap status."))
2677
2678
  );
2678
- active = mergeSwapStatusIntoInvoice(active, swapBody);
2679
2679
  swapStatusFailures = 0;
2680
2680
  } catch (error) {
2681
2681
  swapStatusFailures += 1;
2682
2682
  const definitive = error instanceof BrowserRequestError && error.retryable === false;
2683
- const swap = active.swap;
2684
- if (swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2683
+ if (active.swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2685
2684
  active = {
2686
2685
  ...active,
2687
- swap: { ...swap, attention: true, attention_reason: "swap_status_unavailable" }
2686
+ swap: { ...active.swap, attention: true, attention_reason: "swap_status_unavailable" }
2688
2687
  };
2689
2688
  }
2689
+ if (paymentError === void 0 && !checkoutMonitoring(active).payment) paymentError = error;
2690
2690
  }
2691
2691
  }
2692
+ const state = active.transaction_state;
2692
2693
  const paymentMethods2 = normalizePaymentMethods(payment.payment_methods);
2693
2694
  const others = next.invoices.filter(
2694
2695
  (entry) => entry.invoice_id !== active.invoice_id && entry.rail !== "checkout_lock"
@@ -2700,6 +2701,7 @@ function createStatusFetcher(options) {
2700
2701
  status: state === "settled" ? "paid" : state === "expired" || state === "failed" ? "expired" : "open",
2701
2702
  ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
2702
2703
  };
2704
+ if (paymentError !== void 0 && active.swap === void 0) throw paymentError;
2703
2705
  return snapshot;
2704
2706
  };
2705
2707
  }
@@ -3095,6 +3097,7 @@ async function requestJson(options, url, body) {
3095
3097
  const fetcher = options.fetch;
3096
3098
  const response = await fetcher(url, {
3097
3099
  method: "POST",
3100
+ signal: options.signal,
3098
3101
  headers: requestHeaders(options.headers, options.csrfHeader),
3099
3102
  body: JSON.stringify(body)
3100
3103
  });
@@ -3176,6 +3179,8 @@ var CheckoutWatcher = class {
3176
3179
  countdownTimer;
3177
3180
  pollTimer;
3178
3181
  running = false;
3182
+ generation = 0;
3183
+ pendingRefresh;
3179
3184
  pollInFlight = false;
3180
3185
  pollFailureCount = 0;
3181
3186
  pollBackoffUntil;
@@ -3242,6 +3247,9 @@ var CheckoutWatcher = class {
3242
3247
  }
3243
3248
  stop() {
3244
3249
  this.running = false;
3250
+ this.generation += 1;
3251
+ this.pollInFlight = false;
3252
+ this.pendingRefresh = void 0;
3245
3253
  this.stopCountdown();
3246
3254
  this.stopPolling();
3247
3255
  }
@@ -3280,8 +3288,10 @@ var CheckoutWatcher = class {
3280
3288
  if (refreshStatus === void 0 || current.reference.length === 0) {
3281
3289
  return current;
3282
3290
  }
3291
+ const generation = this.generation;
3283
3292
  try {
3284
- const fetched = await refreshStatus(current.reference);
3293
+ const fetched = await this.fetchSnapshot(current.reference);
3294
+ if (generation !== this.generation) return this.state ?? current;
3285
3295
  if (fetched === null) return current;
3286
3296
  const next = this.publishSnapshot(fetched);
3287
3297
  const nextState = createCheckoutState(next, {
@@ -3297,10 +3307,20 @@ var CheckoutWatcher = class {
3297
3307
  }
3298
3308
  return nextState;
3299
3309
  } catch (error) {
3310
+ if (generation !== this.generation) return this.state ?? current;
3300
3311
  this.options.onError?.(error);
3301
3312
  throw error;
3302
3313
  }
3303
3314
  }
3315
+ fetchSnapshot(reference) {
3316
+ if (this.pendingRefresh !== void 0) return this.pendingRefresh;
3317
+ const pending = Promise.resolve(this.options.refreshStatus?.(reference) ?? null);
3318
+ this.pendingRefresh = pending;
3319
+ void pending.finally(() => {
3320
+ if (this.pendingRefresh === pending) this.pendingRefresh = void 0;
3321
+ }).catch(() => void 0);
3322
+ return pending;
3323
+ }
3304
3324
  applyState(state) {
3305
3325
  if (!this.running) return;
3306
3326
  this.state = state;
@@ -3314,7 +3334,7 @@ var CheckoutWatcher = class {
3314
3334
  this.stop();
3315
3335
  return;
3316
3336
  }
3317
- if (state.settled || state.expires_at === void 0) {
3337
+ if (state.settled || state.expires_at === void 0 || state.expires_in_seconds === 0) {
3318
3338
  this.stopCountdown();
3319
3339
  } else if (this.countdownTimer === void 0) {
3320
3340
  this.countdownTimer = this.setInterval()(() => {
@@ -3330,7 +3350,7 @@ var CheckoutWatcher = class {
3330
3350
  }, 1e3);
3331
3351
  }
3332
3352
  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) {
3353
+ if (this.options.refreshStatus === void 0 || state.reference.length === 0 || !canPollAttempt) {
3334
3354
  this.stopPolling();
3335
3355
  } else if (this.pollTimer === void 0) {
3336
3356
  this.pollTimer = this.setInterval()(() => {
@@ -3342,20 +3362,22 @@ var CheckoutWatcher = class {
3342
3362
  const refreshStatus = this.options.refreshStatus;
3343
3363
  const current = this.state;
3344
3364
  if (!this.running || refreshStatus === void 0 || current === void 0) return;
3345
- if (current.terminal || current.settled) {
3365
+ if (current.terminal) {
3346
3366
  this.stopPolling();
3347
3367
  return;
3348
3368
  }
3349
3369
  if (this.pollInFlight) return;
3350
3370
  if (this.pollBackoffUntil !== void 0 && this.now() < this.pollBackoffUntil) return;
3351
3371
  this.pollInFlight = true;
3372
+ const generation = this.generation;
3352
3373
  try {
3353
- const fetched = await refreshStatus(current.reference);
3374
+ const fetched = await this.fetchSnapshot(current.reference);
3375
+ if (generation !== this.generation) return;
3354
3376
  this.pollFailureCount = 0;
3355
3377
  this.pollBackoffUntil = void 0;
3356
3378
  if (fetched === null) return;
3357
3379
  if (!this.running || this.state === void 0) return;
3358
- if (this.state.settled || this.state.terminal) return;
3380
+ if (this.state.terminal) return;
3359
3381
  const next = this.publishSnapshot(fetched);
3360
3382
  this.applyState(
3361
3383
  createCheckoutState(next, {
@@ -3366,13 +3388,14 @@ var CheckoutWatcher = class {
3366
3388
  })
3367
3389
  );
3368
3390
  } catch (error) {
3391
+ if (generation !== this.generation) return;
3369
3392
  this.pollFailureCount += 1;
3370
3393
  const retryAfterSeconds = error instanceof BrowserRequestError ? error.retryAfterSeconds : void 0;
3371
3394
  const backoffSeconds = retryAfterSeconds ?? Math.min(60, 2 ** Math.min(this.pollFailureCount, 6));
3372
3395
  this.pollBackoffUntil = this.now() + backoffSeconds;
3373
3396
  this.options.onError?.(error);
3374
3397
  } finally {
3375
- this.pollInFlight = false;
3398
+ if (generation === this.generation) this.pollInFlight = false;
3376
3399
  }
3377
3400
  }
3378
3401
  stopCountdown() {
@@ -3399,6 +3422,7 @@ var BrowserCheckoutController = class {
3399
3422
  options;
3400
3423
  watcher;
3401
3424
  state;
3425
+ generation = 0;
3402
3426
  constructor(options) {
3403
3427
  this.options = options;
3404
3428
  this.watcher = this.createWatcher(options);
@@ -3408,6 +3432,7 @@ var BrowserCheckoutController = class {
3408
3432
  return this.state;
3409
3433
  }
3410
3434
  stop() {
3435
+ this.generation += 1;
3411
3436
  this.watcher.stop();
3412
3437
  }
3413
3438
  getState() {
@@ -3469,6 +3494,7 @@ var BrowserCheckoutController = class {
3469
3494
  if (paymentHash === void 0) {
3470
3495
  throw new Error(`No swap attempt ${options.attemptId} in this checkout.`);
3471
3496
  }
3497
+ const generation = this.generation;
3472
3498
  const invoice = await requestSwapRefund({
3473
3499
  fetch: this.options.fetch ?? globalThis.fetch,
3474
3500
  prefix,
@@ -3480,10 +3506,12 @@ var BrowserCheckoutController = class {
3480
3506
  refundAddress: options.refundAddress,
3481
3507
  confirm
3482
3508
  });
3509
+ if (generation !== this.generation) throw new DOMException("Checkout changed.", "AbortError");
3483
3510
  this.watcher.stageSwapInvoice(invoice);
3484
3511
  return invoice;
3485
3512
  }
3486
3513
  cancel() {
3514
+ this.generation += 1;
3487
3515
  this.state = this.watcher.cancel();
3488
3516
  emitBrowserLog(
3489
3517
  this.options.logger,
@@ -3759,6 +3787,7 @@ export {
3759
3787
  createTransactionDetails,
3760
3788
  createTransactionDetailsFromState,
3761
3789
  resolveTransactionDetailRows,
3790
+ createAppBrowserConsoleLogger,
3762
3791
  getWizardEmptyMessage,
3763
3792
  getRouteNetworkLabel,
3764
3793
  createWizardRouteAssetDisplays,
@@ -3790,7 +3819,6 @@ export {
3790
3819
  createPaymentWizardModel,
3791
3820
  updatePaymentWizardSelection,
3792
3821
  createPaymentWizardController,
3793
- createAppBrowserConsoleLogger,
3794
3822
  selectCheckoutDisplayInvoice,
3795
3823
  isPaidCheckoutSnapshot,
3796
3824
  isReusableLightningInvoice,