@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.js CHANGED
@@ -3,9 +3,10 @@ import { useModalDialog } from './chunk-ZBH7JYU4.js';
3
3
  import { findBrand, BrandMark } from './chunk-JNUJGPJC.js';
4
4
  import { useResolvedLocale } from './chunk-T52H5HL3.js';
5
5
  import './chunk-DQBL2DFJ.js';
6
- export { buildPayChoices, cashProviderData, checkoutToAttempt, choiceValue, parseChoice, payErrorResponse, paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders, voucherRows } from './chunk-ISEOCKIR.js';
6
+ import { paymentRecoveryMessages } from './chunk-MNLROALF.js';
7
+ export { buildPayChoices, cashProviderData, checkoutToAttempt, choiceValue, parseChoice, payErrorResponse, paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders, paymentRecoveryMessages, safeHttpStatus, voucherRows } from './chunk-MNLROALF.js';
7
8
  import { forwardRef, useImperativeHandle, useRef, useEffect, useMemo, useId, useState, useCallback, useSyncExternalStore } from 'react';
8
- import { paymentMessages, formatMoney, generateIdempotencyKey, paymentOutcome, paymentErrorView, formatVoucherExpiry } from '@asteby/storefront';
9
+ import { paymentMessages, formatMoney, generateIdempotencyKey, paymentOutcome, paymentErrorView, formatVoucherExpiry, classifyPaymentError } from '@asteby/storefront';
9
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
11
 
11
12
  // src/payments/sdk-loader.ts
@@ -130,6 +131,13 @@ function mercadoPagoBricksAdapter(options = {}) {
130
131
  const device = getDevice();
131
132
  return { provider_data, ...typeof device === "string" && device ? { device_session_id: device } : {} };
132
133
  },
134
+ async update(next) {
135
+ if (gone) throw new Error("payment form is unmounted");
136
+ if (typeof controller.update !== "function") throw new Error("payment form: update not supported");
137
+ const value = Number.parseFloat(next.amount.amount);
138
+ if (!Number.isFinite(value) || value <= 0) throw new Error("payment form: invalid amount");
139
+ await controller.update({ amount: value });
140
+ },
133
141
  async unmount() {
134
142
  gone = true;
135
143
  try {
@@ -142,6 +150,273 @@ function mercadoPagoBricksAdapter(options = {}) {
142
150
  };
143
151
  }
144
152
 
153
+ // src/payments/mount-queue.ts
154
+ var PaymentMountCancelled = class extends Error {
155
+ name = "PaymentMountCancelled";
156
+ constructor(message = "payment form: mount cancelled") {
157
+ super(message);
158
+ }
159
+ };
160
+ var PaymentMountTimeout = class extends Error {
161
+ name = "PaymentMountTimeout";
162
+ constructor(ms) {
163
+ super(`payment form: create timeout after ${ms}ms`);
164
+ }
165
+ };
166
+ var chains = /* @__PURE__ */ new WeakMap();
167
+ var seq = 0;
168
+ function withTimeout(p, ms) {
169
+ return new Promise((resolve, reject) => {
170
+ const t = setTimeout(() => reject(new PaymentMountTimeout(ms)), ms);
171
+ p.then(
172
+ (v) => {
173
+ clearTimeout(t);
174
+ resolve(v);
175
+ },
176
+ (e) => {
177
+ clearTimeout(t);
178
+ reject(e);
179
+ }
180
+ );
181
+ });
182
+ }
183
+ async function mountQueued(base, opts, queue = {}) {
184
+ const container = opts.container;
185
+ const createTimeoutMs = queue.createTimeoutMs ?? 1e4;
186
+ let chain = chains.get(container);
187
+ if (!chain) {
188
+ chain = { tail: Promise.resolve(), active: null, ticket: 0 };
189
+ chains.set(container, chain);
190
+ }
191
+ const me = chain;
192
+ let release;
193
+ const done = new Promise((r) => release = r);
194
+ const previous = me.tail;
195
+ const ticket = ++me.ticket;
196
+ me.tail = done;
197
+ let released = false;
198
+ const finish = () => {
199
+ if (!released) {
200
+ released = true;
201
+ release();
202
+ }
203
+ };
204
+ const cancelled = () => opts.signal?.aborted === true || !container.isConnected || me.ticket !== ticket;
205
+ let slot = null;
206
+ try {
207
+ if (me.active) await me.active.teardown();
208
+ await previous;
209
+ if (cancelled()) throw new PaymentMountCancelled();
210
+ slot = container.ownerDocument.createElement("div");
211
+ slot.id = `${container.id || "sf-pay"}-m${++seq}`;
212
+ container.replaceChildren(slot);
213
+ const node = slot;
214
+ const pending2 = base.mount({ ...opts, container: node });
215
+ let handle;
216
+ try {
217
+ handle = await withTimeout(pending2, createTimeoutMs);
218
+ } catch (err) {
219
+ void pending2.then((h) => Promise.resolve(h.unmount()).catch(() => {
220
+ }), () => {
221
+ });
222
+ throw err;
223
+ }
224
+ let torn = null;
225
+ const teardown = () => torn ??= (async () => {
226
+ try {
227
+ await handle.unmount();
228
+ } catch {
229
+ }
230
+ node.remove();
231
+ if (me.active === entry) me.active = null;
232
+ finish();
233
+ })();
234
+ const entry = { teardown };
235
+ me.active = entry;
236
+ if (cancelled() || !node.isConnected) {
237
+ await teardown();
238
+ throw new PaymentMountCancelled();
239
+ }
240
+ return {
241
+ collect: () => handle.collect(),
242
+ ...handle.update ? { update: (next) => handle.update(next) } : {},
243
+ unmount: teardown
244
+ };
245
+ } catch (err) {
246
+ slot?.remove();
247
+ finish();
248
+ throw err;
249
+ }
250
+ }
251
+ function withMountQueue(adapter, queue = {}) {
252
+ return { kind: adapter.kind, load: (client, options) => adapter.load(client, options), mount: (opts) => mountQueued(adapter, opts, queue) };
253
+ }
254
+
255
+ // src/payments/adapter.ts
256
+ var PaymentHttpError = class extends Error {
257
+ constructor(status, code, message, details, extra) {
258
+ super(message ?? code);
259
+ this.status = status;
260
+ this.code = code;
261
+ this.details = details;
262
+ this.retryable = extra?.retryable;
263
+ this.kind = extra?.kind;
264
+ }
265
+ status;
266
+ code;
267
+ details;
268
+ name = "PaymentHttpError";
269
+ /** `error.retryable` of the shop body: `true` repeat with the SAME key; `false` it is an answer. `undefined`: not said. */
270
+ retryable;
271
+ /** `error.kind` of the shop body (`classifyPaymentError`). */
272
+ kind;
273
+ };
274
+ async function parse(res) {
275
+ const text = await res.text();
276
+ if (!text) return null;
277
+ try {
278
+ return JSON.parse(text);
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+ function createFetchTransport(url, options = {}) {
284
+ const doFetch = (u, init) => (options.fetch ?? globalThis.fetch.bind(globalThis))(u, init);
285
+ async function call(u, init) {
286
+ const res = await doFetch(u, { credentials: options.credentials ?? "same-origin", ...init });
287
+ const body = await parse(res);
288
+ if (!res.ok) {
289
+ const e = body?.error;
290
+ throw new PaymentHttpError(res.status, e?.code ?? "server_error", e?.message, e?.details, {
291
+ retryable: typeof e?.retryable === "boolean" ? e.retryable : void 0,
292
+ kind: typeof e?.kind === "string" ? e.kind : void 0
293
+ });
294
+ }
295
+ const result = body;
296
+ if (!result || typeof result !== "object" || !result.payment || typeof result.payment.status !== "string") {
297
+ throw new PaymentHttpError(502, "invalid_response", "Unexpected payment response");
298
+ }
299
+ return result;
300
+ }
301
+ return {
302
+ submit: (request, { signal }) => call(url, {
303
+ method: "POST",
304
+ signal,
305
+ headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey, ...options.headers },
306
+ body: JSON.stringify(options.buildBody ? options.buildBody(request) : request)
307
+ }),
308
+ ...options.refreshUrl ? {
309
+ refresh: ({ signal }) => call(options.refreshUrl, { method: "POST", signal, headers: { Accept: "application/json", ...options.headers } })
310
+ } : {}
311
+ };
312
+ }
313
+
314
+ // src/payments/resilient-transport.ts
315
+ var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
316
+ function isResolvedAttempt(result) {
317
+ const p = result.payment;
318
+ if (p.status === "paid" || p.status === "failed" || p.status === "expired" || p.status === "cancelled") return true;
319
+ const type = p.action?.type;
320
+ return type === "voucher" || type === "challenge" || type === "redirect";
321
+ }
322
+ async function readJson(res) {
323
+ const text = await res.text().catch(() => "");
324
+ if (!text) return null;
325
+ try {
326
+ return JSON.parse(text);
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+ function createResilientTransport(opts) {
332
+ const doFetch = opts.fetch ?? ((u, i) => globalThis.fetch(u, i));
333
+ const sleep2 = opts.sleep ?? realSleep;
334
+ const retries = opts.retries ?? 2;
335
+ const backoff = opts.backoffMs ?? [800, 2e3];
336
+ const polls = opts.statusPolls ?? 3;
337
+ const pollMs = opts.statusPollMs ?? 2e3;
338
+ const msgs = paymentRecoveryMessages(opts.locale, opts.messages);
339
+ const text = (k) => msgs[`error.${k}`];
340
+ const issue = (i) => opts.onIssue?.(i);
341
+ const confirming = msgs["notice.confirming.title"];
342
+ async function call(url, init) {
343
+ let res;
344
+ try {
345
+ res = await doFetch(url, { credentials: "same-origin", ...init, headers: { ...opts.headers, ...init.headers } });
346
+ } catch (err) {
347
+ if (err?.name === "AbortError") throw err;
348
+ return { fail: { kind: "transient", status: 0, code: "network", message: text("transient"), retryable: true } };
349
+ }
350
+ const body = await readJson(res);
351
+ if (res.ok) {
352
+ const r = body;
353
+ if (r && typeof r === "object" && r.payment && typeof r.payment.status === "string") return { ok: r };
354
+ return { fail: { kind: "transient", status: 502, code: "invalid_response", message: text("transient"), retryable: true } };
355
+ }
356
+ const e = body?.error;
357
+ if (!e) {
358
+ const info2 = classifyPaymentError({ status: res.status >= 500 || res.status === 0 ? 503 : res.status });
359
+ return { fail: { kind: info2.kind, status: res.status, code: "server_error", message: text(info2.kind), retryable: info2.retryable } };
360
+ }
361
+ const byCode = 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 });
362
+ const info = e.kind && !(e.kind === "transient" && !byCode.retryable) ? { kind: e.kind, retryable: e.retryable === true || byCode.retryable } : byCode;
363
+ 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 } };
364
+ }
365
+ 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 });
366
+ async function resolveByStatus(signal) {
367
+ for (let i = 0; i < polls; i++) {
368
+ const r = await call(opts.statusUrl, { method: "POST", signal, headers: { Accept: "application/json" } });
369
+ if ("ok" in r) {
370
+ if (isResolvedAttempt(r.ok)) return r.ok;
371
+ } else if (!r.fail.retryable) {
372
+ return null;
373
+ }
374
+ if (i < polls - 1) await sleep2(pollMs);
375
+ }
376
+ return null;
377
+ }
378
+ return {
379
+ async submit(request, { signal }) {
380
+ const init = {
381
+ method: "POST",
382
+ signal,
383
+ headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey },
384
+ body: JSON.stringify({ ...request, fields: opts.fields() })
385
+ };
386
+ issue(null);
387
+ let last = null;
388
+ for (let n = 0; n <= retries; n++) {
389
+ const r = await call(opts.payUrl, init);
390
+ if ("ok" in r) {
391
+ issue(null);
392
+ return r.ok;
393
+ }
394
+ last = r.fail;
395
+ const waitable = last.retryable && (last.kind === "transient" || last.kind === "in_progress");
396
+ if (!waitable || n === retries) break;
397
+ issue({ kind: "confirming", message: confirming });
398
+ await sleep2(backoff[Math.min(n, backoff.length - 1)] ?? 800);
399
+ }
400
+ const failure = last;
401
+ if (failure.retryable && (failure.kind === "transient" || failure.kind === "in_progress") || failure.kind === "stale_checkout") {
402
+ issue({ kind: "verifying", message: confirming });
403
+ const resolved = await resolveByStatus(signal);
404
+ if (resolved) {
405
+ issue(null);
406
+ return resolved;
407
+ }
408
+ }
409
+ issue({ kind: failure.kind, message: failure.message, retryable: failure.retryable, status: failure.status, code: failure.code });
410
+ throw toError(failure);
411
+ },
412
+ refresh: async ({ signal }) => {
413
+ const r = await call(opts.statusUrl, { method: "POST", signal, headers: { Accept: "application/json" } });
414
+ if ("ok" in r) return r.ok;
415
+ throw toError(r.fail);
416
+ }
417
+ };
418
+ }
419
+
145
420
  // src/payments/theme.ts
146
421
  var PAYMENT_THEME_TOKENS = {
147
422
  "--sf-pay-text": "textPrimaryColor",
@@ -211,6 +486,8 @@ function useEmbeddedPayment(options) {
211
486
  retryDelayMs = 600,
212
487
  pollIntervalMs = 2e3,
213
488
  pollTimeoutMs = 6e4,
489
+ verifyTimeoutMs = 12e4,
490
+ verifyIntervalMs = 2e3,
214
491
  scope = "default"
215
492
  } = options;
216
493
  const msgs = useMemo(() => paymentMessages(locale, options.messages), [locale, options.messages]);
@@ -224,15 +501,27 @@ function useEmbeddedPayment(options) {
224
501
  const [attemptId, setAttemptId] = useState(null);
225
502
  const [container, setContainer] = useState(null);
226
503
  const [mountKey, setMountKey] = useState(0);
504
+ const [verifying, setVerifying] = useState(false);
227
505
  const live = useRef({ ...options, msgs });
228
506
  live.current = { ...options, msgs };
229
507
  const form = useRef(null);
230
508
  const inFlight = useRef(null);
231
509
  const pendingData = useRef(null);
510
+ const memAttempt = useRef(null);
511
+ const memOnly = useRef(false);
512
+ const errorRef = useRef(null);
513
+ const retryRef = useRef(() => Promise.resolve());
232
514
  const alive = useRef(true);
233
515
  const generation = useRef(0);
234
516
  const statusRef = useRef("idle");
235
517
  statusRef.current = status;
518
+ errorRef.current = error;
519
+ const amountKey = `${amount.amount}|${amount.currency_code}`;
520
+ const amountKeyRef = useRef(amountKey);
521
+ amountKeyRef.current = amountKey;
522
+ const appliedAmount = useRef(null);
523
+ const updating = useRef(null);
524
+ const remountRequested = useRef(false);
236
525
  useEffect(() => {
237
526
  alive.current = true;
238
527
  return () => {
@@ -243,19 +532,33 @@ function useEmbeddedPayment(options) {
243
532
  const store = options.storage === void 0 ? safeStorage() : options.storage;
244
533
  const emit = useCallback((e) => live.current.onEvent?.(e), []);
245
534
  const readAttempt = useCallback(() => {
246
- try {
247
- const saved = store?.getItem(storageKey);
248
- if (saved) return saved;
249
- } catch {
535
+ if (!memOnly.current) {
536
+ try {
537
+ const saved = store?.getItem(storageKey);
538
+ if (saved) {
539
+ memAttempt.current = saved;
540
+ return saved;
541
+ }
542
+ } catch {
543
+ memOnly.current = true;
544
+ }
545
+ }
546
+ if (memOnly.current && memAttempt.current) return memAttempt.current;
547
+ if (!store) {
548
+ if (memAttempt.current) return memAttempt.current;
250
549
  }
251
550
  const fresh = (live.current.newAttemptId ?? generateIdempotencyKey)();
551
+ memAttempt.current = fresh;
252
552
  try {
253
553
  store?.setItem(storageKey, fresh);
554
+ if (store && store.getItem(storageKey) !== fresh) memOnly.current = true;
254
555
  } catch {
556
+ memOnly.current = true;
255
557
  }
256
558
  return fresh;
257
559
  }, [store, storageKey]);
258
560
  const rotateAttempt = useCallback(() => {
561
+ memAttempt.current = null;
259
562
  try {
260
563
  store?.removeItem(storageKey);
261
564
  } catch {
@@ -271,7 +574,32 @@ function useEmbeddedPayment(options) {
271
574
  },
272
575
  [emit]
273
576
  );
274
- const amountKey = `${amount.amount}|${amount.currency_code}`;
577
+ const syncAmount = useCallback(() => {
578
+ const m = form.current;
579
+ if (!m || inFlight.current || updating.current || remountRequested.current) return;
580
+ if (statusRef.current === "network_error" || statusRef.current === "blocked" && errorRef.current?.kind === "payment_in_progress") return;
581
+ if (appliedAmount.current === amountKeyRef.current) return;
582
+ const run = (async () => {
583
+ while (form.current === m && alive.current && appliedAmount.current !== amountKeyRef.current) {
584
+ const key = amountKeyRef.current;
585
+ try {
586
+ if (!m.update) throw new Error("payment form: update not supported");
587
+ await m.update({ amount: live.current.amount });
588
+ appliedAmount.current = key;
589
+ } catch {
590
+ if (form.current === m && alive.current) {
591
+ remountRequested.current = true;
592
+ setMountKey((k) => k + 1);
593
+ }
594
+ return;
595
+ }
596
+ }
597
+ })().finally(() => {
598
+ updating.current = null;
599
+ if (alive.current) syncAmount();
600
+ });
601
+ updating.current = run;
602
+ }, []);
275
603
  const kind = client?.kind;
276
604
  const publicKey = client?.public_key;
277
605
  useEffect(() => {
@@ -285,18 +613,22 @@ function useEmbeddedPayment(options) {
285
613
  return;
286
614
  }
287
615
  if (!container) return;
616
+ remountRequested.current = false;
288
617
  const my = ++generation.current;
289
618
  const started = Date.now();
619
+ const abort = new AbortController();
290
620
  let mounted = null;
291
621
  setStatus("loading");
292
622
  const stale = () => my !== generation.current || !alive.current;
293
623
  const fail = (reason) => {
294
624
  if (stale()) return;
295
625
  generation.current++;
626
+ abort.abort();
296
627
  clearTimeout(timer);
297
628
  const m = mounted ?? form.current;
298
629
  mounted = null;
299
630
  form.current = null;
631
+ appliedAmount.current = null;
300
632
  if (m) void Promise.resolve(m.unmount());
301
633
  fallback(reason);
302
634
  };
@@ -310,31 +642,39 @@ function useEmbeddedPayment(options) {
310
642
  if (stale()) return;
311
643
  const theme = live.current.theme ?? readPaymentTheme(container);
312
644
  const colorScheme = live.current.colorScheme ?? readPaymentScheme(container);
313
- const m = await adapter.mount({
314
- container,
315
- client,
316
- amount: live.current.amount,
317
- ...live.current.email ? { email: live.current.email } : {},
318
- ...live.current.locale ? { locale: live.current.locale } : {},
319
- theme,
320
- ...colorScheme ? { colorScheme } : {},
321
- onReady: () => {
322
- if (stale()) return;
323
- clearTimeout(timer);
324
- setStatus((s) => s === "loading" ? "ready" : s);
325
- emit({ type: "form_ready", ms: Date.now() - started });
645
+ const createdWith = amountKeyRef.current;
646
+ const m = await mountQueued(
647
+ adapter,
648
+ {
649
+ container,
650
+ client,
651
+ amount: live.current.amount,
652
+ ...live.current.email ? { email: live.current.email } : {},
653
+ ...live.current.locale ? { locale: live.current.locale } : {},
654
+ theme,
655
+ ...colorScheme ? { colorScheme } : {},
656
+ signal: abort.signal,
657
+ onReady: () => {
658
+ if (stale()) return;
659
+ clearTimeout(timer);
660
+ setStatus((s) => s === "loading" ? "ready" : s);
661
+ emit({ type: "form_ready", ms: Date.now() - started });
662
+ },
663
+ onError: (e) => {
664
+ if (stale()) return;
665
+ if (e.critical) fail("form_error");
666
+ }
326
667
  },
327
- onError: (e) => {
328
- if (stale()) return;
329
- if (e.critical) fail("form_error");
330
- }
331
- });
668
+ { createTimeoutMs: live.current.createTimeoutMs ?? sdkTimeoutMs }
669
+ );
332
670
  if (stale()) {
333
671
  void Promise.resolve(m.unmount());
334
672
  return;
335
673
  }
336
674
  mounted = m;
337
675
  form.current = m;
676
+ appliedAmount.current = createdWith;
677
+ syncAmount();
338
678
  } catch (err) {
339
679
  fail(err instanceof Error && /timeout/i.test(err.message) ? "sdk_timeout" : "sdk_error");
340
680
  }
@@ -342,12 +682,17 @@ function useEmbeddedPayment(options) {
342
682
  return () => {
343
683
  clearTimeout(timer);
344
684
  generation.current++;
685
+ abort.abort();
345
686
  const m = mounted ?? form.current;
346
687
  mounted = null;
347
688
  form.current = null;
689
+ appliedAmount.current = null;
348
690
  if (m) void Promise.resolve(m.unmount());
349
691
  };
350
- }, [enabled, container, kind, publicKey, amountKey, mountKey, sdkTimeoutMs, containerId]);
692
+ }, [enabled, container, kind, publicKey, mountKey, sdkTimeoutMs, containerId]);
693
+ useEffect(() => {
694
+ syncAmount();
695
+ }, [amountKey, syncAmount]);
351
696
  const finish = useCallback(
352
697
  (result) => {
353
698
  const o = paymentOutcome(result, live.current.locale, live.current.messages);
@@ -371,7 +716,7 @@ function useEmbeddedPayment(options) {
371
716
  [emit, rotateAttempt]
372
717
  );
373
718
  const send = useCallback(
374
- async (data, attempt) => {
719
+ async (data, attempt, total) => {
375
720
  const ctl = new AbortController();
376
721
  const maxRetries = live.current.autoRetries ?? autoRetries;
377
722
  for (let n = 0; ; n++) {
@@ -383,7 +728,7 @@ function useEmbeddedPayment(options) {
383
728
  idempotencyKey: attempt,
384
729
  provider_data: data.provider_data,
385
730
  ...data.device_session_id ? { device_session_id: data.device_session_id } : {},
386
- expected_total: live.current.amount
731
+ expected_total: total
387
732
  },
388
733
  { signal: ctl.signal }
389
734
  );
@@ -401,7 +746,7 @@ function useEmbeddedPayment(options) {
401
746
  emit({ type: "error", kind: view.kind });
402
747
  setError(view);
403
748
  if (view.kind === "network") {
404
- pendingData.current = data;
749
+ pendingData.current = { data, total };
405
750
  setStatus("network_error");
406
751
  } else if (view.kind === "price_mismatch") {
407
752
  pendingData.current = null;
@@ -427,7 +772,11 @@ function useEmbeddedPayment(options) {
427
772
  if (inFlight.current) return inFlight.current;
428
773
  const m = form.current;
429
774
  if (!m || statusRef.current !== "ready" && statusRef.current !== "network_error") return Promise.resolve();
775
+ if (statusRef.current === "network_error" && pendingData.current) return retryRef.current();
776
+ const total = live.current.amount;
430
777
  const run = (async () => {
778
+ if (updating.current) await updating.current.catch(() => {
779
+ });
431
780
  setStatus("processing");
432
781
  setError(null);
433
782
  setOutcome(null);
@@ -443,30 +792,33 @@ function useEmbeddedPayment(options) {
443
792
  }
444
793
  const attempt = readAttempt();
445
794
  setAttemptId(attempt);
446
- await send(data, attempt);
795
+ await send(data, attempt, total);
447
796
  })().finally(() => {
448
797
  inFlight.current = null;
798
+ syncAmount();
449
799
  });
450
800
  inFlight.current = run;
451
801
  return run;
452
- }, [emit, readAttempt, send]);
802
+ }, [emit, readAttempt, send, syncAmount]);
453
803
  const retry = useCallback(() => {
454
804
  if (inFlight.current) return inFlight.current;
455
- const data = pendingData.current;
456
- if (!data) return submit();
805
+ const pending2 = pendingData.current;
806
+ if (!pending2) return submit();
457
807
  emit({ type: "retry" });
458
808
  const run = (async () => {
459
809
  setStatus("processing");
460
810
  setError(null);
461
811
  const attempt = readAttempt();
462
812
  setAttemptId(attempt);
463
- await send(data, attempt);
813
+ await send(pending2.data, attempt, pending2.total);
464
814
  })().finally(() => {
465
815
  inFlight.current = null;
816
+ syncAmount();
466
817
  });
467
818
  inFlight.current = run;
468
819
  return run;
469
- }, [emit, readAttempt, send, submit]);
820
+ }, [emit, readAttempt, send, submit, syncAmount]);
821
+ retryRef.current = retry;
470
822
  useEffect(() => {
471
823
  if (status !== "challenge" && status !== "in_review" || !transport.refresh) return;
472
824
  let stop = false;
@@ -479,6 +831,7 @@ function useEmbeddedPayment(options) {
479
831
  try {
480
832
  const r = await transport.refresh({ signal: ctl.signal });
481
833
  if (stop || !alive.current) return;
834
+ if (r.payment.status === "none") continue;
482
835
  const o = paymentOutcome(r, live.current.locale, live.current.messages);
483
836
  if (o.kind !== status) {
484
837
  finish(r);
@@ -493,6 +846,44 @@ function useEmbeddedPayment(options) {
493
846
  ctl.abort();
494
847
  };
495
848
  }, [status, transport, pollIntervalMs, pollTimeoutMs, finish]);
849
+ const unknownOutcome = status === "network_error" || status === "blocked" && error?.kind === "payment_in_progress";
850
+ useEffect(() => {
851
+ if (!unknownOutcome) syncAmount();
852
+ }, [unknownOutcome, syncAmount]);
853
+ useEffect(() => {
854
+ if (!unknownOutcome || !transport.refresh) {
855
+ setVerifying(false);
856
+ return;
857
+ }
858
+ let stop = false;
859
+ const ctl = new AbortController();
860
+ const until = Date.now() + verifyTimeoutMs;
861
+ setVerifying(true);
862
+ (async () => {
863
+ let wait = verifyIntervalMs;
864
+ while (!stop && Date.now() < until) {
865
+ await sleep(wait);
866
+ wait = Math.min(wait * 2, 15e3);
867
+ if (stop || !alive.current) return;
868
+ try {
869
+ const r = await transport.refresh({ signal: ctl.signal });
870
+ if (stop || !alive.current) return;
871
+ if (isResolvedAttempt(r)) {
872
+ setVerifying(false);
873
+ finish(r);
874
+ return;
875
+ }
876
+ } catch {
877
+ }
878
+ }
879
+ if (!stop && alive.current) setVerifying(false);
880
+ })();
881
+ return () => {
882
+ stop = true;
883
+ ctl.abort();
884
+ setVerifying(false);
885
+ };
886
+ }, [unknownOutcome, transport, verifyTimeoutMs, verifyIntervalMs, finish]);
496
887
  const dismiss = useCallback(() => {
497
888
  setError(null);
498
889
  setOutcome((o) => o?.kind === "rejected" ? null : o);
@@ -507,6 +898,7 @@ function useEmbeddedPayment(options) {
507
898
  attemptsLeft,
508
899
  attemptId,
509
900
  busy: status === "loading" || status === "processing",
901
+ verifying,
510
902
  containerRef: setContainer,
511
903
  containerId,
512
904
  submit,
@@ -616,56 +1008,6 @@ var PaymentBrick = forwardRef(function PaymentBrick2(props, ref) {
616
1008
  ] });
617
1009
  });
618
1010
 
619
- // src/payments/adapter.ts
620
- var PaymentHttpError = class extends Error {
621
- constructor(status, code, message, details) {
622
- super(message ?? code);
623
- this.status = status;
624
- this.code = code;
625
- this.details = details;
626
- }
627
- status;
628
- code;
629
- details;
630
- name = "PaymentHttpError";
631
- };
632
- async function parse(res) {
633
- const text = await res.text();
634
- if (!text) return null;
635
- try {
636
- return JSON.parse(text);
637
- } catch {
638
- return null;
639
- }
640
- }
641
- function createFetchTransport(url, options = {}) {
642
- const doFetch = (u, init) => (options.fetch ?? globalThis.fetch.bind(globalThis))(u, init);
643
- async function call(u, init) {
644
- const res = await doFetch(u, { credentials: options.credentials ?? "same-origin", ...init });
645
- const body = await parse(res);
646
- if (!res.ok) {
647
- const e = body?.error;
648
- throw new PaymentHttpError(res.status, e?.code ?? "server_error", e?.message, e?.details);
649
- }
650
- const result = body;
651
- if (!result || typeof result !== "object" || !result.payment || typeof result.payment.status !== "string") {
652
- throw new PaymentHttpError(502, "invalid_response", "Unexpected payment response");
653
- }
654
- return result;
655
- }
656
- return {
657
- submit: (request, { signal }) => call(url, {
658
- method: "POST",
659
- signal,
660
- headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey, ...options.headers },
661
- body: JSON.stringify(options.buildBody ? options.buildBody(request) : request)
662
- }),
663
- ...options.refreshUrl ? {
664
- refresh: ({ signal }) => call(options.refreshUrl, { method: "POST", signal, headers: { Accept: "application/json", ...options.headers } })
665
- } : {}
666
- };
667
- }
668
-
669
1011
  // src/payments/payment-method.messages.ts
670
1012
  var ES = {
671
1013
  legend: "M\xE9todo de pago",
@@ -733,6 +1075,130 @@ function paymentMethodMessages(locale, overrides) {
733
1075
  const { options, ...rest } = overrides;
734
1076
  return { ...base, ...rest, options: { ...base.options, ...options } };
735
1077
  }
1078
+ function alternativeChoices(choices) {
1079
+ return choices.filter((c) => c.mode !== "card");
1080
+ }
1081
+ function pickChoice(form, value) {
1082
+ const input = Array.from(form?.querySelectorAll('input[name="payment_choice"]') ?? []).find((i) => i.value === value);
1083
+ if (!input) return;
1084
+ input.click();
1085
+ input.scrollIntoView?.({ block: "center", behavior: "smooth" });
1086
+ }
1087
+ var INDETERMINATE = /* @__PURE__ */ new Set(["confirming", "verifying", "transient", "in_progress"]);
1088
+ function safeStorage2() {
1089
+ try {
1090
+ return typeof sessionStorage !== "undefined" ? sessionStorage : null;
1091
+ } catch {
1092
+ return null;
1093
+ }
1094
+ }
1095
+ function usePaymentRecovery(options = {}) {
1096
+ const { scope = "default", formAutoRetries = 2 } = options;
1097
+ const [notice, setNotice] = useState(null);
1098
+ const [sectionKey, setSectionKey] = useState(0);
1099
+ const formFailures = useRef(0);
1100
+ const lastIssue = useRef(null);
1101
+ const unknownByEvent = useRef(false);
1102
+ const live = useRef(options);
1103
+ live.current = options;
1104
+ const onIssue = useCallback((issue) => {
1105
+ lastIssue.current = issue;
1106
+ if (!issue) return setNotice((n) => n?.kind === "confirming" ? null : n);
1107
+ if (issue.kind === "confirming" || issue.kind === "verifying") return setNotice({ kind: "confirming" });
1108
+ if (issue.kind === "merchant_config" || issue.kind === "stale_checkout" || issue.kind === "provider_declined") return setNotice({ kind: issue.kind });
1109
+ if (issue.kind === "other") return setNotice({ kind: "other" });
1110
+ setNotice((n) => n?.kind === "confirming" ? null : n);
1111
+ }, []);
1112
+ const retry = useCallback(() => {
1113
+ const issue = lastIssue.current;
1114
+ if (unknownByEvent.current || issue && INDETERMINATE.has(issue.kind)) return false;
1115
+ const store = live.current.storage === void 0 ? safeStorage2() : live.current.storage;
1116
+ try {
1117
+ store?.removeItem(`sf:pay:attempt:${live.current.scope ?? "default"}`);
1118
+ } catch {
1119
+ }
1120
+ formFailures.current = 0;
1121
+ lastIssue.current = null;
1122
+ unknownByEvent.current = false;
1123
+ setNotice(null);
1124
+ setSectionKey((k) => k + 1);
1125
+ return true;
1126
+ }, []);
1127
+ const onEvent = useCallback(
1128
+ (e) => {
1129
+ if (e.type === "form_ready") {
1130
+ formFailures.current = 0;
1131
+ setNotice((n) => n?.kind === "form_failed" ? null : n);
1132
+ }
1133
+ if (e.type === "form_failed") {
1134
+ if (formFailures.current < formAutoRetries) {
1135
+ formFailures.current += 1;
1136
+ setSectionKey((k) => k + 1);
1137
+ } else {
1138
+ setNotice({ kind: "form_failed" });
1139
+ }
1140
+ }
1141
+ if (e.type === "error") unknownByEvent.current = e.kind === "network" || e.kind === "payment_in_progress";
1142
+ if (e.type === "result") {
1143
+ unknownByEvent.current = false;
1144
+ lastIssue.current = null;
1145
+ }
1146
+ if (e.type === "result" && e.status === "rejected") setNotice({ kind: "rejected" });
1147
+ if (e.type === "error" && lastIssue.current?.kind === "store_closed") live.current.onRevalidate?.();
1148
+ },
1149
+ [formAutoRetries]
1150
+ );
1151
+ return useMemo(() => ({ notice, setNotice, sectionKey, onIssue, onEvent, lastIssue, retry, clear: () => setNotice(null) }), [notice, sectionKey, onIssue, onEvent, retry]);
1152
+ }
1153
+ function PaymentRecovery(props) {
1154
+ const { notice, alternatives = [], onRetry, onPick, onRefresh, alternativeLabel, classNames: cx = {}, testIds = {}, children } = props;
1155
+ const locale = useResolvedLocale(props.locale);
1156
+ const m = useMemo(() => paymentRecoveryMessages(locale, props.messages), [locale, props.messages]);
1157
+ const rowNames = useMemo(() => paymentMethodMessages(locale).options, [locale]);
1158
+ const ref = useRef(null);
1159
+ const kind = notice?.kind;
1160
+ useEffect(() => {
1161
+ if (kind && kind !== "confirming") {
1162
+ ref.current?.focus();
1163
+ ref.current?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
1164
+ }
1165
+ }, [kind]);
1166
+ if (!notice) return null;
1167
+ const confirming = notice.kind === "confirming";
1168
+ const canRetry = notice.kind !== "confirming" && notice.kind !== "stale_checkout";
1169
+ const showAlternatives = notice.kind !== "stale_checkout" && !confirming && alternatives.length > 0;
1170
+ const alternativesFirst = notice.kind === "merchant_config";
1171
+ const nameOf = (c) => c.label ?? (c.option ? rowNames[c.option] : void 0) ?? c.option ?? c.methodKey;
1172
+ const labelOf = (c) => alternativeLabel?.(c) ?? m["action.alternative"].replace("{label}", nameOf(c));
1173
+ const retryButton = canRetry ? /* @__PURE__ */ 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;
1174
+ return /* @__PURE__ */ 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: [
1175
+ /* @__PURE__ */ jsx("p", { className: cx.title, children: /* @__PURE__ */ jsx("strong", { children: m[`notice.${notice.kind}.title`] }) }),
1176
+ /* @__PURE__ */ jsx("p", { className: cx.body, children: m[`notice.${notice.kind}.body`] }),
1177
+ !confirming ? /* @__PURE__ */ jsxs("div", { className: cx.actions, children: [
1178
+ alternativesFirst ? null : retryButton,
1179
+ notice.kind === "stale_checkout" ? /* @__PURE__ */ 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,
1180
+ showAlternatives ? alternatives.map((c) => /* @__PURE__ */ 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,
1181
+ alternativesFirst ? retryButton : null
1182
+ ] }) : null,
1183
+ children
1184
+ ] });
1185
+ }
1186
+
1187
+ // src/payments/revalidate.ts
1188
+ function checkoutShouldRevalidate(options) {
1189
+ const paths = options.paths.map((p) => p.replace(/\/+$/, "") || "/");
1190
+ return (args) => {
1191
+ const method = args.formMethod?.toUpperCase();
1192
+ if (!method || method === "GET" || !args.formAction) return args.defaultShouldRevalidate;
1193
+ let pathname = args.formAction;
1194
+ try {
1195
+ pathname = new URL(args.formAction, "http://sf.local").pathname;
1196
+ } catch {
1197
+ }
1198
+ pathname = pathname.replace(/\/+$/, "") || "/";
1199
+ return paths.includes(pathname) ? false : args.defaultShouldRevalidate;
1200
+ };
1201
+ }
736
1202
  var PAYMENT_CHOICE_FIELD = "payment_choice";
737
1203
  var CASH_POINT_FIELD = "cash_point";
738
1204
  function formFields(form) {
@@ -859,6 +1325,15 @@ function PaymentMethodSection(props) {
859
1325
  const requote = wantSync && settled && sync.requote === true;
860
1326
  const syncFailed = wantSync && settled && !sync.ok && !requote;
861
1327
  const brickReady = cardActive && deliveryReady && synced && !syncFailed;
1328
+ const [formLatched, setFormLatched] = useState(false);
1329
+ useEffect(() => {
1330
+ if (brickReady) setFormLatched(true);
1331
+ }, [brickReady]);
1332
+ useEffect(() => {
1333
+ if (!cardActive) setFormLatched(false);
1334
+ }, [cardActive]);
1335
+ const showBrick = cardActive && !syncFailed && (formLatched || brickReady);
1336
+ const totalUpdating = showBrick && !brickReady;
862
1337
  const transport = useMemo(
863
1338
  () => props.transport ?? createFetchTransport(payUrl, {
864
1339
  ...statusUrl ? { refreshUrl: statusUrl } : {},
@@ -972,9 +1447,9 @@ function PaymentMethodSection(props) {
972
1447
  marks.length ? /* @__PURE__ */ jsx("span", { className: cx.brands, "data-sf-pay-brands": choice.option ?? "plain", "aria-hidden": "true", children: marks.map((mk) => /* @__PURE__ */ jsx(BrandMark, { brand: mk.key, logoUrl: mk.logoUrl ?? null, size: 20 }, mk.key)) }) : null
973
1448
  ] }),
974
1449
  /* @__PURE__ */ jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked, marks: brandMarks, visible: cashPointsVisible, openMore: storedPoint, onPointChange }),
975
- choice.mode === "card" && checked && cardChosen && hydrated && choices.cardClient ? /* @__PURE__ */ jsxs("div", { className: cx.brickWrap, "data-sf-pay-card": true, ...tid("card"), children: [
1450
+ choice.mode === "card" && checked && cardChosen && hydrated && choices.cardClient ? /* @__PURE__ */ jsxs("div", { className: cx.brickWrap, "data-sf-pay-card": true, ...totalUpdating ? { "data-sf-pay-updating": "" } : {}, "aria-busy": totalUpdating || void 0, ...tid("card"), children: [
976
1451
  fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : requote ? cardNotice("requote", m.quoteChanged) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
977
- brickReady || fallback ? /* @__PURE__ */ jsx(
1452
+ showBrick || fallback ? /* @__PURE__ */ jsx(
978
1453
  PaymentBrick,
979
1454
  {
980
1455
  ...brickProps,
@@ -986,15 +1461,14 @@ function PaymentMethodSection(props) {
986
1461
  locale,
987
1462
  ...scope ? { scope } : {},
988
1463
  ...payerEmail && !brickProps?.email ? { email: payerEmail } : {},
989
- enabled: brickReady,
1464
+ enabled: showBrick,
990
1465
  ...cx.brick ? { className: cx.brick } : {},
991
1466
  onEvent: handleEvent,
992
1467
  onFallback: () => setFallback(true),
993
1468
  onResult: (o) => {
994
1469
  if (o.kind === "approved" || o.kind === "in_review" || o.kind === "voucher") onDone(o);
995
1470
  }
996
- },
997
- payerEmail || brickProps?.email ? "with-email" : "without-email"
1471
+ }
998
1472
  ) : null,
999
1473
  /* @__PURE__ */ jsx("p", { className: cx.note, children: m.secureCard }),
1000
1474
  expired && rebuildHref ? Link({ href: rebuildHref, ...cx.rebuild ? { className: cx.rebuild } : {}, children: m.rebuildCart, ...testIds.rebuild ? { testId: testIds.rebuild } : {} }) : null
@@ -1013,7 +1487,7 @@ function PaymentMethodSection(props) {
1013
1487
  onClick: onPayClick,
1014
1488
  className: cx.submit,
1015
1489
  disabled: busy || blocked,
1016
- "aria-disabled": blocked || brickReady && (!ready || processing) || void 0,
1490
+ "aria-disabled": blocked || cardActive && !syncFailed && deliveryReady && (!brickReady || !ready || processing) || void 0,
1017
1491
  "aria-busy": processing || void 0,
1018
1492
  "data-sf-pay-method-submit": true,
1019
1493
  ...tid("submit"),
@@ -1104,6 +1578,6 @@ function rowMarks(choice, cardBrands) {
1104
1578
  return [];
1105
1579
  }
1106
1580
 
1107
- export { CASH_POINT_FIELD, MERCADOPAGO_BRICKS_KIND, PAYMENT_CHOICE_FIELD, PAYMENT_THEME_TOKENS, PaymentBrick, PaymentChallenge, PaymentHttpError, PaymentMethodSection, VoucherCard, createFetchTransport, formFields, loadPaymentSdk, mercadoPagoBricksAdapter, paymentMethodMessages, readPaymentScheme, readPaymentTheme, resetPaymentSdkCache, sanitizeMercadoPagoFormData, useEmbeddedPayment };
1581
+ export { CASH_POINT_FIELD, MERCADOPAGO_BRICKS_KIND, PAYMENT_CHOICE_FIELD, PAYMENT_THEME_TOKENS, PaymentBrick, PaymentChallenge, PaymentHttpError, PaymentMethodSection, PaymentMountCancelled, PaymentMountTimeout, PaymentRecovery, VoucherCard, alternativeChoices, checkoutShouldRevalidate, createFetchTransport, createResilientTransport, formFields, isResolvedAttempt, loadPaymentSdk, mercadoPagoBricksAdapter, mountQueued, paymentMethodMessages, pickChoice, readPaymentScheme, readPaymentTheme, resetPaymentSdkCache, sanitizeMercadoPagoFormData, useEmbeddedPayment, usePaymentRecovery, withMountQueue };
1108
1582
  //# sourceMappingURL=payments.js.map
1109
1583
  //# sourceMappingURL=payments.js.map