@asteby/storefront-react 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/payments.cjs CHANGED
@@ -193,6 +193,13 @@ function mercadoPagoBricksAdapter(options = {}) {
193
193
  const device = getDevice();
194
194
  return { provider_data, ...typeof device === "string" && device ? { device_session_id: device } : {} };
195
195
  },
196
+ async update(next) {
197
+ if (gone) throw new Error("payment form is unmounted");
198
+ if (typeof controller.update !== "function") throw new Error("payment form: update not supported");
199
+ const value = Number.parseFloat(next.amount.amount);
200
+ if (!Number.isFinite(value) || value <= 0) throw new Error("payment form: invalid amount");
201
+ await controller.update({ amount: value });
202
+ },
196
203
  async unmount() {
197
204
  gone = true;
198
205
  try {
@@ -204,6 +211,341 @@ function mercadoPagoBricksAdapter(options = {}) {
204
211
  }
205
212
  };
206
213
  }
214
+
215
+ // src/payments/mount-queue.ts
216
+ var PaymentMountCancelled = class extends Error {
217
+ name = "PaymentMountCancelled";
218
+ constructor(message = "payment form: mount cancelled") {
219
+ super(message);
220
+ }
221
+ };
222
+ var PaymentMountTimeout = class extends Error {
223
+ name = "PaymentMountTimeout";
224
+ constructor(ms) {
225
+ super(`payment form: create timeout after ${ms}ms`);
226
+ }
227
+ };
228
+ var chains = /* @__PURE__ */ new WeakMap();
229
+ var seq = 0;
230
+ function withTimeout(p, ms) {
231
+ return new Promise((resolve, reject) => {
232
+ const t = setTimeout(() => reject(new PaymentMountTimeout(ms)), ms);
233
+ p.then(
234
+ (v) => {
235
+ clearTimeout(t);
236
+ resolve(v);
237
+ },
238
+ (e) => {
239
+ clearTimeout(t);
240
+ reject(e);
241
+ }
242
+ );
243
+ });
244
+ }
245
+ async function mountQueued(base, opts, queue = {}) {
246
+ const container = opts.container;
247
+ const createTimeoutMs = queue.createTimeoutMs ?? 1e4;
248
+ let chain = chains.get(container);
249
+ if (!chain) {
250
+ chain = { tail: Promise.resolve(), active: null, ticket: 0 };
251
+ chains.set(container, chain);
252
+ }
253
+ const me = chain;
254
+ let release;
255
+ const done = new Promise((r) => release = r);
256
+ const previous = me.tail;
257
+ const ticket = ++me.ticket;
258
+ me.tail = done;
259
+ let released = false;
260
+ const finish = () => {
261
+ if (!released) {
262
+ released = true;
263
+ release();
264
+ }
265
+ };
266
+ const cancelled = () => opts.signal?.aborted === true || !container.isConnected || me.ticket !== ticket;
267
+ let slot = null;
268
+ try {
269
+ if (me.active) await me.active.teardown();
270
+ await previous;
271
+ if (cancelled()) throw new PaymentMountCancelled();
272
+ slot = container.ownerDocument.createElement("div");
273
+ slot.id = `${container.id || "sf-pay"}-m${++seq}`;
274
+ container.replaceChildren(slot);
275
+ const node = slot;
276
+ const pending2 = base.mount({ ...opts, container: node });
277
+ let handle;
278
+ try {
279
+ handle = await withTimeout(pending2, createTimeoutMs);
280
+ } catch (err) {
281
+ void pending2.then((h) => Promise.resolve(h.unmount()).catch(() => {
282
+ }), () => {
283
+ });
284
+ throw err;
285
+ }
286
+ let torn = null;
287
+ const teardown = () => torn ??= (async () => {
288
+ try {
289
+ await handle.unmount();
290
+ } catch {
291
+ }
292
+ node.remove();
293
+ if (me.active === entry) me.active = null;
294
+ finish();
295
+ })();
296
+ const entry = { teardown };
297
+ me.active = entry;
298
+ if (cancelled() || !node.isConnected) {
299
+ await teardown();
300
+ throw new PaymentMountCancelled();
301
+ }
302
+ return {
303
+ collect: () => handle.collect(),
304
+ ...handle.update ? { update: (next) => handle.update(next) } : {},
305
+ unmount: teardown
306
+ };
307
+ } catch (err) {
308
+ slot?.remove();
309
+ finish();
310
+ throw err;
311
+ }
312
+ }
313
+ function withMountQueue(adapter, queue = {}) {
314
+ return { kind: adapter.kind, load: (client, options) => adapter.load(client, options), mount: (opts) => mountQueued(adapter, opts, queue) };
315
+ }
316
+
317
+ // src/payments/adapter.ts
318
+ var PaymentHttpError = class extends Error {
319
+ constructor(status, code, message, details, extra) {
320
+ super(message ?? code);
321
+ this.status = status;
322
+ this.code = code;
323
+ this.details = details;
324
+ this.retryable = extra?.retryable;
325
+ this.kind = extra?.kind;
326
+ }
327
+ status;
328
+ code;
329
+ details;
330
+ name = "PaymentHttpError";
331
+ /** `error.retryable` of the shop body: `true` repeat with the SAME key; `false` it is an answer. `undefined`: not said. */
332
+ retryable;
333
+ /** `error.kind` of the shop body (`classifyPaymentError`). */
334
+ kind;
335
+ };
336
+ async function parse(res) {
337
+ const text = await res.text();
338
+ if (!text) return null;
339
+ try {
340
+ return JSON.parse(text);
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+ function createFetchTransport(url, options = {}) {
346
+ const doFetch = (u, init) => (options.fetch ?? globalThis.fetch.bind(globalThis))(u, init);
347
+ async function call(u, init) {
348
+ const res = await doFetch(u, { credentials: options.credentials ?? "same-origin", ...init });
349
+ const body = await parse(res);
350
+ if (!res.ok) {
351
+ const e = body?.error;
352
+ throw new PaymentHttpError(res.status, e?.code ?? "server_error", e?.message, e?.details, {
353
+ retryable: typeof e?.retryable === "boolean" ? e.retryable : void 0,
354
+ kind: typeof e?.kind === "string" ? e.kind : void 0
355
+ });
356
+ }
357
+ const result = body;
358
+ if (!result || typeof result !== "object" || !result.payment || typeof result.payment.status !== "string") {
359
+ throw new PaymentHttpError(502, "invalid_response", "Unexpected payment response");
360
+ }
361
+ return result;
362
+ }
363
+ return {
364
+ submit: (request, { signal }) => call(url, {
365
+ method: "POST",
366
+ signal,
367
+ headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey, ...options.headers },
368
+ body: JSON.stringify(options.buildBody ? options.buildBody(request) : request)
369
+ }),
370
+ ...options.refreshUrl ? {
371
+ refresh: ({ signal }) => call(options.refreshUrl, { method: "POST", signal, headers: { Accept: "application/json", ...options.headers } })
372
+ } : {}
373
+ };
374
+ }
375
+
376
+ // src/payments/recovery-messages.ts
377
+ var ES = {
378
+ "error.transient": "Estamos verificando tu pago. Si el cargo se hizo, lo veremos aqu\xED; no te cobraremos dos veces.",
379
+ "error.in_progress": "Ya hay un pago en proceso para este pedido. Estamos verific\xE1ndolo; no vuelvas a pagar.",
380
+ "error.rate_limited": "Hiciste varios intentos seguidos. Espera unos minutos e int\xE9ntalo de nuevo.",
381
+ "error.merchant_config": "Por ahora no podemos procesar pagos con tarjeta. No es un problema con tu tarjeta: puedes pagar de otra forma.",
382
+ "error.provider_declined": "No pudimos procesar tu pago con esa tarjeta. Revisa los datos o prueba con otro m\xE9todo; no se hizo ning\xFAn cobro.",
383
+ "error.stale_checkout": "Este pedido ya qued\xF3 registrado y no se pudo reabrir el pago. Actualiza para ver c\xF3mo va tu pedido.",
384
+ "error.expired": "Tu carrito venci\xF3. Vuelve a armarlo para pagar.",
385
+ "error.price_mismatch": "El total cambi\xF3. Revisa el nuevo total y vuelve a pagar.",
386
+ "error.store_closed": "Esta tienda a\xFAn no est\xE1 abierta para compras. Vuelve pronto.",
387
+ "error.validation": "Completa los campos marcados.",
388
+ "error.other": "No pudimos procesar tu pago. Intenta de nuevo.",
389
+ "notice.confirming.title": "Estamos confirmando tu pago\u2026",
390
+ "notice.confirming.body": "No cierres esta p\xE1gina ni vuelvas a pagar. Si el cargo ya se hizo, lo ver\xE1s reflejado aqu\xED.",
391
+ "notice.form_failed.title": "No pudimos cargar el formulario de tarjeta",
392
+ "notice.form_failed.body": "Suele resolverse al intentar de nuevo. Tambi\xE9n puedes pagar de otra forma.",
393
+ "notice.rejected.title": "El cargo no se complet\xF3",
394
+ "notice.rejected.body": "No se hizo ning\xFAn cobro. Puedes intentar con otra tarjeta o elegir otra forma de pago.",
395
+ "notice.provider_declined.title": "No pudimos procesar tu pago con esa tarjeta",
396
+ "notice.provider_declined.body": "No se hizo ning\xFAn cobro. Revisa los datos o prueba con otra tarjeta u otro m\xE9todo de pago.",
397
+ "notice.merchant_config.title": "Por ahora no podemos cobrar con tarjeta",
398
+ "notice.merchant_config.body": "No es un problema con tu tarjeta: es un tema de nuestro lado. Puedes pagar de otra forma.",
399
+ "notice.stale_checkout.title": "Tu pedido ya qued\xF3 registrado",
400
+ "notice.stale_checkout.body": "No pudimos reabrir el pago de este pedido. Actualiza para ver c\xF3mo va tu pedido; no se cobr\xF3 de nuevo.",
401
+ "notice.other.title": "No pudimos procesar tu pago",
402
+ "notice.other.body": "Intenta de nuevo; si sigue igual, puedes pagar de otra forma.",
403
+ "action.retry": "Reintentar pago",
404
+ "action.retry_form": "Reintentar",
405
+ "action.refresh": "Actualizar mi pedido",
406
+ "action.alternative": "Pagar con {label}"
407
+ };
408
+ var EN = {
409
+ "error.transient": "We're verifying your payment. If the charge went through we'll show it here; you won't be charged twice.",
410
+ "error.in_progress": "A payment is already in progress for this order. We're verifying it; don't pay again.",
411
+ "error.rate_limited": "Too many attempts in a row. Wait a few minutes and try again.",
412
+ "error.merchant_config": "We can't take card payments right now. It isn't a problem with your card: you can pay another way.",
413
+ "error.provider_declined": "We couldn't process your payment with that card. Check the details or try another method; you were not charged.",
414
+ "error.stale_checkout": "This order is already registered and the payment couldn't be reopened. Refresh to see how it's going.",
415
+ "error.expired": "Your cart expired. Rebuild it to pay.",
416
+ "error.price_mismatch": "The total changed. Check the new total and pay again.",
417
+ "error.store_closed": "This store isn't open for purchases yet. Come back soon.",
418
+ "error.validation": "Complete the marked fields.",
419
+ "error.other": "We couldn't process your payment. Try again.",
420
+ "notice.confirming.title": "We're confirming your payment\u2026",
421
+ "notice.confirming.body": "Don't close this page or pay again. If the charge already went through you'll see it here.",
422
+ "notice.form_failed.title": "We couldn't load the card form",
423
+ "notice.form_failed.body": "Trying again usually fixes it. You can also pay another way.",
424
+ "notice.rejected.title": "The charge didn't go through",
425
+ "notice.rejected.body": "You were not charged. Try another card or choose another way to pay.",
426
+ "notice.provider_declined.title": "We couldn't process your payment with that card",
427
+ "notice.provider_declined.body": "You were not charged. Check the details or try another card or method.",
428
+ "notice.merchant_config.title": "We can't charge cards right now",
429
+ "notice.merchant_config.body": "It isn't a problem with your card: it's on our side. You can pay another way.",
430
+ "notice.stale_checkout.title": "Your order is already registered",
431
+ "notice.stale_checkout.body": "We couldn't reopen the payment for this order. Refresh to see how it's going; you were not charged again.",
432
+ "notice.other.title": "We couldn't process your payment",
433
+ "notice.other.body": "Try again; if it keeps failing you can pay another way.",
434
+ "action.retry": "Retry payment",
435
+ "action.retry_form": "Retry",
436
+ "action.refresh": "Refresh my order",
437
+ "action.alternative": "Pay with {label}"
438
+ };
439
+ function paymentRecoveryMessages(locale, overrides) {
440
+ const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN : ES;
441
+ return overrides ? { ...base, ...overrides } : base;
442
+ }
443
+
444
+ // src/payments/resilient-transport.ts
445
+ var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
446
+ function isResolvedAttempt(result) {
447
+ const p = result.payment;
448
+ if (p.status === "paid" || p.status === "failed" || p.status === "expired" || p.status === "cancelled") return true;
449
+ const type = p.action?.type;
450
+ return type === "voucher" || type === "challenge" || type === "redirect";
451
+ }
452
+ async function readJson(res) {
453
+ const text = await res.text().catch(() => "");
454
+ if (!text) return null;
455
+ try {
456
+ return JSON.parse(text);
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+ function createResilientTransport(opts) {
462
+ const doFetch = opts.fetch ?? ((u, i) => globalThis.fetch(u, i));
463
+ const sleep2 = opts.sleep ?? realSleep;
464
+ const retries = opts.retries ?? 2;
465
+ const backoff = opts.backoffMs ?? [800, 2e3];
466
+ const polls = opts.statusPolls ?? 3;
467
+ const pollMs = opts.statusPollMs ?? 2e3;
468
+ const msgs = paymentRecoveryMessages(opts.locale, opts.messages);
469
+ const text = (k) => msgs[`error.${k}`];
470
+ const issue = (i) => opts.onIssue?.(i);
471
+ const confirming = msgs["notice.confirming.title"];
472
+ async function call(url, init) {
473
+ let res;
474
+ try {
475
+ res = await doFetch(url, { credentials: "same-origin", ...init, headers: { ...opts.headers, ...init.headers } });
476
+ } catch (err) {
477
+ if (err?.name === "AbortError") throw err;
478
+ return { fail: { kind: "transient", status: 0, code: "network", message: text("transient"), retryable: true } };
479
+ }
480
+ const body = await readJson(res);
481
+ if (res.ok) {
482
+ const r = body;
483
+ if (r && typeof r === "object" && r.payment && typeof r.payment.status === "string") return { ok: r };
484
+ return { fail: { kind: "transient", status: 502, code: "invalid_response", message: text("transient"), retryable: true } };
485
+ }
486
+ const e = body?.error;
487
+ if (!e) {
488
+ const info2 = storefront.classifyPaymentError({ status: res.status >= 500 || res.status === 0 ? 503 : res.status });
489
+ return { fail: { kind: info2.kind, status: res.status, code: "server_error", message: text(info2.kind), retryable: info2.retryable } };
490
+ }
491
+ const byCode = storefront.classifyPaymentError({ status: res.status, code: e.code ?? null, field: e.field ?? null, retryable: typeof e.retryable === "boolean" ? e.retryable : null, checkoutId: e.checkout_id ?? null, details: e.details });
492
+ const info = e.kind && !(e.kind === "transient" && !byCode.retryable) ? { kind: e.kind, retryable: e.retryable === true || byCode.retryable } : byCode;
493
+ return { fail: { kind: info.kind, status: res.status, code: e.code ?? "server_error", message: e.message || text(info.kind), retryable: info.retryable, details: e.details } };
494
+ }
495
+ const toError = (f) => f.kind === "transient" ? new PaymentHttpError(503, "network", f.message, f.details, { retryable: true, kind: "transient" }) : new PaymentHttpError(f.status || 500, f.code, f.message, f.details, { retryable: f.retryable, kind: f.kind });
496
+ async function resolveByStatus(signal) {
497
+ for (let i = 0; i < polls; i++) {
498
+ const r = await call(opts.statusUrl, { method: "POST", signal, headers: { Accept: "application/json" } });
499
+ if ("ok" in r) {
500
+ if (isResolvedAttempt(r.ok)) return r.ok;
501
+ } else if (!r.fail.retryable) {
502
+ return null;
503
+ }
504
+ if (i < polls - 1) await sleep2(pollMs);
505
+ }
506
+ return null;
507
+ }
508
+ return {
509
+ async submit(request, { signal }) {
510
+ const init = {
511
+ method: "POST",
512
+ signal,
513
+ headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey },
514
+ body: JSON.stringify({ ...request, fields: opts.fields() })
515
+ };
516
+ issue(null);
517
+ let last = null;
518
+ for (let n = 0; n <= retries; n++) {
519
+ const r = await call(opts.payUrl, init);
520
+ if ("ok" in r) {
521
+ issue(null);
522
+ return r.ok;
523
+ }
524
+ last = r.fail;
525
+ const waitable = last.retryable && (last.kind === "transient" || last.kind === "in_progress");
526
+ if (!waitable || n === retries) break;
527
+ issue({ kind: "confirming", message: confirming });
528
+ await sleep2(backoff[Math.min(n, backoff.length - 1)] ?? 800);
529
+ }
530
+ const failure = last;
531
+ if (failure.retryable && (failure.kind === "transient" || failure.kind === "in_progress") || failure.kind === "stale_checkout") {
532
+ issue({ kind: "verifying", message: confirming });
533
+ const resolved = await resolveByStatus(signal);
534
+ if (resolved) {
535
+ issue(null);
536
+ return resolved;
537
+ }
538
+ }
539
+ issue({ kind: failure.kind, message: failure.message, retryable: failure.retryable, status: failure.status, code: failure.code });
540
+ throw toError(failure);
541
+ },
542
+ refresh: async ({ signal }) => {
543
+ const r = await call(opts.statusUrl, { method: "POST", signal, headers: { Accept: "application/json" } });
544
+ if ("ok" in r) return r.ok;
545
+ throw toError(r.fail);
546
+ }
547
+ };
548
+ }
207
549
  function isSafeCssValue(value) {
208
550
  if (!value || value.length > 200) return false;
209
551
  if (/[;{}<>\\]/.test(value)) return false;
@@ -279,7 +621,9 @@ function useEmbeddedPayment(options) {
279
621
  autoRetries = 2,
280
622
  retryDelayMs = 600,
281
623
  pollIntervalMs = 2e3,
282
- pollTimeoutMs = 6e4,
624
+ pollTimeoutMs = 3e5,
625
+ verifyTimeoutMs = 12e4,
626
+ verifyIntervalMs = 2e3,
283
627
  scope = "default"
284
628
  } = options;
285
629
  const msgs = react.useMemo(() => storefront.paymentMessages(locale, options.messages), [locale, options.messages]);
@@ -293,15 +637,28 @@ function useEmbeddedPayment(options) {
293
637
  const [attemptId, setAttemptId] = react.useState(null);
294
638
  const [container, setContainer] = react.useState(null);
295
639
  const [mountKey, setMountKey] = react.useState(0);
640
+ const [verifying, setVerifying] = react.useState(false);
296
641
  const live = react.useRef({ ...options, msgs });
297
642
  live.current = { ...options, msgs };
298
643
  const form = react.useRef(null);
299
644
  const inFlight = react.useRef(null);
300
645
  const pendingData = react.useRef(null);
646
+ const memAttempt = react.useRef(null);
647
+ const memOnly = react.useRef(false);
648
+ const errorRef = react.useRef(null);
649
+ const retryRef = react.useRef(() => Promise.resolve());
301
650
  const alive = react.useRef(true);
302
651
  const generation = react.useRef(0);
303
652
  const statusRef = react.useRef("idle");
653
+ const leftChallenge = react.useRef(false);
304
654
  statusRef.current = status;
655
+ errorRef.current = error;
656
+ const amountKey = `${amount.amount}|${amount.currency_code}`;
657
+ const amountKeyRef = react.useRef(amountKey);
658
+ amountKeyRef.current = amountKey;
659
+ const appliedAmount = react.useRef(null);
660
+ const updating = react.useRef(null);
661
+ const remountRequested = react.useRef(false);
305
662
  react.useEffect(() => {
306
663
  alive.current = true;
307
664
  return () => {
@@ -312,19 +669,33 @@ function useEmbeddedPayment(options) {
312
669
  const store = options.storage === void 0 ? safeStorage() : options.storage;
313
670
  const emit = react.useCallback((e) => live.current.onEvent?.(e), []);
314
671
  const readAttempt = react.useCallback(() => {
315
- try {
316
- const saved = store?.getItem(storageKey);
317
- if (saved) return saved;
318
- } catch {
672
+ if (!memOnly.current) {
673
+ try {
674
+ const saved = store?.getItem(storageKey);
675
+ if (saved) {
676
+ memAttempt.current = saved;
677
+ return saved;
678
+ }
679
+ } catch {
680
+ memOnly.current = true;
681
+ }
682
+ }
683
+ if (memOnly.current && memAttempt.current) return memAttempt.current;
684
+ if (!store) {
685
+ if (memAttempt.current) return memAttempt.current;
319
686
  }
320
687
  const fresh = (live.current.newAttemptId ?? storefront.generateIdempotencyKey)();
688
+ memAttempt.current = fresh;
321
689
  try {
322
690
  store?.setItem(storageKey, fresh);
691
+ if (store && store.getItem(storageKey) !== fresh) memOnly.current = true;
323
692
  } catch {
693
+ memOnly.current = true;
324
694
  }
325
695
  return fresh;
326
696
  }, [store, storageKey]);
327
697
  const rotateAttempt = react.useCallback(() => {
698
+ memAttempt.current = null;
328
699
  try {
329
700
  store?.removeItem(storageKey);
330
701
  } catch {
@@ -340,7 +711,32 @@ function useEmbeddedPayment(options) {
340
711
  },
341
712
  [emit]
342
713
  );
343
- const amountKey = `${amount.amount}|${amount.currency_code}`;
714
+ const syncAmount = react.useCallback(() => {
715
+ const m = form.current;
716
+ if (!m || inFlight.current || updating.current || remountRequested.current) return;
717
+ if (statusRef.current === "network_error" || statusRef.current === "blocked" && errorRef.current?.kind === "payment_in_progress") return;
718
+ if (appliedAmount.current === amountKeyRef.current) return;
719
+ const run = (async () => {
720
+ while (form.current === m && alive.current && appliedAmount.current !== amountKeyRef.current) {
721
+ const key = amountKeyRef.current;
722
+ try {
723
+ if (!m.update) throw new Error("payment form: update not supported");
724
+ await m.update({ amount: live.current.amount });
725
+ appliedAmount.current = key;
726
+ } catch {
727
+ if (form.current === m && alive.current) {
728
+ remountRequested.current = true;
729
+ setMountKey((k) => k + 1);
730
+ }
731
+ return;
732
+ }
733
+ }
734
+ })().finally(() => {
735
+ updating.current = null;
736
+ if (alive.current) syncAmount();
737
+ });
738
+ updating.current = run;
739
+ }, []);
344
740
  const kind = client?.kind;
345
741
  const publicKey = client?.public_key;
346
742
  react.useEffect(() => {
@@ -354,18 +750,22 @@ function useEmbeddedPayment(options) {
354
750
  return;
355
751
  }
356
752
  if (!container) return;
753
+ remountRequested.current = false;
357
754
  const my = ++generation.current;
358
755
  const started = Date.now();
756
+ const abort = new AbortController();
359
757
  let mounted = null;
360
758
  setStatus("loading");
361
759
  const stale = () => my !== generation.current || !alive.current;
362
760
  const fail = (reason) => {
363
761
  if (stale()) return;
364
762
  generation.current++;
763
+ abort.abort();
365
764
  clearTimeout(timer);
366
765
  const m = mounted ?? form.current;
367
766
  mounted = null;
368
767
  form.current = null;
768
+ appliedAmount.current = null;
369
769
  if (m) void Promise.resolve(m.unmount());
370
770
  fallback(reason);
371
771
  };
@@ -379,31 +779,39 @@ function useEmbeddedPayment(options) {
379
779
  if (stale()) return;
380
780
  const theme = live.current.theme ?? readPaymentTheme(container);
381
781
  const colorScheme = live.current.colorScheme ?? readPaymentScheme(container);
382
- const m = await adapter.mount({
383
- container,
384
- client,
385
- amount: live.current.amount,
386
- ...live.current.email ? { email: live.current.email } : {},
387
- ...live.current.locale ? { locale: live.current.locale } : {},
388
- theme,
389
- ...colorScheme ? { colorScheme } : {},
390
- onReady: () => {
391
- if (stale()) return;
392
- clearTimeout(timer);
393
- setStatus((s) => s === "loading" ? "ready" : s);
394
- emit({ type: "form_ready", ms: Date.now() - started });
782
+ const createdWith = amountKeyRef.current;
783
+ const m = await mountQueued(
784
+ adapter,
785
+ {
786
+ container,
787
+ client,
788
+ amount: live.current.amount,
789
+ ...live.current.email ? { email: live.current.email } : {},
790
+ ...live.current.locale ? { locale: live.current.locale } : {},
791
+ theme,
792
+ ...colorScheme ? { colorScheme } : {},
793
+ signal: abort.signal,
794
+ onReady: () => {
795
+ if (stale()) return;
796
+ clearTimeout(timer);
797
+ setStatus((s) => s === "loading" ? "ready" : s);
798
+ emit({ type: "form_ready", ms: Date.now() - started });
799
+ },
800
+ onError: (e) => {
801
+ if (stale()) return;
802
+ if (e.critical) fail("form_error");
803
+ }
395
804
  },
396
- onError: (e) => {
397
- if (stale()) return;
398
- if (e.critical) fail("form_error");
399
- }
400
- });
805
+ { createTimeoutMs: live.current.createTimeoutMs ?? sdkTimeoutMs }
806
+ );
401
807
  if (stale()) {
402
808
  void Promise.resolve(m.unmount());
403
809
  return;
404
810
  }
405
811
  mounted = m;
406
812
  form.current = m;
813
+ appliedAmount.current = createdWith;
814
+ syncAmount();
407
815
  } catch (err) {
408
816
  fail(err instanceof Error && /timeout/i.test(err.message) ? "sdk_timeout" : "sdk_error");
409
817
  }
@@ -411,16 +819,22 @@ function useEmbeddedPayment(options) {
411
819
  return () => {
412
820
  clearTimeout(timer);
413
821
  generation.current++;
822
+ abort.abort();
414
823
  const m = mounted ?? form.current;
415
824
  mounted = null;
416
825
  form.current = null;
826
+ appliedAmount.current = null;
417
827
  if (m) void Promise.resolve(m.unmount());
418
828
  };
419
- }, [enabled, container, kind, publicKey, amountKey, mountKey, sdkTimeoutMs, containerId]);
829
+ }, [enabled, container, kind, publicKey, mountKey, sdkTimeoutMs, containerId]);
830
+ react.useEffect(() => {
831
+ syncAmount();
832
+ }, [amountKey, syncAmount]);
420
833
  const finish = react.useCallback(
421
834
  (result) => {
422
835
  const o = storefront.paymentOutcome(result, live.current.locale, live.current.messages);
423
836
  pendingData.current = null;
837
+ leftChallenge.current = false;
424
838
  setOutcome(o);
425
839
  setError(null);
426
840
  setAttemptsLeft(o.attempts_left);
@@ -440,7 +854,7 @@ function useEmbeddedPayment(options) {
440
854
  [emit, rotateAttempt]
441
855
  );
442
856
  const send = react.useCallback(
443
- async (data, attempt) => {
857
+ async (data, attempt, total) => {
444
858
  const ctl = new AbortController();
445
859
  const maxRetries = live.current.autoRetries ?? autoRetries;
446
860
  for (let n = 0; ; n++) {
@@ -452,7 +866,7 @@ function useEmbeddedPayment(options) {
452
866
  idempotencyKey: attempt,
453
867
  provider_data: data.provider_data,
454
868
  ...data.device_session_id ? { device_session_id: data.device_session_id } : {},
455
- expected_total: live.current.amount
869
+ expected_total: total
456
870
  },
457
871
  { signal: ctl.signal }
458
872
  );
@@ -470,7 +884,7 @@ function useEmbeddedPayment(options) {
470
884
  emit({ type: "error", kind: view.kind });
471
885
  setError(view);
472
886
  if (view.kind === "network") {
473
- pendingData.current = data;
887
+ pendingData.current = { data, total };
474
888
  setStatus("network_error");
475
889
  } else if (view.kind === "price_mismatch") {
476
890
  pendingData.current = null;
@@ -496,7 +910,11 @@ function useEmbeddedPayment(options) {
496
910
  if (inFlight.current) return inFlight.current;
497
911
  const m = form.current;
498
912
  if (!m || statusRef.current !== "ready" && statusRef.current !== "network_error") return Promise.resolve();
913
+ if (statusRef.current === "network_error" && pendingData.current) return retryRef.current();
914
+ const total = live.current.amount;
499
915
  const run = (async () => {
916
+ if (updating.current) await updating.current.catch(() => {
917
+ });
500
918
  setStatus("processing");
501
919
  setError(null);
502
920
  setOutcome(null);
@@ -512,30 +930,33 @@ function useEmbeddedPayment(options) {
512
930
  }
513
931
  const attempt = readAttempt();
514
932
  setAttemptId(attempt);
515
- await send(data, attempt);
933
+ await send(data, attempt, total);
516
934
  })().finally(() => {
517
935
  inFlight.current = null;
936
+ syncAmount();
518
937
  });
519
938
  inFlight.current = run;
520
939
  return run;
521
- }, [emit, readAttempt, send]);
940
+ }, [emit, readAttempt, send, syncAmount]);
522
941
  const retry = react.useCallback(() => {
523
942
  if (inFlight.current) return inFlight.current;
524
- const data = pendingData.current;
525
- if (!data) return submit();
943
+ const pending2 = pendingData.current;
944
+ if (!pending2) return submit();
526
945
  emit({ type: "retry" });
527
946
  const run = (async () => {
528
947
  setStatus("processing");
529
948
  setError(null);
530
949
  const attempt = readAttempt();
531
950
  setAttemptId(attempt);
532
- await send(data, attempt);
951
+ await send(pending2.data, attempt, pending2.total);
533
952
  })().finally(() => {
534
953
  inFlight.current = null;
954
+ syncAmount();
535
955
  });
536
956
  inFlight.current = run;
537
957
  return run;
538
- }, [emit, readAttempt, send, submit]);
958
+ }, [emit, readAttempt, send, submit, syncAmount]);
959
+ retryRef.current = retry;
539
960
  react.useEffect(() => {
540
961
  if (status !== "challenge" && status !== "in_review" || !transport.refresh) return;
541
962
  let stop = false;
@@ -548,6 +969,7 @@ function useEmbeddedPayment(options) {
548
969
  try {
549
970
  const r = await transport.refresh({ signal: ctl.signal });
550
971
  if (stop || !alive.current) return;
972
+ if (r.payment.status === "none") continue;
551
973
  const o = storefront.paymentOutcome(r, live.current.locale, live.current.messages);
552
974
  if (o.kind !== status) {
553
975
  finish(r);
@@ -562,10 +984,57 @@ function useEmbeddedPayment(options) {
562
984
  ctl.abort();
563
985
  };
564
986
  }, [status, transport, pollIntervalMs, pollTimeoutMs, finish]);
987
+ const unknownOutcome = status === "network_error" || status === "blocked" && error?.kind === "payment_in_progress";
988
+ react.useEffect(() => {
989
+ if (!unknownOutcome) syncAmount();
990
+ }, [unknownOutcome, syncAmount]);
991
+ react.useEffect(() => {
992
+ if (!unknownOutcome || !transport.refresh) {
993
+ setVerifying(false);
994
+ return;
995
+ }
996
+ let stop = false;
997
+ const ctl = new AbortController();
998
+ const until = Date.now() + (leftChallenge.current ? Math.max(verifyTimeoutMs, pollTimeoutMs) : verifyTimeoutMs);
999
+ setVerifying(true);
1000
+ (async () => {
1001
+ let wait = verifyIntervalMs;
1002
+ while (!stop && Date.now() < until) {
1003
+ await sleep(wait);
1004
+ wait = Math.min(wait * 2, 15e3);
1005
+ if (stop || !alive.current) return;
1006
+ try {
1007
+ const r = await transport.refresh({ signal: ctl.signal });
1008
+ if (stop || !alive.current) return;
1009
+ if (isResolvedAttempt(r)) {
1010
+ const settled = r.payment.status === "paid" || r.payment.status === "failed" || r.payment.status === "expired" || r.payment.status === "cancelled";
1011
+ if (leftChallenge.current && !settled && r.payment.action?.type === "challenge") continue;
1012
+ setVerifying(false);
1013
+ finish(r);
1014
+ return;
1015
+ }
1016
+ } catch {
1017
+ }
1018
+ }
1019
+ if (!stop && alive.current) setVerifying(false);
1020
+ })();
1021
+ return () => {
1022
+ stop = true;
1023
+ ctl.abort();
1024
+ setVerifying(false);
1025
+ };
1026
+ }, [unknownOutcome, transport, verifyTimeoutMs, verifyIntervalMs, pollTimeoutMs, finish]);
565
1027
  const dismiss = react.useCallback(() => {
566
1028
  setError(null);
567
1029
  setOutcome((o) => o?.kind === "rejected" ? null : o);
568
1030
  }, []);
1031
+ const cancelChallenge = react.useCallback(() => {
1032
+ if (statusRef.current !== "challenge") return;
1033
+ leftChallenge.current = true;
1034
+ setOutcome(null);
1035
+ setError({ kind: "payment_in_progress", message: live.current.msgs["challenge.left"], retrySameAttempt: false, canRetry: false });
1036
+ setStatus("blocked");
1037
+ }, []);
569
1038
  const message = status === "processing" ? msgs["state.processing"] : status === "fallback" ? msgs["state.fallback"] : error?.message ?? outcome?.message ?? (status === "loading" ? msgs["state.loading"] : null);
570
1039
  return {
571
1040
  status,
@@ -576,11 +1045,13 @@ function useEmbeddedPayment(options) {
576
1045
  attemptsLeft,
577
1046
  attemptId,
578
1047
  busy: status === "loading" || status === "processing",
1048
+ verifying,
579
1049
  containerRef: setContainer,
580
1050
  containerId,
581
1051
  submit,
582
1052
  retry,
583
- dismiss
1053
+ dismiss,
1054
+ cancelChallenge
584
1055
  };
585
1056
  }
586
1057
  function VoucherCard({ outcome, locale, messages, className }) {
@@ -617,15 +1088,55 @@ function VoucherCard({ outcome, locale, messages, className }) {
617
1088
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["voucher.email"] })
618
1089
  ] });
619
1090
  }
620
- function PaymentChallenge({ outcome, locale, messages, render, onCancel }) {
1091
+ function PaymentChallenge({ outcome, locale, messages, render, onCancel, escapeAfterMs = 12e4 }) {
621
1092
  const m = storefront.paymentMessages(locale, messages);
622
- const { ref } = useModalDialog({ open: true, onClose: () => onCancel?.() });
1093
+ const { ref } = useModalDialog({ open: true, onClose: () => {
1094
+ } });
1095
+ const [canEscape, setCanEscape] = react.useState(false);
1096
+ const [closed, setClosed] = react.useState(false);
1097
+ const cancelRef = react.useRef(onCancel);
1098
+ cancelRef.current = onCancel;
1099
+ const hasCancel = !!onCancel;
1100
+ react.useEffect(() => {
1101
+ if (!hasCancel) return;
1102
+ const t = setTimeout(() => setCanEscape(true), escapeAfterMs);
1103
+ return () => clearTimeout(t);
1104
+ }, [hasCancel, escapeAfterMs]);
1105
+ react.useEffect(() => {
1106
+ const el = ref.current;
1107
+ if (!el) return;
1108
+ const onClosed = () => setClosed(true);
1109
+ el.addEventListener("close", onClosed);
1110
+ return () => el.removeEventListener("close", onClosed);
1111
+ }, [ref]);
1112
+ const reopen = () => {
1113
+ const el = ref.current;
1114
+ if (!el) return;
1115
+ try {
1116
+ if (typeof el.showModal === "function") el.showModal();
1117
+ else el.setAttribute("open", "");
1118
+ } catch {
1119
+ el.setAttribute("open", "");
1120
+ }
1121
+ setClosed(false);
1122
+ el.querySelector("[data-sf-autofocus]")?.focus({ preventScroll: true });
1123
+ };
623
1124
  const url = outcome.challenge?.url;
624
- return /* @__PURE__ */ jsxRuntime.jsx("dialog", { ref, "aria-labelledby": "sf-challenge-title", "data-sf-challenge": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
625
- /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "sf-challenge-title", "data-sf-autofocus": true, tabIndex: -1, children: m["challenge.title"] }),
626
- render ? render(outcome) : url ? /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: url, title: m["challenge.title"], style: { width: "100%", minHeight: 420, border: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["state.challenge"] }),
627
- onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onCancel, style: { minWidth: 44, minHeight: 44 }, children: m["action.cancel"] })
628
- ] }) });
1125
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1126
+ closed && /* @__PURE__ */ jsxRuntime.jsxs("p", { role: "status", "data-sf-challenge-closed": true, children: [
1127
+ m["state.challenge"],
1128
+ " ",
1129
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: reopen, "data-sf-challenge-reopen": true, style: { minWidth: 44, minHeight: 44 }, children: m["challenge.title"] })
1130
+ ] }),
1131
+ /* @__PURE__ */ jsxRuntime.jsx("dialog", { ref, "aria-labelledby": "sf-challenge-title", "data-sf-challenge": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-sf-challenge-body": true, children: [
1132
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "sf-challenge-title", "data-sf-autofocus": true, tabIndex: -1, children: m["challenge.title"] }),
1133
+ render ? render(outcome) : url ? /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: url, title: m["challenge.title"], "data-sf-challenge-frame": true, style: { width: "100%", minHeight: 420, border: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["state.challenge"] }),
1134
+ hasCancel && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-sf-challenge-escape": true, "aria-live": "polite", children: canEscape && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1135
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["challenge.escape_hint"] }),
1136
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => cancelRef.current?.(), style: { minWidth: 44, minHeight: 44 }, children: m["challenge.escape"] })
1137
+ ] }) })
1138
+ ] }) })
1139
+ ] });
629
1140
  }
630
1141
  var PaymentBrick = react.forwardRef(function PaymentBrick2(props, ref) {
631
1142
  const { submitLabel, hideAmountInButton, renderVoucher, renderChallenge, minHeight = 360, className, style, ...hookOptions } = props;
@@ -663,7 +1174,7 @@ var PaymentBrick = react.forwardRef(function PaymentBrick2(props, ref) {
663
1174
  state.status === "network_error" && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void state.retry(), style: { minHeight: 44 }, children: m["action.retry"] })
664
1175
  ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", "aria-live": "polite", "data-sf-pay-message": true, children: state.message }),
665
1176
  outcome?.kind === "voucher" && (renderVoucher ? renderVoucher(outcome) : /* @__PURE__ */ jsxRuntime.jsx(VoucherCard, { outcome, locale, ...props.messages ? { messages: props.messages } : {} })),
666
- outcome?.kind === "challenge" && /* @__PURE__ */ jsxRuntime.jsx(PaymentChallenge, { outcome, locale, ...props.messages ? { messages: props.messages } : {}, ...renderChallenge ? { render: renderChallenge } : {} }),
1177
+ outcome?.kind === "challenge" && /* @__PURE__ */ jsxRuntime.jsx(PaymentChallenge, { outcome, locale, onCancel: state.cancelChallenge, ...props.messages ? { messages: props.messages } : {}, ...renderChallenge ? { render: renderChallenge } : {} }),
667
1178
  submitLabel !== void 0 && showForm && /* @__PURE__ */ jsxRuntime.jsxs(
668
1179
  "button",
669
1180
  {
@@ -685,53 +1196,195 @@ var PaymentBrick = react.forwardRef(function PaymentBrick2(props, ref) {
685
1196
  ] });
686
1197
  });
687
1198
 
688
- // src/payments/adapter.ts
689
- var PaymentHttpError = class extends Error {
690
- constructor(status, code, message, details) {
691
- super(message ?? code);
692
- this.status = status;
693
- this.code = code;
694
- this.details = details;
1199
+ // src/payments/payment-method.messages.ts
1200
+ var ES2 = {
1201
+ legend: "M\xE9todo de pago",
1202
+ pay: "Pagar",
1203
+ placeOrder: "Realizar el pedido",
1204
+ placeOrderCash: "Generar ficha de pago",
1205
+ chooseDelivery: "Elige la entrega para continuar",
1206
+ payRedirect: "Continuar al pago",
1207
+ continue: "Continuar",
1208
+ retryAddress: "Guardar direcci\xF3n y continuar",
1209
+ processing: "Procesando\u2026",
1210
+ redirectNote: "Te llevamos a la p\xE1gina del proveedor para terminar el pago y regresas aqu\xED.",
1211
+ cashPoints: "Elige d\xF3nde pagar en efectivo",
1212
+ cashMore: "Ver m\xE1s puntos de pago",
1213
+ cashNote: "Al hacer el pedido te damos una ficha con la referencia y la fecha l\xEDmite. El pedido se confirma cuando pagas.",
1214
+ secureCard: "Tus datos de tarjeta los captura directamente nuestro proveedor de pagos; la tienda nunca los ve.",
1215
+ pickDeliveryFirst: "Elige primero la entrega para ver el total y pagar con tarjeta.",
1216
+ updatingTotal: "Actualizando el total\u2026",
1217
+ syncFailed: "No pudimos actualizar el total con tu entrega. Puedes continuar al sitio del proveedor para pagar con tarjeta.",
1218
+ quoteChanged: "Las tarifas de env\xEDo se actualizaron. Revisa tu env\xEDo para ver el total.",
1219
+ rebuildCart: "Volver a armar mi carrito",
1220
+ noPaymentTitle: "Por ahora no hay formas de pago disponibles",
1221
+ noPayment: "Intenta de nuevo en unos minutos o escr\xEDbenos para completar tu pedido.",
1222
+ paymentNote: "El pago se procesa de forma segura con nuestro proveedor de pagos.",
1223
+ options: {
1224
+ card: "Tarjeta de cr\xE9dito o d\xE9bito",
1225
+ cash: "Efectivo",
1226
+ wallet: "Cuenta del proveedor de pagos",
1227
+ credits: "Pagos sin tarjeta"
695
1228
  }
696
- status;
697
- code;
698
- details;
699
- name = "PaymentHttpError";
700
1229
  };
701
- async function parse(res) {
702
- const text = await res.text();
703
- if (!text) return null;
1230
+ var EN2 = {
1231
+ legend: "Payment method",
1232
+ pay: "Pay",
1233
+ placeOrder: "Place order",
1234
+ placeOrderCash: "Get payment slip",
1235
+ chooseDelivery: "Choose delivery to continue",
1236
+ payRedirect: "Continue to payment",
1237
+ continue: "Continue",
1238
+ retryAddress: "Save address and continue",
1239
+ processing: "Processing\u2026",
1240
+ redirectNote: "We'll take you to the provider's page to finish paying, then bring you back here.",
1241
+ cashPoints: "Choose where to pay in cash",
1242
+ cashMore: "Show more payment points",
1243
+ cashNote: "When you place the order you get a slip with the reference and the deadline. The order is confirmed once you pay.",
1244
+ secureCard: "Your card details are captured directly by our payment provider; the shop never sees them.",
1245
+ pickDeliveryFirst: "Choose delivery first to see the total and pay by card.",
1246
+ updatingTotal: "Updating the total\u2026",
1247
+ syncFailed: "We couldn't update the total with your delivery. You can continue to the provider's site to pay by card.",
1248
+ quoteChanged: "Shipping rates were updated. Check your delivery to see the total.",
1249
+ rebuildCart: "Rebuild my cart",
1250
+ noPaymentTitle: "No payment methods are available right now",
1251
+ noPayment: "Try again in a few minutes or contact us to complete your order.",
1252
+ paymentNote: "Payment is processed securely by our payment provider.",
1253
+ options: {
1254
+ card: "Credit or debit card",
1255
+ cash: "Cash",
1256
+ wallet: "Payment provider account",
1257
+ credits: "Pay without a card"
1258
+ }
1259
+ };
1260
+ function paymentMethodMessages(locale, overrides) {
1261
+ const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN2 : ES2;
1262
+ if (!overrides) return base;
1263
+ const { options, ...rest } = overrides;
1264
+ return { ...base, ...rest, options: { ...base.options, ...options } };
1265
+ }
1266
+ function alternativeChoices(choices) {
1267
+ return choices.filter((c) => c.mode !== "card");
1268
+ }
1269
+ function pickChoice(form, value) {
1270
+ const input = Array.from(form?.querySelectorAll('input[name="payment_choice"]') ?? []).find((i) => i.value === value);
1271
+ if (!input) return;
1272
+ input.click();
1273
+ input.scrollIntoView?.({ block: "center", behavior: "smooth" });
1274
+ }
1275
+ var INDETERMINATE = /* @__PURE__ */ new Set(["confirming", "verifying", "transient", "in_progress"]);
1276
+ function safeStorage2() {
704
1277
  try {
705
- return JSON.parse(text);
1278
+ return typeof sessionStorage !== "undefined" ? sessionStorage : null;
706
1279
  } catch {
707
1280
  return null;
708
1281
  }
709
1282
  }
710
- function createFetchTransport(url, options = {}) {
711
- const doFetch = (u, init) => (options.fetch ?? globalThis.fetch.bind(globalThis))(u, init);
712
- async function call(u, init) {
713
- const res = await doFetch(u, { credentials: options.credentials ?? "same-origin", ...init });
714
- const body = await parse(res);
715
- if (!res.ok) {
716
- const e = body?.error;
717
- throw new PaymentHttpError(res.status, e?.code ?? "server_error", e?.message, e?.details);
1283
+ function usePaymentRecovery(options = {}) {
1284
+ const { scope = "default", formAutoRetries = 2 } = options;
1285
+ const [notice, setNotice] = react.useState(null);
1286
+ const [sectionKey, setSectionKey] = react.useState(0);
1287
+ const formFailures = react.useRef(0);
1288
+ const lastIssue = react.useRef(null);
1289
+ const unknownByEvent = react.useRef(false);
1290
+ const live = react.useRef(options);
1291
+ live.current = options;
1292
+ const onIssue = react.useCallback((issue) => {
1293
+ lastIssue.current = issue;
1294
+ if (!issue) return setNotice((n) => n?.kind === "confirming" ? null : n);
1295
+ if (issue.kind === "confirming" || issue.kind === "verifying") return setNotice({ kind: "confirming" });
1296
+ if (issue.kind === "merchant_config" || issue.kind === "stale_checkout" || issue.kind === "provider_declined") return setNotice({ kind: issue.kind });
1297
+ if (issue.kind === "other") return setNotice({ kind: "other" });
1298
+ setNotice((n) => n?.kind === "confirming" ? null : n);
1299
+ }, []);
1300
+ const retry = react.useCallback(() => {
1301
+ const issue = lastIssue.current;
1302
+ if (unknownByEvent.current || issue && INDETERMINATE.has(issue.kind)) return false;
1303
+ const store = live.current.storage === void 0 ? safeStorage2() : live.current.storage;
1304
+ try {
1305
+ store?.removeItem(`sf:pay:attempt:${live.current.scope ?? "default"}`);
1306
+ } catch {
718
1307
  }
719
- const result = body;
720
- if (!result || typeof result !== "object" || !result.payment || typeof result.payment.status !== "string") {
721
- throw new PaymentHttpError(502, "invalid_response", "Unexpected payment response");
1308
+ formFailures.current = 0;
1309
+ lastIssue.current = null;
1310
+ unknownByEvent.current = false;
1311
+ setNotice(null);
1312
+ setSectionKey((k) => k + 1);
1313
+ return true;
1314
+ }, []);
1315
+ const onEvent = react.useCallback(
1316
+ (e) => {
1317
+ if (e.type === "form_ready") {
1318
+ formFailures.current = 0;
1319
+ setNotice((n) => n?.kind === "form_failed" ? null : n);
1320
+ }
1321
+ if (e.type === "form_failed") {
1322
+ if (formFailures.current < formAutoRetries) {
1323
+ formFailures.current += 1;
1324
+ setSectionKey((k) => k + 1);
1325
+ } else {
1326
+ setNotice({ kind: "form_failed" });
1327
+ }
1328
+ }
1329
+ if (e.type === "error") unknownByEvent.current = e.kind === "network" || e.kind === "payment_in_progress";
1330
+ if (e.type === "result") {
1331
+ unknownByEvent.current = false;
1332
+ lastIssue.current = null;
1333
+ }
1334
+ if (e.type === "result" && e.status === "rejected") setNotice({ kind: "rejected" });
1335
+ if (e.type === "error" && lastIssue.current?.kind === "store_closed") live.current.onRevalidate?.();
1336
+ },
1337
+ [formAutoRetries]
1338
+ );
1339
+ return react.useMemo(() => ({ notice, setNotice, sectionKey, onIssue, onEvent, lastIssue, retry, clear: () => setNotice(null) }), [notice, sectionKey, onIssue, onEvent, retry]);
1340
+ }
1341
+ function PaymentRecovery(props) {
1342
+ const { notice, alternatives = [], onRetry, onPick, onRefresh, alternativeLabel, classNames: cx = {}, testIds = {}, children } = props;
1343
+ const locale = useResolvedLocale(props.locale);
1344
+ const m = react.useMemo(() => paymentRecoveryMessages(locale, props.messages), [locale, props.messages]);
1345
+ const rowNames = react.useMemo(() => paymentMethodMessages(locale).options, [locale]);
1346
+ const ref = react.useRef(null);
1347
+ const kind = notice?.kind;
1348
+ react.useEffect(() => {
1349
+ if (kind && kind !== "confirming") {
1350
+ ref.current?.focus();
1351
+ ref.current?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
722
1352
  }
723
- return result;
724
- }
725
- return {
726
- submit: (request, { signal }) => call(url, {
727
- method: "POST",
728
- signal,
729
- headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey, ...options.headers },
730
- body: JSON.stringify(options.buildBody ? options.buildBody(request) : request)
731
- }),
732
- ...options.refreshUrl ? {
733
- refresh: ({ signal }) => call(options.refreshUrl, { method: "POST", signal, headers: { Accept: "application/json", ...options.headers } })
734
- } : {}
1353
+ }, [kind]);
1354
+ if (!notice) return null;
1355
+ const confirming = notice.kind === "confirming";
1356
+ const canRetry = notice.kind !== "confirming" && notice.kind !== "stale_checkout";
1357
+ const showAlternatives = notice.kind !== "stale_checkout" && !confirming && alternatives.length > 0;
1358
+ const alternativesFirst = notice.kind === "merchant_config";
1359
+ const nameOf = (c) => c.label ?? (c.option ? rowNames[c.option] : void 0) ?? c.option ?? c.methodKey;
1360
+ const labelOf = (c) => alternativeLabel?.(c) ?? m["action.alternative"].replace("{label}", nameOf(c));
1361
+ const retryButton = canRetry ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: alternativesFirst ? cx.secondary : cx.primary, onClick: onRetry, style: { minHeight: 44 }, "data-sf-pay-retry": true, ...testIds.retry ? { "data-testid": testIds.retry } : {}, children: notice.kind === "form_failed" ? m["action.retry_form"] : m["action.retry"] }) : null;
1362
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, tabIndex: -1, role: confirming ? "status" : "alert", "aria-live": confirming ? "polite" : void 0, className: cx.root, "data-sf-pay-recovery": notice.kind, children: [
1363
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.title, children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: m[`notice.${notice.kind}.title`] }) }),
1364
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.body, children: m[`notice.${notice.kind}.body`] }),
1365
+ !confirming ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, children: [
1366
+ alternativesFirst ? null : retryButton,
1367
+ notice.kind === "stale_checkout" ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx.primary, onClick: onRefresh, style: { minHeight: 44 }, "data-sf-pay-refresh": true, ...testIds.refresh ? { "data-testid": testIds.refresh } : {}, children: m["action.refresh"] }) : null,
1368
+ showAlternatives ? alternatives.map((c) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: alternativesFirst ? cx.primary : cx.secondary, onClick: () => onPick(c), style: { minHeight: 44 }, "data-sf-pay-alternative": c.value, children: labelOf(c) }, c.value)) : null,
1369
+ alternativesFirst ? retryButton : null
1370
+ ] }) : null,
1371
+ children
1372
+ ] });
1373
+ }
1374
+
1375
+ // src/payments/revalidate.ts
1376
+ function checkoutShouldRevalidate(options) {
1377
+ const paths = options.paths.map((p) => p.replace(/\/+$/, "") || "/");
1378
+ return (args) => {
1379
+ const method = args.formMethod?.toUpperCase();
1380
+ if (!method || method === "GET" || !args.formAction) return args.defaultShouldRevalidate;
1381
+ let pathname = args.formAction;
1382
+ try {
1383
+ pathname = new URL(args.formAction, "http://sf.local").pathname;
1384
+ } catch {
1385
+ }
1386
+ pathname = pathname.replace(/\/+$/, "") || "/";
1387
+ return paths.includes(pathname) ? false : args.defaultShouldRevalidate;
735
1388
  };
736
1389
  }
737
1390
 
@@ -794,74 +1447,6 @@ function paymentNonceFromHeaders(headers) {
794
1447
  return /'nonce-([A-Za-z0-9+/=_-]+)'/.exec(csp)?.[1];
795
1448
  }
796
1449
 
797
- // src/payments/payment-method.messages.ts
798
- var ES = {
799
- legend: "M\xE9todo de pago",
800
- pay: "Pagar",
801
- placeOrder: "Realizar el pedido",
802
- placeOrderCash: "Generar ficha de pago",
803
- chooseDelivery: "Elige la entrega para continuar",
804
- payRedirect: "Continuar al pago",
805
- continue: "Continuar",
806
- retryAddress: "Guardar direcci\xF3n y continuar",
807
- processing: "Procesando\u2026",
808
- redirectNote: "Te llevamos a la p\xE1gina del proveedor para terminar el pago y regresas aqu\xED.",
809
- cashPoints: "Elige d\xF3nde pagar en efectivo",
810
- cashMore: "Ver m\xE1s puntos de pago",
811
- cashNote: "Al hacer el pedido te damos una ficha con la referencia y la fecha l\xEDmite. El pedido se confirma cuando pagas.",
812
- secureCard: "Tus datos de tarjeta los captura directamente nuestro proveedor de pagos; la tienda nunca los ve.",
813
- pickDeliveryFirst: "Elige primero la entrega para ver el total y pagar con tarjeta.",
814
- updatingTotal: "Actualizando el total\u2026",
815
- syncFailed: "No pudimos actualizar el total con tu entrega. Puedes continuar al sitio del proveedor para pagar con tarjeta.",
816
- quoteChanged: "Las tarifas de env\xEDo se actualizaron. Revisa tu env\xEDo para ver el total.",
817
- rebuildCart: "Volver a armar mi carrito",
818
- noPaymentTitle: "Por ahora no hay formas de pago disponibles",
819
- noPayment: "Intenta de nuevo en unos minutos o escr\xEDbenos para completar tu pedido.",
820
- paymentNote: "El pago se procesa de forma segura con nuestro proveedor de pagos.",
821
- options: {
822
- card: "Tarjeta de cr\xE9dito o d\xE9bito",
823
- cash: "Efectivo",
824
- wallet: "Cuenta del proveedor de pagos",
825
- credits: "Pagos sin tarjeta"
826
- }
827
- };
828
- var EN = {
829
- legend: "Payment method",
830
- pay: "Pay",
831
- placeOrder: "Place order",
832
- placeOrderCash: "Get payment slip",
833
- chooseDelivery: "Choose delivery to continue",
834
- payRedirect: "Continue to payment",
835
- continue: "Continue",
836
- retryAddress: "Save address and continue",
837
- processing: "Processing\u2026",
838
- redirectNote: "We'll take you to the provider's page to finish paying, then bring you back here.",
839
- cashPoints: "Choose where to pay in cash",
840
- cashMore: "Show more payment points",
841
- cashNote: "When you place the order you get a slip with the reference and the deadline. The order is confirmed once you pay.",
842
- secureCard: "Your card details are captured directly by our payment provider; the shop never sees them.",
843
- pickDeliveryFirst: "Choose delivery first to see the total and pay by card.",
844
- updatingTotal: "Updating the total\u2026",
845
- syncFailed: "We couldn't update the total with your delivery. You can continue to the provider's site to pay by card.",
846
- quoteChanged: "Shipping rates were updated. Check your delivery to see the total.",
847
- rebuildCart: "Rebuild my cart",
848
- noPaymentTitle: "No payment methods are available right now",
849
- noPayment: "Try again in a few minutes or contact us to complete your order.",
850
- paymentNote: "Payment is processed securely by our payment provider.",
851
- options: {
852
- card: "Credit or debit card",
853
- cash: "Cash",
854
- wallet: "Payment provider account",
855
- credits: "Pay without a card"
856
- }
857
- };
858
- function paymentMethodMessages(locale, overrides) {
859
- const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN : ES;
860
- if (!overrides) return base;
861
- const { options, ...rest } = overrides;
862
- return { ...base, ...rest, options: { ...base.options, ...options } };
863
- }
864
-
865
1450
  // src/checkout/brands/logos.generated.ts
866
1451
  var LOGOS = {
867
1452
  "7eleven": { src: "data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 xmlns:xlink=%22http://www.w3.org/1999/xlink%22 xml:space=%22preserve%22 viewBox=%220 0 45 59.89%22%3E%3Cdefs%3E%3Cpath id=%22a%22 d=%22M0 0h45v59.9H0z%22/%3E%3C/defs%3E%3CclipPath id=%22b%22%3E%3Cuse xlink:href=%22%23a%22 style=%22overflow:visible%22/%3E%3C/clipPath%3E%3Cpath d=%22M34.2 45c.9 0 1.7.8 1.7 1.7s-.7 1.7-1.7 1.7c-.9 0-1.6-.8-1.6-1.7s.7-1.7 1.6-1.7m0 3c.7 0 1.3-.6 1.3-1.3 0-.8-.6-1.3-1.3-1.3s-1.3.6-1.3 1.3c0 .8.6 1.3 1.3 1.3m-.2-.4h-.3v-1.8h.6q.6 0 .6.6c0 .3-.2.5-.4.5l.4.7h-.4l-.4-.7H34zm.2-1q.3 0 .3-.3t-.3-.3H34v.5h.2zm8-14.3c-.9 0-1.5.4-2.1.9l-.4.6-.1-.1.5-.8v-.5h-3.2V43h2.9v-6.7c0-1.1.7-1.4 1.2-1.5s1.1.2 1.1.8V43H45v-7.3c0-2.3-1-3.4-2.8-3.4M9.9 40.6h3v2.3H7V32.3h2.9zM0 42.9h5.9v-2.1h-3v-2.3h3v-2h-3v-2h3v-2.2H0zm13.9 0h6v-2.1h-3.1v-2.3h3.1v-2h-3.1v-2h3.1v-2.2h-6zm16 0h6v-2.1h-3.1v-2.3h3.1v-2h-3.1v-2h3.1v-2.2h-6zm-3.8-10.6L25 39l-.1 1.2v.1h-.1v-.1l-.1-1.2-1.1-6.7h-2.9l2.4 10.6h3.6l2.4-10.6z%22 style=%22clip-path:url(%23b);fill:%23147350%22/%3E%3Cpath d=%22M37.7 0H0v15h19.9C24.4 8.5 30.5 3.3 37.7 0%22 style=%22clip-path:url(%23b);fill:%23ff6c00%22/%3E%3Cpath d=%22M15 59.9h15v-15H15zM45 15V0a40.7 40.7 0 0 0-28.9 30h15.3A25.8 25.8 0 0 1 45 15%22 style=%22clip-path:url(%23b);fill:%23eb0f2a%22/%3E%3C/svg%3E", ar: 0.751, mono: false },
@@ -1255,6 +1840,15 @@ function PaymentMethodSection(props) {
1255
1840
  const requote = wantSync && settled && sync.requote === true;
1256
1841
  const syncFailed = wantSync && settled && !sync.ok && !requote;
1257
1842
  const brickReady = cardActive && deliveryReady && synced && !syncFailed;
1843
+ const [formLatched, setFormLatched] = react.useState(false);
1844
+ react.useEffect(() => {
1845
+ if (brickReady) setFormLatched(true);
1846
+ }, [brickReady]);
1847
+ react.useEffect(() => {
1848
+ if (!cardActive) setFormLatched(false);
1849
+ }, [cardActive]);
1850
+ const showBrick = cardActive && !syncFailed && (formLatched || brickReady);
1851
+ const totalUpdating = showBrick && !brickReady;
1258
1852
  const transport = react.useMemo(
1259
1853
  () => props.transport ?? createFetchTransport(payUrl, {
1260
1854
  ...statusUrl ? { refreshUrl: statusUrl } : {},
@@ -1368,9 +1962,9 @@ function PaymentMethodSection(props) {
1368
1962
  marks.length ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.brands, "data-sf-pay-brands": choice.option ?? "plain", "aria-hidden": "true", children: marks.map((mk) => /* @__PURE__ */ jsxRuntime.jsx(BrandMark, { brand: mk.key, logoUrl: mk.logoUrl ?? null, size: 20 }, mk.key)) }) : null
1369
1963
  ] }),
1370
1964
  /* @__PURE__ */ jsxRuntime.jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked, marks: brandMarks, visible: cashPointsVisible, openMore: storedPoint, onPointChange }),
1371
- choice.mode === "card" && checked && cardChosen && hydrated && choices.cardClient ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.brickWrap, "data-sf-pay-card": true, ...tid("card"), children: [
1965
+ choice.mode === "card" && checked && cardChosen && hydrated && choices.cardClient ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.brickWrap, "data-sf-pay-card": true, ...totalUpdating ? { "data-sf-pay-updating": "" } : {}, "aria-busy": totalUpdating || void 0, ...tid("card"), children: [
1372
1966
  fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : requote ? cardNotice("requote", m.quoteChanged) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
1373
- brickReady || fallback ? /* @__PURE__ */ jsxRuntime.jsx(
1967
+ showBrick || fallback ? /* @__PURE__ */ jsxRuntime.jsx(
1374
1968
  PaymentBrick,
1375
1969
  {
1376
1970
  ...brickProps,
@@ -1382,15 +1976,14 @@ function PaymentMethodSection(props) {
1382
1976
  locale,
1383
1977
  ...scope ? { scope } : {},
1384
1978
  ...payerEmail && !brickProps?.email ? { email: payerEmail } : {},
1385
- enabled: brickReady,
1979
+ enabled: showBrick,
1386
1980
  ...cx.brick ? { className: cx.brick } : {},
1387
1981
  onEvent: handleEvent,
1388
1982
  onFallback: () => setFallback(true),
1389
1983
  onResult: (o) => {
1390
1984
  if (o.kind === "approved" || o.kind === "in_review" || o.kind === "voucher") onDone(o);
1391
1985
  }
1392
- },
1393
- payerEmail || brickProps?.email ? "with-email" : "without-email"
1986
+ }
1394
1987
  ) : null,
1395
1988
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.note, children: m.secureCard }),
1396
1989
  expired && rebuildHref ? Link({ href: rebuildHref, ...cx.rebuild ? { className: cx.rebuild } : {}, children: m.rebuildCart, ...testIds.rebuild ? { testId: testIds.rebuild } : {} }) : null
@@ -1409,7 +2002,7 @@ function PaymentMethodSection(props) {
1409
2002
  onClick: onPayClick,
1410
2003
  className: cx.submit,
1411
2004
  disabled: busy || blocked,
1412
- "aria-disabled": blocked || brickReady && (!ready || processing) || void 0,
2005
+ "aria-disabled": blocked || cardActive && !syncFailed && deliveryReady && (!brickReady || !ready || processing) || void 0,
1413
2006
  "aria-busy": processing || void 0,
1414
2007
  "data-sf-pay-method-submit": true,
1415
2008
  ...tid("submit"),
@@ -1499,8 +2092,6 @@ function rowMarks(choice, cardBrands) {
1499
2092
  }
1500
2093
  return [];
1501
2094
  }
1502
-
1503
- // src/payments/choices.ts
1504
2095
  var SEP = "|";
1505
2096
  function choiceValue(methodKey, option) {
1506
2097
  return option ? `${methodKey}${SEP}${option}` : methodKey;
@@ -1574,13 +2165,33 @@ function checkoutToAttempt(result) {
1574
2165
  if (action?.type === "challenge") return { ...base, payment: { provider: payment.provider, status: "requires_action", outcome: "challenge", action } };
1575
2166
  return { ...base, payment: { provider: payment.provider, status: "pending", outcome: "pending", action: action ?? { type: "none" } } };
1576
2167
  }
1577
- function payErrorResponse(err) {
2168
+ function safeHttpStatus(status) {
2169
+ return status === 502 || status === 503 || status === 504 ? 500 : status;
2170
+ }
2171
+ function payErrorResponse(err, options = {}) {
1578
2172
  const status = typeof err.status === "number" && err.status >= 400 ? err.status : 503;
1579
- let code = err.code ?? "server_error";
2173
+ const { kind: classified, retryable } = storefront.classifyPaymentError({ ...err, status, checkoutId: err.checkoutId ?? null });
2174
+ const kind = options.kind ?? classified;
2175
+ let code = err.code ?? (kind === "transient" ? "unavailable" : "server_error");
1580
2176
  if (status === 409 && err.field === "payment_in_progress") code = "payment_in_progress";
1581
2177
  if (status === 429) code = "too_many_attempts";
1582
2178
  if (status === 422 && err.field === "expected_total") code = "price_mismatch";
1583
- return { status, body: { error: { code, message: err.message ?? "", ...err.details !== void 0 ? { details: err.details } : {} } } };
2179
+ const own = (options.ownMessage || kind === "validation" || kind === "expired" || kind === "price_mismatch" || kind === "other" && status < 500) && err.message;
2180
+ const message = own || paymentRecoveryMessages(options.locale, options.messages)[`error.${kind}`];
2181
+ return {
2182
+ status: safeHttpStatus(status),
2183
+ body: {
2184
+ error: {
2185
+ code,
2186
+ message,
2187
+ retryable: options.kind ? options.kind === "transient" || options.kind === "in_progress" || options.kind === "rate_limited" : retryable,
2188
+ kind,
2189
+ ...err.field ? { field: err.field } : {},
2190
+ ...err.retryAfter ? { retry_after: err.retryAfter } : {},
2191
+ ...err.details !== void 0 ? { details: err.details } : {}
2192
+ }
2193
+ }
2194
+ };
1584
2195
  }
1585
2196
  function voucherRows(voucher, labels, locale) {
1586
2197
  if (!voucher) return [];
@@ -1606,15 +2217,23 @@ exports.PaymentBrick = PaymentBrick;
1606
2217
  exports.PaymentChallenge = PaymentChallenge;
1607
2218
  exports.PaymentHttpError = PaymentHttpError;
1608
2219
  exports.PaymentMethodSection = PaymentMethodSection;
2220
+ exports.PaymentMountCancelled = PaymentMountCancelled;
2221
+ exports.PaymentMountTimeout = PaymentMountTimeout;
2222
+ exports.PaymentRecovery = PaymentRecovery;
1609
2223
  exports.VoucherCard = VoucherCard;
2224
+ exports.alternativeChoices = alternativeChoices;
1610
2225
  exports.buildPayChoices = buildPayChoices;
1611
2226
  exports.cashProviderData = cashProviderData;
2227
+ exports.checkoutShouldRevalidate = checkoutShouldRevalidate;
1612
2228
  exports.checkoutToAttempt = checkoutToAttempt;
1613
2229
  exports.choiceValue = choiceValue;
1614
2230
  exports.createFetchTransport = createFetchTransport;
2231
+ exports.createResilientTransport = createResilientTransport;
1615
2232
  exports.formFields = formFields;
2233
+ exports.isResolvedAttempt = isResolvedAttempt;
1616
2234
  exports.loadPaymentSdk = loadPaymentSdk;
1617
2235
  exports.mercadoPagoBricksAdapter = mercadoPagoBricksAdapter;
2236
+ exports.mountQueued = mountQueued;
1618
2237
  exports.parseChoice = parseChoice;
1619
2238
  exports.payErrorResponse = payErrorResponse;
1620
2239
  exports.paymentCsp = paymentCsp;
@@ -1624,11 +2243,16 @@ exports.paymentCspMode = paymentCspMode;
1624
2243
  exports.paymentMethodMessages = paymentMethodMessages;
1625
2244
  exports.paymentNonce = paymentNonce;
1626
2245
  exports.paymentNonceFromHeaders = paymentNonceFromHeaders;
2246
+ exports.paymentRecoveryMessages = paymentRecoveryMessages;
2247
+ exports.pickChoice = pickChoice;
1627
2248
  exports.readPaymentScheme = readPaymentScheme;
1628
2249
  exports.readPaymentTheme = readPaymentTheme;
1629
2250
  exports.resetPaymentSdkCache = resetPaymentSdkCache;
2251
+ exports.safeHttpStatus = safeHttpStatus;
1630
2252
  exports.sanitizeMercadoPagoFormData = sanitizeMercadoPagoFormData;
1631
2253
  exports.useEmbeddedPayment = useEmbeddedPayment;
2254
+ exports.usePaymentRecovery = usePaymentRecovery;
1632
2255
  exports.voucherRows = voucherRows;
2256
+ exports.withMountQueue = withMountQueue;
1633
2257
  //# sourceMappingURL=payments.cjs.map
1634
2258
  //# sourceMappingURL=payments.cjs.map