@openreceive/browser 0.4.9 → 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.
@@ -864,7 +864,8 @@ import {
864
864
  formatDecimal as formatDecimal2,
865
865
  parseDecimal as parseDecimal2,
866
866
  payInAssetNetwork,
867
- swapAddressNetworkForPayInAsset
867
+ swapAddressNetworkForPayInAsset,
868
+ swapPayInAssetPeggedTo
868
869
  } from "@openreceive/core";
869
870
 
870
871
  // src/internal/unix-seconds.ts
@@ -881,34 +882,56 @@ function resolveNow(now) {
881
882
  }
882
883
 
883
884
  // src/internal/checkout-swap-view.ts
884
- function createSwapFeeBreakdown(fee) {
885
+ function swapFeeInTokenUnits(fee, payInAsset) {
886
+ return swapPayInAssetPeggedTo(payInAsset) === fee.currency;
887
+ }
888
+ function createSwapFeeBreakdown(fee, swap) {
885
889
  if (fee === void 0) return void 0;
886
- let payIn;
887
- let payout;
888
- try {
889
- payIn = parseDecimal2(fee.pay_in_fiat);
890
- payout = parseDecimal2(fee.payout_fiat);
891
- } catch {
892
- return void 0;
893
- }
894
- if (payout.units <= 0n) return void 0;
895
- const scale = Math.max(payIn.scale, payout.scale);
896
- const payInUnits = payIn.units * 10n ** BigInt(scale - payIn.scale);
897
- const payoutUnits = payout.units * 10n ** BigInt(scale - payout.scale);
898
- const feeUnits = payInUnits > payoutUnits ? payInUnits - payoutUnits : 0n;
899
- const format = (units) => {
890
+ const payout = parseFeeDecimal(fee.payout_fiat);
891
+ if (payout === void 0 || payout.units <= 0n) return void 0;
892
+ const fiat = (units, scale) => {
900
893
  const value = formatDecimal2(rescaleHalfUp(units, scale, 2), 2);
901
894
  return formatFiatAmount({ currency: fee.currency, value }) ?? `${value} ${fee.currency}`;
902
895
  };
903
- const percentTenths = roundedDiv(feeUnits * 1000n, payoutUnits);
904
- const feePercent = `${(percentTenths / 10n).toString()}.${(percentTenths % 10n).toString()}%`;
896
+ if (swap !== void 0 && swapFeeInTokenUnits(fee, swap.pay_in_asset)) {
897
+ const deposit = parseFeeDecimal(swap.deposit_amount);
898
+ if (deposit === void 0) return void 0;
899
+ const { assetLabel } = getSwapAssetDisplay(swap.pay_in_asset);
900
+ const gap2 = decimalGap(deposit, payout);
901
+ return {
902
+ cartTotal: fiat(gap2.base, gap2.scale),
903
+ youSend: `${formatDepositAmount(swap.deposit_amount)} ${assetLabel}`,
904
+ fee: `${formatDecimal2(rescaleHalfUp(gap2.fee, gap2.scale, 2), 2)} ${assetLabel}`,
905
+ feePercent: percentOf(gap2.fee, gap2.base)
906
+ };
907
+ }
908
+ const payIn = parseFeeDecimal(fee.pay_in_fiat);
909
+ if (payIn === void 0) return void 0;
910
+ const gap = decimalGap(payIn, payout);
905
911
  return {
906
- cartTotal: format(payoutUnits),
907
- youSend: format(payInUnits),
908
- fee: format(feeUnits),
909
- feePercent
912
+ cartTotal: fiat(gap.base, gap.scale),
913
+ youSend: fiat(gap.base + gap.fee, gap.scale),
914
+ fee: fiat(gap.fee, gap.scale),
915
+ feePercent: percentOf(gap.fee, gap.base)
910
916
  };
911
917
  }
918
+ function parseFeeDecimal(value) {
919
+ try {
920
+ return parseDecimal2(value);
921
+ } catch {
922
+ return void 0;
923
+ }
924
+ }
925
+ function decimalGap(paid, base) {
926
+ const scale = Math.max(paid.scale, base.scale);
927
+ const paidUnits = paid.units * 10n ** BigInt(scale - paid.scale);
928
+ const baseUnits = base.units * 10n ** BigInt(scale - base.scale);
929
+ return { fee: paidUnits > baseUnits ? paidUnits - baseUnits : 0n, base: baseUnits, scale };
930
+ }
931
+ function percentOf(fee, base) {
932
+ const tenths = roundedDiv(fee * 1000n, base);
933
+ return `${(tenths / 10n).toString()}.${(tenths % 10n).toString()}%`;
934
+ }
912
935
  function swapDepositRisk(payInAsset) {
913
936
  const network = swapAddressNetworkForPayInAsset(payInAsset);
914
937
  if (network === void 0 || network === "ETH") return "chain_ambiguous";
@@ -929,7 +952,8 @@ function createSwapDisplayModel(invoice, options = {}) {
929
952
  const depositRisk = swapDepositRisk(swap.pay_in_asset);
930
953
  const doubleSpendWarning = `Pay with one method only \u2014 if you already sent ${asset.assetLabel}, do not also pay the Lightning invoice.`;
931
954
  const settled = invoice.transaction_state === "settled";
932
- const feeBreakdown = createSwapFeeBreakdown(swap.fee);
955
+ const instructionsExpired = expiresInSeconds === 0 && swap.provider_state === "awaiting_deposit";
956
+ const feeBreakdown = createSwapFeeBreakdown(swap.fee, swap);
933
957
  return {
934
958
  provider: swap.provider,
935
959
  attemptId: swap.attempt_id ?? invoice.invoice_id,
@@ -964,14 +988,14 @@ function createSwapDisplayModel(invoice, options = {}) {
964
988
  ],
965
989
  { label: checkoutLabels.swapCopyAmount, value: depositAmount, selectable: true }
966
990
  ],
967
- providerStateLabel: settled ? "Payment complete" : getSwapProviderStateLabel(swap.provider_state),
968
- 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, {
969
993
  refundReason: swap.refund_reason,
970
994
  depositAmount: swap.deposit_amount,
971
995
  depositReceivedAmount: swap.deposit_received_amount,
972
996
  refundAmount: swap.refund_amount
973
997
  }),
974
- state: settled ? "settled" : getSwapPanelState(swap.provider_state),
998
+ state: settled ? "settled" : instructionsExpired ? "progress" : getSwapPanelState(swap.provider_state),
975
999
  expiresInSeconds,
976
1000
  countdownLabel: formatCountdown(expiresInSeconds),
977
1001
  qrPayload: createSwapQrPayload(swap),
@@ -1064,7 +1088,7 @@ function getSwapProviderStateDetail(state, payInAsset, refundContext = {}) {
1064
1088
  return "Your payment is confirmed and being converted. This usually finishes within a minute.";
1065
1089
  }
1066
1090
  if (state === "paying_invoice" || state === "completed") {
1067
- 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.";
1068
1092
  }
1069
1093
  if (state === "expired") return "No payment was received before the payment window closed.";
1070
1094
  if (state === "refund_required" || state === "refund_pending" || state === "refunded") {
@@ -1291,7 +1315,7 @@ function createTransactionDetails(input) {
1291
1315
  push("Lightning payout", swap.payout_tx_id);
1292
1316
  push("Refund address", swap.refund_address, void 0, "address");
1293
1317
  push("Refund transaction", swap.refund_tx_id, void 0, "tx");
1294
- const feeBreakdown = createSwapFeeBreakdown(swap.fee);
1318
+ const feeBreakdown = createSwapFeeBreakdown(swap.fee, swap);
1295
1319
  if (feeBreakdown !== void 0) {
1296
1320
  push("Cart total", feeBreakdown.cartTotal);
1297
1321
  push("You send", feeBreakdown.youSend);
@@ -1301,7 +1325,9 @@ function createTransactionDetails(input) {
1301
1325
  );
1302
1326
  } else if (swap.fee !== void 0) {
1303
1327
  push("Fee currency", swap.fee.currency);
1304
- push("Pay-in fiat", swap.fee.pay_in_fiat);
1328
+ if (!swapFeeInTokenUnits(swap.fee, swap.pay_in_asset)) {
1329
+ push("Pay-in fiat", swap.fee.pay_in_fiat);
1330
+ }
1305
1331
  push("Payout fiat", swap.fee.payout_fiat);
1306
1332
  }
1307
1333
  }
@@ -1336,6 +1362,127 @@ function isCheckoutStateSource(value) {
1336
1362
  return "checkout_id" in value && "reference" in value && "invoice_id" in value && "invoice" in value && "transaction_state" in value && "phase" in value;
1337
1363
  }
1338
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
+
1339
1486
  // src/internal/wizard.ts
1340
1487
  import { getSwapRefundAddressError } from "@openreceive/core/swap-address";
1341
1488
  import {
@@ -1931,176 +2078,6 @@ function createPaymentWizardController(options = {}) {
1931
2078
  return new BrowserPaymentWizardController(options);
1932
2079
  }
1933
2080
 
1934
- // src/internal/log-level.ts
1935
- var BROWSER_LOG_LEVEL_ORDER = {
1936
- debug: 10,
1937
- info: 20,
1938
- warn: 30,
1939
- error: 40
1940
- };
1941
- var BROWSER_LOG_LEVEL_ALIASES = {
1942
- debug: "debug",
1943
- info: "info",
1944
- warn: "warn",
1945
- warning: "warn",
1946
- error: "error"
1947
- };
1948
- function browserLogLevelOrder(level) {
1949
- return BROWSER_LOG_LEVEL_ORDER[level];
1950
- }
1951
- function parseBrowserLogLevel(value) {
1952
- if (value === void 0 || value === null) return void 0;
1953
- const normalized = value.trim().toLowerCase();
1954
- if (normalized.length === 0) return void 0;
1955
- return BROWSER_LOG_LEVEL_ALIASES[normalized];
1956
- }
1957
- function resolveBrowserLogLevel(value, fallback = "info") {
1958
- return parseBrowserLogLevel(value) ?? fallback;
1959
- }
1960
- function readBrowserLogLevelFromEnvironment(source) {
1961
- if (source !== void 0) {
1962
- return resolveBrowserLogLevel(
1963
- source.LOG_LEVEL ?? source.VITE_LOG_LEVEL ?? source.NEXT_PUBLIC_LOG_LEVEL
1964
- );
1965
- }
1966
- const fromGlobal = parseBrowserLogLevel(readGlobalLogLevel());
1967
- if (fromGlobal !== void 0) return fromGlobal;
1968
- const metaEnv = readImportMetaEnv();
1969
- const parsedMeta = parseBrowserLogLevel(
1970
- metaEnv?.LOG_LEVEL ?? metaEnv?.VITE_LOG_LEVEL ?? metaEnv?.NEXT_PUBLIC_LOG_LEVEL
1971
- );
1972
- if (parsedMeta !== void 0) return parsedMeta;
1973
- const processEnv = typeof process !== "undefined" ? process.env : void 0;
1974
- return resolveBrowserLogLevel(processEnv?.LOG_LEVEL);
1975
- }
1976
- function readGlobalLogLevel() {
1977
- try {
1978
- const value = globalThis.__OPENRECEIVE_LOG_LEVEL__;
1979
- return typeof value === "string" ? value : void 0;
1980
- } catch {
1981
- return void 0;
1982
- }
1983
- }
1984
- function readImportMetaEnv() {
1985
- try {
1986
- return import.meta.env;
1987
- } catch {
1988
- return void 0;
1989
- }
1990
- }
1991
-
1992
- // src/internal/console-logger.ts
1993
- function createConsoleWriter(options) {
1994
- const target = options.console ?? console;
1995
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
1996
- return (level, prefix, event, message, fields) => {
1997
- const minLevel = options.minLevel === void 0 ? readBrowserLogLevelFromEnvironment() : resolveBrowserLogLevel(options.minLevel);
1998
- if (browserLogLevelOrder(level) < browserLogLevelOrder(minLevel)) {
1999
- return;
2000
- }
2001
- const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
2002
- const sink = target[method] ?? target.log;
2003
- const {
2004
- level: _level,
2005
- event: _event,
2006
- message: cleanMessage,
2007
- ...cleanFields
2008
- } = sanitizeBrowserLogEntry({ level, event, message, ...fields });
2009
- sink.call(
2010
- target,
2011
- `[${now().toISOString()}] ${level.toUpperCase()} [${prefix}] ${event}: ${cleanMessage}`,
2012
- cleanFields
2013
- );
2014
- };
2015
- }
2016
- function sanitizeBrowserLogEntry(entry) {
2017
- const clean = {};
2018
- for (const [key, value] of Object.entries(entry)) {
2019
- if (/secret|token|authorization|cookie|nwc/i.test(key)) {
2020
- clean[key] = "[REDACTED]";
2021
- } else {
2022
- clean[key] = sanitizeBrowserLogValue(value);
2023
- }
2024
- }
2025
- return clean;
2026
- }
2027
- function sanitizeBrowserLogValue(value) {
2028
- if (typeof value === "string") return redactBrowserSecrets(value);
2029
- if (Array.isArray(value)) return value.map(sanitizeBrowserLogValue);
2030
- if (typeof value !== "object" || value === null) return value;
2031
- const clean = {};
2032
- for (const [key, nested] of Object.entries(value)) {
2033
- if (/secret|token|authorization|cookie|nwc/i.test(key)) {
2034
- clean[key] = "[REDACTED]";
2035
- } else {
2036
- clean[key] = sanitizeBrowserLogValue(nested);
2037
- }
2038
- }
2039
- return clean;
2040
- }
2041
- function redactBrowserSecrets(value) {
2042
- return value.replace(/nostr\+walletconnect:\/\/[^\s"'`<>]+/g, "[REDACTED_NWC]").replace(/([?&](?:_or_evt|token|secret)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
2043
- }
2044
- function createBrowserConsoleLogger(options = {}) {
2045
- const prefix = options.prefix ?? "openreceive";
2046
- const write = createConsoleWriter(options);
2047
- return (entry) => {
2048
- const { level, event, message, ...fields } = entry;
2049
- write(level, prefix, event, message, fields);
2050
- };
2051
- }
2052
- function createAppBrowserConsoleLogger(options) {
2053
- const write = createConsoleWriter(options);
2054
- return (event, message, fields = {}, level = "info") => {
2055
- write(level, options.prefix, event, message, fields);
2056
- };
2057
- }
2058
- var defaultBrowserConsoleLogger;
2059
- function getDefaultBrowserConsoleLogger() {
2060
- defaultBrowserConsoleLogger ??= createBrowserConsoleLogger();
2061
- return defaultBrowserConsoleLogger;
2062
- }
2063
- function resolveBrowserLogger(logger) {
2064
- if (logger === false) return void 0;
2065
- if (logger !== void 0) return logger;
2066
- return getDefaultBrowserConsoleLogger();
2067
- }
2068
-
2069
- // src/internal/checkout-read.ts
2070
- import { nonEmptyString } from "@openreceive/core";
2071
- function optionalRecord(value) {
2072
- if (value === void 0) return void 0;
2073
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
2074
- throw new Error("OpenReceive metadata must be an object.");
2075
- }
2076
- return value;
2077
- }
2078
- function requiredInvoiceRail(value) {
2079
- if (value === "lightning" || value === "swap" || value === "checkout_lock") return value;
2080
- throw new TypeError("OpenReceive invoice rail must be lightning, swap, or checkout_lock.");
2081
- }
2082
- function requiredString(value, fieldName) {
2083
- const text = nonEmptyString(value);
2084
- if (text === void 0) {
2085
- throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2086
- }
2087
- return text;
2088
- }
2089
- function optionalSafeInteger(value, fieldName = "integer") {
2090
- if (value === void 0 || value === null) return void 0;
2091
- if (typeof value !== "number" || !Number.isSafeInteger(value)) {
2092
- throw new TypeError(`OpenReceive checkout response ${fieldName} must be a safe integer.`);
2093
- }
2094
- return value;
2095
- }
2096
- function requiredSafeInteger(value, fieldName) {
2097
- const integer = optionalSafeInteger(value, fieldName);
2098
- if (integer === void 0) {
2099
- throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
2100
- }
2101
- return integer;
2102
- }
2103
-
2104
2081
  // src/internal/checkout-log.ts
2105
2082
  import { compact } from "@openreceive/core";
2106
2083
  function checkoutLogFields(state) {
@@ -2181,6 +2158,41 @@ function emitBrowserLog(logger, level, event, message, fields = {}) {
2181
2158
  }
2182
2159
  }
2183
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
+
2184
2196
  // src/internal/checkout-state.ts
2185
2197
  function selectCheckoutDisplayInvoice(snapshot) {
2186
2198
  if (snapshot.active !== void 0 && snapshot.active.rail !== "checkout_lock") {
@@ -2355,15 +2367,14 @@ function normalizeCheckoutState(state, now) {
2355
2367
  const expiresInSeconds = base.expires_at === void 0 || now === void 0 ? void 0 : Math.max(0, base.expires_at - now);
2356
2368
  const phase = statePhase === "invoice_created" || statePhase === "verifying" ? expiresInSeconds === 0 ? "expired" : statePhase : statePhase;
2357
2369
  const settled = base.paid || base.transaction_state === "settled";
2370
+ const monitoring = checkoutMonitoring(base);
2358
2371
  return {
2359
2372
  ...base,
2360
2373
  ...deriveCheckoutStateLabels(base),
2361
2374
  phase,
2362
2375
  settled,
2363
- // A swap that expired, refunded, failed, or needs support review is over:
2364
- // the shadow invoice behind it will not be paid, so watchers stop polling
2365
- // both /payments/check and the provider instead of asking forever.
2366
- 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,
2367
2378
  ...expiresInSeconds === void 0 ? {} : { expires_in_seconds: expiresInSeconds }
2368
2379
  };
2369
2380
  }
@@ -2384,8 +2395,13 @@ function getCheckoutPhase(transactionState, workflowState) {
2384
2395
  }
2385
2396
  return "invoice_created";
2386
2397
  }
2387
- function isTerminalPhase(phase) {
2388
- 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 };
2389
2405
  }
2390
2406
  function snapshotFromCheckoutState(state) {
2391
2407
  const invoice = {
@@ -2545,6 +2561,7 @@ function normalizeRequestCheckoutOptions(options) {
2545
2561
  const reference = nonEmptyString2(options.reference);
2546
2562
  const metadata = optionalRecord(options.metadata);
2547
2563
  return {
2564
+ signal: options.signal,
2548
2565
  routes: checkoutRoutes(options.prefix),
2549
2566
  reference: reference ?? "",
2550
2567
  fetch: options.fetch,
@@ -2574,6 +2591,7 @@ async function requestCheckout(options) {
2574
2591
  const headers = request.headers === void 0 ? {} : request.headers;
2575
2592
  const response = await fetcher(request.routes.checkouts, {
2576
2593
  method: "POST",
2594
+ signal: request.signal,
2577
2595
  headers: requestHeaders(headers, request.csrfHeader),
2578
2596
  body: JSON.stringify(requestBody)
2579
2597
  });
@@ -2597,6 +2615,7 @@ async function prepareCheckout(options) {
2597
2615
  const headers = request.headers === void 0 ? {} : request.headers;
2598
2616
  const response = await fetcher(request.routes.checkoutsPrepare, {
2599
2617
  method: "POST",
2618
+ signal: request.signal,
2600
2619
  headers: requestHeaders(headers, request.csrfHeader),
2601
2620
  body: JSON.stringify({ reference: request.reference })
2602
2621
  });
@@ -2620,50 +2639,57 @@ function createStatusFetcher(options) {
2620
2639
  if (activePaymentHash === void 0 || !/^[0-9a-f]{64}$/i.test(activePaymentHash)) {
2621
2640
  return snapshot;
2622
2641
  }
2623
- const response = await fetcher(routes.paymentsCheck, {
2624
- method: "POST",
2625
- headers: requestHeaders(headers, options.csrfHeader),
2626
- body: JSON.stringify({
2627
- reference,
2628
- payment_hash: activePaymentHash
2629
- })
2630
- });
2631
- const body = await readJsonResponse(response, "Could not refresh invoice status.");
2632
- const payment = recordOrEmpty(body);
2633
2642
  const next = structuredClone(snapshot);
2634
2643
  if (next.active === void 0) return next;
2635
- const state = nonEmptyString2(payment.status) ?? "pending";
2636
- const paidAt = optionalSafeInteger(payment.paid_at, "paid_at");
2637
- let active = {
2638
- ...next.active,
2639
- transaction_state: state === "not_found" ? "pending" : state,
2640
- ...paidAt === void 0 ? {} : { settled_at: paidAt }
2641
- };
2642
- if (active.rail === "swap" && active.swap !== void 0 && state !== "settled" && // A terminal provider state is final; re-reading it only adds provider load.
2643
- !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) {
2644
2669
  try {
2645
2670
  const swapResponse = await fetcher(routes.swapsStatus, {
2646
2671
  method: "POST",
2647
2672
  headers: requestHeaders(headers, options.csrfHeader),
2648
2673
  body: JSON.stringify({ reference, payment_hash: activePaymentHash })
2649
2674
  });
2650
- const swapBody = recordOrEmpty(
2651
- await readJsonResponse(swapResponse, "Could not refresh swap status.")
2675
+ active = mergeSwapStatusIntoInvoice(
2676
+ active,
2677
+ recordOrEmpty(await readJsonResponse(swapResponse, "Could not refresh swap status."))
2652
2678
  );
2653
- active = mergeSwapStatusIntoInvoice(active, swapBody);
2654
2679
  swapStatusFailures = 0;
2655
2680
  } catch (error) {
2656
2681
  swapStatusFailures += 1;
2657
2682
  const definitive = error instanceof BrowserRequestError && error.retryable === false;
2658
- const swap = active.swap;
2659
- if (swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2683
+ if (active.swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2660
2684
  active = {
2661
2685
  ...active,
2662
- swap: { ...swap, attention: true, attention_reason: "swap_status_unavailable" }
2686
+ swap: { ...active.swap, attention: true, attention_reason: "swap_status_unavailable" }
2663
2687
  };
2664
2688
  }
2689
+ if (paymentError === void 0 && !checkoutMonitoring(active).payment) paymentError = error;
2665
2690
  }
2666
2691
  }
2692
+ const state = active.transaction_state;
2667
2693
  const paymentMethods2 = normalizePaymentMethods(payment.payment_methods);
2668
2694
  const others = next.invoices.filter(
2669
2695
  (entry) => entry.invoice_id !== active.invoice_id && entry.rail !== "checkout_lock"
@@ -2675,6 +2701,7 @@ function createStatusFetcher(options) {
2675
2701
  status: state === "settled" ? "paid" : state === "expired" || state === "failed" ? "expired" : "open",
2676
2702
  ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
2677
2703
  };
2704
+ if (paymentError !== void 0 && active.swap === void 0) throw paymentError;
2678
2705
  return snapshot;
2679
2706
  };
2680
2707
  }
@@ -3070,6 +3097,7 @@ async function requestJson(options, url, body) {
3070
3097
  const fetcher = options.fetch;
3071
3098
  const response = await fetcher(url, {
3072
3099
  method: "POST",
3100
+ signal: options.signal,
3073
3101
  headers: requestHeaders(options.headers, options.csrfHeader),
3074
3102
  body: JSON.stringify(body)
3075
3103
  });
@@ -3151,6 +3179,8 @@ var CheckoutWatcher = class {
3151
3179
  countdownTimer;
3152
3180
  pollTimer;
3153
3181
  running = false;
3182
+ generation = 0;
3183
+ pendingRefresh;
3154
3184
  pollInFlight = false;
3155
3185
  pollFailureCount = 0;
3156
3186
  pollBackoffUntil;
@@ -3217,6 +3247,9 @@ var CheckoutWatcher = class {
3217
3247
  }
3218
3248
  stop() {
3219
3249
  this.running = false;
3250
+ this.generation += 1;
3251
+ this.pollInFlight = false;
3252
+ this.pendingRefresh = void 0;
3220
3253
  this.stopCountdown();
3221
3254
  this.stopPolling();
3222
3255
  }
@@ -3255,8 +3288,10 @@ var CheckoutWatcher = class {
3255
3288
  if (refreshStatus === void 0 || current.reference.length === 0) {
3256
3289
  return current;
3257
3290
  }
3291
+ const generation = this.generation;
3258
3292
  try {
3259
- const fetched = await refreshStatus(current.reference);
3293
+ const fetched = await this.fetchSnapshot(current.reference);
3294
+ if (generation !== this.generation) return this.state ?? current;
3260
3295
  if (fetched === null) return current;
3261
3296
  const next = this.publishSnapshot(fetched);
3262
3297
  const nextState = createCheckoutState(next, {
@@ -3272,10 +3307,20 @@ var CheckoutWatcher = class {
3272
3307
  }
3273
3308
  return nextState;
3274
3309
  } catch (error) {
3310
+ if (generation !== this.generation) return this.state ?? current;
3275
3311
  this.options.onError?.(error);
3276
3312
  throw error;
3277
3313
  }
3278
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
+ }
3279
3324
  applyState(state) {
3280
3325
  if (!this.running) return;
3281
3326
  this.state = state;
@@ -3289,7 +3334,7 @@ var CheckoutWatcher = class {
3289
3334
  this.stop();
3290
3335
  return;
3291
3336
  }
3292
- if (state.settled || state.expires_at === void 0) {
3337
+ if (state.settled || state.expires_at === void 0 || state.expires_in_seconds === 0) {
3293
3338
  this.stopCountdown();
3294
3339
  } else if (this.countdownTimer === void 0) {
3295
3340
  this.countdownTimer = this.setInterval()(() => {
@@ -3305,7 +3350,7 @@ var CheckoutWatcher = class {
3305
3350
  }, 1e3);
3306
3351
  }
3307
3352
  const canPollAttempt = state.rail !== "checkout_lock" && typeof state.payment_hash === "string" && /^[0-9a-f]{64}$/i.test(state.payment_hash);
3308
- 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) {
3309
3354
  this.stopPolling();
3310
3355
  } else if (this.pollTimer === void 0) {
3311
3356
  this.pollTimer = this.setInterval()(() => {
@@ -3317,20 +3362,22 @@ var CheckoutWatcher = class {
3317
3362
  const refreshStatus = this.options.refreshStatus;
3318
3363
  const current = this.state;
3319
3364
  if (!this.running || refreshStatus === void 0 || current === void 0) return;
3320
- if (current.terminal || current.settled) {
3365
+ if (current.terminal) {
3321
3366
  this.stopPolling();
3322
3367
  return;
3323
3368
  }
3324
3369
  if (this.pollInFlight) return;
3325
3370
  if (this.pollBackoffUntil !== void 0 && this.now() < this.pollBackoffUntil) return;
3326
3371
  this.pollInFlight = true;
3372
+ const generation = this.generation;
3327
3373
  try {
3328
- const fetched = await refreshStatus(current.reference);
3374
+ const fetched = await this.fetchSnapshot(current.reference);
3375
+ if (generation !== this.generation) return;
3329
3376
  this.pollFailureCount = 0;
3330
3377
  this.pollBackoffUntil = void 0;
3331
3378
  if (fetched === null) return;
3332
3379
  if (!this.running || this.state === void 0) return;
3333
- if (this.state.settled || this.state.terminal) return;
3380
+ if (this.state.terminal) return;
3334
3381
  const next = this.publishSnapshot(fetched);
3335
3382
  this.applyState(
3336
3383
  createCheckoutState(next, {
@@ -3341,13 +3388,14 @@ var CheckoutWatcher = class {
3341
3388
  })
3342
3389
  );
3343
3390
  } catch (error) {
3391
+ if (generation !== this.generation) return;
3344
3392
  this.pollFailureCount += 1;
3345
3393
  const retryAfterSeconds = error instanceof BrowserRequestError ? error.retryAfterSeconds : void 0;
3346
3394
  const backoffSeconds = retryAfterSeconds ?? Math.min(60, 2 ** Math.min(this.pollFailureCount, 6));
3347
3395
  this.pollBackoffUntil = this.now() + backoffSeconds;
3348
3396
  this.options.onError?.(error);
3349
3397
  } finally {
3350
- this.pollInFlight = false;
3398
+ if (generation === this.generation) this.pollInFlight = false;
3351
3399
  }
3352
3400
  }
3353
3401
  stopCountdown() {
@@ -3374,6 +3422,7 @@ var BrowserCheckoutController = class {
3374
3422
  options;
3375
3423
  watcher;
3376
3424
  state;
3425
+ generation = 0;
3377
3426
  constructor(options) {
3378
3427
  this.options = options;
3379
3428
  this.watcher = this.createWatcher(options);
@@ -3383,6 +3432,7 @@ var BrowserCheckoutController = class {
3383
3432
  return this.state;
3384
3433
  }
3385
3434
  stop() {
3435
+ this.generation += 1;
3386
3436
  this.watcher.stop();
3387
3437
  }
3388
3438
  getState() {
@@ -3444,6 +3494,7 @@ var BrowserCheckoutController = class {
3444
3494
  if (paymentHash === void 0) {
3445
3495
  throw new Error(`No swap attempt ${options.attemptId} in this checkout.`);
3446
3496
  }
3497
+ const generation = this.generation;
3447
3498
  const invoice = await requestSwapRefund({
3448
3499
  fetch: this.options.fetch ?? globalThis.fetch,
3449
3500
  prefix,
@@ -3455,10 +3506,12 @@ var BrowserCheckoutController = class {
3455
3506
  refundAddress: options.refundAddress,
3456
3507
  confirm
3457
3508
  });
3509
+ if (generation !== this.generation) throw new DOMException("Checkout changed.", "AbortError");
3458
3510
  this.watcher.stageSwapInvoice(invoice);
3459
3511
  return invoice;
3460
3512
  }
3461
3513
  cancel() {
3514
+ this.generation += 1;
3462
3515
  this.state = this.watcher.cancel();
3463
3516
  emitBrowserLog(
3464
3517
  this.options.logger,
@@ -3734,6 +3787,7 @@ export {
3734
3787
  createTransactionDetails,
3735
3788
  createTransactionDetailsFromState,
3736
3789
  resolveTransactionDetailRows,
3790
+ createAppBrowserConsoleLogger,
3737
3791
  getWizardEmptyMessage,
3738
3792
  getRouteNetworkLabel,
3739
3793
  createWizardRouteAssetDisplays,
@@ -3765,7 +3819,6 @@ export {
3765
3819
  createPaymentWizardModel,
3766
3820
  updatePaymentWizardSelection,
3767
3821
  createPaymentWizardController,
3768
- createAppBrowserConsoleLogger,
3769
3822
  selectCheckoutDisplayInvoice,
3770
3823
  isPaidCheckoutSnapshot,
3771
3824
  isReusableLightningInvoice,