@asteby/storefront-react 0.4.0 → 0.6.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.
@@ -0,0 +1,766 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var storefront = require('@asteby/storefront');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/payments/payment-brick.tsx
8
+ function useModalDialog({ open, onClose, initialFocus }) {
9
+ const ref = react.useRef(null);
10
+ const opener = react.useRef(null);
11
+ const onCloseRef = react.useRef(onClose);
12
+ onCloseRef.current = onClose;
13
+ react.useEffect(() => {
14
+ const el = ref.current;
15
+ if (!el || !open) return;
16
+ opener.current = typeof document !== "undefined" ? document.activeElement : null;
17
+ const hasModal = typeof el.showModal === "function";
18
+ if (!el.open) {
19
+ if (hasModal) {
20
+ try {
21
+ el.showModal();
22
+ } catch {
23
+ el.setAttribute("open", "");
24
+ }
25
+ } else el.setAttribute("open", "");
26
+ }
27
+ const target = initialFocus?.current ?? el.querySelector("[data-sf-autofocus]") ?? el;
28
+ if (!target.hasAttribute("tabindex") && target === el) el.setAttribute("tabindex", "-1");
29
+ target.focus({ preventScroll: true });
30
+ const onCancel = (e) => {
31
+ e.preventDefault();
32
+ onCloseRef.current("esc");
33
+ };
34
+ const onKey = (e) => {
35
+ if (e.key === "Escape") {
36
+ e.preventDefault();
37
+ onCloseRef.current("esc");
38
+ }
39
+ };
40
+ el.addEventListener("cancel", onCancel);
41
+ if (!hasModal) el.addEventListener("keydown", onKey);
42
+ return () => {
43
+ el.removeEventListener("cancel", onCancel);
44
+ el.removeEventListener("keydown", onKey);
45
+ if (el.open) {
46
+ if (typeof el.close === "function") {
47
+ try {
48
+ el.close();
49
+ } catch {
50
+ el.removeAttribute("open");
51
+ }
52
+ } else el.removeAttribute("open");
53
+ }
54
+ const back = opener.current;
55
+ if (back instanceof HTMLElement && back.isConnected) back.focus({ preventScroll: true });
56
+ };
57
+ }, [open]);
58
+ const onBackdropClick = react.useCallback((e) => {
59
+ if (e.target === e.currentTarget) onCloseRef.current("backdrop");
60
+ }, []);
61
+ return { ref, onBackdropClick };
62
+ }
63
+ var StorefrontContext = react.createContext(null);
64
+ function useOptionalStorefront() {
65
+ return react.useContext(StorefrontContext);
66
+ }
67
+
68
+ // src/locale.ts
69
+ function useResolvedLocale(locale) {
70
+ const sf = useOptionalStorefront();
71
+ return locale ?? sf?.locale ?? "es-MX";
72
+ }
73
+
74
+ // src/payments/sdk-loader.ts
75
+ var pending = /* @__PURE__ */ new Map();
76
+ function loadPaymentSdk(url, options) {
77
+ let parsed;
78
+ try {
79
+ parsed = new URL(url);
80
+ } catch {
81
+ return Promise.reject(new Error("payment sdk: invalid url"));
82
+ }
83
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password) return Promise.reject(new Error("payment sdk: https required"));
84
+ if (options.allowedOrigins && !options.allowedOrigins.includes(parsed.origin)) return Promise.reject(new Error("payment sdk: origin not allowed"));
85
+ if (options.isLoaded?.()) return Promise.resolve();
86
+ const doc = options.document ?? (typeof document !== "undefined" ? document : void 0);
87
+ if (!doc) return Promise.reject(new Error("payment sdk: no document (server)"));
88
+ const key = parsed.toString();
89
+ const existing = pending.get(key);
90
+ if (existing) return existing;
91
+ const p = new Promise((resolve, reject) => {
92
+ const script = doc.createElement("script");
93
+ script.src = key;
94
+ script.async = true;
95
+ if (options.nonce) script.nonce = options.nonce;
96
+ script.dataset.sfPaymentSdk = "true";
97
+ let done = false;
98
+ const finish = (err) => {
99
+ if (done) return;
100
+ done = true;
101
+ clearTimeout(timer);
102
+ if (err) {
103
+ script.remove();
104
+ pending.delete(key);
105
+ reject(err);
106
+ } else resolve();
107
+ };
108
+ const timer = setTimeout(() => finish(new Error("payment sdk: timeout")), options.timeoutMs);
109
+ script.onload = () => finish();
110
+ script.onerror = () => finish(new Error("payment sdk: failed to load"));
111
+ doc.head.appendChild(script);
112
+ });
113
+ pending.set(key, p);
114
+ return p;
115
+ }
116
+ function resetPaymentSdkCache() {
117
+ pending.clear();
118
+ }
119
+
120
+ // src/payments/mercadopago.ts
121
+ var MERCADOPAGO_BRICKS_KIND = "mercadopago_bricks";
122
+ function sanitizeMercadoPagoFormData(data) {
123
+ const d = data ?? {};
124
+ const str = (v, max = 256) => typeof v === "string" && v.length > 0 && v.length <= max ? v : void 0;
125
+ const out = {};
126
+ const token = str(d.token, 128);
127
+ if (token) out.token = token;
128
+ const pm = str(d.payment_method_id, 64);
129
+ if (pm) out.payment_method_id = pm;
130
+ const issuer = typeof d.issuer_id === "number" ? String(d.issuer_id) : str(d.issuer_id, 32);
131
+ if (issuer) out.issuer_id = issuer;
132
+ const inst = typeof d.installments === "number" ? d.installments : Number.parseInt(String(d.installments ?? ""), 10);
133
+ if (Number.isInteger(inst) && inst >= 1 && inst <= 48) out.installments = inst;
134
+ const payer = d.payer;
135
+ if (payer && typeof payer === "object") {
136
+ const p = {};
137
+ const email = str(payer.email, 254);
138
+ if (email) p.email = email;
139
+ const idType = str(payer.identification?.type, 16);
140
+ const idNumber = str(payer.identification?.number, 32);
141
+ if (idType && idNumber) p.identification = { type: idType, number: idNumber };
142
+ if (Object.keys(p).length) out.payer = p;
143
+ }
144
+ return out;
145
+ }
146
+ function mercadoPagoBricksAdapter(options = {}) {
147
+ const allowed = options.allowedSdkOrigins ?? ["https://sdk.mercadopago.com"];
148
+ const getSdk = options.getSdk ?? (() => typeof window !== "undefined" ? window.MercadoPago : void 0);
149
+ const getDevice = options.getDeviceSessionId ?? (() => typeof window !== "undefined" ? window.MP_DEVICE_SESSION_ID : void 0);
150
+ return {
151
+ kind: MERCADOPAGO_BRICKS_KIND,
152
+ async load(client, { nonce, timeoutMs }) {
153
+ await loadPaymentSdk(client.sdk_url, { ...nonce ? { nonce } : {}, timeoutMs, allowedOrigins: allowed, isLoaded: () => !!getSdk() });
154
+ if (!getSdk()) throw new Error("payment sdk: MercadoPago global missing after load");
155
+ },
156
+ async mount(opts) {
157
+ const Ctor = getSdk();
158
+ if (!Ctor) throw new Error("payment sdk: not loaded");
159
+ const { client, container } = opts;
160
+ const card = client.options?.find((o) => o.key === "card");
161
+ const mp = new Ctor(client.public_key, { locale: opts.locale ?? client.locale ?? "es-MX" });
162
+ const amount = Number.parseFloat(opts.amount.amount);
163
+ const controller = await mp.bricks().create("cardPayment", container.id, {
164
+ initialization: { amount, ...opts.email ? { payer: { email: opts.email } } : {} },
165
+ customization: {
166
+ paymentMethods: { minInstallments: 1, ...card?.max_installments ? { maxInstallments: card.max_installments } : {} },
167
+ visual: {
168
+ hidePaymentButton: true,
169
+ style: { theme: "default", ...opts.theme && Object.keys(opts.theme).length ? { customVariables: opts.theme } : {} }
170
+ }
171
+ },
172
+ callbacks: {
173
+ onReady: () => opts.onReady(),
174
+ onError: (e) => {
175
+ opts.onError({ critical: e?.type === "critical", ...e?.cause ? { message: String(e.cause) } : {} });
176
+ },
177
+ onSubmit: async () => void 0
178
+ }
179
+ });
180
+ let gone = false;
181
+ return {
182
+ async collect() {
183
+ if (gone) throw new Error("payment form is unmounted");
184
+ const raw = await controller.getFormData();
185
+ const provider_data = sanitizeMercadoPagoFormData(raw);
186
+ if (!provider_data.token) throw new Error("payment form incomplete");
187
+ const device = getDevice();
188
+ return { provider_data, ...typeof device === "string" && device ? { device_session_id: device } : {} };
189
+ },
190
+ async unmount() {
191
+ gone = true;
192
+ try {
193
+ await controller.unmount();
194
+ } catch {
195
+ }
196
+ }
197
+ };
198
+ }
199
+ };
200
+ }
201
+ function isSafeCssValue(value) {
202
+ if (!value || value.length > 200) return false;
203
+ if (/[;{}<>\\]/.test(value)) return false;
204
+ if (/url\s*\(|expression\s*\(|@import/i.test(value)) return false;
205
+ return true;
206
+ }
207
+
208
+ // src/payments/theme.ts
209
+ var PAYMENT_THEME_TOKENS = {
210
+ "--sf-pay-text": "textPrimaryColor",
211
+ "--sf-pay-text-muted": "textSecondaryColor",
212
+ "--sf-pay-background": "formBackgroundColor",
213
+ "--sf-pay-input-background": "inputBackgroundColor",
214
+ "--sf-pay-primary": "baseColor",
215
+ "--sf-pay-primary-contrast": "buttonTextColor",
216
+ "--sf-pay-error": "errorColor",
217
+ "--sf-pay-success": "successColor",
218
+ "--sf-pay-outline": "outlinePrimaryColor",
219
+ "--sf-pay-radius": "borderRadiusMedium"
220
+ };
221
+ function readPaymentTheme(el, tokens = PAYMENT_THEME_TOKENS) {
222
+ const out = {};
223
+ if (!el || typeof getComputedStyle !== "function") return out;
224
+ const style = getComputedStyle(el);
225
+ for (const [cssVar, key] of Object.entries(tokens)) {
226
+ const value = style.getPropertyValue(cssVar).trim();
227
+ if (value && isSafeCssValue(value)) out[key] = value;
228
+ }
229
+ return out;
230
+ }
231
+
232
+ // src/payments/use-embedded-payment.ts
233
+ var DEFAULT_ADAPTERS = {};
234
+ function defaultAdapterFor(kind) {
235
+ if (kind === "mercadopago_bricks") return DEFAULT_ADAPTERS[kind] ??= mercadoPagoBricksAdapter();
236
+ return void 0;
237
+ }
238
+ function safeStorage() {
239
+ try {
240
+ return typeof sessionStorage !== "undefined" ? sessionStorage : null;
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
246
+ function useEmbeddedPayment(options) {
247
+ const {
248
+ client,
249
+ amount,
250
+ transport,
251
+ enabled = true,
252
+ locale,
253
+ sdkTimeoutMs = 8e3,
254
+ autoRetries = 2,
255
+ retryDelayMs = 600,
256
+ pollIntervalMs = 2e3,
257
+ pollTimeoutMs = 6e4,
258
+ scope = "default"
259
+ } = options;
260
+ const msgs = react.useMemo(() => storefront.paymentMessages(locale, options.messages), [locale, options.messages]);
261
+ const rawId = react.useId();
262
+ const containerId = react.useMemo(() => `sf-pay-${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`, [rawId]);
263
+ const [status, setStatus] = react.useState("idle");
264
+ const [outcome, setOutcome] = react.useState(null);
265
+ const [error, setError] = react.useState(null);
266
+ const [fallbackReason, setFallbackReason] = react.useState(null);
267
+ const [attemptsLeft, setAttemptsLeft] = react.useState(null);
268
+ const [attemptId, setAttemptId] = react.useState(null);
269
+ const [container, setContainer] = react.useState(null);
270
+ const [mountKey, setMountKey] = react.useState(0);
271
+ const live = react.useRef({ ...options, msgs });
272
+ live.current = { ...options, msgs };
273
+ const form = react.useRef(null);
274
+ const inFlight = react.useRef(null);
275
+ const pendingData = react.useRef(null);
276
+ const alive = react.useRef(true);
277
+ const generation = react.useRef(0);
278
+ const statusRef = react.useRef("idle");
279
+ statusRef.current = status;
280
+ react.useEffect(() => {
281
+ alive.current = true;
282
+ return () => {
283
+ alive.current = false;
284
+ };
285
+ }, []);
286
+ const storageKey = `sf:pay:attempt:${scope}`;
287
+ const store = options.storage === void 0 ? safeStorage() : options.storage;
288
+ const emit = react.useCallback((e) => live.current.onEvent?.(e), []);
289
+ const readAttempt = react.useCallback(() => {
290
+ try {
291
+ const saved = store?.getItem(storageKey);
292
+ if (saved) return saved;
293
+ } catch {
294
+ }
295
+ const fresh = (live.current.newAttemptId ?? storefront.generateIdempotencyKey)();
296
+ try {
297
+ store?.setItem(storageKey, fresh);
298
+ } catch {
299
+ }
300
+ return fresh;
301
+ }, [store, storageKey]);
302
+ const rotateAttempt = react.useCallback(() => {
303
+ try {
304
+ store?.removeItem(storageKey);
305
+ } catch {
306
+ }
307
+ setAttemptId(null);
308
+ }, [store, storageKey]);
309
+ const fallback = react.useCallback(
310
+ (reason) => {
311
+ setFallbackReason(reason);
312
+ setStatus("fallback");
313
+ emit({ type: "form_failed", reason });
314
+ live.current.onFallback?.(reason);
315
+ },
316
+ [emit]
317
+ );
318
+ const amountKey = `${amount.amount}|${amount.currency_code}`;
319
+ const kind = client?.kind;
320
+ const publicKey = client?.public_key;
321
+ react.useEffect(() => {
322
+ if (!enabled || !client) {
323
+ setStatus((s) => s === "loading" || s === "ready" ? "idle" : s);
324
+ return;
325
+ }
326
+ const adapter = live.current.adapter ?? live.current.adapters?.[client.kind] ?? defaultAdapterFor(client.kind);
327
+ if (!adapter) {
328
+ fallback("unsupported_provider");
329
+ return;
330
+ }
331
+ if (!container) return;
332
+ const my = ++generation.current;
333
+ const started = Date.now();
334
+ let mounted = null;
335
+ setStatus("loading");
336
+ const stale = () => my !== generation.current || !alive.current;
337
+ const fail = (reason) => {
338
+ if (stale()) return;
339
+ generation.current++;
340
+ clearTimeout(timer);
341
+ const m = mounted ?? form.current;
342
+ mounted = null;
343
+ form.current = null;
344
+ if (m) void Promise.resolve(m.unmount());
345
+ fallback(reason);
346
+ };
347
+ const timer = setTimeout(() => {
348
+ if (statusRef.current !== "ready") fail("sdk_timeout");
349
+ }, sdkTimeoutMs);
350
+ container.id = containerId;
351
+ (async () => {
352
+ try {
353
+ await adapter.load(client, { ...live.current.nonce ? { nonce: live.current.nonce } : {}, timeoutMs: sdkTimeoutMs });
354
+ if (stale()) return;
355
+ const theme = live.current.theme ?? readPaymentTheme(container);
356
+ const m = await adapter.mount({
357
+ container,
358
+ client,
359
+ amount: live.current.amount,
360
+ ...live.current.email ? { email: live.current.email } : {},
361
+ ...live.current.locale ? { locale: live.current.locale } : {},
362
+ theme,
363
+ onReady: () => {
364
+ if (stale()) return;
365
+ clearTimeout(timer);
366
+ setStatus((s) => s === "loading" ? "ready" : s);
367
+ emit({ type: "form_ready", ms: Date.now() - started });
368
+ },
369
+ onError: (e) => {
370
+ if (stale()) return;
371
+ if (e.critical) fail("form_error");
372
+ }
373
+ });
374
+ if (stale()) {
375
+ void Promise.resolve(m.unmount());
376
+ return;
377
+ }
378
+ mounted = m;
379
+ form.current = m;
380
+ } catch (err) {
381
+ fail(err instanceof Error && /timeout/i.test(err.message) ? "sdk_timeout" : "sdk_error");
382
+ }
383
+ })();
384
+ return () => {
385
+ clearTimeout(timer);
386
+ generation.current++;
387
+ const m = mounted ?? form.current;
388
+ mounted = null;
389
+ form.current = null;
390
+ if (m) void Promise.resolve(m.unmount());
391
+ };
392
+ }, [enabled, container, kind, publicKey, amountKey, mountKey, sdkTimeoutMs, containerId]);
393
+ const finish = react.useCallback(
394
+ (result) => {
395
+ const o = storefront.paymentOutcome(result, live.current.locale, live.current.messages);
396
+ pendingData.current = null;
397
+ setOutcome(o);
398
+ setError(null);
399
+ setAttemptsLeft(o.attempts_left);
400
+ setStatus(o.kind);
401
+ emit({ type: "result", status: o.kind, decline_code: o.decline_code });
402
+ if (o.kind === "approved") rotateAttempt();
403
+ if (o.kind === "rejected" || o.kind === "cancelled") {
404
+ rotateAttempt();
405
+ setMountKey((k) => k + 1);
406
+ }
407
+ live.current.onResult?.(o, result);
408
+ if (o.kind === "redirect" && o.redirect_url) {
409
+ if (live.current.onRedirect) live.current.onRedirect(o.redirect_url);
410
+ else if (typeof window !== "undefined") window.location.assign(o.redirect_url);
411
+ }
412
+ },
413
+ [emit, rotateAttempt]
414
+ );
415
+ const send = react.useCallback(
416
+ async (data, attempt) => {
417
+ const ctl = new AbortController();
418
+ const maxRetries = live.current.autoRetries ?? autoRetries;
419
+ for (let n = 0; ; n++) {
420
+ try {
421
+ const result = await transport.submit(
422
+ {
423
+ option: "card",
424
+ attempt,
425
+ idempotencyKey: attempt,
426
+ provider_data: data.provider_data,
427
+ ...data.device_session_id ? { device_session_id: data.device_session_id } : {},
428
+ expected_total: live.current.amount
429
+ },
430
+ { signal: ctl.signal }
431
+ );
432
+ if (!alive.current) return;
433
+ finish(result);
434
+ return;
435
+ } catch (err) {
436
+ if (!alive.current) return;
437
+ const view = storefront.paymentErrorView(err, live.current.locale, live.current.messages);
438
+ if (view.retrySameAttempt && n < maxRetries) {
439
+ await sleep((live.current.retryDelayMs ?? retryDelayMs) * 2 ** n);
440
+ if (!alive.current) return;
441
+ continue;
442
+ }
443
+ emit({ type: "error", kind: view.kind });
444
+ setError(view);
445
+ if (view.kind === "network") {
446
+ pendingData.current = data;
447
+ setStatus("network_error");
448
+ } else if (view.kind === "price_mismatch") {
449
+ pendingData.current = null;
450
+ rotateAttempt();
451
+ setMountKey((k) => k + 1);
452
+ setStatus("ready");
453
+ } else if (view.kind === "generic") {
454
+ pendingData.current = null;
455
+ rotateAttempt();
456
+ setMountKey((k) => k + 1);
457
+ setStatus("ready");
458
+ } else {
459
+ pendingData.current = null;
460
+ setStatus("blocked");
461
+ }
462
+ return;
463
+ }
464
+ }
465
+ },
466
+ [autoRetries, emit, finish, retryDelayMs, rotateAttempt, transport]
467
+ );
468
+ const submit = react.useCallback(() => {
469
+ if (inFlight.current) return inFlight.current;
470
+ const m = form.current;
471
+ if (!m || statusRef.current !== "ready" && statusRef.current !== "network_error") return Promise.resolve();
472
+ const run = (async () => {
473
+ setStatus("processing");
474
+ setError(null);
475
+ setOutcome(null);
476
+ emit({ type: "submit" });
477
+ let data;
478
+ try {
479
+ data = await m.collect();
480
+ } catch {
481
+ emit({ type: "tokenize_error" });
482
+ setError({ kind: "generic", message: live.current.msgs["error.card_data"], retrySameAttempt: false, canRetry: true });
483
+ setStatus("ready");
484
+ return;
485
+ }
486
+ const attempt = readAttempt();
487
+ setAttemptId(attempt);
488
+ await send(data, attempt);
489
+ })().finally(() => {
490
+ inFlight.current = null;
491
+ });
492
+ inFlight.current = run;
493
+ return run;
494
+ }, [emit, readAttempt, send]);
495
+ const retry = react.useCallback(() => {
496
+ if (inFlight.current) return inFlight.current;
497
+ const data = pendingData.current;
498
+ if (!data) return submit();
499
+ emit({ type: "retry" });
500
+ const run = (async () => {
501
+ setStatus("processing");
502
+ setError(null);
503
+ const attempt = readAttempt();
504
+ setAttemptId(attempt);
505
+ await send(data, attempt);
506
+ })().finally(() => {
507
+ inFlight.current = null;
508
+ });
509
+ inFlight.current = run;
510
+ return run;
511
+ }, [emit, readAttempt, send, submit]);
512
+ react.useEffect(() => {
513
+ if (status !== "challenge" && status !== "in_review" || !transport.refresh) return;
514
+ let stop = false;
515
+ const ctl = new AbortController();
516
+ const until = Date.now() + pollTimeoutMs;
517
+ (async () => {
518
+ while (!stop && Date.now() < until) {
519
+ await sleep(pollIntervalMs);
520
+ if (stop || !alive.current) return;
521
+ try {
522
+ const r = await transport.refresh({ signal: ctl.signal });
523
+ if (stop || !alive.current) return;
524
+ const o = storefront.paymentOutcome(r, live.current.locale, live.current.messages);
525
+ if (o.kind !== status) {
526
+ finish(r);
527
+ return;
528
+ }
529
+ } catch {
530
+ }
531
+ }
532
+ })();
533
+ return () => {
534
+ stop = true;
535
+ ctl.abort();
536
+ };
537
+ }, [status, transport, pollIntervalMs, pollTimeoutMs, finish]);
538
+ const dismiss = react.useCallback(() => {
539
+ setError(null);
540
+ setOutcome((o) => o?.kind === "rejected" ? null : o);
541
+ }, []);
542
+ const message = status === "processing" ? msgs["state.processing"] : status === "fallback" ? msgs["state.fallback"] : error?.message ?? outcome?.message ?? (status === "loading" ? msgs["state.loading"] : null);
543
+ return {
544
+ status,
545
+ message,
546
+ outcome,
547
+ error,
548
+ fallbackReason,
549
+ attemptsLeft,
550
+ attemptId,
551
+ busy: status === "loading" || status === "processing",
552
+ containerRef: setContainer,
553
+ containerId,
554
+ submit,
555
+ retry,
556
+ dismiss
557
+ };
558
+ }
559
+ function VoucherCard({ outcome, locale, messages, className }) {
560
+ const v = outcome.voucher;
561
+ if (!v) return null;
562
+ const m = storefront.paymentMessages(locale, messages);
563
+ const expiry = storefront.formatVoucherExpiry(v.expires_at, locale, messages);
564
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className, "data-sf-voucher": true, "aria-labelledby": "sf-voucher-title", children: [
565
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { id: "sf-voucher-title", children: m["voucher.title"] }),
566
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { children: [
567
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
568
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: m["voucher.point"] }),
569
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: v.payment_point })
570
+ ] }),
571
+ v.amount && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
572
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: m["voucher.amount"] }),
573
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: storefront.formatMoney(v.amount, locale) })
574
+ ] }),
575
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
576
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: m["voucher.reference"] }),
577
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { "data-sf-voucher-reference": true, children: v.reference })
578
+ ] }),
579
+ v.clabe && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
580
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: m["voucher.clabe"] }),
581
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: v.clabe })
582
+ ] }),
583
+ v.barcode && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
584
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: m["voucher.barcode"] }),
585
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: v.barcode })
586
+ ] })
587
+ ] }),
588
+ expiry && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-sf-voucher-expiry": true, children: expiry }),
589
+ v.voucher_url && /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: v.voucher_url, target: "_blank", rel: "noopener noreferrer", children: m["voucher.open"] }) }),
590
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["voucher.email"] })
591
+ ] });
592
+ }
593
+ function PaymentChallenge({ outcome, locale, messages, render, onCancel }) {
594
+ const m = storefront.paymentMessages(locale, messages);
595
+ const { ref } = useModalDialog({ open: true, onClose: () => onCancel?.() });
596
+ const url = outcome.challenge?.url;
597
+ return /* @__PURE__ */ jsxRuntime.jsx("dialog", { ref, "aria-labelledby": "sf-challenge-title", "data-sf-challenge": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
598
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "sf-challenge-title", "data-sf-autofocus": true, tabIndex: -1, children: m["challenge.title"] }),
599
+ render ? render(outcome) : url ? /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: url, title: m["challenge.title"], style: { width: "100%", minHeight: 420, border: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["state.challenge"] }),
600
+ onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onCancel, style: { minWidth: 44, minHeight: 44 }, children: m["action.cancel"] })
601
+ ] }) });
602
+ }
603
+ var PaymentBrick = react.forwardRef(function PaymentBrick2(props, ref) {
604
+ const { submitLabel, hideAmountInButton, renderVoucher, renderChallenge, minHeight = 360, className, style, ...hookOptions } = props;
605
+ const locale = useResolvedLocale(props.locale);
606
+ const state = useEmbeddedPayment({ ...hookOptions, locale });
607
+ react.useImperativeHandle(ref, () => state);
608
+ const m = storefront.paymentMessages(locale, props.messages);
609
+ const alertRef = react.useRef(null);
610
+ const isError = !!state.error || state.outcome?.kind === "rejected" || state.status === "blocked";
611
+ react.useEffect(() => {
612
+ if (isError) alertRef.current?.focus();
613
+ }, [isError, state.message]);
614
+ if (state.status === "fallback") {
615
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, "data-sf-pay-status": "fallback", children: /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", "data-sf-pay-fallback": true, children: m["state.fallback"] }) });
616
+ }
617
+ const outcome = state.outcome;
618
+ const showForm = state.status !== "approved" && state.status !== "voucher" && state.status !== "in_review" && state.status !== "redirect" && state.status !== "expired";
619
+ const busy = state.busy;
620
+ const total = storefront.formatMoney(props.amount, locale);
621
+ const label = state.status === "processing" ? m["state.processing"] : submitLabel ?? m["action.pay"];
622
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style, "data-sf-pay-status": state.status, children: [
623
+ showForm && /* @__PURE__ */ jsxRuntime.jsx(
624
+ "div",
625
+ {
626
+ ref: state.containerRef,
627
+ role: "group",
628
+ "aria-label": m["form.label"],
629
+ "aria-busy": state.status === "loading" || state.status === "processing",
630
+ "data-sf-pay-container": true,
631
+ style: { minHeight: `var(--sf-pay-min-height, ${typeof minHeight === "number" ? `${minHeight}px` : minHeight})` }
632
+ }
633
+ ),
634
+ isError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: alertRef, role: "alert", tabIndex: -1, "data-sf-pay-alert": true, "data-sf-pay-code": state.error?.kind ?? outcome?.decline_code ?? void 0, children: [
635
+ state.message,
636
+ state.status === "network_error" && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void state.retry(), style: { minHeight: 44 }, children: m["action.retry"] })
637
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", "aria-live": "polite", "data-sf-pay-message": true, children: state.message }),
638
+ outcome?.kind === "voucher" && (renderVoucher ? renderVoucher(outcome) : /* @__PURE__ */ jsxRuntime.jsx(VoucherCard, { outcome, locale, ...props.messages ? { messages: props.messages } : {} })),
639
+ outcome?.kind === "challenge" && /* @__PURE__ */ jsxRuntime.jsx(PaymentChallenge, { outcome, locale, ...props.messages ? { messages: props.messages } : {}, ...renderChallenge ? { render: renderChallenge } : {} }),
640
+ submitLabel !== void 0 && showForm && /* @__PURE__ */ jsxRuntime.jsxs(
641
+ "button",
642
+ {
643
+ type: "button",
644
+ onClick: () => void state.submit(),
645
+ "aria-busy": busy,
646
+ "aria-disabled": state.status !== "ready",
647
+ "data-sf-pay-submit": true,
648
+ style: { minHeight: 44 },
649
+ children: [
650
+ label,
651
+ !hideAmountInButton && state.status !== "processing" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
652
+ " \xB7 ",
653
+ total
654
+ ] })
655
+ ]
656
+ }
657
+ )
658
+ ] });
659
+ });
660
+
661
+ // src/payments/adapter.ts
662
+ var PaymentHttpError = class extends Error {
663
+ constructor(status, code, message, details) {
664
+ super(message ?? code);
665
+ this.status = status;
666
+ this.code = code;
667
+ this.details = details;
668
+ }
669
+ status;
670
+ code;
671
+ details;
672
+ name = "PaymentHttpError";
673
+ };
674
+ async function parse(res) {
675
+ const text = await res.text();
676
+ if (!text) return null;
677
+ try {
678
+ return JSON.parse(text);
679
+ } catch {
680
+ return null;
681
+ }
682
+ }
683
+ function createFetchTransport(url, options = {}) {
684
+ const doFetch = (u, init) => (options.fetch ?? globalThis.fetch.bind(globalThis))(u, init);
685
+ async function call(u, init) {
686
+ const res = await doFetch(u, { credentials: options.credentials ?? "same-origin", ...init });
687
+ const body = await parse(res);
688
+ if (!res.ok) {
689
+ const e = body?.error;
690
+ throw new PaymentHttpError(res.status, e?.code ?? "server_error", e?.message, e?.details);
691
+ }
692
+ const result = body;
693
+ if (!result || typeof result !== "object" || !result.payment || typeof result.payment.status !== "string") {
694
+ throw new PaymentHttpError(502, "invalid_response", "Unexpected payment response");
695
+ }
696
+ return result;
697
+ }
698
+ return {
699
+ submit: (request, { signal }) => call(url, {
700
+ method: "POST",
701
+ signal,
702
+ headers: { "Content-Type": "application/json", Accept: "application/json", "Idempotency-Key": request.idempotencyKey, ...options.headers },
703
+ body: JSON.stringify(options.buildBody ? options.buildBody(request) : request)
704
+ }),
705
+ ...options.refreshUrl ? {
706
+ refresh: ({ signal }) => call(options.refreshUrl, { method: "POST", signal, headers: { Accept: "application/json", ...options.headers } })
707
+ } : {}
708
+ };
709
+ }
710
+
711
+ // src/payments/csp.ts
712
+ var ORIGIN = /^https:\/\/(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:\d{1,5})?$/i;
713
+ function origins(...lists) {
714
+ const out = /* @__PURE__ */ new Set();
715
+ for (const list of lists) for (const o of list ?? []) if (ORIGIN.test(o)) out.add(o);
716
+ return [...out];
717
+ }
718
+ function paymentCsp(clients, options = {}) {
719
+ const list = clients.filter((c) => !!c);
720
+ const pick = (k) => list.map((c) => c.csp?.[k]);
721
+ const nonce = options.nonce && /^[A-Za-z0-9+/=_-]+$/.test(options.nonce) ? options.nonce : void 0;
722
+ const sdkOrigins = list.flatMap((c) => {
723
+ try {
724
+ const u = new URL(c.sdk_url);
725
+ return u.protocol === "https:" ? [u.origin] : [];
726
+ } catch {
727
+ return [];
728
+ }
729
+ });
730
+ const dir = (name, values) => `${name} ${values.join(" ")}`;
731
+ const parts = [
732
+ "default-src 'self'",
733
+ dir("script-src", ["'self'", ...nonce ? [`'nonce-${nonce}'`] : [], ...origins(sdkOrigins, ...pick("script"), options.extra?.script)]),
734
+ dir("frame-src", origins(...pick("frame"), options.extra?.frame).length ? origins(...pick("frame"), options.extra?.frame) : ["'none'"]),
735
+ dir("connect-src", ["'self'", ...origins(...pick("connect"), options.extra?.connect)]),
736
+ dir("img-src", ["'self'", "data:", ...origins(...pick("img"), options.extra?.img)]),
737
+ "style-src 'self' 'unsafe-inline'",
738
+ dir("form-action", ["'self'", ...origins(...pick("form"), options.extra?.form)]),
739
+ "frame-ancestors 'none'",
740
+ "base-uri 'self'",
741
+ "object-src 'none'"
742
+ ];
743
+ if (options.reportUri && /^(https:\/\/|\/)[^\s;,]+$/.test(options.reportUri)) parts.push(`report-uri ${options.reportUri}`);
744
+ return parts.join("; ");
745
+ }
746
+ function paymentCspHeaderName(reportOnly) {
747
+ return reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy";
748
+ }
749
+
750
+ exports.MERCADOPAGO_BRICKS_KIND = MERCADOPAGO_BRICKS_KIND;
751
+ exports.PAYMENT_THEME_TOKENS = PAYMENT_THEME_TOKENS;
752
+ exports.PaymentBrick = PaymentBrick;
753
+ exports.PaymentChallenge = PaymentChallenge;
754
+ exports.PaymentHttpError = PaymentHttpError;
755
+ exports.VoucherCard = VoucherCard;
756
+ exports.createFetchTransport = createFetchTransport;
757
+ exports.loadPaymentSdk = loadPaymentSdk;
758
+ exports.mercadoPagoBricksAdapter = mercadoPagoBricksAdapter;
759
+ exports.paymentCsp = paymentCsp;
760
+ exports.paymentCspHeaderName = paymentCspHeaderName;
761
+ exports.readPaymentTheme = readPaymentTheme;
762
+ exports.resetPaymentSdkCache = resetPaymentSdkCache;
763
+ exports.sanitizeMercadoPagoFormData = sanitizeMercadoPagoFormData;
764
+ exports.useEmbeddedPayment = useEmbeddedPayment;
765
+ //# sourceMappingURL=payments.cjs.map
766
+ //# sourceMappingURL=payments.cjs.map