@asteby/storefront-react 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkout.cjs +65 -14
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +7 -0
- package/dist/checkout.d.ts +7 -0
- package/dist/checkout.js +65 -14
- package/dist/checkout.js.map +1 -1
- package/dist/payments.cjs +64 -12
- package/dist/payments.cjs.map +1 -1
- package/dist/payments.css +65 -0
- package/dist/payments.d.cts +20 -2
- package/dist/payments.d.ts +20 -2
- package/dist/payments.js +64 -12
- package/dist/payments.js.map +1 -1
- package/package.json +2 -2
package/dist/payments.css
CHANGED
|
@@ -87,3 +87,68 @@
|
|
|
87
87
|
flex-wrap: wrap;
|
|
88
88
|
gap: 0.5rem;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* 3DS challenge (`<PaymentChallenge>`): a near full-screen modal. Colors are fixed on purpose: the bank page inside
|
|
93
|
+
* the iframe is always white, and the dialog often lives in a themed (dark) panel. Override with a more specific rule.
|
|
94
|
+
*/
|
|
95
|
+
dialog[data-sf-challenge] {
|
|
96
|
+
margin: auto;
|
|
97
|
+
box-sizing: border-box;
|
|
98
|
+
width: min(60rem, calc(100% - 2rem));
|
|
99
|
+
max-width: none;
|
|
100
|
+
max-height: 94dvh;
|
|
101
|
+
overflow: auto;
|
|
102
|
+
border: 1px solid #c9d2ee;
|
|
103
|
+
border-radius: 0.75rem;
|
|
104
|
+
background: #ffffff;
|
|
105
|
+
color: #0b1220;
|
|
106
|
+
/* Claro aunque la página esté en oscuro: barras de desplazamiento y controles del navegador sobre fondo blanco. */
|
|
107
|
+
color-scheme: light;
|
|
108
|
+
padding: 1.25rem;
|
|
109
|
+
}
|
|
110
|
+
dialog[data-sf-challenge]::backdrop {
|
|
111
|
+
background: rgb(0 8 31 / 0.72);
|
|
112
|
+
}
|
|
113
|
+
dialog[data-sf-challenge] h2 {
|
|
114
|
+
margin: 0 0 0.75rem;
|
|
115
|
+
font-weight: 800;
|
|
116
|
+
color: #001963;
|
|
117
|
+
}
|
|
118
|
+
dialog[data-sf-challenge] h2:focus {
|
|
119
|
+
outline: none;
|
|
120
|
+
}
|
|
121
|
+
dialog[data-sf-challenge] [data-sf-challenge-frame] {
|
|
122
|
+
display: block;
|
|
123
|
+
height: min(70dvh, 40rem);
|
|
124
|
+
background: #ffffff;
|
|
125
|
+
}
|
|
126
|
+
dialog[data-sf-challenge] [data-sf-challenge-escape] {
|
|
127
|
+
margin-top: 0.75rem;
|
|
128
|
+
font-size: 0.875rem;
|
|
129
|
+
}
|
|
130
|
+
dialog[data-sf-challenge] [data-sf-challenge-escape]:empty {
|
|
131
|
+
margin: 0;
|
|
132
|
+
}
|
|
133
|
+
dialog[data-sf-challenge] [data-sf-challenge-escape] button {
|
|
134
|
+
margin-top: 0.5rem;
|
|
135
|
+
padding: 0 1rem;
|
|
136
|
+
background: #ffffff;
|
|
137
|
+
color: #0b1220;
|
|
138
|
+
border: 1px solid #8a94b3;
|
|
139
|
+
border-radius: 0.5rem;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
}
|
|
142
|
+
@media (max-width: 40rem) {
|
|
143
|
+
dialog[data-sf-challenge] {
|
|
144
|
+
width: 100%;
|
|
145
|
+
height: 100dvh;
|
|
146
|
+
max-height: 100dvh;
|
|
147
|
+
border-radius: 0;
|
|
148
|
+
border: 0;
|
|
149
|
+
padding: 1rem;
|
|
150
|
+
}
|
|
151
|
+
dialog[data-sf-challenge] [data-sf-challenge-frame] {
|
|
152
|
+
height: calc(100dvh - 8rem);
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/payments.d.cts
CHANGED
|
@@ -226,6 +226,14 @@ type EmbeddedPaymentState = {
|
|
|
226
226
|
retry: () => Promise<void>;
|
|
227
227
|
/** After a rejection: clears the message (the form is remounted by the hook). */
|
|
228
228
|
dismiss: () => void;
|
|
229
|
+
/**
|
|
230
|
+
* Leaves a pending 3DS challenge (the buyer's emergency exit). The outcome is UNKNOWN (the bank may still approve the
|
|
231
|
+
* attempt the buyer just verified), so the attempt key is kept and no new payment can start: the hook verifies the
|
|
232
|
+
* payment state through `transport.refresh` (status `blocked`, `error.kind = 'payment_in_progress'`, `verifying`).
|
|
233
|
+
* Only a definitive answer settles it: approved, or rejected / expired / cancelled, which gives a new attempt and a
|
|
234
|
+
* fresh form. A still-pending challenge does not reopen the dialog.
|
|
235
|
+
*/
|
|
236
|
+
cancelChallenge: () => void;
|
|
229
237
|
};
|
|
230
238
|
/**
|
|
231
239
|
* Embedded payment state machine for one card form.
|
|
@@ -256,10 +264,20 @@ type PaymentChallengeProps = {
|
|
|
256
264
|
messages?: Partial<PaymentMessages>;
|
|
257
265
|
/** Replace the default iframe (e.g. mount the provider's status screen). */
|
|
258
266
|
render?: (outcome: PaymentOutcome) => ReactNode;
|
|
267
|
+
/**
|
|
268
|
+
* The buyer's emergency exit. The dialog is NOT dismissible (no Esc, no backdrop click) while the payment is in
|
|
269
|
+
* progress; this button only appears after `escapeAfterMs`, so a stuck bank page never traps the buyer (WCAG 2.2.1).
|
|
270
|
+
*/
|
|
259
271
|
onCancel?: () => void;
|
|
272
|
+
/** How long the dialog stays locked before the exit shows. Default 120000. */
|
|
273
|
+
escapeAfterMs?: number;
|
|
260
274
|
};
|
|
261
|
-
/**
|
|
262
|
-
|
|
275
|
+
/**
|
|
276
|
+
* 3DS inside the page: a near full-screen modal dialog (`showModal()`: focus trap, inert page) with an accessible title.
|
|
277
|
+
* Esc and backdrop clicks do nothing until the payment ends (the owner unmounts it). Default content is the bank's
|
|
278
|
+
* https iframe. Layout comes from `payments.css` (`dialog[data-sf-challenge]`), overridable with your own CSS.
|
|
279
|
+
*/
|
|
280
|
+
declare function PaymentChallenge({ outcome, locale, messages, render, onCancel, escapeAfterMs }: PaymentChallengeProps): react.JSX.Element;
|
|
263
281
|
type PaymentBrickHandle = EmbeddedPaymentState;
|
|
264
282
|
type PaymentBrickProps = Omit<UseEmbeddedPaymentOptions, 'amount'> & {
|
|
265
283
|
/** The server's total. Shown on the button and passed to the provider widget. */
|
package/dist/payments.d.ts
CHANGED
|
@@ -226,6 +226,14 @@ type EmbeddedPaymentState = {
|
|
|
226
226
|
retry: () => Promise<void>;
|
|
227
227
|
/** After a rejection: clears the message (the form is remounted by the hook). */
|
|
228
228
|
dismiss: () => void;
|
|
229
|
+
/**
|
|
230
|
+
* Leaves a pending 3DS challenge (the buyer's emergency exit). The outcome is UNKNOWN (the bank may still approve the
|
|
231
|
+
* attempt the buyer just verified), so the attempt key is kept and no new payment can start: the hook verifies the
|
|
232
|
+
* payment state through `transport.refresh` (status `blocked`, `error.kind = 'payment_in_progress'`, `verifying`).
|
|
233
|
+
* Only a definitive answer settles it: approved, or rejected / expired / cancelled, which gives a new attempt and a
|
|
234
|
+
* fresh form. A still-pending challenge does not reopen the dialog.
|
|
235
|
+
*/
|
|
236
|
+
cancelChallenge: () => void;
|
|
229
237
|
};
|
|
230
238
|
/**
|
|
231
239
|
* Embedded payment state machine for one card form.
|
|
@@ -256,10 +264,20 @@ type PaymentChallengeProps = {
|
|
|
256
264
|
messages?: Partial<PaymentMessages>;
|
|
257
265
|
/** Replace the default iframe (e.g. mount the provider's status screen). */
|
|
258
266
|
render?: (outcome: PaymentOutcome) => ReactNode;
|
|
267
|
+
/**
|
|
268
|
+
* The buyer's emergency exit. The dialog is NOT dismissible (no Esc, no backdrop click) while the payment is in
|
|
269
|
+
* progress; this button only appears after `escapeAfterMs`, so a stuck bank page never traps the buyer (WCAG 2.2.1).
|
|
270
|
+
*/
|
|
259
271
|
onCancel?: () => void;
|
|
272
|
+
/** How long the dialog stays locked before the exit shows. Default 120000. */
|
|
273
|
+
escapeAfterMs?: number;
|
|
260
274
|
};
|
|
261
|
-
/**
|
|
262
|
-
|
|
275
|
+
/**
|
|
276
|
+
* 3DS inside the page: a near full-screen modal dialog (`showModal()`: focus trap, inert page) with an accessible title.
|
|
277
|
+
* Esc and backdrop clicks do nothing until the payment ends (the owner unmounts it). Default content is the bank's
|
|
278
|
+
* https iframe. Layout comes from `payments.css` (`dialog[data-sf-challenge]`), overridable with your own CSS.
|
|
279
|
+
*/
|
|
280
|
+
declare function PaymentChallenge({ outcome, locale, messages, render, onCancel, escapeAfterMs }: PaymentChallengeProps): react.JSX.Element;
|
|
263
281
|
type PaymentBrickHandle = EmbeddedPaymentState;
|
|
264
282
|
type PaymentBrickProps = Omit<UseEmbeddedPaymentOptions, 'amount'> & {
|
|
265
283
|
/** The server's total. Shown on the button and passed to the provider widget. */
|
package/dist/payments.js
CHANGED
|
@@ -485,7 +485,7 @@ function useEmbeddedPayment(options) {
|
|
|
485
485
|
autoRetries = 2,
|
|
486
486
|
retryDelayMs = 600,
|
|
487
487
|
pollIntervalMs = 2e3,
|
|
488
|
-
pollTimeoutMs =
|
|
488
|
+
pollTimeoutMs = 3e5,
|
|
489
489
|
verifyTimeoutMs = 12e4,
|
|
490
490
|
verifyIntervalMs = 2e3,
|
|
491
491
|
scope = "default"
|
|
@@ -514,6 +514,7 @@ function useEmbeddedPayment(options) {
|
|
|
514
514
|
const alive = useRef(true);
|
|
515
515
|
const generation = useRef(0);
|
|
516
516
|
const statusRef = useRef("idle");
|
|
517
|
+
const leftChallenge = useRef(false);
|
|
517
518
|
statusRef.current = status;
|
|
518
519
|
errorRef.current = error;
|
|
519
520
|
const amountKey = `${amount.amount}|${amount.currency_code}`;
|
|
@@ -697,6 +698,7 @@ function useEmbeddedPayment(options) {
|
|
|
697
698
|
(result) => {
|
|
698
699
|
const o = paymentOutcome(result, live.current.locale, live.current.messages);
|
|
699
700
|
pendingData.current = null;
|
|
701
|
+
leftChallenge.current = false;
|
|
700
702
|
setOutcome(o);
|
|
701
703
|
setError(null);
|
|
702
704
|
setAttemptsLeft(o.attempts_left);
|
|
@@ -857,7 +859,7 @@ function useEmbeddedPayment(options) {
|
|
|
857
859
|
}
|
|
858
860
|
let stop = false;
|
|
859
861
|
const ctl = new AbortController();
|
|
860
|
-
const until = Date.now() + verifyTimeoutMs;
|
|
862
|
+
const until = Date.now() + (leftChallenge.current ? Math.max(verifyTimeoutMs, pollTimeoutMs) : verifyTimeoutMs);
|
|
861
863
|
setVerifying(true);
|
|
862
864
|
(async () => {
|
|
863
865
|
let wait = verifyIntervalMs;
|
|
@@ -869,6 +871,8 @@ function useEmbeddedPayment(options) {
|
|
|
869
871
|
const r = await transport.refresh({ signal: ctl.signal });
|
|
870
872
|
if (stop || !alive.current) return;
|
|
871
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;
|
|
872
876
|
setVerifying(false);
|
|
873
877
|
finish(r);
|
|
874
878
|
return;
|
|
@@ -883,11 +887,18 @@ function useEmbeddedPayment(options) {
|
|
|
883
887
|
ctl.abort();
|
|
884
888
|
setVerifying(false);
|
|
885
889
|
};
|
|
886
|
-
}, [unknownOutcome, transport, verifyTimeoutMs, verifyIntervalMs, finish]);
|
|
890
|
+
}, [unknownOutcome, transport, verifyTimeoutMs, verifyIntervalMs, pollTimeoutMs, finish]);
|
|
887
891
|
const dismiss = useCallback(() => {
|
|
888
892
|
setError(null);
|
|
889
893
|
setOutcome((o) => o?.kind === "rejected" ? null : o);
|
|
890
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
|
+
}, []);
|
|
891
902
|
const message = status === "processing" ? msgs["state.processing"] : status === "fallback" ? msgs["state.fallback"] : error?.message ?? outcome?.message ?? (status === "loading" ? msgs["state.loading"] : null);
|
|
892
903
|
return {
|
|
893
904
|
status,
|
|
@@ -903,7 +914,8 @@ function useEmbeddedPayment(options) {
|
|
|
903
914
|
containerId,
|
|
904
915
|
submit,
|
|
905
916
|
retry,
|
|
906
|
-
dismiss
|
|
917
|
+
dismiss,
|
|
918
|
+
cancelChallenge
|
|
907
919
|
};
|
|
908
920
|
}
|
|
909
921
|
function VoucherCard({ outcome, locale, messages, className }) {
|
|
@@ -940,15 +952,55 @@ function VoucherCard({ outcome, locale, messages, className }) {
|
|
|
940
952
|
/* @__PURE__ */ jsx("p", { children: m["voucher.email"] })
|
|
941
953
|
] });
|
|
942
954
|
}
|
|
943
|
-
function PaymentChallenge({ outcome, locale, messages, render, onCancel }) {
|
|
955
|
+
function PaymentChallenge({ outcome, locale, messages, render, onCancel, escapeAfterMs = 12e4 }) {
|
|
944
956
|
const m = paymentMessages(locale, messages);
|
|
945
|
-
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
|
+
};
|
|
946
988
|
const url = outcome.challenge?.url;
|
|
947
|
-
return /* @__PURE__ */
|
|
948
|
-
/* @__PURE__ */
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
+
] });
|
|
952
1004
|
}
|
|
953
1005
|
var PaymentBrick = forwardRef(function PaymentBrick2(props, ref) {
|
|
954
1006
|
const { submitLabel, hideAmountInButton, renderVoucher, renderChallenge, minHeight = 360, className, style, ...hookOptions } = props;
|
|
@@ -986,7 +1038,7 @@ var PaymentBrick = forwardRef(function PaymentBrick2(props, ref) {
|
|
|
986
1038
|
state.status === "network_error" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void state.retry(), style: { minHeight: 44 }, children: m["action.retry"] })
|
|
987
1039
|
] }) : /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", "data-sf-pay-message": true, children: state.message }),
|
|
988
1040
|
outcome?.kind === "voucher" && (renderVoucher ? renderVoucher(outcome) : /* @__PURE__ */ jsx(VoucherCard, { outcome, locale, ...props.messages ? { messages: props.messages } : {} })),
|
|
989
|
-
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 } : {} }),
|
|
990
1042
|
submitLabel !== void 0 && showForm && /* @__PURE__ */ jsxs(
|
|
991
1043
|
"button",
|
|
992
1044
|
{
|