@openreceive/browser 0.2.1 → 0.2.2

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.
@@ -671,12 +671,22 @@ function assertInvoice(invoice) {
671
671
  }
672
672
 
673
673
  // src/internal/checkout-swap-view.ts
674
- import {
675
- formatDecimal as formatDecimal2,
676
- parseDecimal as parseDecimal2,
677
- payInAssetNetwork,
678
- unixSeconds as unixSeconds2
679
- } from "@openreceive/core";
674
+ import { formatDecimal as formatDecimal2, parseDecimal as parseDecimal2, payInAssetNetwork } from "@openreceive/core";
675
+
676
+ // src/internal/unix-seconds.ts
677
+ import { unixSeconds as unixSeconds2 } from "@openreceive/core";
678
+ var MILLISECONDS_THRESHOLD = 1e12;
679
+ function resolveNow(now) {
680
+ if (now === void 0) return unixSeconds2();
681
+ if (now > MILLISECONDS_THRESHOLD) {
682
+ throw new RangeError(
683
+ `OpenReceive \`now\` is a unix timestamp in SECONDS; received ${now}, which is milliseconds. Pass Math.floor(Date.now() / 1000).`
684
+ );
685
+ }
686
+ return now;
687
+ }
688
+
689
+ // src/internal/checkout-swap-view.ts
680
690
  function createSwapFeeBreakdown(fee) {
681
691
  if (fee === void 0) return void 0;
682
692
  let payIn;
@@ -712,7 +722,7 @@ function createSwapDisplayModel(invoice, options = {}) {
712
722
  swap.provider_expires_at,
713
723
  invoice.expires_at ?? swap.provider_expires_at
714
724
  );
715
- const expiresInSeconds = Math.max(0, expiresAt - (options.now ?? unixSeconds2()));
725
+ const expiresInSeconds = Math.max(0, expiresAt - resolveNow(options.now));
716
726
  const asset = getSwapAssetDisplay(swap.pay_in_asset);
717
727
  const depositAmount = formatDepositAmount(swap.deposit_amount);
718
728
  const networkWarningEmphasis = `${depositAmount} ${asset.assetLabel} on the ${asset.networkLabel} network`;
@@ -1099,1274 +1109,1316 @@ function createPaymentDataEntries(source) {
1099
1109
  return entries;
1100
1110
  }
1101
1111
 
1102
- // src/internal/checkout-transport.ts
1103
- import { isRecord, nonEmptyString as nonEmptyString2, recordOrEmpty } from "@openreceive/core";
1104
-
1105
- // src/internal/checkout-read.ts
1106
- import { nonEmptyString } from "@openreceive/core";
1107
- function optionalRecord(value) {
1108
- if (value === void 0) return void 0;
1109
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1110
- throw new Error("OpenReceive metadata must be an object.");
1111
- }
1112
- return value;
1112
+ // src/internal/wizard.ts
1113
+ import { getSwapRefundAddressError } from "@openreceive/core/swap-address";
1114
+ import {
1115
+ getPaymentWizardRoutes,
1116
+ listAssets,
1117
+ payTutorialUrls,
1118
+ providerIconUrls
1119
+ } from "@openreceive/provider-data";
1120
+ function getBitcoinAssets() {
1121
+ return listAssets().filter((asset) => asset.symbol === "btc" && asset.route !== void 0);
1113
1122
  }
1114
- function requiredInvoiceRail(value) {
1115
- if (value === "lightning" || value === "swap" || value === "checkout_lock") return value;
1116
- throw new TypeError("OpenReceive invoice rail must be lightning, swap, or checkout_lock.");
1123
+ function getDefaultBitcoinRoute() {
1124
+ const routes = [
1125
+ ...new Set(
1126
+ getBitcoinAssets().flatMap((asset) => asset.route === void 0 ? [] : [asset.route])
1127
+ )
1128
+ ];
1129
+ return routes.length === 1 ? routes[0] ?? null : null;
1117
1130
  }
1118
- function requiredString(value, fieldName) {
1119
- const text = nonEmptyString(value);
1120
- if (text === void 0) {
1121
- throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
1131
+ function getPaymentStatusText(phase) {
1132
+ if (phase === "settled") {
1133
+ return {
1134
+ title: checkoutLabels.paymentStatus.settledTitle,
1135
+ detail: checkoutLabels.paymentStatus.settledDetail
1136
+ };
1122
1137
  }
1123
- return text;
1124
- }
1125
- function optionalSafeInteger(value, fieldName = "integer") {
1126
- if (value === void 0 || value === null) return void 0;
1127
- if (typeof value !== "number" || !Number.isSafeInteger(value)) {
1128
- throw new TypeError(`OpenReceive checkout response ${fieldName} must be a safe integer.`);
1138
+ if (phase === "expired") {
1139
+ return {
1140
+ title: checkoutLabels.paymentStatus.expiredTitle,
1141
+ detail: checkoutLabels.paymentStatus.expiredDetail
1142
+ };
1129
1143
  }
1130
- return value;
1144
+ return {
1145
+ title: checkoutLabels.paymentStatus.waitingTitle,
1146
+ detail: checkoutLabels.paymentStatus.waitingDetail
1147
+ };
1131
1148
  }
1132
- function requiredSafeInteger(value, fieldName) {
1133
- const integer = optionalSafeInteger(value, fieldName);
1134
- if (integer === void 0) {
1135
- throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
1136
- }
1137
- return integer;
1149
+ function getWizardEmptyMessage() {
1150
+ return checkoutLabels.emptyBitcoin;
1138
1151
  }
1139
-
1140
- // src/internal/request-headers.ts
1141
- function requestHeaders(host) {
1152
+ function getCheckoutProviderOpenLabel() {
1153
+ return checkoutLabels.openProvider;
1154
+ }
1155
+ function getCheckoutProviderIcon(provider) {
1156
+ return providerIconUrls[provider.icon_path] ?? paymentIconUrls.crypto;
1157
+ }
1158
+ function getCheckoutProviderTutorials(provider) {
1159
+ return (provider.tutorials ?? []).map((tutorial) => ({
1160
+ index: tutorial.index,
1161
+ path: tutorial.path,
1162
+ image: payTutorialUrls[tutorial.path] ?? tutorial.path,
1163
+ caption: tutorial.caption
1164
+ }));
1165
+ }
1166
+ function getRouteNetworkLabel(routeId) {
1167
+ return routeId === "lightning" || routeId === "btc-lightning" ? checkoutLabels.lightningNetwork : routeId;
1168
+ }
1169
+ function createWizardRouteAssetDisplays(assets, options = {}) {
1170
+ return assets.map((asset) => {
1171
+ const id = asset.route ?? asset.symbol;
1172
+ return {
1173
+ id,
1174
+ label: asset.label,
1175
+ subtitle: getRouteNetworkLabel(id),
1176
+ icon: getRouteIcon(asset),
1177
+ selected: options.selectedRoute === id
1178
+ };
1179
+ });
1180
+ }
1181
+ function createWizardRouteDisplays(routes, options = {}) {
1182
+ return routes.map((route) => ({
1183
+ key: getWizardRouteDisplayKey(route),
1184
+ title: getWizardRouteDisplayTitle(route),
1185
+ subtitle: getWizardRouteDisplaySubtitle(route),
1186
+ providers: (options.providerPreviewLimit === void 0 ? route.providers : route.providers.slice(0, options.providerPreviewLimit)).map((entry) => createWizardProviderDisplay(entry))
1187
+ }));
1188
+ }
1189
+ function getWizardRouteDisplayKey(route) {
1190
+ return route.route.id;
1191
+ }
1192
+ function getWizardRouteDisplayTitle(route) {
1193
+ return route.route.label;
1194
+ }
1195
+ function getWizardRouteDisplaySubtitle(route) {
1196
+ return route.route.symbol.toUpperCase();
1197
+ }
1198
+ function createWizardProviderDisplay(entry) {
1142
1199
  return {
1143
- "Content-Type": "application/json",
1144
- ...csrfTokenHeader(),
1145
- ...host
1200
+ id: entry.provider.id,
1201
+ name: entry.provider.name,
1202
+ kind: entry.provider.kind,
1203
+ url: entry.provider.lightning_docs_url ?? entry.provider.url,
1204
+ icon: getCheckoutProviderIcon(entry.provider),
1205
+ tutorials: getCheckoutProviderTutorials(entry.provider),
1206
+ copyLabel: checkoutLabels.copyInvoice,
1207
+ copiedLabel: checkoutLabels.copied,
1208
+ openLabel: getCheckoutProviderOpenLabel()
1146
1209
  };
1147
1210
  }
1148
- function csrfTokenHeader() {
1149
- if (typeof document === "undefined") return {};
1150
- const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content");
1151
- return token ? { "X-CSRF-Token": token } : {};
1211
+ function getPaymentMethodIcon(method) {
1212
+ return paymentIconUrls[paymentMethodIconIds[method]];
1152
1213
  }
1153
-
1154
- // src/internal/routes.ts
1155
- function checkoutRoutes(prefix) {
1156
- if (typeof prefix !== "string") {
1157
- throw new TypeError(
1158
- `OpenReceive requires \`prefix\`, the base path the shipped router is mounted at (for example "/openreceive"); every checkout, status and swap route is derived from it. Received ${prefix === null ? "null" : typeof prefix}.`
1159
- );
1214
+ function getAssetIcon(symbol) {
1215
+ return paymentIconUrls[assetIconIds[symbol] ?? "crypto"];
1216
+ }
1217
+ function getNetworkIcon(networkLabel) {
1218
+ const key = networkLabel.trim().toLowerCase();
1219
+ if (key === "tron" || key === "trx") return paymentIconUrls.trx;
1220
+ if (key === "solana" || key === "sol") return paymentIconUrls.sol;
1221
+ if (key === "ethereum" || key === "eth") return paymentIconUrls.eth;
1222
+ return paymentIconUrls.crypto;
1223
+ }
1224
+ function getSwapOptionIcon(option) {
1225
+ return getAssetIcon(option.label.trim().toLowerCase());
1226
+ }
1227
+ function groupSwapOptionsByLabel(options) {
1228
+ const groups = [];
1229
+ const indexByLabel = /* @__PURE__ */ new Map();
1230
+ for (const option of options) {
1231
+ const key = option.label.trim().toUpperCase();
1232
+ const existing = indexByLabel.get(key);
1233
+ if (existing === void 0) {
1234
+ indexByLabel.set(key, groups.length);
1235
+ groups.push({ label: option.label, options: [option] });
1236
+ continue;
1237
+ }
1238
+ const group = groups[existing];
1239
+ if (group === void 0) continue;
1240
+ groups[existing] = { label: group.label, options: [...group.options, option] };
1160
1241
  }
1161
- const base = prefix.replace(/\/+$/, "");
1162
- return {
1163
- checkouts: `${base}/checkouts`,
1164
- checkoutsPrepare: `${base}/checkouts/prepare`,
1165
- paymentsCheck: `${base}/payments/check`,
1166
- swaps: `${base}/swaps`,
1167
- swapsQuote: `${base}/swaps/quote`,
1168
- swapsStatus: `${base}/swaps/status`,
1169
- swapsRefunds: `${base}/swaps/refunds`
1170
- };
1242
+ return groups;
1171
1243
  }
1172
-
1173
- // src/internal/checkout-transport.ts
1174
- var SWAP_STATUS_FAILURE_LIMIT = 3;
1175
- var BrowserRequestError = class extends Error {
1176
- status;
1177
- code;
1178
- retryable;
1179
- retryAfterSeconds;
1180
- constructor(message, options) {
1181
- super(message);
1182
- this.name = "BrowserRequestError";
1183
- this.status = options.status;
1184
- if (options.code !== void 0) this.code = options.code;
1185
- if (options.retryable !== void 0) this.retryable = options.retryable;
1186
- if (options.retryAfterSeconds !== void 0) this.retryAfterSeconds = options.retryAfterSeconds;
1244
+ var OPENRECEIVE_METHOD_GRID_ORDER = [
1245
+ { kind: "method", id: "bitcoin" },
1246
+ { kind: "swap", label: "USDT" },
1247
+ { kind: "swap", label: "USDC" },
1248
+ { kind: "swap", label: "SOL" },
1249
+ { kind: "swap", label: "ETH" }
1250
+ ];
1251
+ function buildMethodGridEntries(paymentMethods2, swapOptions) {
1252
+ const swapGroups = groupSwapOptionsByLabel(swapOptions);
1253
+ if (swapGroups.length === 0) {
1254
+ return paymentMethods2.map((method) => ({ kind: "method", method }));
1187
1255
  }
1188
- };
1189
- async function readJsonResponse(response, fallbackMessage) {
1190
- let body;
1191
- try {
1192
- body = await response.json();
1193
- } catch {
1194
- if (response.ok) {
1195
- throw new BrowserRequestError(`${fallbackMessage} The server returned a non-JSON body.`, {
1196
- status: response.status,
1197
- retryable: false
1198
- });
1256
+ const methodsById = new Map(paymentMethods2.map((method) => [method.id, method]));
1257
+ const groupsByLabel = new Map(
1258
+ swapGroups.map((group) => [group.label.trim().toUpperCase(), group])
1259
+ );
1260
+ const usedMethodIds = /* @__PURE__ */ new Set();
1261
+ const usedSwapLabels = /* @__PURE__ */ new Set();
1262
+ const entries = [];
1263
+ for (const slot of OPENRECEIVE_METHOD_GRID_ORDER) {
1264
+ if (slot.kind === "method") {
1265
+ const method = methodsById.get(slot.id);
1266
+ if (method === void 0) continue;
1267
+ usedMethodIds.add(method.id);
1268
+ entries.push({ kind: "method", method });
1269
+ continue;
1199
1270
  }
1200
- body = void 0;
1271
+ const group = groupsByLabel.get(slot.label);
1272
+ if (group === void 0) continue;
1273
+ usedSwapLabels.add(slot.label);
1274
+ entries.push({ kind: "swap", group });
1201
1275
  }
1202
- if (!response.ok) {
1203
- const record = body !== null && typeof body === "object" ? body : void 0;
1204
- const retryAfterRaw = Number(response.headers?.get?.("retry-after") ?? Number.NaN);
1205
- throw new BrowserRequestError(
1206
- typeof record?.message === "string" ? record.message : fallbackMessage,
1207
- {
1208
- status: response.status,
1209
- ...typeof record?.code === "string" ? { code: record.code } : {},
1210
- ...typeof record?.retryable === "boolean" ? { retryable: record.retryable } : {},
1211
- ...Number.isFinite(retryAfterRaw) && retryAfterRaw > 0 ? { retryAfterSeconds: retryAfterRaw } : {}
1212
- }
1213
- );
1276
+ for (const method of paymentMethods2) {
1277
+ if (usedMethodIds.has(method.id)) continue;
1278
+ entries.push({ kind: "method", method });
1214
1279
  }
1215
- return body;
1280
+ for (const group of swapGroups) {
1281
+ const key = group.label.trim().toUpperCase();
1282
+ if (usedSwapLabels.has(key)) continue;
1283
+ entries.push({ kind: "swap", group });
1284
+ }
1285
+ return entries;
1216
1286
  }
1217
- function normalizeRequestCheckoutOptions(options) {
1218
- const reference = nonEmptyString2(options.reference);
1219
- const metadata = optionalRecord(options.metadata);
1220
- return {
1221
- routes: checkoutRoutes(options.prefix),
1222
- reference: reference ?? "",
1223
- fetch: options.fetch,
1224
- headers: options.headers,
1225
- ...options.memo === void 0 ? {} : { memo: options.memo },
1226
- ...metadata === void 0 ? {} : { metadata }
1227
- };
1287
+ function swapPickerKey(label) {
1288
+ return `swap:${label.trim().toUpperCase()}`;
1228
1289
  }
1229
- async function requestCheckout(options) {
1230
- const request = normalizeRequestCheckoutOptions(options);
1231
- if (request.reference.length === 0) {
1232
- throw new Error("OpenReceive checkout creation requires reference.");
1290
+ function parseMethodPickerKey(key) {
1291
+ if (!key.startsWith("method:")) return null;
1292
+ return { kind: "method", methodId: key.slice("method:".length) };
1293
+ }
1294
+ function parseSwapPickerKey(key) {
1295
+ if (!key.startsWith("swap:")) return null;
1296
+ return { kind: "swap", label: key.slice("swap:".length) };
1297
+ }
1298
+ function paymentAccentId(labelOrMethodId) {
1299
+ const key = labelOrMethodId.trim().toLowerCase();
1300
+ if (key === "bitcoin" || key === "btc") return "bitcoin";
1301
+ if (key === "usdt") return "usdt";
1302
+ if (key === "usdc") return "usdc";
1303
+ if (key === "sol" || key === "solana") return "sol";
1304
+ if (key === "eth" || key === "ethereum") return "eth";
1305
+ return "default";
1306
+ }
1307
+ var assetActiveClassByAccent = {
1308
+ bitcoin: orClasses.methodCardActiveBitcoin,
1309
+ usdt: orClasses.methodCardActiveUsdt,
1310
+ usdc: orClasses.methodCardActiveUsdc,
1311
+ sol: orClasses.methodCardActiveSol,
1312
+ eth: orClasses.methodCardActiveEth,
1313
+ default: orClasses.methodCardActiveDefault
1314
+ };
1315
+ var networkActiveClassByAccent = {
1316
+ usdt: orClasses.methodNetworkButtonActiveUsdt,
1317
+ usdc: orClasses.methodNetworkButtonActiveUsdc,
1318
+ default: orClasses.methodNetworkButtonActiveDefault
1319
+ };
1320
+ function assetButtonClasses(options) {
1321
+ const base = options.disabled ? orClasses.methodCardUnavailable : orClasses.methodCardReady;
1322
+ if (!options.selected || options.disabled) return base;
1323
+ return `${base} ${assetActiveClassByAccent[options.accent]}`;
1324
+ }
1325
+ function networkButtonClasses(options) {
1326
+ if (options.disabled) return orClasses.methodNetworkButtonUnavailable;
1327
+ if (!options.selected) return orClasses.methodNetworkButton;
1328
+ const accent = options.accent === "usdt" || options.accent === "usdc" ? options.accent : "default";
1329
+ return `${orClasses.methodNetworkButton} ${networkActiveClassByAccent[accent]}`;
1330
+ }
1331
+ function networkMobileRevealClasses(accent) {
1332
+ if (accent === "usdt") return orClasses.methodNetworkRevealMobileUsdt;
1333
+ if (accent === "usdc") return orClasses.methodNetworkRevealMobileUsdc;
1334
+ return orClasses.methodNetworkRevealMobile;
1335
+ }
1336
+ function networkCheckClasses(accent) {
1337
+ return accent === "usdc" ? orClasses.methodNetworkCheckUsdc : orClasses.methodNetworkCheck;
1338
+ }
1339
+ function networkSummaryIconClasses(accent) {
1340
+ return accent === "usdc" ? orClasses.methodNetworkSummaryIconUsdc : orClasses.methodNetworkSummaryIcon;
1341
+ }
1342
+ function formatNetworkSummary(assetLabel, networkLabel) {
1343
+ return checkoutLabels.networkSummary.replace("{asset}", assetLabel).replace("{network}", networkLabel);
1344
+ }
1345
+ function formatChooseNetworkHeading(assetLabel) {
1346
+ return checkoutLabels.chooseAssetNetwork.replace("{asset}", assetLabel);
1347
+ }
1348
+ function resolvePreservedNetworkSelection(options) {
1349
+ const nextKey = options.nextGroup.label.trim().toUpperCase();
1350
+ const current = options.selectedNetworks[nextKey];
1351
+ if (current !== void 0 && options.nextGroup.options.some(
1352
+ (option) => option.pay_in_asset === current && option.available !== false
1353
+ )) {
1354
+ return current;
1233
1355
  }
1234
- const fetcher = request.fetch ?? globalThis.fetch;
1235
- if (fetcher === void 0) {
1236
- throw new Error("OpenReceive checkout creation requires fetch.");
1356
+ const previous = options.previousGroup;
1357
+ if (previous === void 0) return void 0;
1358
+ const previousKey = previous.label.trim().toUpperCase();
1359
+ const previousAsset = options.selectedNetworks[previousKey];
1360
+ if (previousAsset === void 0) return void 0;
1361
+ const previousOption = previous.options.find((option) => option.pay_in_asset === previousAsset);
1362
+ if (previousOption === void 0) return void 0;
1363
+ const match = options.nextGroup.options.find(
1364
+ (option) => option.network_label === previousOption.network_label && option.available !== false
1365
+ );
1366
+ return match?.pay_in_asset;
1367
+ }
1368
+ function findSwapGridGroup(entries, pickerKey) {
1369
+ if (pickerKey === null || pickerKey === void 0) return void 0;
1370
+ const parsed = parseSwapPickerKey(pickerKey);
1371
+ if (parsed === null) return void 0;
1372
+ const entry = entries.find(
1373
+ (entry2) => entry2.kind === "swap" && entry2.group.label.trim().toUpperCase() === parsed.label
1374
+ );
1375
+ return entry?.kind === "swap" ? entry.group : void 0;
1376
+ }
1377
+ function updateSelectedSwapNetworks(options) {
1378
+ const nextGroup = findSwapGridGroup(options.entries, options.nextKey);
1379
+ if (nextGroup === void 0 || nextGroup.options.length <= 1) {
1380
+ return options.selectedNetworks;
1237
1381
  }
1238
- if (request.memo !== void 0 && request.memo.length > 500) {
1239
- throw new Error("OpenReceive memo must be 500 characters or fewer.");
1382
+ const previousGroup = findSwapGridGroup(options.entries, options.previousKey);
1383
+ const preserved = resolvePreservedNetworkSelection({
1384
+ previousGroup,
1385
+ nextGroup,
1386
+ selectedNetworks: options.selectedNetworks
1387
+ });
1388
+ const groupKey = nextGroup.label.trim().toUpperCase();
1389
+ if (preserved === void 0) {
1390
+ const { [groupKey]: _removed, ...rest } = options.selectedNetworks;
1391
+ return rest;
1240
1392
  }
1241
- const requestBody = {
1242
- reference: request.reference,
1243
- ...request.memo === void 0 ? {} : { memo: request.memo },
1244
- ...request.metadata === void 0 ? {} : { metadata: structuredClone(request.metadata) }
1393
+ return { ...options.selectedNetworks, [groupKey]: preserved };
1394
+ }
1395
+ function wizardNetworkGroupIds(groupKey) {
1396
+ const slug = groupKey.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
1397
+ return { panelId: `network-panel-${slug}`, headingId: `network-heading-${slug}` };
1398
+ }
1399
+ function createSwapUnavailableModel(option, checkout) {
1400
+ const detail = swapOptionLimitMessage(option, checkout) ?? option.unavailable_message ?? checkoutLabels.swapUnavailableFallback.replace("{asset}", option.label);
1401
+ const range = option.minimum_pay_amount === void 0 ? void 0 : option.maximum_pay_amount === void 0 ? checkoutLabels.swapUnavailableMinimumOnly.replace("{minimum}", option.minimum_pay_amount).replace("{asset}", option.label) : checkoutLabels.swapUnavailableRange.replace("{minimum}", option.minimum_pay_amount).replace("{maximum}", option.maximum_pay_amount).replace("{asset}", option.label);
1402
+ return {
1403
+ title: checkoutLabels.swapUnavailableTitle.replace("{asset}", option.label),
1404
+ detail,
1405
+ ...range === void 0 ? {} : { range },
1406
+ hint: checkoutLabels.swapUnavailableHint
1245
1407
  };
1246
- const headers = request.headers === void 0 ? {} : request.headers;
1247
- const response = await fetcher(request.routes.checkouts, {
1248
- method: "POST",
1249
- headers: requestHeaders(headers),
1250
- body: JSON.stringify(requestBody)
1251
- });
1252
- const body = await readJsonResponse(response, "Could not create checkout.");
1253
- const snapshot = checkoutSnapshotFromResponseBody(body);
1254
- const responseInvoice = snapshot.active;
1255
- if (isRecord(responseInvoice) && typeof responseInvoice.invoice === "string") {
1256
- assertDisplayInvoice(responseInvoice.invoice);
1408
+ }
1409
+ function swapOptionLimitMessage(option, checkout) {
1410
+ if (option.available !== false) return void 0;
1411
+ if (option.unavailable_reason === "amount_too_small") {
1412
+ const fiat = checkout === void 0 ? void 0 : formatSwapLimit(checkout, option.minimum_invoice_amount_msats, "ceil");
1413
+ if (fiat !== void 0) return `Minimum amount ${fiat}`;
1414
+ if (option.minimum_pay_amount !== void 0) {
1415
+ return `Minimum ${option.minimum_pay_amount} ${option.label}`;
1416
+ }
1417
+ }
1418
+ if (option.unavailable_reason === "amount_too_large") {
1419
+ const fiat = checkout === void 0 ? void 0 : formatSwapLimit(checkout, option.maximum_invoice_amount_msats, "floor");
1420
+ if (fiat !== void 0) return `Maximum amount ${fiat}`;
1421
+ if (option.maximum_pay_amount !== void 0) {
1422
+ return `Maximum ${option.maximum_pay_amount} ${option.label}`;
1423
+ }
1257
1424
  }
1258
- return snapshot;
1425
+ return option.unavailable_message;
1259
1426
  }
1260
- async function prepareCheckout(options) {
1261
- const request = normalizeRequestCheckoutOptions(options);
1262
- if (request.reference.length === 0) {
1263
- throw new Error("OpenReceive checkout prepare requires reference.");
1427
+ function swapGroupLimitOption(options) {
1428
+ if (options.length === 0) return void 0;
1429
+ const unavailable = options.filter((option) => option.available === false);
1430
+ const tooSmall = unavailable.filter((option) => option.unavailable_reason === "amount_too_small");
1431
+ const candidates = tooSmall.length > 0 ? tooSmall : unavailable.length > 0 ? unavailable : options;
1432
+ let best = candidates[0];
1433
+ for (const option of candidates) {
1434
+ if (best === void 0) {
1435
+ best = option;
1436
+ continue;
1437
+ }
1438
+ const bestMin = best.minimum_invoice_amount_msats;
1439
+ const optionMin = option.minimum_invoice_amount_msats;
1440
+ if (optionMin === void 0) continue;
1441
+ if (bestMin === void 0 || optionMin < bestMin) best = option;
1264
1442
  }
1265
- const fetcher = request.fetch ?? globalThis.fetch;
1266
- if (fetcher === void 0) {
1267
- throw new Error("OpenReceive checkout prepare requires fetch.");
1443
+ return best;
1444
+ }
1445
+ function getSwapRefundFormError(payInAsset, address, networkLabel) {
1446
+ if (address.length === 0) return "Enter a refund address.";
1447
+ return getSwapRefundAddressError(payInAsset, address, networkLabel);
1448
+ }
1449
+ function getRouteIcon(asset) {
1450
+ const routeId = asset.route ?? asset.symbol;
1451
+ if (asset.symbol === "btc" && routeId.includes("lightning")) {
1452
+ return paymentIconUrls.lightning;
1268
1453
  }
1269
- const headers = request.headers === void 0 ? {} : request.headers;
1270
- const response = await fetcher(request.routes.checkoutsPrepare, {
1271
- method: "POST",
1272
- headers: requestHeaders(headers),
1273
- body: JSON.stringify({ reference: request.reference })
1454
+ return getAssetIcon(asset.symbol);
1455
+ }
1456
+ function createPaymentWizardState(request) {
1457
+ const selectedRouteId = request.selectedMethod === "bitcoin" ? request.selectedBitcoinRoute ?? getDefaultBitcoinRoute() : null;
1458
+ const routes = selectedRouteId === null ? [] : getPaymentWizardRoutes({ route: selectedRouteId });
1459
+ return {
1460
+ selectedRouteId,
1461
+ routes
1462
+ };
1463
+ }
1464
+ function createPaymentWizardSelection() {
1465
+ return {
1466
+ selectedMethod: null,
1467
+ selectedBitcoinRoute: null
1468
+ };
1469
+ }
1470
+ function createPaymentWizardModel(selection) {
1471
+ const wizard = createPaymentWizardState({
1472
+ selectedMethod: selection.selectedMethod,
1473
+ selectedBitcoinRoute: selection.selectedBitcoinRoute
1274
1474
  });
1275
- const body = await readJsonResponse(response, "Could not prepare checkout.");
1276
- return checkoutLockSnapshotFromPrepareBody(body, request.reference);
1475
+ const routeAssets = selection.selectedMethod === "bitcoin" ? getBitcoinAssets() : [];
1476
+ const selectedRoute = wizard.selectedRouteId;
1477
+ return {
1478
+ selection,
1479
+ wizard,
1480
+ routeAssets,
1481
+ selectedRoute
1482
+ };
1277
1483
  }
1278
- function createStatusFetcher(options) {
1279
- let snapshot = options.snapshot;
1280
- let swapStatusFailures = 0;
1281
- const routes = checkoutRoutes(options.prefix);
1282
- return async (reference) => {
1283
- if (reference.length === 0) {
1284
- throw new Error("OpenReceive status refresh requires reference.");
1285
- }
1286
- const fetcher = options.fetch ?? globalThis.fetch;
1287
- if (fetcher === void 0) {
1288
- throw new Error("OpenReceive status refresh requires fetch.");
1289
- }
1290
- const headers = options.headers === void 0 ? {} : options.headers;
1291
- const activePaymentHash = nonEmptyString2(snapshot.active?.payment_hash) ?? (snapshot.active?.rail === "checkout_lock" ? void 0 : nonEmptyString2(snapshot.active?.invoice_id));
1292
- if (activePaymentHash === void 0 || !/^[0-9a-f]{64}$/i.test(activePaymentHash)) {
1293
- return snapshot;
1484
+ function updatePaymentWizardSelection(selection, action) {
1485
+ switch (action.type) {
1486
+ case "select_method": {
1487
+ return {
1488
+ ...selection,
1489
+ selectedMethod: action.method,
1490
+ selectedBitcoinRoute: action.method === "bitcoin" ? selection.selectedBitcoinRoute ?? getDefaultBitcoinRoute() : selection.selectedBitcoinRoute
1491
+ };
1294
1492
  }
1295
- const response = await fetcher(routes.paymentsCheck, {
1296
- method: "POST",
1297
- headers: requestHeaders(headers),
1298
- body: JSON.stringify({
1299
- reference,
1300
- payment_hash: activePaymentHash
1301
- })
1302
- });
1303
- const body = await readJsonResponse(response, "Could not refresh invoice status.");
1304
- const payment = recordOrEmpty(body);
1305
- const next = structuredClone(snapshot);
1306
- if (next.active === void 0) return next;
1307
- const state = nonEmptyString2(payment.status) ?? "pending";
1308
- const paidAt = optionalSafeInteger(payment.paid_at, "paid_at");
1309
- let active = {
1310
- ...next.active,
1311
- transaction_state: state === "not_found" ? "pending" : state,
1312
- ...paidAt === void 0 ? {} : { settled_at: paidAt }
1313
- };
1314
- if (active.rail === "swap" && active.swap !== void 0 && state !== "settled" && // A terminal provider state is final; re-reading it only adds provider load.
1315
- !isTerminalSwapProviderState(nonEmptyString2(active.swap.provider_state))) {
1316
- try {
1317
- const swapResponse = await fetcher(routes.swapsStatus, {
1318
- method: "POST",
1319
- headers: requestHeaders(headers),
1320
- body: JSON.stringify({ reference, payment_hash: activePaymentHash })
1321
- });
1322
- const swapBody = recordOrEmpty(
1323
- await readJsonResponse(swapResponse, "Could not refresh swap status.")
1324
- );
1325
- active = mergeSwapStatusIntoInvoice(active, swapBody);
1326
- swapStatusFailures = 0;
1327
- } catch (error) {
1328
- swapStatusFailures += 1;
1329
- const definitive = error instanceof BrowserRequestError && error.retryable === false;
1330
- const swap = active.swap;
1331
- if (swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
1332
- active = {
1333
- ...active,
1334
- swap: { ...swap, attention: true, attention_reason: "swap_status_unavailable" }
1335
- };
1336
- }
1493
+ case "change_method": {
1494
+ return {
1495
+ ...selection,
1496
+ selectedMethod: null,
1497
+ selectedBitcoinRoute: null
1498
+ };
1499
+ }
1500
+ case "change_route": {
1501
+ if (selection.selectedMethod === "bitcoin") {
1502
+ return {
1503
+ ...selection,
1504
+ selectedBitcoinRoute: null
1505
+ };
1337
1506
  }
1507
+ return selection;
1508
+ }
1509
+ case "select_route": {
1510
+ if (selection.selectedMethod === "bitcoin") {
1511
+ return {
1512
+ ...selection,
1513
+ selectedBitcoinRoute: action.route
1514
+ };
1515
+ }
1516
+ return selection;
1338
1517
  }
1339
- const paymentMethods2 = normalizePaymentMethods(payment.payment_methods);
1340
- const others = next.invoices.filter(
1341
- (entry) => entry.invoice_id !== active.invoice_id && entry.rail !== "checkout_lock"
1342
- );
1343
- snapshot = {
1344
- ...next,
1345
- active,
1346
- invoices: [active, ...others],
1347
- status: state === "settled" ? "paid" : state === "expired" || state === "failed" ? "expired" : "open",
1348
- ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
1349
- };
1350
- return snapshot;
1351
- };
1352
- }
1353
- function mergeSwapStatusIntoInvoice(invoice, status) {
1354
- const swap = invoice.swap;
1355
- if (swap === void 0) return invoice;
1356
- const merged = { ...swap };
1357
- const providerState = nonEmptyString2(status.provider_state);
1358
- if (providerState !== void 0) merged.provider_state = providerState;
1359
- const providerExpiresAt = optionalSafeInteger(status.provider_expires_at, "provider_expires_at");
1360
- if (providerExpiresAt !== void 0) merged.provider_expires_at = providerExpiresAt;
1361
- for (const key of [
1362
- "deposit_tx_id",
1363
- "payout_tx_id",
1364
- "refund_tx_id",
1365
- "refund_reason",
1366
- "refund_amount",
1367
- "deposit_received_amount",
1368
- "refund_address",
1369
- "attention_reason"
1370
- ]) {
1371
- if (typeof status[key] === "string") merged[key] = status[key];
1372
1518
  }
1373
- if (typeof status.attention === "boolean") merged.attention = status.attention;
1374
- return {
1375
- ...invoice,
1376
- swap: merged,
1377
- ...providerExpiresAt === void 0 ? {} : { expires_at: providerExpiresAt }
1378
- };
1379
1519
  }
1380
- function checkoutLockSnapshotFromPrepareBody(body, fallbackReference) {
1381
- const record = recordOrEmpty(body);
1382
- const reference = requiredString(record.reference ?? fallbackReference, "reference");
1383
- const amountMsats = requiredSafeInteger(record.amount_msats, "amount_msats");
1384
- const lockId = `lock:${reference}`;
1385
- const lockInvoice = {
1386
- invoice_id: lockId,
1387
- rail: "checkout_lock",
1388
- amount_msats: amountMsats,
1389
- transaction_state: "pending",
1390
- workflow_state: "invoice_created"
1391
- };
1392
- const fiatQuote = record.fiat_quote;
1393
- const fiatRecord = isRecord(fiatQuote) && isRecord(fiatQuote.fiat) ? fiatQuote.fiat : void 0;
1394
- const fiatCurrency = nonEmptyString2(fiatRecord?.currency);
1395
- const fiatValue = nonEmptyString2(fiatRecord?.value);
1396
- const fiat = fiatCurrency === void 0 || fiatValue === void 0 ? void 0 : { currency: fiatCurrency, value: fiatValue };
1397
- const paymentMethods2 = normalizePaymentMethods(record.payment_methods);
1398
- return {
1399
- checkout_id: lockId,
1400
- reference,
1401
- status: "open",
1402
- amount_msats: amountMsats,
1403
- ...fiat === void 0 ? {} : { fiat },
1404
- active: lockInvoice,
1405
- invoices: [lockInvoice],
1406
- ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
1407
- };
1520
+ var BrowserPaymentWizardController = class {
1521
+ options;
1522
+ selection;
1523
+ constructor(options = {}) {
1524
+ this.options = options;
1525
+ this.selection = options.selection ?? createPaymentWizardSelection();
1526
+ }
1527
+ getSelection() {
1528
+ return this.selection;
1529
+ }
1530
+ getModel() {
1531
+ return createPaymentWizardModel(this.selection);
1532
+ }
1533
+ update(action) {
1534
+ const next = updatePaymentWizardSelection(this.selection, action);
1535
+ this.selection = next;
1536
+ this.options.onSelection?.(next);
1537
+ return next;
1538
+ }
1539
+ selectMethod(method) {
1540
+ return this.update({
1541
+ type: "select_method",
1542
+ method
1543
+ });
1544
+ }
1545
+ changeMethod() {
1546
+ return this.update({
1547
+ type: "change_method"
1548
+ });
1549
+ }
1550
+ selectRoute(route) {
1551
+ return this.update({
1552
+ type: "select_route",
1553
+ route
1554
+ });
1555
+ }
1556
+ };
1557
+ function createPaymentWizardController(options = {}) {
1558
+ return new BrowserPaymentWizardController(options);
1408
1559
  }
1409
- function checkoutSnapshotFromResponseBody(body) {
1410
- const record = recordOrEmpty(body);
1411
- const wrapped = recordOrEmpty(record.checkout);
1412
- return checkoutSnapshot(wrapped);
1560
+
1561
+ // src/internal/log-level.ts
1562
+ var BROWSER_LOG_LEVEL_ORDER = {
1563
+ debug: 10,
1564
+ info: 20,
1565
+ warn: 30,
1566
+ error: 40
1567
+ };
1568
+ var BROWSER_LOG_LEVEL_ALIASES = {
1569
+ debug: "debug",
1570
+ info: "info",
1571
+ warn: "warn",
1572
+ warning: "warn",
1573
+ error: "error"
1574
+ };
1575
+ function browserLogLevelOrder(level) {
1576
+ return BROWSER_LOG_LEVEL_ORDER[level];
1413
1577
  }
1414
- function checkoutSnapshot(checkout) {
1415
- const paymentHash = requiredString(checkout.payment_hash, "payment_hash");
1416
- const reference = requiredString(checkout.reference, "reference");
1417
- const amountMsats = requiredSafeInteger(checkout.amount_msats, "amount_msats");
1418
- const invoice = {
1419
- invoice_id: paymentHash,
1420
- rail: "lightning",
1421
- invoice: requiredString(checkout.bolt11, "bolt11"),
1422
- payment_hash: paymentHash,
1423
- amount_msats: amountMsats,
1424
- transaction_state: "pending",
1425
- workflow_state: "invoice_created",
1426
- expires_at: requiredSafeInteger(checkout.expires_at, "expires_at"),
1427
- ...isRecord(checkout.fiat_quote) || checkout.fiat_quote === null ? { fiat_quote: checkout.fiat_quote } : {}
1428
- };
1429
- return {
1430
- checkout_id: paymentHash,
1431
- reference,
1432
- status: "open",
1433
- amount_msats: amountMsats,
1434
- active: invoice,
1435
- invoices: [invoice]
1436
- };
1578
+ function parseBrowserLogLevel(value) {
1579
+ if (value === void 0 || value === null) return void 0;
1580
+ const normalized = value.trim().toLowerCase();
1581
+ if (normalized.length === 0) return void 0;
1582
+ return BROWSER_LOG_LEVEL_ALIASES[normalized];
1437
1583
  }
1438
- function normalizePaymentMethods(value) {
1439
- if (!Array.isArray(value)) return void 0;
1440
- return value.map(normalizePaymentMethod);
1584
+ function resolveBrowserLogLevel(value, fallback = "info") {
1585
+ return parseBrowserLogLevel(value) ?? fallback;
1441
1586
  }
1442
- function normalizePaymentMethod(input) {
1443
- const record = recordOrEmpty(input);
1444
- const provider = record.provider;
1445
- if (typeof provider !== "string") {
1446
- throw new TypeError("payment method provider must be a string");
1587
+ function readBrowserLogLevelFromEnvironment(source) {
1588
+ if (source !== void 0) {
1589
+ return resolveBrowserLogLevel(
1590
+ source.LOG_LEVEL ?? source.VITE_LOG_LEVEL ?? source.NEXT_PUBLIC_LOG_LEVEL
1591
+ );
1447
1592
  }
1448
- const unavailableReason = nonEmptyString2(record.unavailable_reason);
1449
- const unavailableMessage = nonEmptyString2(record.unavailable_message);
1450
- const payAmount = nonEmptyString2(record.pay_amount);
1451
- const minimumPayAmount = nonEmptyString2(record.minimum_pay_amount);
1452
- const maximumPayAmount = nonEmptyString2(record.maximum_pay_amount);
1453
- const minimumInvoiceAmountMsats = optionalSafeInteger(
1454
- record.minimum_invoice_amount_msats,
1455
- "minimum_invoice_amount_msats"
1456
- );
1457
- const maximumInvoiceAmountMsats = optionalSafeInteger(
1458
- record.maximum_invoice_amount_msats,
1459
- "maximum_invoice_amount_msats"
1593
+ const fromGlobal = parseBrowserLogLevel(readGlobalLogLevel());
1594
+ if (fromGlobal !== void 0) return fromGlobal;
1595
+ const metaEnv = readImportMetaEnv();
1596
+ const parsedMeta = parseBrowserLogLevel(
1597
+ metaEnv?.LOG_LEVEL ?? metaEnv?.VITE_LOG_LEVEL ?? metaEnv?.NEXT_PUBLIC_LOG_LEVEL
1460
1598
  );
1461
- return {
1462
- pay_in_asset: requiredString(record.pay_in_asset, "pay_in_asset"),
1463
- label: requiredString(record.label, "label"),
1464
- network_label: requiredString(record.network_label, "network_label"),
1465
- provider,
1466
- available: record.available === true,
1467
- ...unavailableReason === void 0 ? {} : { unavailable_reason: unavailableReason },
1468
- ...unavailableMessage === void 0 ? {} : { unavailable_message: unavailableMessage },
1469
- ...payAmount === void 0 ? {} : { pay_amount: payAmount },
1470
- ...minimumPayAmount === void 0 ? {} : { minimum_pay_amount: minimumPayAmount },
1471
- ...maximumPayAmount === void 0 ? {} : { maximum_pay_amount: maximumPayAmount },
1472
- ...minimumInvoiceAmountMsats === void 0 ? {} : { minimum_invoice_amount_msats: minimumInvoiceAmountMsats },
1473
- ...maximumInvoiceAmountMsats === void 0 ? {} : { maximum_invoice_amount_msats: maximumInvoiceAmountMsats }
1474
- };
1475
- }
1476
-
1477
- // src/internal/wizard.ts
1478
- import { getSwapRefundAddressError } from "@openreceive/core/swap-address";
1479
- import {
1480
- getPaymentWizardRoutes,
1481
- listAssets,
1482
- payTutorialUrls,
1483
- providerIconUrls
1484
- } from "@openreceive/provider-data";
1485
- function getBitcoinAssets() {
1486
- return listAssets().filter((asset) => asset.symbol === "btc" && asset.route !== void 0);
1487
- }
1488
- function getDefaultBitcoinRoute() {
1489
- const routes = [
1490
- ...new Set(
1491
- getBitcoinAssets().flatMap((asset) => asset.route === void 0 ? [] : [asset.route])
1492
- )
1493
- ];
1494
- return routes.length === 1 ? routes[0] ?? null : null;
1599
+ if (parsedMeta !== void 0) return parsedMeta;
1600
+ const processEnv = typeof process !== "undefined" ? process.env : void 0;
1601
+ return resolveBrowserLogLevel(processEnv?.LOG_LEVEL);
1495
1602
  }
1496
- function getPaymentStatusText(phase) {
1497
- if (phase === "settled") {
1498
- return {
1499
- title: checkoutLabels.paymentStatus.settledTitle,
1500
- detail: checkoutLabels.paymentStatus.settledDetail
1501
- };
1502
- }
1503
- if (phase === "expired") {
1504
- return {
1505
- title: checkoutLabels.paymentStatus.expiredTitle,
1506
- detail: checkoutLabels.paymentStatus.expiredDetail
1507
- };
1603
+ function readGlobalLogLevel() {
1604
+ try {
1605
+ const value = globalThis.__OPENRECEIVE_LOG_LEVEL__;
1606
+ return typeof value === "string" ? value : void 0;
1607
+ } catch {
1608
+ return void 0;
1508
1609
  }
1509
- return {
1510
- title: checkoutLabels.paymentStatus.waitingTitle,
1511
- detail: checkoutLabels.paymentStatus.waitingDetail
1512
- };
1513
- }
1514
- function getWizardEmptyMessage() {
1515
- return checkoutLabels.emptyBitcoin;
1516
- }
1517
- function getCheckoutProviderOpenLabel() {
1518
- return checkoutLabels.openProvider;
1519
- }
1520
- function getCheckoutProviderIcon(provider) {
1521
- return providerIconUrls[provider.icon_path] ?? paymentIconUrls.crypto;
1522
- }
1523
- function getCheckoutProviderTutorials(provider) {
1524
- return (provider.tutorials ?? []).map((tutorial) => ({
1525
- index: tutorial.index,
1526
- path: tutorial.path,
1527
- image: payTutorialUrls[tutorial.path] ?? tutorial.path,
1528
- caption: tutorial.caption
1529
- }));
1530
1610
  }
1531
- function getRouteNetworkLabel(routeId) {
1532
- return routeId === "lightning" || routeId === "btc-lightning" ? checkoutLabels.lightningNetwork : routeId;
1611
+ function readImportMetaEnv() {
1612
+ try {
1613
+ return import.meta.env;
1614
+ } catch {
1615
+ return void 0;
1616
+ }
1533
1617
  }
1534
- function createWizardRouteAssetDisplays(assets, options = {}) {
1535
- return assets.map((asset) => {
1536
- const id = asset.route ?? asset.symbol;
1537
- return {
1538
- id,
1539
- label: asset.label,
1540
- subtitle: getRouteNetworkLabel(id),
1541
- icon: getRouteIcon(asset),
1542
- selected: options.selectedRoute === id
1543
- };
1544
- });
1618
+
1619
+ // src/internal/console-logger.ts
1620
+ function createConsoleWriter(options) {
1621
+ const target = options.console ?? console;
1622
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1623
+ return (level, prefix, event, message, fields) => {
1624
+ const minLevel = options.minLevel === void 0 ? readBrowserLogLevelFromEnvironment() : resolveBrowserLogLevel(options.minLevel);
1625
+ if (browserLogLevelOrder(level) < browserLogLevelOrder(minLevel)) {
1626
+ return;
1627
+ }
1628
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
1629
+ const sink = target[method] ?? target.log;
1630
+ const {
1631
+ level: _level,
1632
+ event: _event,
1633
+ message: cleanMessage,
1634
+ ...cleanFields
1635
+ } = sanitizeBrowserLogEntry({ level, event, message, ...fields });
1636
+ sink.call(
1637
+ target,
1638
+ `[${now().toISOString()}] ${level.toUpperCase()} [${prefix}] ${event}: ${cleanMessage}`,
1639
+ cleanFields
1640
+ );
1641
+ };
1545
1642
  }
1546
- function createWizardRouteDisplays(routes, options = {}) {
1547
- return routes.map((route) => ({
1548
- key: getWizardRouteDisplayKey(route),
1549
- title: getWizardRouteDisplayTitle(route),
1550
- subtitle: getWizardRouteDisplaySubtitle(route),
1551
- providers: (options.providerPreviewLimit === void 0 ? route.providers : route.providers.slice(0, options.providerPreviewLimit)).map((entry) => createWizardProviderDisplay(entry))
1552
- }));
1643
+ function sanitizeBrowserLogEntry(entry) {
1644
+ const clean = {};
1645
+ for (const [key, value] of Object.entries(entry)) {
1646
+ if (/secret|token|authorization|cookie|nwc/i.test(key)) {
1647
+ clean[key] = "[REDACTED]";
1648
+ } else {
1649
+ clean[key] = sanitizeBrowserLogValue(value);
1650
+ }
1651
+ }
1652
+ return clean;
1553
1653
  }
1554
- function getWizardRouteDisplayKey(route) {
1555
- return route.route.id;
1654
+ function sanitizeBrowserLogValue(value) {
1655
+ if (typeof value === "string") return redactBrowserSecrets(value);
1656
+ if (Array.isArray(value)) return value.map(sanitizeBrowserLogValue);
1657
+ if (typeof value !== "object" || value === null) return value;
1658
+ const clean = {};
1659
+ for (const [key, nested] of Object.entries(value)) {
1660
+ if (/secret|token|authorization|cookie|nwc/i.test(key)) {
1661
+ clean[key] = "[REDACTED]";
1662
+ } else {
1663
+ clean[key] = sanitizeBrowserLogValue(nested);
1664
+ }
1665
+ }
1666
+ return clean;
1556
1667
  }
1557
- function getWizardRouteDisplayTitle(route) {
1558
- return route.route.label;
1668
+ function redactBrowserSecrets(value) {
1669
+ return value.replace(/nostr\+walletconnect:\/\/[^\s"'`<>]+/g, "[REDACTED_NWC]").replace(/([?&](?:_or_evt|token|secret)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
1559
1670
  }
1560
- function getWizardRouteDisplaySubtitle(route) {
1561
- return route.route.symbol.toUpperCase();
1671
+ function createBrowserConsoleLogger(options = {}) {
1672
+ const prefix = options.prefix ?? "openreceive";
1673
+ const write = createConsoleWriter(options);
1674
+ return (entry) => {
1675
+ const { level, event, message, ...fields } = entry;
1676
+ write(level, prefix, event, message, fields);
1677
+ };
1562
1678
  }
1563
- function createWizardProviderDisplay(entry) {
1564
- return {
1565
- id: entry.provider.id,
1566
- name: entry.provider.name,
1567
- kind: entry.provider.kind,
1568
- url: entry.provider.lightning_docs_url ?? entry.provider.url,
1569
- icon: getCheckoutProviderIcon(entry.provider),
1570
- tutorials: getCheckoutProviderTutorials(entry.provider),
1571
- copyLabel: checkoutLabels.copyInvoice,
1572
- copiedLabel: checkoutLabels.copied,
1573
- openLabel: getCheckoutProviderOpenLabel()
1679
+ function createAppBrowserConsoleLogger(options) {
1680
+ const write = createConsoleWriter(options);
1681
+ return (event, message, fields = {}, level = "info") => {
1682
+ write(level, options.prefix, event, message, fields);
1574
1683
  };
1575
1684
  }
1576
- function getPaymentMethodIcon(method) {
1577
- return paymentIconUrls[paymentMethodIconIds[method]];
1685
+ var defaultBrowserConsoleLogger;
1686
+ function getDefaultBrowserConsoleLogger() {
1687
+ defaultBrowserConsoleLogger ??= createBrowserConsoleLogger();
1688
+ return defaultBrowserConsoleLogger;
1578
1689
  }
1579
- function getAssetIcon(symbol) {
1580
- return paymentIconUrls[assetIconIds[symbol] ?? "crypto"];
1690
+ function resolveBrowserLogger(logger) {
1691
+ if (logger === false) return void 0;
1692
+ if (logger !== void 0) return logger;
1693
+ return getDefaultBrowserConsoleLogger();
1581
1694
  }
1582
- function getNetworkIcon(networkLabel) {
1583
- const key = networkLabel.trim().toLowerCase();
1584
- if (key === "tron" || key === "trx") return paymentIconUrls.trx;
1585
- if (key === "solana" || key === "sol") return paymentIconUrls.sol;
1586
- if (key === "ethereum" || key === "eth") return paymentIconUrls.eth;
1587
- return paymentIconUrls.crypto;
1695
+
1696
+ // src/internal/checkout-read.ts
1697
+ import { nonEmptyString } from "@openreceive/core";
1698
+ function optionalRecord(value) {
1699
+ if (value === void 0) return void 0;
1700
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1701
+ throw new Error("OpenReceive metadata must be an object.");
1702
+ }
1703
+ return value;
1588
1704
  }
1589
- function getSwapOptionIcon(option) {
1590
- return getAssetIcon(option.label.trim().toLowerCase());
1705
+ function requiredInvoiceRail(value) {
1706
+ if (value === "lightning" || value === "swap" || value === "checkout_lock") return value;
1707
+ throw new TypeError("OpenReceive invoice rail must be lightning, swap, or checkout_lock.");
1591
1708
  }
1592
- function groupSwapOptionsByLabel(options) {
1593
- const groups = [];
1594
- const indexByLabel = /* @__PURE__ */ new Map();
1595
- for (const option of options) {
1596
- const key = option.label.trim().toUpperCase();
1597
- const existing = indexByLabel.get(key);
1598
- if (existing === void 0) {
1599
- indexByLabel.set(key, groups.length);
1600
- groups.push({ label: option.label, options: [option] });
1601
- continue;
1602
- }
1603
- const group = groups[existing];
1604
- if (group === void 0) continue;
1605
- groups[existing] = { label: group.label, options: [...group.options, option] };
1709
+ function requiredString(value, fieldName) {
1710
+ const text = nonEmptyString(value);
1711
+ if (text === void 0) {
1712
+ throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
1606
1713
  }
1607
- return groups;
1714
+ return text;
1608
1715
  }
1609
- var OPENRECEIVE_METHOD_GRID_ORDER = [
1610
- { kind: "method", id: "bitcoin" },
1611
- { kind: "swap", label: "USDT" },
1612
- { kind: "swap", label: "USDC" },
1613
- { kind: "swap", label: "SOL" },
1614
- { kind: "swap", label: "ETH" }
1615
- ];
1616
- function buildMethodGridEntries(paymentMethods2, swapOptions) {
1617
- const swapGroups = groupSwapOptionsByLabel(swapOptions);
1618
- if (swapGroups.length === 0) {
1619
- return paymentMethods2.map((method) => ({ kind: "method", method }));
1716
+ function optionalSafeInteger(value, fieldName = "integer") {
1717
+ if (value === void 0 || value === null) return void 0;
1718
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
1719
+ throw new TypeError(`OpenReceive checkout response ${fieldName} must be a safe integer.`);
1620
1720
  }
1621
- const methodsById = new Map(paymentMethods2.map((method) => [method.id, method]));
1622
- const groupsByLabel = new Map(
1623
- swapGroups.map((group) => [group.label.trim().toUpperCase(), group])
1624
- );
1625
- const usedMethodIds = /* @__PURE__ */ new Set();
1626
- const usedSwapLabels = /* @__PURE__ */ new Set();
1627
- const entries = [];
1628
- for (const slot of OPENRECEIVE_METHOD_GRID_ORDER) {
1629
- if (slot.kind === "method") {
1630
- const method = methodsById.get(slot.id);
1631
- if (method === void 0) continue;
1632
- usedMethodIds.add(method.id);
1633
- entries.push({ kind: "method", method });
1634
- continue;
1635
- }
1636
- const group = groupsByLabel.get(slot.label);
1637
- if (group === void 0) continue;
1638
- usedSwapLabels.add(slot.label);
1639
- entries.push({ kind: "swap", group });
1640
- }
1641
- for (const method of paymentMethods2) {
1642
- if (usedMethodIds.has(method.id)) continue;
1643
- entries.push({ kind: "method", method });
1644
- }
1645
- for (const group of swapGroups) {
1646
- const key = group.label.trim().toUpperCase();
1647
- if (usedSwapLabels.has(key)) continue;
1648
- entries.push({ kind: "swap", group });
1649
- }
1650
- return entries;
1651
- }
1652
- function swapPickerKey(label) {
1653
- return `swap:${label.trim().toUpperCase()}`;
1654
- }
1655
- function parseMethodPickerKey(key) {
1656
- if (!key.startsWith("method:")) return null;
1657
- return { kind: "method", methodId: key.slice("method:".length) };
1658
- }
1659
- function parseSwapPickerKey(key) {
1660
- if (!key.startsWith("swap:")) return null;
1661
- return { kind: "swap", label: key.slice("swap:".length) };
1662
- }
1663
- function paymentAccentId(labelOrMethodId) {
1664
- const key = labelOrMethodId.trim().toLowerCase();
1665
- if (key === "bitcoin" || key === "btc") return "bitcoin";
1666
- if (key === "usdt") return "usdt";
1667
- if (key === "usdc") return "usdc";
1668
- if (key === "sol" || key === "solana") return "sol";
1669
- if (key === "eth" || key === "ethereum") return "eth";
1670
- return "default";
1671
- }
1672
- var assetActiveClassByAccent = {
1673
- bitcoin: orClasses.methodCardActiveBitcoin,
1674
- usdt: orClasses.methodCardActiveUsdt,
1675
- usdc: orClasses.methodCardActiveUsdc,
1676
- sol: orClasses.methodCardActiveSol,
1677
- eth: orClasses.methodCardActiveEth,
1678
- default: orClasses.methodCardActiveDefault
1679
- };
1680
- var networkActiveClassByAccent = {
1681
- usdt: orClasses.methodNetworkButtonActiveUsdt,
1682
- usdc: orClasses.methodNetworkButtonActiveUsdc,
1683
- default: orClasses.methodNetworkButtonActiveDefault
1684
- };
1685
- function assetButtonClasses(options) {
1686
- const base = options.disabled ? orClasses.methodCardUnavailable : orClasses.methodCardReady;
1687
- if (!options.selected || options.disabled) return base;
1688
- return `${base} ${assetActiveClassByAccent[options.accent]}`;
1689
- }
1690
- function networkButtonClasses(options) {
1691
- if (options.disabled) return orClasses.methodNetworkButtonUnavailable;
1692
- if (!options.selected) return orClasses.methodNetworkButton;
1693
- const accent = options.accent === "usdt" || options.accent === "usdc" ? options.accent : "default";
1694
- return `${orClasses.methodNetworkButton} ${networkActiveClassByAccent[accent]}`;
1695
- }
1696
- function networkMobileRevealClasses(accent) {
1697
- if (accent === "usdt") return orClasses.methodNetworkRevealMobileUsdt;
1698
- if (accent === "usdc") return orClasses.methodNetworkRevealMobileUsdc;
1699
- return orClasses.methodNetworkRevealMobile;
1700
- }
1701
- function networkCheckClasses(accent) {
1702
- return accent === "usdc" ? orClasses.methodNetworkCheckUsdc : orClasses.methodNetworkCheck;
1703
- }
1704
- function networkSummaryIconClasses(accent) {
1705
- return accent === "usdc" ? orClasses.methodNetworkSummaryIconUsdc : orClasses.methodNetworkSummaryIcon;
1721
+ return value;
1706
1722
  }
1707
- function formatNetworkSummary(assetLabel, networkLabel) {
1708
- return checkoutLabels.networkSummary.replace("{asset}", assetLabel).replace("{network}", networkLabel);
1723
+ function requiredSafeInteger(value, fieldName) {
1724
+ const integer = optionalSafeInteger(value, fieldName);
1725
+ if (integer === void 0) {
1726
+ throw new TypeError(`OpenReceive checkout response requires ${fieldName}.`);
1727
+ }
1728
+ return integer;
1709
1729
  }
1710
- function formatChooseNetworkHeading(assetLabel) {
1711
- return checkoutLabels.chooseAssetNetwork.replace("{asset}", assetLabel);
1730
+
1731
+ // src/internal/checkout-log.ts
1732
+ import { compact } from "@openreceive/core";
1733
+ function checkoutLogFields(state) {
1734
+ return compact({
1735
+ checkout_id: state.checkout_id,
1736
+ reference: state.reference,
1737
+ invoice_id: state.invoice_id,
1738
+ payment_hash: state.payment_hash,
1739
+ amount_msats: state.amount_msats,
1740
+ transaction_state: state.transaction_state,
1741
+ workflow_state: state.workflow_state,
1742
+ phase: state.phase,
1743
+ expires_in_seconds: state.expires_in_seconds,
1744
+ settled: state.settled,
1745
+ paid: state.paid,
1746
+ rail: state.rail,
1747
+ ...swapAuditLogFields(state.swap)
1748
+ });
1712
1749
  }
1713
- function resolvePreservedNetworkSelection(options) {
1714
- const nextKey = options.nextGroup.label.trim().toUpperCase();
1715
- const current = options.selectedNetworks[nextKey];
1716
- if (current !== void 0 && options.nextGroup.options.some(
1717
- (option) => option.pay_in_asset === current && option.available !== false
1718
- )) {
1719
- return current;
1720
- }
1721
- const previous = options.previousGroup;
1722
- if (previous === void 0) return void 0;
1723
- const previousKey = previous.label.trim().toUpperCase();
1724
- const previousAsset = options.selectedNetworks[previousKey];
1725
- if (previousAsset === void 0) return void 0;
1726
- const previousOption = previous.options.find((option) => option.pay_in_asset === previousAsset);
1727
- if (previousOption === void 0) return void 0;
1728
- const match = options.nextGroup.options.find(
1729
- (option) => option.network_label === previousOption.network_label && option.available !== false
1730
- );
1731
- return match?.pay_in_asset;
1750
+ function swapAuditLogFields(swap) {
1751
+ if (swap === void 0) return {};
1752
+ return compact({
1753
+ attempt_id: swap.attempt_id,
1754
+ provider: swap.provider,
1755
+ provider_order_id: swap.provider_order_id,
1756
+ pay_in_asset: swap.pay_in_asset,
1757
+ provider_state: swap.provider_state,
1758
+ attention: swap.attention,
1759
+ attention_reason: swap.attention_reason,
1760
+ refund_reason: swap.refund_reason,
1761
+ refund_tx_id: swap.refund_tx_id,
1762
+ deposit_tx_id: swap.deposit_tx_id,
1763
+ payout_tx_id: swap.payout_tx_id
1764
+ });
1732
1765
  }
1733
- function findSwapGridGroup(entries, pickerKey) {
1734
- if (pickerKey === null || pickerKey === void 0) return void 0;
1735
- const parsed = parseSwapPickerKey(pickerKey);
1736
- if (parsed === null) return void 0;
1737
- const entry = entries.find(
1738
- (entry2) => entry2.kind === "swap" && entry2.group.label.trim().toUpperCase() === parsed.label
1766
+ function emitBrowserSwapTransition(logger, previous, next) {
1767
+ const previousSwap = previous?.swap;
1768
+ const nextSwap = next.swap;
1769
+ if (nextSwap === void 0) return;
1770
+ const previousState = previousSwap?.provider_state;
1771
+ const nextState = nextSwap.provider_state;
1772
+ const previousAttention = previousSwap?.attention_reason;
1773
+ const nextAttention = nextSwap.attention_reason;
1774
+ const previousSettled = previous?.settled === true || previous?.paid === true;
1775
+ const nextSettled = next.settled === true || next.paid === true;
1776
+ const stateChanged = previousState !== nextState;
1777
+ const attentionChanged = previousAttention !== nextAttention;
1778
+ const settlementChanged = previousSettled !== nextSettled;
1779
+ if (!stateChanged && !attentionChanged && !settlementChanged) return;
1780
+ const level = nextState === "attention" || nextSwap.attention === true ? "warn" : nextState === "refund_required" || nextState === "refund_pending" || nextState === "refunded" || nextState === "failed" || nextState === "expired" || settlementChanged ? "info" : "debug";
1781
+ emitBrowserLog(
1782
+ logger,
1783
+ level,
1784
+ "swap.state.changed",
1785
+ "Swap attempt state changed in checkout UI.",
1786
+ {
1787
+ ...checkoutLogFields(next),
1788
+ previous_provider_state: previousState,
1789
+ previous_settled: previousSettled,
1790
+ wallet_settled: nextSettled,
1791
+ ui_label: nextSettled ? "Payment complete" : getSwapProviderStateLabel(nextState)
1792
+ }
1739
1793
  );
1740
- return entry?.kind === "swap" ? entry.group : void 0;
1741
1794
  }
1742
- function updateSelectedSwapNetworks(options) {
1743
- const nextGroup = findSwapGridGroup(options.entries, options.nextKey);
1744
- if (nextGroup === void 0 || nextGroup.options.length <= 1) {
1745
- return options.selectedNetworks;
1746
- }
1747
- const previousGroup = findSwapGridGroup(options.entries, options.previousKey);
1748
- const preserved = resolvePreservedNetworkSelection({
1749
- previousGroup,
1750
- nextGroup,
1751
- selectedNetworks: options.selectedNetworks
1752
- });
1753
- const groupKey = nextGroup.label.trim().toUpperCase();
1754
- if (preserved === void 0) {
1755
- const { [groupKey]: _removed, ...rest } = options.selectedNetworks;
1756
- return rest;
1795
+ function emitBrowserLog(logger, level, event, message, fields = {}) {
1796
+ const sink = resolveBrowserLogger(logger);
1797
+ if (sink === void 0) return;
1798
+ try {
1799
+ sink(
1800
+ sanitizeBrowserLogEntry({
1801
+ level,
1802
+ event,
1803
+ message,
1804
+ ...fields
1805
+ })
1806
+ );
1807
+ } catch {
1757
1808
  }
1758
- return { ...options.selectedNetworks, [groupKey]: preserved };
1759
1809
  }
1760
- function wizardNetworkGroupIds(groupKey) {
1761
- const slug = groupKey.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
1762
- return { panelId: `network-panel-${slug}`, headingId: `network-heading-${slug}` };
1810
+
1811
+ // src/internal/checkout-state.ts
1812
+ function selectCheckoutDisplayInvoice(snapshot) {
1813
+ if (snapshot.active !== void 0 && snapshot.active.rail !== "checkout_lock") {
1814
+ return snapshot.active;
1815
+ }
1816
+ const nonLock = snapshot.invoices.filter((invoice) => invoice.rail !== "checkout_lock");
1817
+ const withBolt11 = nonLock.filter(
1818
+ (invoice) => typeof invoice.invoice === "string" && invoice.invoice.length > 0
1819
+ );
1820
+ const settledBolt11 = withBolt11.find((invoice) => invoice.transaction_state === "settled");
1821
+ if (settledBolt11 !== void 0) return settledBolt11;
1822
+ if (withBolt11[0] !== void 0) return withBolt11[0];
1823
+ const settledSwap = nonLock.find((invoice) => invoice.transaction_state === "settled");
1824
+ return settledSwap ?? nonLock[0];
1763
1825
  }
1764
- function createSwapUnavailableModel(option, checkout) {
1765
- const detail = swapOptionLimitMessage(option, checkout) ?? option.unavailable_message ?? checkoutLabels.swapUnavailableFallback.replace("{asset}", option.label);
1766
- const range = option.minimum_pay_amount === void 0 ? void 0 : option.maximum_pay_amount === void 0 ? checkoutLabels.swapUnavailableMinimumOnly.replace("{minimum}", option.minimum_pay_amount).replace("{asset}", option.label) : checkoutLabels.swapUnavailableRange.replace("{minimum}", option.minimum_pay_amount).replace("{maximum}", option.maximum_pay_amount).replace("{asset}", option.label);
1767
- return {
1768
- title: checkoutLabels.swapUnavailableTitle.replace("{asset}", option.label),
1769
- detail,
1770
- ...range === void 0 ? {} : { range },
1771
- hint: checkoutLabels.swapUnavailableHint
1772
- };
1826
+ function isPaidCheckoutSnapshot(snapshot) {
1827
+ return snapshot.status === "paid";
1773
1828
  }
1774
- function swapOptionLimitMessage(option, checkout) {
1775
- if (option.available !== false) return void 0;
1776
- if (option.unavailable_reason === "amount_too_small") {
1777
- const fiat = checkout === void 0 ? void 0 : formatSwapLimit(checkout, option.minimum_invoice_amount_msats, "ceil");
1778
- if (fiat !== void 0) return `Minimum amount ${fiat}`;
1779
- if (option.minimum_pay_amount !== void 0) {
1780
- return `Minimum ${option.minimum_pay_amount} ${option.label}`;
1781
- }
1782
- }
1783
- if (option.unavailable_reason === "amount_too_large") {
1784
- const fiat = checkout === void 0 ? void 0 : formatSwapLimit(checkout, option.maximum_invoice_amount_msats, "floor");
1785
- if (fiat !== void 0) return `Maximum amount ${fiat}`;
1786
- if (option.maximum_pay_amount !== void 0) {
1787
- return `Maximum ${option.maximum_pay_amount} ${option.label}`;
1788
- }
1789
- }
1790
- return option.unavailable_message;
1829
+ function isReusableLightningInvoice(expiresAt, now) {
1830
+ return expiresAt - resolveNow(now) > OPENRECEIVE_LIGHTNING_REUSE_BUFFER_SECONDS;
1791
1831
  }
1792
- function swapGroupLimitOption(options) {
1793
- if (options.length === 0) return void 0;
1794
- const unavailable = options.filter((option) => option.available === false);
1795
- const tooSmall = unavailable.filter((option) => option.unavailable_reason === "amount_too_small");
1796
- const candidates = tooSmall.length > 0 ? tooSmall : unavailable.length > 0 ? unavailable : options;
1797
- let best = candidates[0];
1798
- for (const option of candidates) {
1799
- if (best === void 0) {
1800
- best = option;
1801
- continue;
1802
- }
1803
- const bestMin = best.minimum_invoice_amount_msats;
1804
- const optionMin = option.minimum_invoice_amount_msats;
1805
- if (optionMin === void 0) continue;
1806
- if (bestMin === void 0 || optionMin < bestMin) best = option;
1832
+ function createCheckoutState(snapshot, options = {}) {
1833
+ const invoiceRecord = selectCheckoutDisplayInvoice(snapshot);
1834
+ if (invoiceRecord === void 0) {
1835
+ return normalizeCheckoutState(
1836
+ {
1837
+ checkout_id: snapshot.checkout_id,
1838
+ reference: snapshot.reference,
1839
+ invoice_id: "",
1840
+ invoice: "",
1841
+ rail: "checkout_lock",
1842
+ lightning_uri: "",
1843
+ ...snapshot.amount_msats === void 0 ? {} : { amount_msats: snapshot.amount_msats },
1844
+ ...snapshot.fiat !== void 0 ? { fiat_quote: { fiat: snapshot.fiat } } : {},
1845
+ transaction_state: "pending",
1846
+ workflow_state: "invoice_created",
1847
+ paid: false
1848
+ },
1849
+ resolveNow(options.now)
1850
+ );
1807
1851
  }
1808
- return best;
1809
- }
1810
- function getSwapRefundFormError(payInAsset, address, networkLabel) {
1811
- if (address.length === 0) return "Enter a refund address.";
1812
- return getSwapRefundAddressError(payInAsset, address, networkLabel);
1813
- }
1814
- function getRouteIcon(asset) {
1815
- const routeId = asset.route ?? asset.symbol;
1816
- if (asset.symbol === "btc" && routeId.includes("lightning")) {
1817
- return paymentIconUrls.lightning;
1852
+ const invoice = invoiceRecord;
1853
+ const bolt11 = typeof invoice.invoice === "string" && invoice.invoice.length > 0 ? invoice.invoice : invoice.rail === "swap" || invoice.rail === "checkout_lock" ? "" : missingDisplayInvoice();
1854
+ const paid = isPaidCheckoutSnapshot(snapshot);
1855
+ const settledAt = snapshot.paid_at ?? invoice.settled_at;
1856
+ const transactionState = paid ? "settled" : invoice.transaction_state ?? "pending";
1857
+ const workflowState = paid ? "paid" : invoice.workflow_state ?? "invoice_created";
1858
+ const fiatQuote = invoice.fiat_quote === null && snapshot.fiat !== void 0 ? { fiat: snapshot.fiat } : invoice.fiat_quote ?? (snapshot.fiat === void 0 ? void 0 : { fiat: snapshot.fiat });
1859
+ const state = normalizeCheckoutState(
1860
+ {
1861
+ checkout_id: snapshot.checkout_id,
1862
+ reference: snapshot.reference,
1863
+ invoice_id: invoice.invoice_id,
1864
+ invoice: bolt11,
1865
+ rail: invoice.rail,
1866
+ // G4 divergence (d): the swap rail gets NO `lightning:` URI even when the
1867
+ // attempt happens to carry a bolt11. A swap is paid at the deposit
1868
+ // address, and a wallet-jump link on that screen sends the payer down the
1869
+ // wrong rail. `invoice` above still holds the bolt11 for anything that
1870
+ // legitimately needs it.
1871
+ lightning_uri: invoice.rail === "checkout_lock" || invoice.rail === "swap" || bolt11 === "" ? "" : createLightningUri(bolt11),
1872
+ ...invoice.payment_hash === void 0 ? {} : { payment_hash: invoice.payment_hash },
1873
+ // G4 divergence (b): the checkout's amount is what is owed, so an attempt
1874
+ // that omits its own amount still shows one rather than no amount at all.
1875
+ amount_msats: invoice.amount_msats ?? snapshot.amount_msats,
1876
+ ...fiatQuote === void 0 ? {} : { fiat_quote: fiatQuote },
1877
+ transaction_state: transactionState,
1878
+ workflow_state: workflowState,
1879
+ ...invoice.expires_at === void 0 ? {} : { expires_at: invoice.expires_at },
1880
+ ...settledAt === void 0 ? {} : { settled_at: settledAt },
1881
+ ...invoice.swap === void 0 ? {} : { swap: invoice.swap },
1882
+ paid
1883
+ },
1884
+ resolveNow(options.now)
1885
+ );
1886
+ const source = options.source ?? "create";
1887
+ if (source === "create") {
1888
+ emitBrowserLog(
1889
+ options.logger,
1890
+ "info",
1891
+ "checkout.state.created",
1892
+ "Created checkout state from order snapshot.",
1893
+ checkoutLogFields(state)
1894
+ );
1895
+ } else if (source === "refresh") {
1896
+ emitBrowserLog(
1897
+ options.logger,
1898
+ "debug",
1899
+ "checkout.state.refreshed",
1900
+ "Refreshed checkout state from order status.",
1901
+ checkoutLogFields(state)
1902
+ );
1903
+ emitBrowserSwapTransition(options.logger, options.previousState, state);
1818
1904
  }
1819
- return getAssetIcon(asset.symbol);
1905
+ return state;
1820
1906
  }
1821
- function createPaymentWizardState(request) {
1822
- const selectedRouteId = request.selectedMethod === "bitcoin" ? request.selectedBitcoinRoute ?? getDefaultBitcoinRoute() : null;
1823
- const routes = selectedRouteId === null ? [] : getPaymentWizardRoutes({ route: selectedRouteId });
1907
+ function createCheckoutSnapshotFromInvoice(attempt, identity = {}) {
1908
+ const rail = requiredInvoiceRail(attempt.rail);
1909
+ const invoiceId = requiredString(attempt.invoice_id, "invoice_id");
1910
+ const invoice = {
1911
+ invoice_id: invoiceId,
1912
+ invoice: attempt.invoice,
1913
+ rail,
1914
+ ...attempt.payment_hash === void 0 ? {} : { payment_hash: attempt.payment_hash },
1915
+ ...attempt.amount_msats === void 0 ? {} : { amount_msats: attempt.amount_msats },
1916
+ ...attempt.fiat_quote === void 0 ? {} : { fiat_quote: attempt.fiat_quote },
1917
+ ...attempt.transaction_state === void 0 ? {} : { transaction_state: attempt.transaction_state },
1918
+ ...attempt.workflow_state === void 0 ? {} : { workflow_state: attempt.workflow_state },
1919
+ ...attempt.expires_at === void 0 ? {} : { expires_at: attempt.expires_at },
1920
+ ...attempt.settled_at === void 0 ? {} : { settled_at: attempt.settled_at },
1921
+ ...attempt.swap === void 0 ? {} : { swap: attempt.swap }
1922
+ };
1923
+ const paid = attempt.transaction_state === "settled";
1824
1924
  return {
1825
- selectedRouteId,
1826
- routes
1925
+ checkout_id: identity.checkout_id ?? invoiceId,
1926
+ reference: identity.reference ?? invoiceId,
1927
+ status: paid ? "paid" : "open",
1928
+ ...attempt.settled_at === void 0 ? {} : { paid_at: attempt.settled_at },
1929
+ amount_msats: attempt.amount_msats ?? 0,
1930
+ active: paid ? void 0 : invoice,
1931
+ invoices: [invoice]
1827
1932
  };
1828
1933
  }
1829
- function createPaymentWizardSelection() {
1934
+ function refreshCheckoutState(state, options = {}) {
1935
+ return createCheckoutState(snapshotFromCheckoutState(state), {
1936
+ ...options,
1937
+ source: options.source ?? "countdown",
1938
+ previousState: options.previousState ?? state
1939
+ });
1940
+ }
1941
+ function shouldCheckoutShowWaiting(state, options = {}) {
1942
+ if (state.terminal || state.settled) return false;
1943
+ if (state.expires_at === void 0) return true;
1944
+ return state.expires_at > resolveNow(options.now);
1945
+ }
1946
+ function createCheckoutStatusModel(source, options = {}) {
1947
+ const isCheckoutState = source !== void 0 && "invoice_id" in source && "invoice" in source;
1948
+ const phase = source?.phase ?? "invoice_created";
1949
+ const expiresInSeconds = source?.expires_in_seconds;
1950
+ const displayPhase = phase !== "settled" && phase !== "failed" && phase !== "cancelled" && expiresInSeconds === 0 ? "expired" : phase;
1951
+ const statusText = getPaymentStatusText(displayPhase);
1830
1952
  return {
1831
- selectedMethod: null,
1832
- selectedBitcoinRoute: null
1953
+ phase: displayPhase,
1954
+ waiting: displayPhase === "expired" ? false : source === void 0 ? false : isCheckoutState ? shouldCheckoutShowWaiting(source, options) : source.waiting ?? false,
1955
+ title: statusText.title,
1956
+ detail: statusText.detail,
1957
+ countdownPrefix: checkoutLabels.countdownPrefix,
1958
+ // No countdown once settled — the invoice's remaining lifetime is meaningless
1959
+ // to a payer whose payment already landed.
1960
+ ...expiresInSeconds === void 0 || displayPhase === "expired" || displayPhase === "settled" ? {} : {
1961
+ expires_in_seconds: expiresInSeconds,
1962
+ countdownLabel: formatCountdown(expiresInSeconds)
1963
+ }
1833
1964
  };
1834
1965
  }
1835
- function createPaymentWizardModel(selection) {
1836
- const wizard = createPaymentWizardState({
1837
- selectedMethod: selection.selectedMethod,
1838
- selectedBitcoinRoute: selection.selectedBitcoinRoute
1839
- });
1840
- const routeAssets = selection.selectedMethod === "bitcoin" ? getBitcoinAssets() : [];
1841
- const selectedRoute = wizard.selectedRouteId;
1966
+ function normalizeCheckoutState(state, now) {
1967
+ const {
1968
+ phase: _phase,
1969
+ settled: _settled,
1970
+ terminal: _terminal,
1971
+ expires_in_seconds: _expiresInSeconds,
1972
+ // Labels are derived, never carried: a caller handing back a previous state
1973
+ // with one field changed must not keep that state's stale labels.
1974
+ amountLabel: _amountLabel,
1975
+ fiatLabel: _fiatLabel,
1976
+ paymentHashLabel: _paymentHashLabel,
1977
+ ...base
1978
+ } = state;
1979
+ const statePhase = getCheckoutPhase(state.transaction_state, state.workflow_state);
1980
+ const expiresInSeconds = base.expires_at === void 0 || now === void 0 ? void 0 : Math.max(0, base.expires_at - now);
1981
+ const phase = statePhase === "invoice_created" || statePhase === "verifying" ? expiresInSeconds === 0 ? "expired" : statePhase : statePhase;
1982
+ const settled = base.paid || base.transaction_state === "settled";
1842
1983
  return {
1843
- selection,
1844
- wizard,
1845
- routeAssets,
1846
- selectedRoute
1984
+ ...base,
1985
+ ...deriveCheckoutStateLabels(base),
1986
+ phase,
1987
+ settled,
1988
+ // A swap that expired, refunded, failed, or needs support review is over:
1989
+ // the shadow invoice behind it will not be paid, so watchers stop polling
1990
+ // both /payments/check and the provider instead of asking forever.
1991
+ terminal: isTerminalPhase(phase) || !settled && isTerminalSwapProviderState(base.swap?.provider_state),
1992
+ ...expiresInSeconds === void 0 ? {} : { expires_in_seconds: expiresInSeconds }
1847
1993
  };
1848
1994
  }
1849
- function updatePaymentWizardSelection(selection, action) {
1850
- switch (action.type) {
1851
- case "select_method": {
1852
- return {
1853
- ...selection,
1854
- selectedMethod: action.method,
1855
- selectedBitcoinRoute: action.method === "bitcoin" ? selection.selectedBitcoinRoute ?? getDefaultBitcoinRoute() : selection.selectedBitcoinRoute
1856
- };
1857
- }
1858
- case "change_method": {
1859
- return {
1860
- ...selection,
1861
- selectedMethod: null,
1862
- selectedBitcoinRoute: null
1863
- };
1864
- }
1865
- case "change_route": {
1866
- if (selection.selectedMethod === "bitcoin") {
1867
- return {
1868
- ...selection,
1869
- selectedBitcoinRoute: null
1870
- };
1871
- }
1872
- return selection;
1873
- }
1874
- case "select_route": {
1875
- if (selection.selectedMethod === "bitcoin") {
1876
- return {
1877
- ...selection,
1878
- selectedBitcoinRoute: action.route
1879
- };
1880
- }
1881
- return selection;
1882
- }
1883
- }
1995
+ function missingDisplayInvoice() {
1996
+ throw new TypeError("OpenReceive checkout requires a display Lightning invoice.");
1884
1997
  }
1885
- var BrowserPaymentWizardController = class {
1886
- options;
1887
- selection;
1888
- constructor(options = {}) {
1889
- this.options = options;
1890
- this.selection = options.selection ?? createPaymentWizardSelection();
1891
- }
1892
- getSelection() {
1893
- return this.selection;
1998
+ function getCheckoutPhase(transactionState, workflowState) {
1999
+ if (workflowState === "cancelled") return "cancelled";
2000
+ if (transactionState === "settled") return "settled";
2001
+ if (transactionState === "expired" || workflowState === "expired") {
2002
+ return "expired";
1894
2003
  }
1895
- getModel() {
1896
- return createPaymentWizardModel(this.selection);
2004
+ if (transactionState === "failed" || workflowState === "failed") {
2005
+ return "failed";
1897
2006
  }
1898
- update(action) {
1899
- const next = updatePaymentWizardSelection(this.selection, action);
1900
- this.selection = next;
1901
- this.options.onSelection?.(next);
1902
- return next;
2007
+ if (workflowState === "verifying") {
2008
+ return "verifying";
1903
2009
  }
1904
- selectMethod(method) {
1905
- return this.update({
1906
- type: "select_method",
1907
- method
1908
- });
1909
- }
1910
- changeMethod() {
1911
- return this.update({
1912
- type: "change_method"
1913
- });
1914
- }
1915
- selectRoute(route) {
1916
- return this.update({
1917
- type: "select_route",
1918
- route
1919
- });
1920
- }
1921
- };
1922
- function createPaymentWizardController(options = {}) {
1923
- return new BrowserPaymentWizardController(options);
2010
+ return "invoice_created";
1924
2011
  }
1925
-
1926
- // src/internal/log-level.ts
1927
- var BROWSER_LOG_LEVEL_ORDER = {
1928
- debug: 10,
1929
- info: 20,
1930
- warn: 30,
1931
- error: 40
1932
- };
1933
- var BROWSER_LOG_LEVEL_ALIASES = {
1934
- debug: "debug",
1935
- info: "info",
1936
- warn: "warn",
1937
- warning: "warn",
1938
- error: "error"
1939
- };
1940
- function browserLogLevelOrder(level) {
1941
- return BROWSER_LOG_LEVEL_ORDER[level];
2012
+ function isTerminalPhase(phase) {
2013
+ return phase === "expired" || phase === "failed" || phase === "cancelled";
1942
2014
  }
1943
- function parseBrowserLogLevel(value) {
1944
- if (value === void 0 || value === null) return void 0;
1945
- const normalized = value.trim().toLowerCase();
1946
- if (normalized.length === 0) return void 0;
1947
- return BROWSER_LOG_LEVEL_ALIASES[normalized];
2015
+ function snapshotFromCheckoutState(state) {
2016
+ const invoice = {
2017
+ invoice_id: state.invoice_id,
2018
+ invoice: state.invoice,
2019
+ rail: state.rail,
2020
+ ...state.payment_hash === void 0 ? {} : { payment_hash: state.payment_hash },
2021
+ ...state.amount_msats === void 0 ? {} : { amount_msats: state.amount_msats },
2022
+ ...state.fiat_quote === void 0 ? {} : { fiat_quote: state.fiat_quote },
2023
+ transaction_state: state.transaction_state,
2024
+ workflow_state: state.workflow_state,
2025
+ ...state.expires_at === void 0 ? {} : { expires_at: state.expires_at },
2026
+ ...state.settled_at === void 0 ? {} : { settled_at: state.settled_at },
2027
+ ...state.swap === void 0 ? {} : { swap: state.swap }
2028
+ };
2029
+ return {
2030
+ checkout_id: state.checkout_id,
2031
+ reference: state.reference,
2032
+ status: state.paid ? "paid" : state.terminal ? "expired" : "open",
2033
+ ...state.settled_at === void 0 ? {} : { paid_at: state.settled_at },
2034
+ amount_msats: state.amount_msats ?? 0,
2035
+ active: state.paid ? void 0 : invoice,
2036
+ invoices: [invoice]
2037
+ };
1948
2038
  }
1949
- function resolveBrowserLogLevel(value, fallback = "info") {
1950
- return parseBrowserLogLevel(value) ?? fallback;
2039
+
2040
+ // src/internal/checkout-merge.ts
2041
+ function findReusableLightningInvoice(snapshot, now) {
2042
+ return snapshot.invoices.find(
2043
+ (invoice) => invoice.rail === "lightning" && typeof invoice.invoice === "string" && invoice.invoice.length > 0 && invoice.expires_at !== void 0 && isReusableLightningInvoice(invoice.expires_at, now)
2044
+ );
1951
2045
  }
1952
- function readBrowserLogLevelFromEnvironment(source) {
1953
- if (source !== void 0) {
1954
- return resolveBrowserLogLevel(
1955
- source.LOG_LEVEL ?? source.VITE_LOG_LEVEL ?? source.NEXT_PUBLIC_LOG_LEVEL
1956
- );
1957
- }
1958
- const fromGlobal = parseBrowserLogLevel(readGlobalLogLevel());
1959
- if (fromGlobal !== void 0) return fromGlobal;
1960
- const metaEnv = readImportMetaEnv();
1961
- const parsedMeta = parseBrowserLogLevel(
1962
- metaEnv?.LOG_LEVEL ?? metaEnv?.VITE_LOG_LEVEL ?? metaEnv?.NEXT_PUBLIC_LOG_LEVEL
2046
+ function mergeAttemptIntoSnapshot(invoice, base) {
2047
+ const withoutSame = base.invoices.filter(
2048
+ (entry) => entry.invoice_id !== invoice.invoice_id && entry.rail !== "checkout_lock"
1963
2049
  );
1964
- if (parsedMeta !== void 0) return parsedMeta;
1965
- const processEnv = typeof process !== "undefined" ? process.env : void 0;
1966
- return resolveBrowserLogLevel(processEnv?.LOG_LEVEL);
2050
+ return {
2051
+ ...base,
2052
+ checkout_id: invoice.invoice_id,
2053
+ active: invoice,
2054
+ invoices: [invoice, ...withoutSame],
2055
+ ...invoice.amount_msats === void 0 ? {} : { amount_msats: invoice.amount_msats }
2056
+ };
1967
2057
  }
1968
- function readGlobalLogLevel() {
1969
- try {
1970
- const value = globalThis.__OPENRECEIVE_LOG_LEVEL__;
1971
- return typeof value === "string" ? value : void 0;
1972
- } catch {
1973
- return void 0;
1974
- }
2058
+ function mergeAttemptIntoCheckout(invoice, previous, reference) {
2059
+ const base = previous ?? {
2060
+ checkout_id: invoice.invoice_id,
2061
+ reference,
2062
+ status: "open",
2063
+ amount_msats: invoice.amount_msats ?? 0,
2064
+ invoices: []
2065
+ };
2066
+ return mergeAttemptIntoSnapshot(invoice, base);
1975
2067
  }
1976
- function readImportMetaEnv() {
1977
- try {
1978
- return import.meta.env;
1979
- } catch {
1980
- return void 0;
2068
+ function mergeMintedCheckout(checkout, previous) {
2069
+ const minted = selectCheckoutDisplayInvoice(checkout) ?? checkout.active;
2070
+ if (minted === void 0) {
2071
+ return {
2072
+ ...checkout,
2073
+ ...previous?.payment_methods === void 0 ? {} : { payment_methods: previous.payment_methods }
2074
+ };
1981
2075
  }
2076
+ if (previous === void 0) return mergeAttemptIntoSnapshot(minted, checkout);
2077
+ return mergeAttemptIntoSnapshot(minted, {
2078
+ ...previous,
2079
+ ...checkout,
2080
+ invoices: previous.invoices,
2081
+ payment_methods: previous.payment_methods ?? checkout.payment_methods
2082
+ });
1982
2083
  }
1983
2084
 
1984
- // src/internal/console-logger.ts
1985
- function createConsoleWriter(options) {
1986
- const target = options.console ?? console;
1987
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
1988
- return (level, prefix, event, message, fields) => {
1989
- const minLevel = options.minLevel === void 0 ? readBrowserLogLevelFromEnvironment() : resolveBrowserLogLevel(options.minLevel);
1990
- if (browserLogLevelOrder(level) < browserLogLevelOrder(minLevel)) {
1991
- return;
1992
- }
1993
- const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
1994
- const sink = target[method] ?? target.log;
1995
- const {
1996
- level: _level,
1997
- event: _event,
1998
- message: cleanMessage,
1999
- ...cleanFields
2000
- } = sanitizeBrowserLogEntry({ level, event, message, ...fields });
2001
- sink.call(
2002
- target,
2003
- `[${now().toISOString()}] ${level.toUpperCase()} [${prefix}] ${event}: ${cleanMessage}`,
2004
- cleanFields
2005
- );
2085
+ // src/internal/checkout-transport.ts
2086
+ import { isRecord, nonEmptyString as nonEmptyString2, recordOrEmpty } from "@openreceive/core";
2087
+
2088
+ // src/internal/request-headers.ts
2089
+ function requestHeaders(host) {
2090
+ return {
2091
+ "Content-Type": "application/json",
2092
+ ...csrfTokenHeader(),
2093
+ ...host
2006
2094
  };
2007
2095
  }
2008
- function sanitizeBrowserLogEntry(entry) {
2009
- const clean = {};
2010
- for (const [key, value] of Object.entries(entry)) {
2011
- if (/secret|token|authorization|cookie|nwc/i.test(key)) {
2012
- clean[key] = "[REDACTED]";
2013
- } else {
2014
- clean[key] = sanitizeBrowserLogValue(value);
2015
- }
2096
+ function csrfTokenHeader() {
2097
+ if (typeof document === "undefined") return {};
2098
+ const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content");
2099
+ return token ? { "X-CSRF-Token": token } : {};
2100
+ }
2101
+
2102
+ // src/internal/routes.ts
2103
+ function checkoutRoutes(prefix) {
2104
+ if (typeof prefix !== "string") {
2105
+ throw new TypeError(
2106
+ `OpenReceive requires \`prefix\`, the base path the shipped router is mounted at (for example "/openreceive"); every checkout, status and swap route is derived from it. Received ${prefix === null ? "null" : typeof prefix}.`
2107
+ );
2016
2108
  }
2017
- return clean;
2109
+ const base = prefix.replace(/\/+$/, "");
2110
+ return {
2111
+ checkouts: `${base}/checkouts`,
2112
+ checkoutsPrepare: `${base}/checkouts/prepare`,
2113
+ paymentsCheck: `${base}/payments/check`,
2114
+ swaps: `${base}/swaps`,
2115
+ swapsQuote: `${base}/swaps/quote`,
2116
+ swapsStatus: `${base}/swaps/status`,
2117
+ swapsRefunds: `${base}/swaps/refunds`
2118
+ };
2018
2119
  }
2019
- function sanitizeBrowserLogValue(value) {
2020
- if (typeof value === "string") return redactBrowserSecrets(value);
2021
- if (Array.isArray(value)) return value.map(sanitizeBrowserLogValue);
2022
- if (typeof value !== "object" || value === null) return value;
2023
- const clean = {};
2024
- for (const [key, nested] of Object.entries(value)) {
2025
- if (/secret|token|authorization|cookie|nwc/i.test(key)) {
2026
- clean[key] = "[REDACTED]";
2027
- } else {
2028
- clean[key] = sanitizeBrowserLogValue(nested);
2120
+
2121
+ // src/internal/checkout-transport.ts
2122
+ var SWAP_STATUS_FAILURE_LIMIT = 3;
2123
+ var BrowserRequestError = class extends Error {
2124
+ status;
2125
+ code;
2126
+ retryable;
2127
+ retryAfterSeconds;
2128
+ constructor(message, options) {
2129
+ super(message);
2130
+ this.name = "BrowserRequestError";
2131
+ this.status = options.status;
2132
+ if (options.code !== void 0) this.code = options.code;
2133
+ if (options.retryable !== void 0) this.retryable = options.retryable;
2134
+ if (options.retryAfterSeconds !== void 0) this.retryAfterSeconds = options.retryAfterSeconds;
2135
+ }
2136
+ };
2137
+ async function readJsonResponse(response, fallbackMessage) {
2138
+ let body;
2139
+ try {
2140
+ body = await response.json();
2141
+ } catch {
2142
+ if (response.ok) {
2143
+ throw new BrowserRequestError(`${fallbackMessage} The server returned a non-JSON body.`, {
2144
+ status: response.status,
2145
+ retryable: false
2146
+ });
2029
2147
  }
2148
+ body = void 0;
2030
2149
  }
2031
- return clean;
2032
- }
2033
- function redactBrowserSecrets(value) {
2034
- return value.replace(/nostr\+walletconnect:\/\/[^\s"'`<>]+/g, "[REDACTED_NWC]").replace(/([?&](?:_or_evt|token|secret)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
2035
- }
2036
- function createBrowserConsoleLogger(options = {}) {
2037
- const prefix = options.prefix ?? "openreceive";
2038
- const write = createConsoleWriter(options);
2039
- return (entry) => {
2040
- const { level, event, message, ...fields } = entry;
2041
- write(level, prefix, event, message, fields);
2042
- };
2150
+ if (!response.ok) {
2151
+ const record = body !== null && typeof body === "object" ? body : void 0;
2152
+ const retryAfterRaw = Number(response.headers?.get?.("retry-after") ?? Number.NaN);
2153
+ throw new BrowserRequestError(
2154
+ typeof record?.message === "string" ? record.message : fallbackMessage,
2155
+ {
2156
+ status: response.status,
2157
+ ...typeof record?.code === "string" ? { code: record.code } : {},
2158
+ ...typeof record?.retryable === "boolean" ? { retryable: record.retryable } : {},
2159
+ ...Number.isFinite(retryAfterRaw) && retryAfterRaw > 0 ? { retryAfterSeconds: retryAfterRaw } : {}
2160
+ }
2161
+ );
2162
+ }
2163
+ return body;
2043
2164
  }
2044
- function createAppBrowserConsoleLogger(options) {
2045
- const write = createConsoleWriter(options);
2046
- return (event, message, fields = {}, level = "info") => {
2047
- write(level, options.prefix, event, message, fields);
2165
+ function normalizeRequestCheckoutOptions(options) {
2166
+ const reference = nonEmptyString2(options.reference);
2167
+ const metadata = optionalRecord(options.metadata);
2168
+ return {
2169
+ routes: checkoutRoutes(options.prefix),
2170
+ reference: reference ?? "",
2171
+ fetch: options.fetch,
2172
+ headers: options.headers,
2173
+ ...options.memo === void 0 ? {} : { memo: options.memo },
2174
+ ...metadata === void 0 ? {} : { metadata }
2048
2175
  };
2049
2176
  }
2050
- var defaultBrowserConsoleLogger;
2051
- function getDefaultBrowserConsoleLogger() {
2052
- defaultBrowserConsoleLogger ??= createBrowserConsoleLogger();
2053
- return defaultBrowserConsoleLogger;
2054
- }
2055
- function resolveBrowserLogger(logger) {
2056
- if (logger === false) return void 0;
2057
- if (logger !== void 0) return logger;
2058
- return getDefaultBrowserConsoleLogger();
2059
- }
2060
-
2061
- // src/internal/checkout-state.ts
2062
- import { unixSeconds as unixSeconds3 } from "@openreceive/core";
2063
-
2064
- // src/internal/checkout-log.ts
2065
- import { compact } from "@openreceive/core";
2066
- function checkoutLogFields(state) {
2067
- return compact({
2068
- checkout_id: state.checkout_id,
2069
- reference: state.reference,
2070
- invoice_id: state.invoice_id,
2071
- payment_hash: state.payment_hash,
2072
- amount_msats: state.amount_msats,
2073
- transaction_state: state.transaction_state,
2074
- workflow_state: state.workflow_state,
2075
- phase: state.phase,
2076
- expires_in_seconds: state.expires_in_seconds,
2077
- settled: state.settled,
2078
- paid: state.paid,
2079
- rail: state.rail,
2080
- ...swapAuditLogFields(state.swap)
2177
+ async function requestCheckout(options) {
2178
+ const request = normalizeRequestCheckoutOptions(options);
2179
+ if (request.reference.length === 0) {
2180
+ throw new Error("OpenReceive checkout creation requires reference.");
2181
+ }
2182
+ const fetcher = request.fetch ?? globalThis.fetch;
2183
+ if (fetcher === void 0) {
2184
+ throw new Error("OpenReceive checkout creation requires fetch.");
2185
+ }
2186
+ if (request.memo !== void 0 && request.memo.length > 500) {
2187
+ throw new Error("OpenReceive memo must be 500 characters or fewer.");
2188
+ }
2189
+ const requestBody = {
2190
+ reference: request.reference,
2191
+ ...request.memo === void 0 ? {} : { memo: request.memo },
2192
+ ...request.metadata === void 0 ? {} : { metadata: structuredClone(request.metadata) }
2193
+ };
2194
+ const headers = request.headers === void 0 ? {} : request.headers;
2195
+ const response = await fetcher(request.routes.checkouts, {
2196
+ method: "POST",
2197
+ headers: requestHeaders(headers),
2198
+ body: JSON.stringify(requestBody)
2081
2199
  });
2200
+ const body = await readJsonResponse(response, "Could not create checkout.");
2201
+ const snapshot = checkoutSnapshotFromResponseBody(body);
2202
+ const responseInvoice = snapshot.active;
2203
+ if (isRecord(responseInvoice) && typeof responseInvoice.invoice === "string") {
2204
+ assertDisplayInvoice(responseInvoice.invoice);
2205
+ }
2206
+ return options.previous === void 0 ? snapshot : mergeMintedCheckout(snapshot, options.previous);
2082
2207
  }
2083
- function swapAuditLogFields(swap) {
2084
- if (swap === void 0) return {};
2085
- return compact({
2086
- attempt_id: swap.attempt_id,
2087
- provider: swap.provider,
2088
- provider_order_id: swap.provider_order_id,
2089
- pay_in_asset: swap.pay_in_asset,
2090
- provider_state: swap.provider_state,
2091
- attention: swap.attention,
2092
- attention_reason: swap.attention_reason,
2093
- refund_reason: swap.refund_reason,
2094
- refund_tx_id: swap.refund_tx_id,
2095
- deposit_tx_id: swap.deposit_tx_id,
2096
- payout_tx_id: swap.payout_tx_id
2208
+ async function prepareCheckout(options) {
2209
+ const request = normalizeRequestCheckoutOptions(options);
2210
+ if (request.reference.length === 0) {
2211
+ throw new Error("OpenReceive checkout prepare requires reference.");
2212
+ }
2213
+ const fetcher = request.fetch ?? globalThis.fetch;
2214
+ if (fetcher === void 0) {
2215
+ throw new Error("OpenReceive checkout prepare requires fetch.");
2216
+ }
2217
+ const headers = request.headers === void 0 ? {} : request.headers;
2218
+ const response = await fetcher(request.routes.checkoutsPrepare, {
2219
+ method: "POST",
2220
+ headers: requestHeaders(headers),
2221
+ body: JSON.stringify({ reference: request.reference })
2097
2222
  });
2223
+ const body = await readJsonResponse(response, "Could not prepare checkout.");
2224
+ return checkoutLockSnapshotFromPrepareBody(body, request.reference);
2098
2225
  }
2099
- function emitBrowserSwapTransition(logger, previous, next) {
2100
- const previousSwap = previous?.swap;
2101
- const nextSwap = next.swap;
2102
- if (nextSwap === void 0) return;
2103
- const previousState = previousSwap?.provider_state;
2104
- const nextState = nextSwap.provider_state;
2105
- const previousAttention = previousSwap?.attention_reason;
2106
- const nextAttention = nextSwap.attention_reason;
2107
- const previousSettled = previous?.settled === true || previous?.paid === true;
2108
- const nextSettled = next.settled === true || next.paid === true;
2109
- const stateChanged = previousState !== nextState;
2110
- const attentionChanged = previousAttention !== nextAttention;
2111
- const settlementChanged = previousSettled !== nextSettled;
2112
- if (!stateChanged && !attentionChanged && !settlementChanged) return;
2113
- const level = nextState === "attention" || nextSwap.attention === true ? "warn" : nextState === "refund_required" || nextState === "refund_pending" || nextState === "refunded" || nextState === "failed" || nextState === "expired" || settlementChanged ? "info" : "debug";
2114
- emitBrowserLog(
2115
- logger,
2116
- level,
2117
- "swap.state.changed",
2118
- "Swap attempt state changed in checkout UI.",
2119
- {
2120
- ...checkoutLogFields(next),
2121
- previous_provider_state: previousState,
2122
- previous_settled: previousSettled,
2123
- wallet_settled: nextSettled,
2124
- ui_label: nextSettled ? "Payment complete" : getSwapProviderStateLabel(nextState)
2226
+ function createStatusFetcher(options) {
2227
+ let snapshot = options.snapshot;
2228
+ let swapStatusFailures = 0;
2229
+ const routes = checkoutRoutes(options.prefix);
2230
+ return async (reference) => {
2231
+ if (reference.length === 0) {
2232
+ throw new Error("OpenReceive status refresh requires reference.");
2125
2233
  }
2126
- );
2127
- }
2128
- function emitBrowserLog(logger, level, event, message, fields = {}) {
2129
- const sink = resolveBrowserLogger(logger);
2130
- if (sink === void 0) return;
2131
- try {
2132
- sink(
2133
- sanitizeBrowserLogEntry({
2134
- level,
2135
- event,
2136
- message,
2137
- ...fields
2234
+ const fetcher = options.fetch ?? globalThis.fetch;
2235
+ if (fetcher === void 0) {
2236
+ throw new Error("OpenReceive status refresh requires fetch.");
2237
+ }
2238
+ const headers = options.headers === void 0 ? {} : options.headers;
2239
+ const activePaymentHash = nonEmptyString2(snapshot.active?.payment_hash) ?? (snapshot.active?.rail === "checkout_lock" ? void 0 : nonEmptyString2(snapshot.active?.invoice_id));
2240
+ if (activePaymentHash === void 0 || !/^[0-9a-f]{64}$/i.test(activePaymentHash)) {
2241
+ return snapshot;
2242
+ }
2243
+ const response = await fetcher(routes.paymentsCheck, {
2244
+ method: "POST",
2245
+ headers: requestHeaders(headers),
2246
+ body: JSON.stringify({
2247
+ reference,
2248
+ payment_hash: activePaymentHash
2138
2249
  })
2250
+ });
2251
+ const body = await readJsonResponse(response, "Could not refresh invoice status.");
2252
+ const payment = recordOrEmpty(body);
2253
+ const next = structuredClone(snapshot);
2254
+ if (next.active === void 0) return next;
2255
+ const state = nonEmptyString2(payment.status) ?? "pending";
2256
+ const paidAt = optionalSafeInteger(payment.paid_at, "paid_at");
2257
+ let active = {
2258
+ ...next.active,
2259
+ transaction_state: state === "not_found" ? "pending" : state,
2260
+ ...paidAt === void 0 ? {} : { settled_at: paidAt }
2261
+ };
2262
+ if (active.rail === "swap" && active.swap !== void 0 && state !== "settled" && // A terminal provider state is final; re-reading it only adds provider load.
2263
+ !isTerminalSwapProviderState(nonEmptyString2(active.swap.provider_state))) {
2264
+ try {
2265
+ const swapResponse = await fetcher(routes.swapsStatus, {
2266
+ method: "POST",
2267
+ headers: requestHeaders(headers),
2268
+ body: JSON.stringify({ reference, payment_hash: activePaymentHash })
2269
+ });
2270
+ const swapBody = recordOrEmpty(
2271
+ await readJsonResponse(swapResponse, "Could not refresh swap status.")
2272
+ );
2273
+ active = mergeSwapStatusIntoInvoice(active, swapBody);
2274
+ swapStatusFailures = 0;
2275
+ } catch (error) {
2276
+ swapStatusFailures += 1;
2277
+ const definitive = error instanceof BrowserRequestError && error.retryable === false;
2278
+ const swap = active.swap;
2279
+ if (swap !== void 0 && (definitive || swapStatusFailures >= SWAP_STATUS_FAILURE_LIMIT)) {
2280
+ active = {
2281
+ ...active,
2282
+ swap: { ...swap, attention: true, attention_reason: "swap_status_unavailable" }
2283
+ };
2284
+ }
2285
+ }
2286
+ }
2287
+ const paymentMethods2 = normalizePaymentMethods(payment.payment_methods);
2288
+ const others = next.invoices.filter(
2289
+ (entry) => entry.invoice_id !== active.invoice_id && entry.rail !== "checkout_lock"
2139
2290
  );
2140
- } catch {
2141
- }
2142
- }
2143
-
2144
- // src/internal/checkout-state.ts
2145
- function selectCheckoutDisplayInvoice(snapshot) {
2146
- if (snapshot.active !== void 0 && snapshot.active.rail !== "checkout_lock") {
2147
- return snapshot.active;
2148
- }
2149
- const nonLock = snapshot.invoices.filter((invoice) => invoice.rail !== "checkout_lock");
2150
- const withBolt11 = nonLock.filter(
2151
- (invoice) => typeof invoice.invoice === "string" && invoice.invoice.length > 0
2152
- );
2153
- const settledBolt11 = withBolt11.find((invoice) => invoice.transaction_state === "settled");
2154
- if (settledBolt11 !== void 0) return settledBolt11;
2155
- if (withBolt11[0] !== void 0) return withBolt11[0];
2156
- const settledSwap = nonLock.find((invoice) => invoice.transaction_state === "settled");
2157
- return settledSwap ?? nonLock[0];
2158
- }
2159
- function isPaidCheckoutSnapshot(snapshot) {
2160
- return snapshot.status === "paid";
2161
- }
2162
- function isReusableLightningInvoice(expiresAt, now) {
2163
- return expiresAt - (now ?? unixSeconds3()) > OPENRECEIVE_LIGHTNING_REUSE_BUFFER_SECONDS;
2164
- }
2165
- function createCheckoutState(snapshot, options = {}) {
2166
- const invoiceRecord = selectCheckoutDisplayInvoice(snapshot);
2167
- if (invoiceRecord === void 0) {
2168
- return normalizeCheckoutState(
2169
- {
2170
- checkout_id: snapshot.checkout_id,
2171
- reference: snapshot.reference,
2172
- invoice_id: "",
2173
- invoice: "",
2174
- rail: "checkout_lock",
2175
- lightning_uri: "",
2176
- ...snapshot.amount_msats === void 0 ? {} : { amount_msats: snapshot.amount_msats },
2177
- ...snapshot.fiat !== void 0 ? { fiat_quote: { fiat: snapshot.fiat } } : {},
2178
- transaction_state: "pending",
2179
- workflow_state: "invoice_created",
2180
- paid: false
2181
- },
2182
- options.now ?? unixSeconds3()
2183
- );
2184
- }
2185
- const invoice = invoiceRecord;
2186
- const bolt11 = typeof invoice.invoice === "string" && invoice.invoice.length > 0 ? invoice.invoice : invoice.rail === "swap" || invoice.rail === "checkout_lock" ? "" : missingDisplayInvoice();
2187
- const paid = isPaidCheckoutSnapshot(snapshot);
2188
- const settledAt = snapshot.paid_at ?? invoice.settled_at;
2189
- const transactionState = paid ? "settled" : invoice.transaction_state ?? "pending";
2190
- const workflowState = paid ? "paid" : invoice.workflow_state ?? "invoice_created";
2191
- const fiatQuote = invoice.fiat_quote === null && snapshot.fiat !== void 0 ? { fiat: snapshot.fiat } : invoice.fiat_quote ?? (snapshot.fiat === void 0 ? void 0 : { fiat: snapshot.fiat });
2192
- const state = normalizeCheckoutState(
2193
- {
2194
- checkout_id: snapshot.checkout_id,
2195
- reference: snapshot.reference,
2196
- invoice_id: invoice.invoice_id,
2197
- invoice: bolt11,
2198
- rail: invoice.rail,
2199
- // G4 divergence (d): the swap rail gets NO `lightning:` URI even when the
2200
- // attempt happens to carry a bolt11. A swap is paid at the deposit
2201
- // address, and a wallet-jump link on that screen sends the payer down the
2202
- // wrong rail. `invoice` above still holds the bolt11 for anything that
2203
- // legitimately needs it.
2204
- lightning_uri: invoice.rail === "checkout_lock" || invoice.rail === "swap" || bolt11 === "" ? "" : createLightningUri(bolt11),
2205
- ...invoice.payment_hash === void 0 ? {} : { payment_hash: invoice.payment_hash },
2206
- // G4 divergence (b): the checkout's amount is what is owed, so an attempt
2207
- // that omits its own amount still shows one rather than no amount at all.
2208
- amount_msats: invoice.amount_msats ?? snapshot.amount_msats,
2209
- ...fiatQuote === void 0 ? {} : { fiat_quote: fiatQuote },
2210
- transaction_state: transactionState,
2211
- workflow_state: workflowState,
2212
- ...invoice.expires_at === void 0 ? {} : { expires_at: invoice.expires_at },
2213
- ...settledAt === void 0 ? {} : { settled_at: settledAt },
2214
- ...invoice.swap === void 0 ? {} : { swap: invoice.swap },
2215
- paid
2216
- },
2217
- options.now ?? unixSeconds3()
2218
- );
2219
- const source = options.source ?? "create";
2220
- if (source === "create") {
2221
- emitBrowserLog(
2222
- options.logger,
2223
- "info",
2224
- "checkout.state.created",
2225
- "Created checkout state from order snapshot.",
2226
- checkoutLogFields(state)
2227
- );
2228
- } else if (source === "refresh") {
2229
- emitBrowserLog(
2230
- options.logger,
2231
- "debug",
2232
- "checkout.state.refreshed",
2233
- "Refreshed checkout state from order status.",
2234
- checkoutLogFields(state)
2235
- );
2236
- emitBrowserSwapTransition(options.logger, options.previousState, state);
2291
+ snapshot = {
2292
+ ...next,
2293
+ active,
2294
+ invoices: [active, ...others],
2295
+ status: state === "settled" ? "paid" : state === "expired" || state === "failed" ? "expired" : "open",
2296
+ ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
2297
+ };
2298
+ return snapshot;
2299
+ };
2300
+ }
2301
+ function mergeSwapStatusIntoInvoice(invoice, status) {
2302
+ const swap = invoice.swap;
2303
+ if (swap === void 0) return invoice;
2304
+ const merged = { ...swap };
2305
+ const providerState = nonEmptyString2(status.provider_state);
2306
+ if (providerState !== void 0) merged.provider_state = providerState;
2307
+ const providerExpiresAt = optionalSafeInteger(status.provider_expires_at, "provider_expires_at");
2308
+ if (providerExpiresAt !== void 0) merged.provider_expires_at = providerExpiresAt;
2309
+ for (const key of [
2310
+ "deposit_tx_id",
2311
+ "payout_tx_id",
2312
+ "refund_tx_id",
2313
+ "refund_reason",
2314
+ "refund_amount",
2315
+ "deposit_received_amount",
2316
+ "refund_address",
2317
+ "attention_reason"
2318
+ ]) {
2319
+ if (typeof status[key] === "string") merged[key] = status[key];
2237
2320
  }
2238
- return state;
2321
+ if (typeof status.attention === "boolean") merged.attention = status.attention;
2322
+ return {
2323
+ ...invoice,
2324
+ swap: merged,
2325
+ ...providerExpiresAt === void 0 ? {} : { expires_at: providerExpiresAt }
2326
+ };
2239
2327
  }
2240
- function createCheckoutSnapshotFromInvoice(attempt, identity = {}) {
2241
- const rail = requiredInvoiceRail(attempt.rail);
2242
- const invoiceId = requiredString(attempt.invoice_id, "invoice_id");
2243
- const invoice = {
2244
- invoice_id: invoiceId,
2245
- invoice: attempt.invoice,
2246
- rail,
2247
- ...attempt.payment_hash === void 0 ? {} : { payment_hash: attempt.payment_hash },
2248
- ...attempt.amount_msats === void 0 ? {} : { amount_msats: attempt.amount_msats },
2249
- ...attempt.fiat_quote === void 0 ? {} : { fiat_quote: attempt.fiat_quote },
2250
- ...attempt.transaction_state === void 0 ? {} : { transaction_state: attempt.transaction_state },
2251
- ...attempt.workflow_state === void 0 ? {} : { workflow_state: attempt.workflow_state },
2252
- ...attempt.expires_at === void 0 ? {} : { expires_at: attempt.expires_at },
2253
- ...attempt.settled_at === void 0 ? {} : { settled_at: attempt.settled_at },
2254
- ...attempt.swap === void 0 ? {} : { swap: attempt.swap }
2328
+ function checkoutLockSnapshotFromPrepareBody(body, fallbackReference) {
2329
+ const record = recordOrEmpty(body);
2330
+ const reference = requiredString(record.reference ?? fallbackReference, "reference");
2331
+ const amountMsats = requiredSafeInteger(record.amount_msats, "amount_msats");
2332
+ const lockId = `lock:${reference}`;
2333
+ const lockInvoice = {
2334
+ invoice_id: lockId,
2335
+ rail: "checkout_lock",
2336
+ amount_msats: amountMsats,
2337
+ transaction_state: "pending",
2338
+ workflow_state: "invoice_created"
2255
2339
  };
2256
- const paid = attempt.transaction_state === "settled";
2340
+ const fiatQuote = record.fiat_quote;
2341
+ const fiatRecord = isRecord(fiatQuote) && isRecord(fiatQuote.fiat) ? fiatQuote.fiat : void 0;
2342
+ const fiatCurrency = nonEmptyString2(fiatRecord?.currency);
2343
+ const fiatValue = nonEmptyString2(fiatRecord?.value);
2344
+ const fiat = fiatCurrency === void 0 || fiatValue === void 0 ? void 0 : { currency: fiatCurrency, value: fiatValue };
2345
+ const paymentMethods2 = normalizePaymentMethods(record.payment_methods);
2257
2346
  return {
2258
- checkout_id: identity.checkout_id ?? invoiceId,
2259
- reference: identity.reference ?? invoiceId,
2260
- status: paid ? "paid" : "open",
2261
- ...attempt.settled_at === void 0 ? {} : { paid_at: attempt.settled_at },
2262
- amount_msats: attempt.amount_msats ?? 0,
2263
- active: paid ? void 0 : invoice,
2264
- invoices: [invoice]
2347
+ checkout_id: lockId,
2348
+ reference,
2349
+ status: "open",
2350
+ amount_msats: amountMsats,
2351
+ ...fiat === void 0 ? {} : { fiat },
2352
+ active: lockInvoice,
2353
+ invoices: [lockInvoice],
2354
+ ...paymentMethods2 === void 0 ? {} : { payment_methods: paymentMethods2 }
2265
2355
  };
2266
2356
  }
2267
- function refreshCheckoutState(state, options = {}) {
2268
- return createCheckoutState(snapshotFromCheckoutState(state), {
2269
- ...options,
2270
- source: options.source ?? "countdown",
2271
- previousState: options.previousState ?? state
2272
- });
2273
- }
2274
- function shouldCheckoutShowWaiting(state, options = {}) {
2275
- if (state.terminal || state.settled) return false;
2276
- if (state.expires_at === void 0) return true;
2277
- return state.expires_at > (options.now ?? unixSeconds3());
2357
+ function checkoutSnapshotFromResponseBody(body) {
2358
+ const record = recordOrEmpty(body);
2359
+ const wrapped = recordOrEmpty(record.checkout);
2360
+ return checkoutSnapshot(wrapped);
2278
2361
  }
2279
- function createCheckoutStatusModel(source, options = {}) {
2280
- const isCheckoutState = source !== void 0 && "invoice_id" in source && "invoice" in source;
2281
- const phase = source?.phase ?? "invoice_created";
2282
- const expiresInSeconds = source?.expires_in_seconds;
2283
- const displayPhase = phase !== "settled" && phase !== "failed" && phase !== "cancelled" && expiresInSeconds === 0 ? "expired" : phase;
2284
- const statusText = getPaymentStatusText(displayPhase);
2285
- return {
2286
- phase: displayPhase,
2287
- waiting: displayPhase === "expired" ? false : source === void 0 ? false : isCheckoutState ? shouldCheckoutShowWaiting(source, options) : source.waiting ?? false,
2288
- title: statusText.title,
2289
- detail: statusText.detail,
2290
- countdownPrefix: checkoutLabels.countdownPrefix,
2291
- // No countdown once settled — the invoice's remaining lifetime is meaningless
2292
- // to a payer whose payment already landed.
2293
- ...expiresInSeconds === void 0 || displayPhase === "expired" || displayPhase === "settled" ? {} : {
2294
- expires_in_seconds: expiresInSeconds,
2295
- countdownLabel: formatCountdown(expiresInSeconds)
2296
- }
2362
+ function checkoutSnapshot(checkout) {
2363
+ const paymentHash = requiredString(checkout.payment_hash, "payment_hash");
2364
+ const reference = requiredString(checkout.reference, "reference");
2365
+ const amountMsats = requiredSafeInteger(checkout.amount_msats, "amount_msats");
2366
+ const invoice = {
2367
+ invoice_id: paymentHash,
2368
+ rail: "lightning",
2369
+ invoice: requiredString(checkout.bolt11, "bolt11"),
2370
+ payment_hash: paymentHash,
2371
+ amount_msats: amountMsats,
2372
+ transaction_state: "pending",
2373
+ workflow_state: "invoice_created",
2374
+ expires_at: requiredSafeInteger(checkout.expires_at, "expires_at"),
2375
+ ...isRecord(checkout.fiat_quote) || checkout.fiat_quote === null ? { fiat_quote: checkout.fiat_quote } : {}
2297
2376
  };
2298
- }
2299
- function normalizeCheckoutState(state, now) {
2300
- const {
2301
- phase: _phase,
2302
- settled: _settled,
2303
- terminal: _terminal,
2304
- expires_in_seconds: _expiresInSeconds,
2305
- // Labels are derived, never carried: a caller handing back a previous state
2306
- // with one field changed must not keep that state's stale labels.
2307
- amountLabel: _amountLabel,
2308
- fiatLabel: _fiatLabel,
2309
- paymentHashLabel: _paymentHashLabel,
2310
- ...base
2311
- } = state;
2312
- const statePhase = getCheckoutPhase(state.transaction_state, state.workflow_state);
2313
- const expiresInSeconds = base.expires_at === void 0 || now === void 0 ? void 0 : Math.max(0, base.expires_at - now);
2314
- const phase = statePhase === "invoice_created" || statePhase === "verifying" ? expiresInSeconds === 0 ? "expired" : statePhase : statePhase;
2315
- const settled = base.paid || base.transaction_state === "settled";
2316
2377
  return {
2317
- ...base,
2318
- ...deriveCheckoutStateLabels(base),
2319
- phase,
2320
- settled,
2321
- // A swap that expired, refunded, failed, or needs support review is over:
2322
- // the shadow invoice behind it will not be paid, so watchers stop polling
2323
- // both /payments/check and the provider instead of asking forever.
2324
- terminal: isTerminalPhase(phase) || !settled && isTerminalSwapProviderState(base.swap?.provider_state),
2325
- ...expiresInSeconds === void 0 ? {} : { expires_in_seconds: expiresInSeconds }
2378
+ checkout_id: paymentHash,
2379
+ reference,
2380
+ status: "open",
2381
+ amount_msats: amountMsats,
2382
+ active: invoice,
2383
+ invoices: [invoice]
2326
2384
  };
2327
2385
  }
2328
- function missingDisplayInvoice() {
2329
- throw new TypeError("OpenReceive checkout requires a display Lightning invoice.");
2386
+ function normalizePaymentMethods(value) {
2387
+ if (!Array.isArray(value)) return void 0;
2388
+ return value.map(normalizePaymentMethod);
2330
2389
  }
2331
- function getCheckoutPhase(transactionState, workflowState) {
2332
- if (workflowState === "cancelled") return "cancelled";
2333
- if (transactionState === "settled") return "settled";
2334
- if (transactionState === "expired" || workflowState === "expired") {
2335
- return "expired";
2336
- }
2337
- if (transactionState === "failed" || workflowState === "failed") {
2338
- return "failed";
2339
- }
2340
- if (workflowState === "verifying") {
2341
- return "verifying";
2390
+ function normalizePaymentMethod(input) {
2391
+ const record = recordOrEmpty(input);
2392
+ const provider = record.provider;
2393
+ if (typeof provider !== "string") {
2394
+ throw new TypeError("payment method provider must be a string");
2342
2395
  }
2343
- return "invoice_created";
2344
- }
2345
- function isTerminalPhase(phase) {
2346
- return phase === "expired" || phase === "failed" || phase === "cancelled";
2347
- }
2348
- function snapshotFromCheckoutState(state) {
2349
- const invoice = {
2350
- invoice_id: state.invoice_id,
2351
- invoice: state.invoice,
2352
- rail: state.rail,
2353
- ...state.payment_hash === void 0 ? {} : { payment_hash: state.payment_hash },
2354
- ...state.amount_msats === void 0 ? {} : { amount_msats: state.amount_msats },
2355
- ...state.fiat_quote === void 0 ? {} : { fiat_quote: state.fiat_quote },
2356
- transaction_state: state.transaction_state,
2357
- workflow_state: state.workflow_state,
2358
- ...state.expires_at === void 0 ? {} : { expires_at: state.expires_at },
2359
- ...state.settled_at === void 0 ? {} : { settled_at: state.settled_at },
2360
- ...state.swap === void 0 ? {} : { swap: state.swap }
2361
- };
2396
+ const unavailableReason = nonEmptyString2(record.unavailable_reason);
2397
+ const unavailableMessage = nonEmptyString2(record.unavailable_message);
2398
+ const payAmount = nonEmptyString2(record.pay_amount);
2399
+ const minimumPayAmount = nonEmptyString2(record.minimum_pay_amount);
2400
+ const maximumPayAmount = nonEmptyString2(record.maximum_pay_amount);
2401
+ const minimumInvoiceAmountMsats = optionalSafeInteger(
2402
+ record.minimum_invoice_amount_msats,
2403
+ "minimum_invoice_amount_msats"
2404
+ );
2405
+ const maximumInvoiceAmountMsats = optionalSafeInteger(
2406
+ record.maximum_invoice_amount_msats,
2407
+ "maximum_invoice_amount_msats"
2408
+ );
2362
2409
  return {
2363
- checkout_id: state.checkout_id,
2364
- reference: state.reference,
2365
- status: state.paid ? "paid" : state.terminal ? "expired" : "open",
2366
- ...state.settled_at === void 0 ? {} : { paid_at: state.settled_at },
2367
- amount_msats: state.amount_msats ?? 0,
2368
- active: state.paid ? void 0 : invoice,
2369
- invoices: [invoice]
2410
+ pay_in_asset: requiredString(record.pay_in_asset, "pay_in_asset"),
2411
+ label: requiredString(record.label, "label"),
2412
+ network_label: requiredString(record.network_label, "network_label"),
2413
+ provider,
2414
+ available: record.available === true,
2415
+ ...unavailableReason === void 0 ? {} : { unavailable_reason: unavailableReason },
2416
+ ...unavailableMessage === void 0 ? {} : { unavailable_message: unavailableMessage },
2417
+ ...payAmount === void 0 ? {} : { pay_amount: payAmount },
2418
+ ...minimumPayAmount === void 0 ? {} : { minimum_pay_amount: minimumPayAmount },
2419
+ ...maximumPayAmount === void 0 ? {} : { maximum_pay_amount: maximumPayAmount },
2420
+ ...minimumInvoiceAmountMsats === void 0 ? {} : { minimum_invoice_amount_msats: minimumInvoiceAmountMsats },
2421
+ ...maximumInvoiceAmountMsats === void 0 ? {} : { maximum_invoice_amount_msats: maximumInvoiceAmountMsats }
2370
2422
  };
2371
2423
  }
2372
2424
 
@@ -2460,7 +2512,7 @@ function isQrEncoder(value) {
2460
2512
  }
2461
2513
 
2462
2514
  // src/internal/checkout-watcher.ts
2463
- import { unixSeconds as unixSeconds4 } from "@openreceive/core";
2515
+ import { unixSeconds as unixSeconds3 } from "@openreceive/core";
2464
2516
  var CheckoutWatcher = class {
2465
2517
  options;
2466
2518
  state;
@@ -2630,7 +2682,7 @@ var CheckoutWatcher = class {
2630
2682
  this.pollTimer = void 0;
2631
2683
  }
2632
2684
  now() {
2633
- return this.options.now?.() ?? unixSeconds4();
2685
+ return this.options.now?.() ?? unixSeconds3();
2634
2686
  }
2635
2687
  setInterval() {
2636
2688
  return this.options.setInterval ?? globalThis.setInterval;
@@ -2718,13 +2770,12 @@ function createCheckoutController(options) {
2718
2770
  }
2719
2771
 
2720
2772
  // src/status.ts
2721
- import { unixSeconds as unixSeconds5 } from "@openreceive/core";
2722
2773
  function deriveStatus(invoice, options = {}) {
2723
2774
  if (invoice.transaction_state === "settled") return "settled";
2724
2775
  if (invoice.transaction_state === "failed") return "failed";
2725
2776
  if (invoice.transaction_state === "expired") return "expired";
2726
2777
  const expiresAt = readUnixSeconds(invoice.expires_at);
2727
- if (expiresAt !== void 0 && expiresAt <= (options.now ?? unixSeconds5())) {
2778
+ if (expiresAt !== void 0 && expiresAt <= resolveNow(options.now)) {
2728
2779
  return "expired";
2729
2780
  }
2730
2781
  return "pending";
@@ -2936,14 +2987,6 @@ export {
2936
2987
  createTransactionDetailsFromState,
2937
2988
  resolveTransactionDetailRows,
2938
2989
  createPaymentDataEntries,
2939
- optionalSafeInteger,
2940
- requestHeaders,
2941
- checkoutRoutes,
2942
- BrowserRequestError,
2943
- readJsonResponse,
2944
- requestCheckout,
2945
- prepareCheckout,
2946
- createStatusFetcher,
2947
2990
  getWizardEmptyMessage,
2948
2991
  getRouteNetworkLabel,
2949
2992
  createWizardRouteAssetDisplays,
@@ -2974,6 +3017,7 @@ export {
2974
3017
  createPaymentWizardModel,
2975
3018
  updatePaymentWizardSelection,
2976
3019
  createPaymentWizardController,
3020
+ optionalSafeInteger,
2977
3021
  sanitizeBrowserLogEntry,
2978
3022
  createAppBrowserConsoleLogger,
2979
3023
  resolveBrowserLogger,
@@ -2983,6 +3027,17 @@ export {
2983
3027
  createCheckoutState,
2984
3028
  createCheckoutSnapshotFromInvoice,
2985
3029
  createCheckoutStatusModel,
3030
+ findReusableLightningInvoice,
3031
+ mergeAttemptIntoSnapshot,
3032
+ mergeAttemptIntoCheckout,
3033
+ mergeMintedCheckout,
3034
+ requestHeaders,
3035
+ checkoutRoutes,
3036
+ BrowserRequestError,
3037
+ readJsonResponse,
3038
+ requestCheckout,
3039
+ prepareCheckout,
3040
+ createStatusFetcher,
2986
3041
  createQrSvg,
2987
3042
  createQrPayloadSvg,
2988
3043
  createQrPngDataUrl,