@asteby/storefront-react 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -1
- package/dist/checkout.cjs +108 -20
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +15 -1
- package/dist/checkout.d.ts +15 -1
- package/dist/checkout.js +107 -21
- package/dist/checkout.js.map +1 -1
- package/dist/{chunk-ISEOCKIR.js → chunk-MNLROALF.js} +96 -8
- package/dist/chunk-MNLROALF.js.map +1 -0
- package/dist/{choices-DEPE9KRq.d.cts → csp-DZPuExzu.d.cts} +93 -55
- package/dist/{choices-DEPE9KRq.d.ts → csp-DZPuExzu.d.ts} +93 -55
- package/dist/payments-server.cjs +159 -12
- package/dist/payments-server.cjs.map +1 -1
- package/dist/payments-server.d.cts +23 -3
- package/dist/payments-server.d.ts +23 -3
- package/dist/payments-server.js +70 -13
- package/dist/payments-server.js.map +1 -1
- package/dist/payments.cjs +784 -160
- package/dist/payments.cjs.map +1 -1
- package/dist/payments.css +84 -0
- package/dist/payments.d.cts +227 -8
- package/dist/payments.d.ts +227 -8
- package/dist/payments.js +628 -102
- package/dist/payments.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-ISEOCKIR.js.map +0 -1
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
|
-
|
|
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",
|
|
@@ -210,7 +485,9 @@ function useEmbeddedPayment(options) {
|
|
|
210
485
|
autoRetries = 2,
|
|
211
486
|
retryDelayMs = 600,
|
|
212
487
|
pollIntervalMs = 2e3,
|
|
213
|
-
pollTimeoutMs =
|
|
488
|
+
pollTimeoutMs = 3e5,
|
|
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,28 @@ 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");
|
|
517
|
+
const leftChallenge = useRef(false);
|
|
235
518
|
statusRef.current = status;
|
|
519
|
+
errorRef.current = error;
|
|
520
|
+
const amountKey = `${amount.amount}|${amount.currency_code}`;
|
|
521
|
+
const amountKeyRef = useRef(amountKey);
|
|
522
|
+
amountKeyRef.current = amountKey;
|
|
523
|
+
const appliedAmount = useRef(null);
|
|
524
|
+
const updating = useRef(null);
|
|
525
|
+
const remountRequested = useRef(false);
|
|
236
526
|
useEffect(() => {
|
|
237
527
|
alive.current = true;
|
|
238
528
|
return () => {
|
|
@@ -243,19 +533,33 @@ function useEmbeddedPayment(options) {
|
|
|
243
533
|
const store = options.storage === void 0 ? safeStorage() : options.storage;
|
|
244
534
|
const emit = useCallback((e) => live.current.onEvent?.(e), []);
|
|
245
535
|
const readAttempt = useCallback(() => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
536
|
+
if (!memOnly.current) {
|
|
537
|
+
try {
|
|
538
|
+
const saved = store?.getItem(storageKey);
|
|
539
|
+
if (saved) {
|
|
540
|
+
memAttempt.current = saved;
|
|
541
|
+
return saved;
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
memOnly.current = true;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (memOnly.current && memAttempt.current) return memAttempt.current;
|
|
548
|
+
if (!store) {
|
|
549
|
+
if (memAttempt.current) return memAttempt.current;
|
|
250
550
|
}
|
|
251
551
|
const fresh = (live.current.newAttemptId ?? generateIdempotencyKey)();
|
|
552
|
+
memAttempt.current = fresh;
|
|
252
553
|
try {
|
|
253
554
|
store?.setItem(storageKey, fresh);
|
|
555
|
+
if (store && store.getItem(storageKey) !== fresh) memOnly.current = true;
|
|
254
556
|
} catch {
|
|
557
|
+
memOnly.current = true;
|
|
255
558
|
}
|
|
256
559
|
return fresh;
|
|
257
560
|
}, [store, storageKey]);
|
|
258
561
|
const rotateAttempt = useCallback(() => {
|
|
562
|
+
memAttempt.current = null;
|
|
259
563
|
try {
|
|
260
564
|
store?.removeItem(storageKey);
|
|
261
565
|
} catch {
|
|
@@ -271,7 +575,32 @@ function useEmbeddedPayment(options) {
|
|
|
271
575
|
},
|
|
272
576
|
[emit]
|
|
273
577
|
);
|
|
274
|
-
const
|
|
578
|
+
const syncAmount = useCallback(() => {
|
|
579
|
+
const m = form.current;
|
|
580
|
+
if (!m || inFlight.current || updating.current || remountRequested.current) return;
|
|
581
|
+
if (statusRef.current === "network_error" || statusRef.current === "blocked" && errorRef.current?.kind === "payment_in_progress") return;
|
|
582
|
+
if (appliedAmount.current === amountKeyRef.current) return;
|
|
583
|
+
const run = (async () => {
|
|
584
|
+
while (form.current === m && alive.current && appliedAmount.current !== amountKeyRef.current) {
|
|
585
|
+
const key = amountKeyRef.current;
|
|
586
|
+
try {
|
|
587
|
+
if (!m.update) throw new Error("payment form: update not supported");
|
|
588
|
+
await m.update({ amount: live.current.amount });
|
|
589
|
+
appliedAmount.current = key;
|
|
590
|
+
} catch {
|
|
591
|
+
if (form.current === m && alive.current) {
|
|
592
|
+
remountRequested.current = true;
|
|
593
|
+
setMountKey((k) => k + 1);
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
})().finally(() => {
|
|
599
|
+
updating.current = null;
|
|
600
|
+
if (alive.current) syncAmount();
|
|
601
|
+
});
|
|
602
|
+
updating.current = run;
|
|
603
|
+
}, []);
|
|
275
604
|
const kind = client?.kind;
|
|
276
605
|
const publicKey = client?.public_key;
|
|
277
606
|
useEffect(() => {
|
|
@@ -285,18 +614,22 @@ function useEmbeddedPayment(options) {
|
|
|
285
614
|
return;
|
|
286
615
|
}
|
|
287
616
|
if (!container) return;
|
|
617
|
+
remountRequested.current = false;
|
|
288
618
|
const my = ++generation.current;
|
|
289
619
|
const started = Date.now();
|
|
620
|
+
const abort = new AbortController();
|
|
290
621
|
let mounted = null;
|
|
291
622
|
setStatus("loading");
|
|
292
623
|
const stale = () => my !== generation.current || !alive.current;
|
|
293
624
|
const fail = (reason) => {
|
|
294
625
|
if (stale()) return;
|
|
295
626
|
generation.current++;
|
|
627
|
+
abort.abort();
|
|
296
628
|
clearTimeout(timer);
|
|
297
629
|
const m = mounted ?? form.current;
|
|
298
630
|
mounted = null;
|
|
299
631
|
form.current = null;
|
|
632
|
+
appliedAmount.current = null;
|
|
300
633
|
if (m) void Promise.resolve(m.unmount());
|
|
301
634
|
fallback(reason);
|
|
302
635
|
};
|
|
@@ -310,31 +643,39 @@ function useEmbeddedPayment(options) {
|
|
|
310
643
|
if (stale()) return;
|
|
311
644
|
const theme = live.current.theme ?? readPaymentTheme(container);
|
|
312
645
|
const colorScheme = live.current.colorScheme ?? readPaymentScheme(container);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
646
|
+
const createdWith = amountKeyRef.current;
|
|
647
|
+
const m = await mountQueued(
|
|
648
|
+
adapter,
|
|
649
|
+
{
|
|
650
|
+
container,
|
|
651
|
+
client,
|
|
652
|
+
amount: live.current.amount,
|
|
653
|
+
...live.current.email ? { email: live.current.email } : {},
|
|
654
|
+
...live.current.locale ? { locale: live.current.locale } : {},
|
|
655
|
+
theme,
|
|
656
|
+
...colorScheme ? { colorScheme } : {},
|
|
657
|
+
signal: abort.signal,
|
|
658
|
+
onReady: () => {
|
|
659
|
+
if (stale()) return;
|
|
660
|
+
clearTimeout(timer);
|
|
661
|
+
setStatus((s) => s === "loading" ? "ready" : s);
|
|
662
|
+
emit({ type: "form_ready", ms: Date.now() - started });
|
|
663
|
+
},
|
|
664
|
+
onError: (e) => {
|
|
665
|
+
if (stale()) return;
|
|
666
|
+
if (e.critical) fail("form_error");
|
|
667
|
+
}
|
|
326
668
|
},
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (e.critical) fail("form_error");
|
|
330
|
-
}
|
|
331
|
-
});
|
|
669
|
+
{ createTimeoutMs: live.current.createTimeoutMs ?? sdkTimeoutMs }
|
|
670
|
+
);
|
|
332
671
|
if (stale()) {
|
|
333
672
|
void Promise.resolve(m.unmount());
|
|
334
673
|
return;
|
|
335
674
|
}
|
|
336
675
|
mounted = m;
|
|
337
676
|
form.current = m;
|
|
677
|
+
appliedAmount.current = createdWith;
|
|
678
|
+
syncAmount();
|
|
338
679
|
} catch (err) {
|
|
339
680
|
fail(err instanceof Error && /timeout/i.test(err.message) ? "sdk_timeout" : "sdk_error");
|
|
340
681
|
}
|
|
@@ -342,16 +683,22 @@ function useEmbeddedPayment(options) {
|
|
|
342
683
|
return () => {
|
|
343
684
|
clearTimeout(timer);
|
|
344
685
|
generation.current++;
|
|
686
|
+
abort.abort();
|
|
345
687
|
const m = mounted ?? form.current;
|
|
346
688
|
mounted = null;
|
|
347
689
|
form.current = null;
|
|
690
|
+
appliedAmount.current = null;
|
|
348
691
|
if (m) void Promise.resolve(m.unmount());
|
|
349
692
|
};
|
|
350
|
-
}, [enabled, container, kind, publicKey,
|
|
693
|
+
}, [enabled, container, kind, publicKey, mountKey, sdkTimeoutMs, containerId]);
|
|
694
|
+
useEffect(() => {
|
|
695
|
+
syncAmount();
|
|
696
|
+
}, [amountKey, syncAmount]);
|
|
351
697
|
const finish = useCallback(
|
|
352
698
|
(result) => {
|
|
353
699
|
const o = paymentOutcome(result, live.current.locale, live.current.messages);
|
|
354
700
|
pendingData.current = null;
|
|
701
|
+
leftChallenge.current = false;
|
|
355
702
|
setOutcome(o);
|
|
356
703
|
setError(null);
|
|
357
704
|
setAttemptsLeft(o.attempts_left);
|
|
@@ -371,7 +718,7 @@ function useEmbeddedPayment(options) {
|
|
|
371
718
|
[emit, rotateAttempt]
|
|
372
719
|
);
|
|
373
720
|
const send = useCallback(
|
|
374
|
-
async (data, attempt) => {
|
|
721
|
+
async (data, attempt, total) => {
|
|
375
722
|
const ctl = new AbortController();
|
|
376
723
|
const maxRetries = live.current.autoRetries ?? autoRetries;
|
|
377
724
|
for (let n = 0; ; n++) {
|
|
@@ -383,7 +730,7 @@ function useEmbeddedPayment(options) {
|
|
|
383
730
|
idempotencyKey: attempt,
|
|
384
731
|
provider_data: data.provider_data,
|
|
385
732
|
...data.device_session_id ? { device_session_id: data.device_session_id } : {},
|
|
386
|
-
expected_total:
|
|
733
|
+
expected_total: total
|
|
387
734
|
},
|
|
388
735
|
{ signal: ctl.signal }
|
|
389
736
|
);
|
|
@@ -401,7 +748,7 @@ function useEmbeddedPayment(options) {
|
|
|
401
748
|
emit({ type: "error", kind: view.kind });
|
|
402
749
|
setError(view);
|
|
403
750
|
if (view.kind === "network") {
|
|
404
|
-
pendingData.current = data;
|
|
751
|
+
pendingData.current = { data, total };
|
|
405
752
|
setStatus("network_error");
|
|
406
753
|
} else if (view.kind === "price_mismatch") {
|
|
407
754
|
pendingData.current = null;
|
|
@@ -427,7 +774,11 @@ function useEmbeddedPayment(options) {
|
|
|
427
774
|
if (inFlight.current) return inFlight.current;
|
|
428
775
|
const m = form.current;
|
|
429
776
|
if (!m || statusRef.current !== "ready" && statusRef.current !== "network_error") return Promise.resolve();
|
|
777
|
+
if (statusRef.current === "network_error" && pendingData.current) return retryRef.current();
|
|
778
|
+
const total = live.current.amount;
|
|
430
779
|
const run = (async () => {
|
|
780
|
+
if (updating.current) await updating.current.catch(() => {
|
|
781
|
+
});
|
|
431
782
|
setStatus("processing");
|
|
432
783
|
setError(null);
|
|
433
784
|
setOutcome(null);
|
|
@@ -443,30 +794,33 @@ function useEmbeddedPayment(options) {
|
|
|
443
794
|
}
|
|
444
795
|
const attempt = readAttempt();
|
|
445
796
|
setAttemptId(attempt);
|
|
446
|
-
await send(data, attempt);
|
|
797
|
+
await send(data, attempt, total);
|
|
447
798
|
})().finally(() => {
|
|
448
799
|
inFlight.current = null;
|
|
800
|
+
syncAmount();
|
|
449
801
|
});
|
|
450
802
|
inFlight.current = run;
|
|
451
803
|
return run;
|
|
452
|
-
}, [emit, readAttempt, send]);
|
|
804
|
+
}, [emit, readAttempt, send, syncAmount]);
|
|
453
805
|
const retry = useCallback(() => {
|
|
454
806
|
if (inFlight.current) return inFlight.current;
|
|
455
|
-
const
|
|
456
|
-
if (!
|
|
807
|
+
const pending2 = pendingData.current;
|
|
808
|
+
if (!pending2) return submit();
|
|
457
809
|
emit({ type: "retry" });
|
|
458
810
|
const run = (async () => {
|
|
459
811
|
setStatus("processing");
|
|
460
812
|
setError(null);
|
|
461
813
|
const attempt = readAttempt();
|
|
462
814
|
setAttemptId(attempt);
|
|
463
|
-
await send(data, attempt);
|
|
815
|
+
await send(pending2.data, attempt, pending2.total);
|
|
464
816
|
})().finally(() => {
|
|
465
817
|
inFlight.current = null;
|
|
818
|
+
syncAmount();
|
|
466
819
|
});
|
|
467
820
|
inFlight.current = run;
|
|
468
821
|
return run;
|
|
469
|
-
}, [emit, readAttempt, send, submit]);
|
|
822
|
+
}, [emit, readAttempt, send, submit, syncAmount]);
|
|
823
|
+
retryRef.current = retry;
|
|
470
824
|
useEffect(() => {
|
|
471
825
|
if (status !== "challenge" && status !== "in_review" || !transport.refresh) return;
|
|
472
826
|
let stop = false;
|
|
@@ -479,6 +833,7 @@ function useEmbeddedPayment(options) {
|
|
|
479
833
|
try {
|
|
480
834
|
const r = await transport.refresh({ signal: ctl.signal });
|
|
481
835
|
if (stop || !alive.current) return;
|
|
836
|
+
if (r.payment.status === "none") continue;
|
|
482
837
|
const o = paymentOutcome(r, live.current.locale, live.current.messages);
|
|
483
838
|
if (o.kind !== status) {
|
|
484
839
|
finish(r);
|
|
@@ -493,10 +848,57 @@ function useEmbeddedPayment(options) {
|
|
|
493
848
|
ctl.abort();
|
|
494
849
|
};
|
|
495
850
|
}, [status, transport, pollIntervalMs, pollTimeoutMs, finish]);
|
|
851
|
+
const unknownOutcome = status === "network_error" || status === "blocked" && error?.kind === "payment_in_progress";
|
|
852
|
+
useEffect(() => {
|
|
853
|
+
if (!unknownOutcome) syncAmount();
|
|
854
|
+
}, [unknownOutcome, syncAmount]);
|
|
855
|
+
useEffect(() => {
|
|
856
|
+
if (!unknownOutcome || !transport.refresh) {
|
|
857
|
+
setVerifying(false);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
let stop = false;
|
|
861
|
+
const ctl = new AbortController();
|
|
862
|
+
const until = Date.now() + (leftChallenge.current ? Math.max(verifyTimeoutMs, pollTimeoutMs) : verifyTimeoutMs);
|
|
863
|
+
setVerifying(true);
|
|
864
|
+
(async () => {
|
|
865
|
+
let wait = verifyIntervalMs;
|
|
866
|
+
while (!stop && Date.now() < until) {
|
|
867
|
+
await sleep(wait);
|
|
868
|
+
wait = Math.min(wait * 2, 15e3);
|
|
869
|
+
if (stop || !alive.current) return;
|
|
870
|
+
try {
|
|
871
|
+
const r = await transport.refresh({ signal: ctl.signal });
|
|
872
|
+
if (stop || !alive.current) return;
|
|
873
|
+
if (isResolvedAttempt(r)) {
|
|
874
|
+
const settled = r.payment.status === "paid" || r.payment.status === "failed" || r.payment.status === "expired" || r.payment.status === "cancelled";
|
|
875
|
+
if (leftChallenge.current && !settled && r.payment.action?.type === "challenge") continue;
|
|
876
|
+
setVerifying(false);
|
|
877
|
+
finish(r);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (!stop && alive.current) setVerifying(false);
|
|
884
|
+
})();
|
|
885
|
+
return () => {
|
|
886
|
+
stop = true;
|
|
887
|
+
ctl.abort();
|
|
888
|
+
setVerifying(false);
|
|
889
|
+
};
|
|
890
|
+
}, [unknownOutcome, transport, verifyTimeoutMs, verifyIntervalMs, pollTimeoutMs, finish]);
|
|
496
891
|
const dismiss = useCallback(() => {
|
|
497
892
|
setError(null);
|
|
498
893
|
setOutcome((o) => o?.kind === "rejected" ? null : o);
|
|
499
894
|
}, []);
|
|
895
|
+
const cancelChallenge = useCallback(() => {
|
|
896
|
+
if (statusRef.current !== "challenge") return;
|
|
897
|
+
leftChallenge.current = true;
|
|
898
|
+
setOutcome(null);
|
|
899
|
+
setError({ kind: "payment_in_progress", message: live.current.msgs["challenge.left"], retrySameAttempt: false, canRetry: false });
|
|
900
|
+
setStatus("blocked");
|
|
901
|
+
}, []);
|
|
500
902
|
const message = status === "processing" ? msgs["state.processing"] : status === "fallback" ? msgs["state.fallback"] : error?.message ?? outcome?.message ?? (status === "loading" ? msgs["state.loading"] : null);
|
|
501
903
|
return {
|
|
502
904
|
status,
|
|
@@ -507,11 +909,13 @@ function useEmbeddedPayment(options) {
|
|
|
507
909
|
attemptsLeft,
|
|
508
910
|
attemptId,
|
|
509
911
|
busy: status === "loading" || status === "processing",
|
|
912
|
+
verifying,
|
|
510
913
|
containerRef: setContainer,
|
|
511
914
|
containerId,
|
|
512
915
|
submit,
|
|
513
916
|
retry,
|
|
514
|
-
dismiss
|
|
917
|
+
dismiss,
|
|
918
|
+
cancelChallenge
|
|
515
919
|
};
|
|
516
920
|
}
|
|
517
921
|
function VoucherCard({ outcome, locale, messages, className }) {
|
|
@@ -548,15 +952,55 @@ function VoucherCard({ outcome, locale, messages, className }) {
|
|
|
548
952
|
/* @__PURE__ */ jsx("p", { children: m["voucher.email"] })
|
|
549
953
|
] });
|
|
550
954
|
}
|
|
551
|
-
function PaymentChallenge({ outcome, locale, messages, render, onCancel }) {
|
|
955
|
+
function PaymentChallenge({ outcome, locale, messages, render, onCancel, escapeAfterMs = 12e4 }) {
|
|
552
956
|
const m = paymentMessages(locale, messages);
|
|
553
|
-
const { ref } = useModalDialog({ open: true, onClose: () =>
|
|
957
|
+
const { ref } = useModalDialog({ open: true, onClose: () => {
|
|
958
|
+
} });
|
|
959
|
+
const [canEscape, setCanEscape] = useState(false);
|
|
960
|
+
const [closed, setClosed] = useState(false);
|
|
961
|
+
const cancelRef = useRef(onCancel);
|
|
962
|
+
cancelRef.current = onCancel;
|
|
963
|
+
const hasCancel = !!onCancel;
|
|
964
|
+
useEffect(() => {
|
|
965
|
+
if (!hasCancel) return;
|
|
966
|
+
const t = setTimeout(() => setCanEscape(true), escapeAfterMs);
|
|
967
|
+
return () => clearTimeout(t);
|
|
968
|
+
}, [hasCancel, escapeAfterMs]);
|
|
969
|
+
useEffect(() => {
|
|
970
|
+
const el = ref.current;
|
|
971
|
+
if (!el) return;
|
|
972
|
+
const onClosed = () => setClosed(true);
|
|
973
|
+
el.addEventListener("close", onClosed);
|
|
974
|
+
return () => el.removeEventListener("close", onClosed);
|
|
975
|
+
}, [ref]);
|
|
976
|
+
const reopen = () => {
|
|
977
|
+
const el = ref.current;
|
|
978
|
+
if (!el) return;
|
|
979
|
+
try {
|
|
980
|
+
if (typeof el.showModal === "function") el.showModal();
|
|
981
|
+
else el.setAttribute("open", "");
|
|
982
|
+
} catch {
|
|
983
|
+
el.setAttribute("open", "");
|
|
984
|
+
}
|
|
985
|
+
setClosed(false);
|
|
986
|
+
el.querySelector("[data-sf-autofocus]")?.focus({ preventScroll: true });
|
|
987
|
+
};
|
|
554
988
|
const url = outcome.challenge?.url;
|
|
555
|
-
return /* @__PURE__ */
|
|
556
|
-
/* @__PURE__ */
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
989
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
990
|
+
closed && /* @__PURE__ */ jsxs("p", { role: "status", "data-sf-challenge-closed": true, children: [
|
|
991
|
+
m["state.challenge"],
|
|
992
|
+
" ",
|
|
993
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: reopen, "data-sf-challenge-reopen": true, style: { minWidth: 44, minHeight: 44 }, children: m["challenge.title"] })
|
|
994
|
+
] }),
|
|
995
|
+
/* @__PURE__ */ jsx("dialog", { ref, "aria-labelledby": "sf-challenge-title", "data-sf-challenge": true, children: /* @__PURE__ */ jsxs("div", { "data-sf-challenge-body": true, children: [
|
|
996
|
+
/* @__PURE__ */ jsx("h2", { id: "sf-challenge-title", "data-sf-autofocus": true, tabIndex: -1, children: m["challenge.title"] }),
|
|
997
|
+
render ? render(outcome) : url ? /* @__PURE__ */ jsx("iframe", { src: url, title: m["challenge.title"], "data-sf-challenge-frame": true, style: { width: "100%", minHeight: 420, border: 0 } }) : /* @__PURE__ */ jsx("p", { children: m["state.challenge"] }),
|
|
998
|
+
hasCancel && /* @__PURE__ */ jsx("div", { "data-sf-challenge-escape": true, "aria-live": "polite", children: canEscape && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
999
|
+
/* @__PURE__ */ jsx("p", { children: m["challenge.escape_hint"] }),
|
|
1000
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => cancelRef.current?.(), style: { minWidth: 44, minHeight: 44 }, children: m["challenge.escape"] })
|
|
1001
|
+
] }) })
|
|
1002
|
+
] }) })
|
|
1003
|
+
] });
|
|
560
1004
|
}
|
|
561
1005
|
var PaymentBrick = forwardRef(function PaymentBrick2(props, ref) {
|
|
562
1006
|
const { submitLabel, hideAmountInButton, renderVoucher, renderChallenge, minHeight = 360, className, style, ...hookOptions } = props;
|
|
@@ -594,7 +1038,7 @@ var PaymentBrick = forwardRef(function PaymentBrick2(props, ref) {
|
|
|
594
1038
|
state.status === "network_error" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void state.retry(), style: { minHeight: 44 }, children: m["action.retry"] })
|
|
595
1039
|
] }) : /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", "data-sf-pay-message": true, children: state.message }),
|
|
596
1040
|
outcome?.kind === "voucher" && (renderVoucher ? renderVoucher(outcome) : /* @__PURE__ */ jsx(VoucherCard, { outcome, locale, ...props.messages ? { messages: props.messages } : {} })),
|
|
597
|
-
outcome?.kind === "challenge" && /* @__PURE__ */ jsx(PaymentChallenge, { outcome, locale, ...props.messages ? { messages: props.messages } : {}, ...renderChallenge ? { render: renderChallenge } : {} }),
|
|
1041
|
+
outcome?.kind === "challenge" && /* @__PURE__ */ jsx(PaymentChallenge, { outcome, locale, onCancel: state.cancelChallenge, ...props.messages ? { messages: props.messages } : {}, ...renderChallenge ? { render: renderChallenge } : {} }),
|
|
598
1042
|
submitLabel !== void 0 && showForm && /* @__PURE__ */ jsxs(
|
|
599
1043
|
"button",
|
|
600
1044
|
{
|
|
@@ -616,56 +1060,6 @@ var PaymentBrick = forwardRef(function PaymentBrick2(props, ref) {
|
|
|
616
1060
|
] });
|
|
617
1061
|
});
|
|
618
1062
|
|
|
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
1063
|
// src/payments/payment-method.messages.ts
|
|
670
1064
|
var ES = {
|
|
671
1065
|
legend: "M\xE9todo de pago",
|
|
@@ -733,6 +1127,130 @@ function paymentMethodMessages(locale, overrides) {
|
|
|
733
1127
|
const { options, ...rest } = overrides;
|
|
734
1128
|
return { ...base, ...rest, options: { ...base.options, ...options } };
|
|
735
1129
|
}
|
|
1130
|
+
function alternativeChoices(choices) {
|
|
1131
|
+
return choices.filter((c) => c.mode !== "card");
|
|
1132
|
+
}
|
|
1133
|
+
function pickChoice(form, value) {
|
|
1134
|
+
const input = Array.from(form?.querySelectorAll('input[name="payment_choice"]') ?? []).find((i) => i.value === value);
|
|
1135
|
+
if (!input) return;
|
|
1136
|
+
input.click();
|
|
1137
|
+
input.scrollIntoView?.({ block: "center", behavior: "smooth" });
|
|
1138
|
+
}
|
|
1139
|
+
var INDETERMINATE = /* @__PURE__ */ new Set(["confirming", "verifying", "transient", "in_progress"]);
|
|
1140
|
+
function safeStorage2() {
|
|
1141
|
+
try {
|
|
1142
|
+
return typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
1143
|
+
} catch {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function usePaymentRecovery(options = {}) {
|
|
1148
|
+
const { scope = "default", formAutoRetries = 2 } = options;
|
|
1149
|
+
const [notice, setNotice] = useState(null);
|
|
1150
|
+
const [sectionKey, setSectionKey] = useState(0);
|
|
1151
|
+
const formFailures = useRef(0);
|
|
1152
|
+
const lastIssue = useRef(null);
|
|
1153
|
+
const unknownByEvent = useRef(false);
|
|
1154
|
+
const live = useRef(options);
|
|
1155
|
+
live.current = options;
|
|
1156
|
+
const onIssue = useCallback((issue) => {
|
|
1157
|
+
lastIssue.current = issue;
|
|
1158
|
+
if (!issue) return setNotice((n) => n?.kind === "confirming" ? null : n);
|
|
1159
|
+
if (issue.kind === "confirming" || issue.kind === "verifying") return setNotice({ kind: "confirming" });
|
|
1160
|
+
if (issue.kind === "merchant_config" || issue.kind === "stale_checkout" || issue.kind === "provider_declined") return setNotice({ kind: issue.kind });
|
|
1161
|
+
if (issue.kind === "other") return setNotice({ kind: "other" });
|
|
1162
|
+
setNotice((n) => n?.kind === "confirming" ? null : n);
|
|
1163
|
+
}, []);
|
|
1164
|
+
const retry = useCallback(() => {
|
|
1165
|
+
const issue = lastIssue.current;
|
|
1166
|
+
if (unknownByEvent.current || issue && INDETERMINATE.has(issue.kind)) return false;
|
|
1167
|
+
const store = live.current.storage === void 0 ? safeStorage2() : live.current.storage;
|
|
1168
|
+
try {
|
|
1169
|
+
store?.removeItem(`sf:pay:attempt:${live.current.scope ?? "default"}`);
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
formFailures.current = 0;
|
|
1173
|
+
lastIssue.current = null;
|
|
1174
|
+
unknownByEvent.current = false;
|
|
1175
|
+
setNotice(null);
|
|
1176
|
+
setSectionKey((k) => k + 1);
|
|
1177
|
+
return true;
|
|
1178
|
+
}, []);
|
|
1179
|
+
const onEvent = useCallback(
|
|
1180
|
+
(e) => {
|
|
1181
|
+
if (e.type === "form_ready") {
|
|
1182
|
+
formFailures.current = 0;
|
|
1183
|
+
setNotice((n) => n?.kind === "form_failed" ? null : n);
|
|
1184
|
+
}
|
|
1185
|
+
if (e.type === "form_failed") {
|
|
1186
|
+
if (formFailures.current < formAutoRetries) {
|
|
1187
|
+
formFailures.current += 1;
|
|
1188
|
+
setSectionKey((k) => k + 1);
|
|
1189
|
+
} else {
|
|
1190
|
+
setNotice({ kind: "form_failed" });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (e.type === "error") unknownByEvent.current = e.kind === "network" || e.kind === "payment_in_progress";
|
|
1194
|
+
if (e.type === "result") {
|
|
1195
|
+
unknownByEvent.current = false;
|
|
1196
|
+
lastIssue.current = null;
|
|
1197
|
+
}
|
|
1198
|
+
if (e.type === "result" && e.status === "rejected") setNotice({ kind: "rejected" });
|
|
1199
|
+
if (e.type === "error" && lastIssue.current?.kind === "store_closed") live.current.onRevalidate?.();
|
|
1200
|
+
},
|
|
1201
|
+
[formAutoRetries]
|
|
1202
|
+
);
|
|
1203
|
+
return useMemo(() => ({ notice, setNotice, sectionKey, onIssue, onEvent, lastIssue, retry, clear: () => setNotice(null) }), [notice, sectionKey, onIssue, onEvent, retry]);
|
|
1204
|
+
}
|
|
1205
|
+
function PaymentRecovery(props) {
|
|
1206
|
+
const { notice, alternatives = [], onRetry, onPick, onRefresh, alternativeLabel, classNames: cx = {}, testIds = {}, children } = props;
|
|
1207
|
+
const locale = useResolvedLocale(props.locale);
|
|
1208
|
+
const m = useMemo(() => paymentRecoveryMessages(locale, props.messages), [locale, props.messages]);
|
|
1209
|
+
const rowNames = useMemo(() => paymentMethodMessages(locale).options, [locale]);
|
|
1210
|
+
const ref = useRef(null);
|
|
1211
|
+
const kind = notice?.kind;
|
|
1212
|
+
useEffect(() => {
|
|
1213
|
+
if (kind && kind !== "confirming") {
|
|
1214
|
+
ref.current?.focus();
|
|
1215
|
+
ref.current?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
|
|
1216
|
+
}
|
|
1217
|
+
}, [kind]);
|
|
1218
|
+
if (!notice) return null;
|
|
1219
|
+
const confirming = notice.kind === "confirming";
|
|
1220
|
+
const canRetry = notice.kind !== "confirming" && notice.kind !== "stale_checkout";
|
|
1221
|
+
const showAlternatives = notice.kind !== "stale_checkout" && !confirming && alternatives.length > 0;
|
|
1222
|
+
const alternativesFirst = notice.kind === "merchant_config";
|
|
1223
|
+
const nameOf = (c) => c.label ?? (c.option ? rowNames[c.option] : void 0) ?? c.option ?? c.methodKey;
|
|
1224
|
+
const labelOf = (c) => alternativeLabel?.(c) ?? m["action.alternative"].replace("{label}", nameOf(c));
|
|
1225
|
+
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;
|
|
1226
|
+
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: [
|
|
1227
|
+
/* @__PURE__ */ jsx("p", { className: cx.title, children: /* @__PURE__ */ jsx("strong", { children: m[`notice.${notice.kind}.title`] }) }),
|
|
1228
|
+
/* @__PURE__ */ jsx("p", { className: cx.body, children: m[`notice.${notice.kind}.body`] }),
|
|
1229
|
+
!confirming ? /* @__PURE__ */ jsxs("div", { className: cx.actions, children: [
|
|
1230
|
+
alternativesFirst ? null : retryButton,
|
|
1231
|
+
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,
|
|
1232
|
+
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,
|
|
1233
|
+
alternativesFirst ? retryButton : null
|
|
1234
|
+
] }) : null,
|
|
1235
|
+
children
|
|
1236
|
+
] });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/payments/revalidate.ts
|
|
1240
|
+
function checkoutShouldRevalidate(options) {
|
|
1241
|
+
const paths = options.paths.map((p) => p.replace(/\/+$/, "") || "/");
|
|
1242
|
+
return (args) => {
|
|
1243
|
+
const method = args.formMethod?.toUpperCase();
|
|
1244
|
+
if (!method || method === "GET" || !args.formAction) return args.defaultShouldRevalidate;
|
|
1245
|
+
let pathname = args.formAction;
|
|
1246
|
+
try {
|
|
1247
|
+
pathname = new URL(args.formAction, "http://sf.local").pathname;
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
pathname = pathname.replace(/\/+$/, "") || "/";
|
|
1251
|
+
return paths.includes(pathname) ? false : args.defaultShouldRevalidate;
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
736
1254
|
var PAYMENT_CHOICE_FIELD = "payment_choice";
|
|
737
1255
|
var CASH_POINT_FIELD = "cash_point";
|
|
738
1256
|
function formFields(form) {
|
|
@@ -859,6 +1377,15 @@ function PaymentMethodSection(props) {
|
|
|
859
1377
|
const requote = wantSync && settled && sync.requote === true;
|
|
860
1378
|
const syncFailed = wantSync && settled && !sync.ok && !requote;
|
|
861
1379
|
const brickReady = cardActive && deliveryReady && synced && !syncFailed;
|
|
1380
|
+
const [formLatched, setFormLatched] = useState(false);
|
|
1381
|
+
useEffect(() => {
|
|
1382
|
+
if (brickReady) setFormLatched(true);
|
|
1383
|
+
}, [brickReady]);
|
|
1384
|
+
useEffect(() => {
|
|
1385
|
+
if (!cardActive) setFormLatched(false);
|
|
1386
|
+
}, [cardActive]);
|
|
1387
|
+
const showBrick = cardActive && !syncFailed && (formLatched || brickReady);
|
|
1388
|
+
const totalUpdating = showBrick && !brickReady;
|
|
862
1389
|
const transport = useMemo(
|
|
863
1390
|
() => props.transport ?? createFetchTransport(payUrl, {
|
|
864
1391
|
...statusUrl ? { refreshUrl: statusUrl } : {},
|
|
@@ -972,9 +1499,9 @@ function PaymentMethodSection(props) {
|
|
|
972
1499
|
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
1500
|
] }),
|
|
974
1501
|
/* @__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: [
|
|
1502
|
+
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
1503
|
fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : requote ? cardNotice("requote", m.quoteChanged) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
|
|
977
|
-
|
|
1504
|
+
showBrick || fallback ? /* @__PURE__ */ jsx(
|
|
978
1505
|
PaymentBrick,
|
|
979
1506
|
{
|
|
980
1507
|
...brickProps,
|
|
@@ -986,15 +1513,14 @@ function PaymentMethodSection(props) {
|
|
|
986
1513
|
locale,
|
|
987
1514
|
...scope ? { scope } : {},
|
|
988
1515
|
...payerEmail && !brickProps?.email ? { email: payerEmail } : {},
|
|
989
|
-
enabled:
|
|
1516
|
+
enabled: showBrick,
|
|
990
1517
|
...cx.brick ? { className: cx.brick } : {},
|
|
991
1518
|
onEvent: handleEvent,
|
|
992
1519
|
onFallback: () => setFallback(true),
|
|
993
1520
|
onResult: (o) => {
|
|
994
1521
|
if (o.kind === "approved" || o.kind === "in_review" || o.kind === "voucher") onDone(o);
|
|
995
1522
|
}
|
|
996
|
-
}
|
|
997
|
-
payerEmail || brickProps?.email ? "with-email" : "without-email"
|
|
1523
|
+
}
|
|
998
1524
|
) : null,
|
|
999
1525
|
/* @__PURE__ */ jsx("p", { className: cx.note, children: m.secureCard }),
|
|
1000
1526
|
expired && rebuildHref ? Link({ href: rebuildHref, ...cx.rebuild ? { className: cx.rebuild } : {}, children: m.rebuildCart, ...testIds.rebuild ? { testId: testIds.rebuild } : {} }) : null
|
|
@@ -1013,7 +1539,7 @@ function PaymentMethodSection(props) {
|
|
|
1013
1539
|
onClick: onPayClick,
|
|
1014
1540
|
className: cx.submit,
|
|
1015
1541
|
disabled: busy || blocked,
|
|
1016
|
-
"aria-disabled": blocked ||
|
|
1542
|
+
"aria-disabled": blocked || cardActive && !syncFailed && deliveryReady && (!brickReady || !ready || processing) || void 0,
|
|
1017
1543
|
"aria-busy": processing || void 0,
|
|
1018
1544
|
"data-sf-pay-method-submit": true,
|
|
1019
1545
|
...tid("submit"),
|
|
@@ -1104,6 +1630,6 @@ function rowMarks(choice, cardBrands) {
|
|
|
1104
1630
|
return [];
|
|
1105
1631
|
}
|
|
1106
1632
|
|
|
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 };
|
|
1633
|
+
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
1634
|
//# sourceMappingURL=payments.js.map
|
|
1109
1635
|
//# sourceMappingURL=payments.js.map
|