@asteby/storefront-react 0.16.0 → 0.17.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;
@@ -280,6 +622,8 @@ function useEmbeddedPayment(options) {
280
622
  retryDelayMs = 600,
281
623
  pollIntervalMs = 2e3,
282
624
  pollTimeoutMs = 6e4,
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,27 @@ 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");
304
653
  statusRef.current = status;
654
+ errorRef.current = error;
655
+ const amountKey = `${amount.amount}|${amount.currency_code}`;
656
+ const amountKeyRef = react.useRef(amountKey);
657
+ amountKeyRef.current = amountKey;
658
+ const appliedAmount = react.useRef(null);
659
+ const updating = react.useRef(null);
660
+ const remountRequested = react.useRef(false);
305
661
  react.useEffect(() => {
306
662
  alive.current = true;
307
663
  return () => {
@@ -312,19 +668,33 @@ function useEmbeddedPayment(options) {
312
668
  const store = options.storage === void 0 ? safeStorage() : options.storage;
313
669
  const emit = react.useCallback((e) => live.current.onEvent?.(e), []);
314
670
  const readAttempt = react.useCallback(() => {
315
- try {
316
- const saved = store?.getItem(storageKey);
317
- if (saved) return saved;
318
- } catch {
671
+ if (!memOnly.current) {
672
+ try {
673
+ const saved = store?.getItem(storageKey);
674
+ if (saved) {
675
+ memAttempt.current = saved;
676
+ return saved;
677
+ }
678
+ } catch {
679
+ memOnly.current = true;
680
+ }
681
+ }
682
+ if (memOnly.current && memAttempt.current) return memAttempt.current;
683
+ if (!store) {
684
+ if (memAttempt.current) return memAttempt.current;
319
685
  }
320
686
  const fresh = (live.current.newAttemptId ?? storefront.generateIdempotencyKey)();
687
+ memAttempt.current = fresh;
321
688
  try {
322
689
  store?.setItem(storageKey, fresh);
690
+ if (store && store.getItem(storageKey) !== fresh) memOnly.current = true;
323
691
  } catch {
692
+ memOnly.current = true;
324
693
  }
325
694
  return fresh;
326
695
  }, [store, storageKey]);
327
696
  const rotateAttempt = react.useCallback(() => {
697
+ memAttempt.current = null;
328
698
  try {
329
699
  store?.removeItem(storageKey);
330
700
  } catch {
@@ -340,7 +710,32 @@ function useEmbeddedPayment(options) {
340
710
  },
341
711
  [emit]
342
712
  );
343
- const amountKey = `${amount.amount}|${amount.currency_code}`;
713
+ const syncAmount = react.useCallback(() => {
714
+ const m = form.current;
715
+ if (!m || inFlight.current || updating.current || remountRequested.current) return;
716
+ if (statusRef.current === "network_error" || statusRef.current === "blocked" && errorRef.current?.kind === "payment_in_progress") return;
717
+ if (appliedAmount.current === amountKeyRef.current) return;
718
+ const run = (async () => {
719
+ while (form.current === m && alive.current && appliedAmount.current !== amountKeyRef.current) {
720
+ const key = amountKeyRef.current;
721
+ try {
722
+ if (!m.update) throw new Error("payment form: update not supported");
723
+ await m.update({ amount: live.current.amount });
724
+ appliedAmount.current = key;
725
+ } catch {
726
+ if (form.current === m && alive.current) {
727
+ remountRequested.current = true;
728
+ setMountKey((k) => k + 1);
729
+ }
730
+ return;
731
+ }
732
+ }
733
+ })().finally(() => {
734
+ updating.current = null;
735
+ if (alive.current) syncAmount();
736
+ });
737
+ updating.current = run;
738
+ }, []);
344
739
  const kind = client?.kind;
345
740
  const publicKey = client?.public_key;
346
741
  react.useEffect(() => {
@@ -354,18 +749,22 @@ function useEmbeddedPayment(options) {
354
749
  return;
355
750
  }
356
751
  if (!container) return;
752
+ remountRequested.current = false;
357
753
  const my = ++generation.current;
358
754
  const started = Date.now();
755
+ const abort = new AbortController();
359
756
  let mounted = null;
360
757
  setStatus("loading");
361
758
  const stale = () => my !== generation.current || !alive.current;
362
759
  const fail = (reason) => {
363
760
  if (stale()) return;
364
761
  generation.current++;
762
+ abort.abort();
365
763
  clearTimeout(timer);
366
764
  const m = mounted ?? form.current;
367
765
  mounted = null;
368
766
  form.current = null;
767
+ appliedAmount.current = null;
369
768
  if (m) void Promise.resolve(m.unmount());
370
769
  fallback(reason);
371
770
  };
@@ -379,31 +778,39 @@ function useEmbeddedPayment(options) {
379
778
  if (stale()) return;
380
779
  const theme = live.current.theme ?? readPaymentTheme(container);
381
780
  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 });
781
+ const createdWith = amountKeyRef.current;
782
+ const m = await mountQueued(
783
+ adapter,
784
+ {
785
+ container,
786
+ client,
787
+ amount: live.current.amount,
788
+ ...live.current.email ? { email: live.current.email } : {},
789
+ ...live.current.locale ? { locale: live.current.locale } : {},
790
+ theme,
791
+ ...colorScheme ? { colorScheme } : {},
792
+ signal: abort.signal,
793
+ onReady: () => {
794
+ if (stale()) return;
795
+ clearTimeout(timer);
796
+ setStatus((s) => s === "loading" ? "ready" : s);
797
+ emit({ type: "form_ready", ms: Date.now() - started });
798
+ },
799
+ onError: (e) => {
800
+ if (stale()) return;
801
+ if (e.critical) fail("form_error");
802
+ }
395
803
  },
396
- onError: (e) => {
397
- if (stale()) return;
398
- if (e.critical) fail("form_error");
399
- }
400
- });
804
+ { createTimeoutMs: live.current.createTimeoutMs ?? sdkTimeoutMs }
805
+ );
401
806
  if (stale()) {
402
807
  void Promise.resolve(m.unmount());
403
808
  return;
404
809
  }
405
810
  mounted = m;
406
811
  form.current = m;
812
+ appliedAmount.current = createdWith;
813
+ syncAmount();
407
814
  } catch (err) {
408
815
  fail(err instanceof Error && /timeout/i.test(err.message) ? "sdk_timeout" : "sdk_error");
409
816
  }
@@ -411,12 +818,17 @@ function useEmbeddedPayment(options) {
411
818
  return () => {
412
819
  clearTimeout(timer);
413
820
  generation.current++;
821
+ abort.abort();
414
822
  const m = mounted ?? form.current;
415
823
  mounted = null;
416
824
  form.current = null;
825
+ appliedAmount.current = null;
417
826
  if (m) void Promise.resolve(m.unmount());
418
827
  };
419
- }, [enabled, container, kind, publicKey, amountKey, mountKey, sdkTimeoutMs, containerId]);
828
+ }, [enabled, container, kind, publicKey, mountKey, sdkTimeoutMs, containerId]);
829
+ react.useEffect(() => {
830
+ syncAmount();
831
+ }, [amountKey, syncAmount]);
420
832
  const finish = react.useCallback(
421
833
  (result) => {
422
834
  const o = storefront.paymentOutcome(result, live.current.locale, live.current.messages);
@@ -440,7 +852,7 @@ function useEmbeddedPayment(options) {
440
852
  [emit, rotateAttempt]
441
853
  );
442
854
  const send = react.useCallback(
443
- async (data, attempt) => {
855
+ async (data, attempt, total) => {
444
856
  const ctl = new AbortController();
445
857
  const maxRetries = live.current.autoRetries ?? autoRetries;
446
858
  for (let n = 0; ; n++) {
@@ -452,7 +864,7 @@ function useEmbeddedPayment(options) {
452
864
  idempotencyKey: attempt,
453
865
  provider_data: data.provider_data,
454
866
  ...data.device_session_id ? { device_session_id: data.device_session_id } : {},
455
- expected_total: live.current.amount
867
+ expected_total: total
456
868
  },
457
869
  { signal: ctl.signal }
458
870
  );
@@ -470,7 +882,7 @@ function useEmbeddedPayment(options) {
470
882
  emit({ type: "error", kind: view.kind });
471
883
  setError(view);
472
884
  if (view.kind === "network") {
473
- pendingData.current = data;
885
+ pendingData.current = { data, total };
474
886
  setStatus("network_error");
475
887
  } else if (view.kind === "price_mismatch") {
476
888
  pendingData.current = null;
@@ -496,7 +908,11 @@ function useEmbeddedPayment(options) {
496
908
  if (inFlight.current) return inFlight.current;
497
909
  const m = form.current;
498
910
  if (!m || statusRef.current !== "ready" && statusRef.current !== "network_error") return Promise.resolve();
911
+ if (statusRef.current === "network_error" && pendingData.current) return retryRef.current();
912
+ const total = live.current.amount;
499
913
  const run = (async () => {
914
+ if (updating.current) await updating.current.catch(() => {
915
+ });
500
916
  setStatus("processing");
501
917
  setError(null);
502
918
  setOutcome(null);
@@ -512,30 +928,33 @@ function useEmbeddedPayment(options) {
512
928
  }
513
929
  const attempt = readAttempt();
514
930
  setAttemptId(attempt);
515
- await send(data, attempt);
931
+ await send(data, attempt, total);
516
932
  })().finally(() => {
517
933
  inFlight.current = null;
934
+ syncAmount();
518
935
  });
519
936
  inFlight.current = run;
520
937
  return run;
521
- }, [emit, readAttempt, send]);
938
+ }, [emit, readAttempt, send, syncAmount]);
522
939
  const retry = react.useCallback(() => {
523
940
  if (inFlight.current) return inFlight.current;
524
- const data = pendingData.current;
525
- if (!data) return submit();
941
+ const pending2 = pendingData.current;
942
+ if (!pending2) return submit();
526
943
  emit({ type: "retry" });
527
944
  const run = (async () => {
528
945
  setStatus("processing");
529
946
  setError(null);
530
947
  const attempt = readAttempt();
531
948
  setAttemptId(attempt);
532
- await send(data, attempt);
949
+ await send(pending2.data, attempt, pending2.total);
533
950
  })().finally(() => {
534
951
  inFlight.current = null;
952
+ syncAmount();
535
953
  });
536
954
  inFlight.current = run;
537
955
  return run;
538
- }, [emit, readAttempt, send, submit]);
956
+ }, [emit, readAttempt, send, submit, syncAmount]);
957
+ retryRef.current = retry;
539
958
  react.useEffect(() => {
540
959
  if (status !== "challenge" && status !== "in_review" || !transport.refresh) return;
541
960
  let stop = false;
@@ -548,6 +967,7 @@ function useEmbeddedPayment(options) {
548
967
  try {
549
968
  const r = await transport.refresh({ signal: ctl.signal });
550
969
  if (stop || !alive.current) return;
970
+ if (r.payment.status === "none") continue;
551
971
  const o = storefront.paymentOutcome(r, live.current.locale, live.current.messages);
552
972
  if (o.kind !== status) {
553
973
  finish(r);
@@ -562,6 +982,44 @@ function useEmbeddedPayment(options) {
562
982
  ctl.abort();
563
983
  };
564
984
  }, [status, transport, pollIntervalMs, pollTimeoutMs, finish]);
985
+ const unknownOutcome = status === "network_error" || status === "blocked" && error?.kind === "payment_in_progress";
986
+ react.useEffect(() => {
987
+ if (!unknownOutcome) syncAmount();
988
+ }, [unknownOutcome, syncAmount]);
989
+ react.useEffect(() => {
990
+ if (!unknownOutcome || !transport.refresh) {
991
+ setVerifying(false);
992
+ return;
993
+ }
994
+ let stop = false;
995
+ const ctl = new AbortController();
996
+ const until = Date.now() + verifyTimeoutMs;
997
+ setVerifying(true);
998
+ (async () => {
999
+ let wait = verifyIntervalMs;
1000
+ while (!stop && Date.now() < until) {
1001
+ await sleep(wait);
1002
+ wait = Math.min(wait * 2, 15e3);
1003
+ if (stop || !alive.current) return;
1004
+ try {
1005
+ const r = await transport.refresh({ signal: ctl.signal });
1006
+ if (stop || !alive.current) return;
1007
+ if (isResolvedAttempt(r)) {
1008
+ setVerifying(false);
1009
+ finish(r);
1010
+ return;
1011
+ }
1012
+ } catch {
1013
+ }
1014
+ }
1015
+ if (!stop && alive.current) setVerifying(false);
1016
+ })();
1017
+ return () => {
1018
+ stop = true;
1019
+ ctl.abort();
1020
+ setVerifying(false);
1021
+ };
1022
+ }, [unknownOutcome, transport, verifyTimeoutMs, verifyIntervalMs, finish]);
565
1023
  const dismiss = react.useCallback(() => {
566
1024
  setError(null);
567
1025
  setOutcome((o) => o?.kind === "rejected" ? null : o);
@@ -576,6 +1034,7 @@ function useEmbeddedPayment(options) {
576
1034
  attemptsLeft,
577
1035
  attemptId,
578
1036
  busy: status === "loading" || status === "processing",
1037
+ verifying,
579
1038
  containerRef: setContainer,
580
1039
  containerId,
581
1040
  submit,
@@ -685,53 +1144,195 @@ var PaymentBrick = react.forwardRef(function PaymentBrick2(props, ref) {
685
1144
  ] });
686
1145
  });
687
1146
 
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;
1147
+ // src/payments/payment-method.messages.ts
1148
+ var ES2 = {
1149
+ legend: "M\xE9todo de pago",
1150
+ pay: "Pagar",
1151
+ placeOrder: "Realizar el pedido",
1152
+ placeOrderCash: "Generar ficha de pago",
1153
+ chooseDelivery: "Elige la entrega para continuar",
1154
+ payRedirect: "Continuar al pago",
1155
+ continue: "Continuar",
1156
+ retryAddress: "Guardar direcci\xF3n y continuar",
1157
+ processing: "Procesando\u2026",
1158
+ redirectNote: "Te llevamos a la p\xE1gina del proveedor para terminar el pago y regresas aqu\xED.",
1159
+ cashPoints: "Elige d\xF3nde pagar en efectivo",
1160
+ cashMore: "Ver m\xE1s puntos de pago",
1161
+ cashNote: "Al hacer el pedido te damos una ficha con la referencia y la fecha l\xEDmite. El pedido se confirma cuando pagas.",
1162
+ secureCard: "Tus datos de tarjeta los captura directamente nuestro proveedor de pagos; la tienda nunca los ve.",
1163
+ pickDeliveryFirst: "Elige primero la entrega para ver el total y pagar con tarjeta.",
1164
+ updatingTotal: "Actualizando el total\u2026",
1165
+ syncFailed: "No pudimos actualizar el total con tu entrega. Puedes continuar al sitio del proveedor para pagar con tarjeta.",
1166
+ quoteChanged: "Las tarifas de env\xEDo se actualizaron. Revisa tu env\xEDo para ver el total.",
1167
+ rebuildCart: "Volver a armar mi carrito",
1168
+ noPaymentTitle: "Por ahora no hay formas de pago disponibles",
1169
+ noPayment: "Intenta de nuevo en unos minutos o escr\xEDbenos para completar tu pedido.",
1170
+ paymentNote: "El pago se procesa de forma segura con nuestro proveedor de pagos.",
1171
+ options: {
1172
+ card: "Tarjeta de cr\xE9dito o d\xE9bito",
1173
+ cash: "Efectivo",
1174
+ wallet: "Cuenta del proveedor de pagos",
1175
+ credits: "Pagos sin tarjeta"
695
1176
  }
696
- status;
697
- code;
698
- details;
699
- name = "PaymentHttpError";
700
1177
  };
701
- async function parse(res) {
702
- const text = await res.text();
703
- if (!text) return null;
1178
+ var EN2 = {
1179
+ legend: "Payment method",
1180
+ pay: "Pay",
1181
+ placeOrder: "Place order",
1182
+ placeOrderCash: "Get payment slip",
1183
+ chooseDelivery: "Choose delivery to continue",
1184
+ payRedirect: "Continue to payment",
1185
+ continue: "Continue",
1186
+ retryAddress: "Save address and continue",
1187
+ processing: "Processing\u2026",
1188
+ redirectNote: "We'll take you to the provider's page to finish paying, then bring you back here.",
1189
+ cashPoints: "Choose where to pay in cash",
1190
+ cashMore: "Show more payment points",
1191
+ cashNote: "When you place the order you get a slip with the reference and the deadline. The order is confirmed once you pay.",
1192
+ secureCard: "Your card details are captured directly by our payment provider; the shop never sees them.",
1193
+ pickDeliveryFirst: "Choose delivery first to see the total and pay by card.",
1194
+ updatingTotal: "Updating the total\u2026",
1195
+ syncFailed: "We couldn't update the total with your delivery. You can continue to the provider's site to pay by card.",
1196
+ quoteChanged: "Shipping rates were updated. Check your delivery to see the total.",
1197
+ rebuildCart: "Rebuild my cart",
1198
+ noPaymentTitle: "No payment methods are available right now",
1199
+ noPayment: "Try again in a few minutes or contact us to complete your order.",
1200
+ paymentNote: "Payment is processed securely by our payment provider.",
1201
+ options: {
1202
+ card: "Credit or debit card",
1203
+ cash: "Cash",
1204
+ wallet: "Payment provider account",
1205
+ credits: "Pay without a card"
1206
+ }
1207
+ };
1208
+ function paymentMethodMessages(locale, overrides) {
1209
+ const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN2 : ES2;
1210
+ if (!overrides) return base;
1211
+ const { options, ...rest } = overrides;
1212
+ return { ...base, ...rest, options: { ...base.options, ...options } };
1213
+ }
1214
+ function alternativeChoices(choices) {
1215
+ return choices.filter((c) => c.mode !== "card");
1216
+ }
1217
+ function pickChoice(form, value) {
1218
+ const input = Array.from(form?.querySelectorAll('input[name="payment_choice"]') ?? []).find((i) => i.value === value);
1219
+ if (!input) return;
1220
+ input.click();
1221
+ input.scrollIntoView?.({ block: "center", behavior: "smooth" });
1222
+ }
1223
+ var INDETERMINATE = /* @__PURE__ */ new Set(["confirming", "verifying", "transient", "in_progress"]);
1224
+ function safeStorage2() {
704
1225
  try {
705
- return JSON.parse(text);
1226
+ return typeof sessionStorage !== "undefined" ? sessionStorage : null;
706
1227
  } catch {
707
1228
  return null;
708
1229
  }
709
1230
  }
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);
1231
+ function usePaymentRecovery(options = {}) {
1232
+ const { scope = "default", formAutoRetries = 2 } = options;
1233
+ const [notice, setNotice] = react.useState(null);
1234
+ const [sectionKey, setSectionKey] = react.useState(0);
1235
+ const formFailures = react.useRef(0);
1236
+ const lastIssue = react.useRef(null);
1237
+ const unknownByEvent = react.useRef(false);
1238
+ const live = react.useRef(options);
1239
+ live.current = options;
1240
+ const onIssue = react.useCallback((issue) => {
1241
+ lastIssue.current = issue;
1242
+ if (!issue) return setNotice((n) => n?.kind === "confirming" ? null : n);
1243
+ if (issue.kind === "confirming" || issue.kind === "verifying") return setNotice({ kind: "confirming" });
1244
+ if (issue.kind === "merchant_config" || issue.kind === "stale_checkout" || issue.kind === "provider_declined") return setNotice({ kind: issue.kind });
1245
+ if (issue.kind === "other") return setNotice({ kind: "other" });
1246
+ setNotice((n) => n?.kind === "confirming" ? null : n);
1247
+ }, []);
1248
+ const retry = react.useCallback(() => {
1249
+ const issue = lastIssue.current;
1250
+ if (unknownByEvent.current || issue && INDETERMINATE.has(issue.kind)) return false;
1251
+ const store = live.current.storage === void 0 ? safeStorage2() : live.current.storage;
1252
+ try {
1253
+ store?.removeItem(`sf:pay:attempt:${live.current.scope ?? "default"}`);
1254
+ } catch {
718
1255
  }
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");
1256
+ formFailures.current = 0;
1257
+ lastIssue.current = null;
1258
+ unknownByEvent.current = false;
1259
+ setNotice(null);
1260
+ setSectionKey((k) => k + 1);
1261
+ return true;
1262
+ }, []);
1263
+ const onEvent = react.useCallback(
1264
+ (e) => {
1265
+ if (e.type === "form_ready") {
1266
+ formFailures.current = 0;
1267
+ setNotice((n) => n?.kind === "form_failed" ? null : n);
1268
+ }
1269
+ if (e.type === "form_failed") {
1270
+ if (formFailures.current < formAutoRetries) {
1271
+ formFailures.current += 1;
1272
+ setSectionKey((k) => k + 1);
1273
+ } else {
1274
+ setNotice({ kind: "form_failed" });
1275
+ }
1276
+ }
1277
+ if (e.type === "error") unknownByEvent.current = e.kind === "network" || e.kind === "payment_in_progress";
1278
+ if (e.type === "result") {
1279
+ unknownByEvent.current = false;
1280
+ lastIssue.current = null;
1281
+ }
1282
+ if (e.type === "result" && e.status === "rejected") setNotice({ kind: "rejected" });
1283
+ if (e.type === "error" && lastIssue.current?.kind === "store_closed") live.current.onRevalidate?.();
1284
+ },
1285
+ [formAutoRetries]
1286
+ );
1287
+ return react.useMemo(() => ({ notice, setNotice, sectionKey, onIssue, onEvent, lastIssue, retry, clear: () => setNotice(null) }), [notice, sectionKey, onIssue, onEvent, retry]);
1288
+ }
1289
+ function PaymentRecovery(props) {
1290
+ const { notice, alternatives = [], onRetry, onPick, onRefresh, alternativeLabel, classNames: cx = {}, testIds = {}, children } = props;
1291
+ const locale = useResolvedLocale(props.locale);
1292
+ const m = react.useMemo(() => paymentRecoveryMessages(locale, props.messages), [locale, props.messages]);
1293
+ const rowNames = react.useMemo(() => paymentMethodMessages(locale).options, [locale]);
1294
+ const ref = react.useRef(null);
1295
+ const kind = notice?.kind;
1296
+ react.useEffect(() => {
1297
+ if (kind && kind !== "confirming") {
1298
+ ref.current?.focus();
1299
+ ref.current?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
722
1300
  }
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
- } : {}
1301
+ }, [kind]);
1302
+ if (!notice) return null;
1303
+ const confirming = notice.kind === "confirming";
1304
+ const canRetry = notice.kind !== "confirming" && notice.kind !== "stale_checkout";
1305
+ const showAlternatives = notice.kind !== "stale_checkout" && !confirming && alternatives.length > 0;
1306
+ const alternativesFirst = notice.kind === "merchant_config";
1307
+ const nameOf = (c) => c.label ?? (c.option ? rowNames[c.option] : void 0) ?? c.option ?? c.methodKey;
1308
+ const labelOf = (c) => alternativeLabel?.(c) ?? m["action.alternative"].replace("{label}", nameOf(c));
1309
+ 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;
1310
+ 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: [
1311
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.title, children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: m[`notice.${notice.kind}.title`] }) }),
1312
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.body, children: m[`notice.${notice.kind}.body`] }),
1313
+ !confirming ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, children: [
1314
+ alternativesFirst ? null : retryButton,
1315
+ 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,
1316
+ 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,
1317
+ alternativesFirst ? retryButton : null
1318
+ ] }) : null,
1319
+ children
1320
+ ] });
1321
+ }
1322
+
1323
+ // src/payments/revalidate.ts
1324
+ function checkoutShouldRevalidate(options) {
1325
+ const paths = options.paths.map((p) => p.replace(/\/+$/, "") || "/");
1326
+ return (args) => {
1327
+ const method = args.formMethod?.toUpperCase();
1328
+ if (!method || method === "GET" || !args.formAction) return args.defaultShouldRevalidate;
1329
+ let pathname = args.formAction;
1330
+ try {
1331
+ pathname = new URL(args.formAction, "http://sf.local").pathname;
1332
+ } catch {
1333
+ }
1334
+ pathname = pathname.replace(/\/+$/, "") || "/";
1335
+ return paths.includes(pathname) ? false : args.defaultShouldRevalidate;
735
1336
  };
736
1337
  }
737
1338
 
@@ -794,74 +1395,6 @@ function paymentNonceFromHeaders(headers) {
794
1395
  return /'nonce-([A-Za-z0-9+/=_-]+)'/.exec(csp)?.[1];
795
1396
  }
796
1397
 
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
1398
  // src/checkout/brands/logos.generated.ts
866
1399
  var LOGOS = {
867
1400
  "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 +1788,15 @@ function PaymentMethodSection(props) {
1255
1788
  const requote = wantSync && settled && sync.requote === true;
1256
1789
  const syncFailed = wantSync && settled && !sync.ok && !requote;
1257
1790
  const brickReady = cardActive && deliveryReady && synced && !syncFailed;
1791
+ const [formLatched, setFormLatched] = react.useState(false);
1792
+ react.useEffect(() => {
1793
+ if (brickReady) setFormLatched(true);
1794
+ }, [brickReady]);
1795
+ react.useEffect(() => {
1796
+ if (!cardActive) setFormLatched(false);
1797
+ }, [cardActive]);
1798
+ const showBrick = cardActive && !syncFailed && (formLatched || brickReady);
1799
+ const totalUpdating = showBrick && !brickReady;
1258
1800
  const transport = react.useMemo(
1259
1801
  () => props.transport ?? createFetchTransport(payUrl, {
1260
1802
  ...statusUrl ? { refreshUrl: statusUrl } : {},
@@ -1368,9 +1910,9 @@ function PaymentMethodSection(props) {
1368
1910
  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
1911
  ] }),
1370
1912
  /* @__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: [
1913
+ 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
1914
  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(
1915
+ showBrick || fallback ? /* @__PURE__ */ jsxRuntime.jsx(
1374
1916
  PaymentBrick,
1375
1917
  {
1376
1918
  ...brickProps,
@@ -1382,15 +1924,14 @@ function PaymentMethodSection(props) {
1382
1924
  locale,
1383
1925
  ...scope ? { scope } : {},
1384
1926
  ...payerEmail && !brickProps?.email ? { email: payerEmail } : {},
1385
- enabled: brickReady,
1927
+ enabled: showBrick,
1386
1928
  ...cx.brick ? { className: cx.brick } : {},
1387
1929
  onEvent: handleEvent,
1388
1930
  onFallback: () => setFallback(true),
1389
1931
  onResult: (o) => {
1390
1932
  if (o.kind === "approved" || o.kind === "in_review" || o.kind === "voucher") onDone(o);
1391
1933
  }
1392
- },
1393
- payerEmail || brickProps?.email ? "with-email" : "without-email"
1934
+ }
1394
1935
  ) : null,
1395
1936
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.note, children: m.secureCard }),
1396
1937
  expired && rebuildHref ? Link({ href: rebuildHref, ...cx.rebuild ? { className: cx.rebuild } : {}, children: m.rebuildCart, ...testIds.rebuild ? { testId: testIds.rebuild } : {} }) : null
@@ -1409,7 +1950,7 @@ function PaymentMethodSection(props) {
1409
1950
  onClick: onPayClick,
1410
1951
  className: cx.submit,
1411
1952
  disabled: busy || blocked,
1412
- "aria-disabled": blocked || brickReady && (!ready || processing) || void 0,
1953
+ "aria-disabled": blocked || cardActive && !syncFailed && deliveryReady && (!brickReady || !ready || processing) || void 0,
1413
1954
  "aria-busy": processing || void 0,
1414
1955
  "data-sf-pay-method-submit": true,
1415
1956
  ...tid("submit"),
@@ -1499,8 +2040,6 @@ function rowMarks(choice, cardBrands) {
1499
2040
  }
1500
2041
  return [];
1501
2042
  }
1502
-
1503
- // src/payments/choices.ts
1504
2043
  var SEP = "|";
1505
2044
  function choiceValue(methodKey, option) {
1506
2045
  return option ? `${methodKey}${SEP}${option}` : methodKey;
@@ -1574,13 +2113,33 @@ function checkoutToAttempt(result) {
1574
2113
  if (action?.type === "challenge") return { ...base, payment: { provider: payment.provider, status: "requires_action", outcome: "challenge", action } };
1575
2114
  return { ...base, payment: { provider: payment.provider, status: "pending", outcome: "pending", action: action ?? { type: "none" } } };
1576
2115
  }
1577
- function payErrorResponse(err) {
2116
+ function safeHttpStatus(status) {
2117
+ return status === 502 || status === 503 || status === 504 ? 500 : status;
2118
+ }
2119
+ function payErrorResponse(err, options = {}) {
1578
2120
  const status = typeof err.status === "number" && err.status >= 400 ? err.status : 503;
1579
- let code = err.code ?? "server_error";
2121
+ const { kind: classified, retryable } = storefront.classifyPaymentError({ ...err, status, checkoutId: err.checkoutId ?? null });
2122
+ const kind = options.kind ?? classified;
2123
+ let code = err.code ?? (kind === "transient" ? "unavailable" : "server_error");
1580
2124
  if (status === 409 && err.field === "payment_in_progress") code = "payment_in_progress";
1581
2125
  if (status === 429) code = "too_many_attempts";
1582
2126
  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 } : {} } } };
2127
+ const own = (options.ownMessage || kind === "validation" || kind === "expired" || kind === "price_mismatch" || kind === "other" && status < 500) && err.message;
2128
+ const message = own || paymentRecoveryMessages(options.locale, options.messages)[`error.${kind}`];
2129
+ return {
2130
+ status: safeHttpStatus(status),
2131
+ body: {
2132
+ error: {
2133
+ code,
2134
+ message,
2135
+ retryable: options.kind ? options.kind === "transient" || options.kind === "in_progress" || options.kind === "rate_limited" : retryable,
2136
+ kind,
2137
+ ...err.field ? { field: err.field } : {},
2138
+ ...err.retryAfter ? { retry_after: err.retryAfter } : {},
2139
+ ...err.details !== void 0 ? { details: err.details } : {}
2140
+ }
2141
+ }
2142
+ };
1584
2143
  }
1585
2144
  function voucherRows(voucher, labels, locale) {
1586
2145
  if (!voucher) return [];
@@ -1606,15 +2165,23 @@ exports.PaymentBrick = PaymentBrick;
1606
2165
  exports.PaymentChallenge = PaymentChallenge;
1607
2166
  exports.PaymentHttpError = PaymentHttpError;
1608
2167
  exports.PaymentMethodSection = PaymentMethodSection;
2168
+ exports.PaymentMountCancelled = PaymentMountCancelled;
2169
+ exports.PaymentMountTimeout = PaymentMountTimeout;
2170
+ exports.PaymentRecovery = PaymentRecovery;
1609
2171
  exports.VoucherCard = VoucherCard;
2172
+ exports.alternativeChoices = alternativeChoices;
1610
2173
  exports.buildPayChoices = buildPayChoices;
1611
2174
  exports.cashProviderData = cashProviderData;
2175
+ exports.checkoutShouldRevalidate = checkoutShouldRevalidate;
1612
2176
  exports.checkoutToAttempt = checkoutToAttempt;
1613
2177
  exports.choiceValue = choiceValue;
1614
2178
  exports.createFetchTransport = createFetchTransport;
2179
+ exports.createResilientTransport = createResilientTransport;
1615
2180
  exports.formFields = formFields;
2181
+ exports.isResolvedAttempt = isResolvedAttempt;
1616
2182
  exports.loadPaymentSdk = loadPaymentSdk;
1617
2183
  exports.mercadoPagoBricksAdapter = mercadoPagoBricksAdapter;
2184
+ exports.mountQueued = mountQueued;
1618
2185
  exports.parseChoice = parseChoice;
1619
2186
  exports.payErrorResponse = payErrorResponse;
1620
2187
  exports.paymentCsp = paymentCsp;
@@ -1624,11 +2191,16 @@ exports.paymentCspMode = paymentCspMode;
1624
2191
  exports.paymentMethodMessages = paymentMethodMessages;
1625
2192
  exports.paymentNonce = paymentNonce;
1626
2193
  exports.paymentNonceFromHeaders = paymentNonceFromHeaders;
2194
+ exports.paymentRecoveryMessages = paymentRecoveryMessages;
2195
+ exports.pickChoice = pickChoice;
1627
2196
  exports.readPaymentScheme = readPaymentScheme;
1628
2197
  exports.readPaymentTheme = readPaymentTheme;
1629
2198
  exports.resetPaymentSdkCache = resetPaymentSdkCache;
2199
+ exports.safeHttpStatus = safeHttpStatus;
1630
2200
  exports.sanitizeMercadoPagoFormData = sanitizeMercadoPagoFormData;
1631
2201
  exports.useEmbeddedPayment = useEmbeddedPayment;
2202
+ exports.usePaymentRecovery = usePaymentRecovery;
1632
2203
  exports.voucherRows = voucherRows;
2204
+ exports.withMountQueue = withMountQueue;
1633
2205
  //# sourceMappingURL=payments.cjs.map
1634
2206
  //# sourceMappingURL=payments.cjs.map