@vtex/faststore 0.0.5 → 0.0.6
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/CHANGELOG.md +45 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1543 -495
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +28 -7
- package/dist/components.js +31 -12
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/address-summary.md +24 -3
- package/dist/docs/alert-dialog.md +204 -0
- package/dist/docs/billing-address.md +284 -0
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/client-preferences-fieldset.md +148 -0
- package/dist/docs/client-preferences.md +2 -1
- package/dist/docs/client-profile-fieldset.md +184 -0
- package/dist/docs/client-profile.md +1 -1
- package/dist/docs/document-field.md +58 -4
- package/dist/docs/field.md +317 -0
- package/dist/docs/form.md +307 -0
- package/dist/docs/identity-edit.md +6 -6
- package/dist/docs/identity-summary.md +5 -5
- package/dist/docs/installments.md +248 -0
- package/dist/docs/my-account-profile.md +1 -0
- package/dist/docs/order-placed-payment-summary.md +138 -0
- package/dist/docs/payment-method-badges.md +232 -0
- package/dist/docs/payment-methods.md +1 -0
- package/dist/docs/payment.md +218 -0
- package/dist/docs/phone-field.md +58 -4
- package/dist/docs/place-order.md +157 -0
- package/dist/docs/save-card.md +186 -0
- package/dist/docs/saved-cards.md +218 -0
- package/dist/docs/select.md +465 -0
- package/dist/docs/shipping-address.md +110 -258
- package/dist/index.d.ts +4 -1
- package/dist/internal/client.d.ts +3 -1
- package/dist/internal/client.js +3 -1
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/billing-address/billing-address.d.ts +165 -0
- package/dist/matter/billing-address/billing-address.js +408 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -0
- package/dist/matter/card-entry/card-capture.d.ts +25 -0
- package/dist/matter/card-entry/card-capture.js +25 -0
- package/dist/matter/card-entry/card-capture.js.map +1 -0
- package/dist/matter/card-entry/card-entry.d.ts +120 -0
- package/dist/matter/card-entry/card-entry.js +832 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -0
- package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
- package/dist/matter/cardholder-document/cardholder-document.js +70 -0
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
- package/dist/matter/client-preferences/client-preferences.js +6 -6
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +4 -6
- package/dist/matter/client-profile/client-profile.js +9 -6
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
- package/dist/matter/identity-edit/identity-edit.js +2 -2
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.js +2 -2
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +42 -0
- package/dist/matter/installments/installments-select.js +167 -0
- package/dist/matter/installments/installments-select.js.map +1 -0
- package/dist/matter/installments/installments.d.ts +58 -0
- package/dist/matter/installments/installments.js +176 -0
- package/dist/matter/installments/installments.js.map +1 -0
- package/dist/matter/installments/use-installments-selection.d.ts +29 -0
- package/dist/matter/installments/use-installments-selection.js +95 -0
- package/dist/matter/installments/use-installments-selection.js.map +1 -0
- package/dist/matter/my-account-profile/my-account-profile.js +4 -4
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.d.ts +19 -0
- package/dist/matter/payment/payment-card-capture.js +129 -0
- package/dist/matter/payment/payment-card-capture.js.map +1 -0
- package/dist/matter/payment/payment-controller.d.ts +24 -0
- package/dist/matter/payment/payment-controller.js +107 -0
- package/dist/matter/payment/payment-controller.js.map +1 -0
- package/dist/matter/payment/payment-method-badges.d.ts +57 -0
- package/dist/matter/payment/payment-method-badges.js +192 -0
- package/dist/matter/payment/payment-method-badges.js.map +1 -0
- package/dist/matter/payment/payment.d.ts +89 -0
- package/dist/matter/payment/payment.js +263 -0
- package/dist/matter/payment/payment.js.map +1 -0
- package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
- package/dist/matter/payment/use-payment-method-selection.js +458 -0
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
- package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
- package/dist/matter/payment/use-payment-submit-gate.js +144 -0
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
- package/dist/matter/place-order/place-order.d.ts +74 -0
- package/dist/matter/place-order/place-order.js +379 -0
- package/dist/matter/place-order/place-order.js.map +1 -0
- package/dist/matter/product-list/product-list.d.ts +1 -0
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
- package/dist/matter/product-shelf/product-shelf.js +2 -0
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +49 -0
- package/dist/matter/save-card/save-card.js +188 -0
- package/dist/matter/save-card/save-card.js.map +1 -0
- package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
- package/dist/matter/saved-cards/saved-cards.js +387 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +20 -10
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
- package/dist/matter/shipping-address/shipping-address.js +480 -791
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +1 -1
- package/dist/particles/address-summary/address-summary.d.ts +24 -0
- package/dist/particles/address-summary/address-summary.js +83 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
- package/dist/particles/alert-dialog/alert-dialog.js +187 -0
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
- package/dist/particles/checkbox/checkbox.d.ts +7 -3
- package/dist/particles/checkbox/checkbox.js +4 -1
- package/dist/particles/checkbox/checkbox.js.map +1 -1
- package/dist/particles/document-field/document-field.d.ts +48 -5
- package/dist/particles/document-field/document-field.js +189 -32
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/bound-field-registry.js +52 -0
- package/dist/particles/field/bound-field-registry.js.map +1 -0
- package/dist/particles/field/field.d.ts +116 -0
- package/dist/particles/field/field.js +497 -6
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/fieldset/fieldset.js +2 -2
- package/dist/particles/fieldset/fieldset.js.map +1 -1
- package/dist/particles/form/form.d.ts +104 -0
- package/dist/particles/form/form.js +485 -0
- package/dist/particles/form/form.js.map +1 -0
- package/dist/particles/phone-field/phone-field.d.ts +38 -10
- package/dist/particles/phone-field/phone-field.js +183 -39
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/postal-code-field/postal-code-field.js +2 -2
- package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
- package/dist/particles/select/field-select.d.ts +43 -0
- package/dist/particles/select/field-select.js +150 -0
- package/dist/particles/select/field-select.js.map +1 -0
- package/dist/particles/select/select.d.ts +2 -0
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/shared/compose-refs.js +21 -0
- package/dist/particles/shared/compose-refs.js.map +1 -0
- package/dist/schema.graphql +701 -2
- package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
- package/dist/sdk/actions/analytics-action-provider.js +19 -0
- package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
- package/dist/sdk/actions/payment-action-provider.js +26 -0
- package/dist/sdk/actions/payment-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
- package/dist/sdk/analytics/analytics-destination.js +90 -0
- package/dist/sdk/analytics/analytics-destination.js.map +1 -0
- package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
- package/dist/sdk/analytics/use-analytics-actions.js +108 -0
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
- package/dist/sdk/cart/use-cart-actions.js +1 -1
- package/dist/sdk/data/entity-resolution-context.js +31 -0
- package/dist/sdk/data/entity-resolution-context.js.map +1 -0
- package/dist/sdk/data/entity-subscriber.js +18 -8
- package/dist/sdk/data/entity-subscriber.js.map +1 -1
- package/dist/sdk/data/index.js +1 -0
- package/dist/sdk/data/page-reactive-data-root.d.ts +36 -0
- package/dist/sdk/data/page-reactive-data-root.js +4 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.js +7 -3
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +12 -4
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-data-settled.js +39 -0
- package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
- package/dist/sdk/data/use-reactive-entity.js +5 -1
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +3 -0
- package/dist/sdk/i18n/messages/en-US.json +3 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
- package/dist/sdk/i18n/messages/es-ES.js +3 -0
- package/dist/sdk/i18n/messages/es-ES.json +3 -0
- package/dist/sdk/i18n/messages/pt-BR.js +3 -0
- package/dist/sdk/i18n/messages/pt-BR.json +3 -0
- package/dist/sdk/identity/use-identity-actions.js +3 -2
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/gateway-origin.js +35 -0
- package/dist/sdk/payment/gateway-origin.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +85 -0
- package/dist/sdk/payment/gateway.js +183 -0
- package/dist/sdk/payment/gateway.js.map +1 -0
- package/dist/sdk/payment/operations.d.ts +52 -0
- package/dist/sdk/payment/operations.js +223 -0
- package/dist/sdk/payment/operations.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +71 -0
- package/dist/sdk/payment/security-code-mask.js.map +1 -0
- package/dist/sdk/payment/submission-registry.d.ts +33 -0
- package/dist/sdk/payment/submission-registry.js +58 -0
- package/dist/sdk/payment/submission-registry.js.map +1 -0
- package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
- package/dist/sdk/payment/use-payment-actions.js +276 -0
- package/dist/sdk/payment/use-payment-actions.js.map +1 -0
- package/dist/sdk/payment/use-place-order.d.ts +65 -0
- package/dist/sdk/payment/use-place-order.js +640 -0
- package/dist/sdk/payment/use-place-order.js.map +1 -0
- package/dist/sdk/shipping/use-shipping-actions.js +1 -1
- package/dist/utils/loggable-error-name.js +49 -0
- package/dist/utils/loggable-error-name.js.map +1 -0
- package/dist/utils/resolve-authored-copy.js +18 -0
- package/dist/utils/resolve-authored-copy.js.map +1 -0
- package/dist/utils/substitute-template.js +33 -0
- package/dist/utils/substitute-template.js.map +1 -0
- package/package.json +2 -1
- package/src/styles/tailwind.css +34 -0
- package/dist/docs/client-profile-form.md +0 -192
- package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
- package/dist/matter/client-profile-form/client-profile-form.js +0 -662
- package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { Field } from "../../particles/field/field.js";
|
|
5
|
+
import { Icon } from "../../particles/icon/icon.js";
|
|
6
|
+
import { Fieldset } from "../../particles/fieldset/fieldset.js";
|
|
7
|
+
import { useCardCapture, useRegisterCardCapture } from "./card-capture.js";
|
|
8
|
+
import { applyMask } from "../../particles/shared/input-mask.js";
|
|
9
|
+
import "../../particles/document-field/document-field.js";
|
|
10
|
+
import { SECURITY_CODE_MASK_STYLE, isSecurityCodeComplete } from "../../sdk/payment/security-code-mask.js";
|
|
11
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
//#region src/matter/card-entry/card-entry.tsx
|
|
14
|
+
/** Visual/attribute order used for first-invalid focus and the `data-fs-card-entry-field` values. */
|
|
15
|
+
const FIELD_ATTR = {
|
|
16
|
+
number: "number",
|
|
17
|
+
holderName: "holder-name",
|
|
18
|
+
expiration: "expiration",
|
|
19
|
+
cvv: "cvv"
|
|
20
|
+
};
|
|
21
|
+
const VALIDATION_ORDER = [
|
|
22
|
+
"number",
|
|
23
|
+
"holderName",
|
|
24
|
+
"expiration",
|
|
25
|
+
"cvv"
|
|
26
|
+
];
|
|
27
|
+
const DEFAULT_VALIDATOR = {
|
|
28
|
+
numberMask: null,
|
|
29
|
+
numberRegex: null,
|
|
30
|
+
cvvMask: null,
|
|
31
|
+
cvvRegex: null,
|
|
32
|
+
cvvRequired: true,
|
|
33
|
+
expirationRequired: true,
|
|
34
|
+
holderNameRequired: true
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_NUMBER_MASK = "9999 9999 9999 9999";
|
|
37
|
+
const DEFAULT_EXPIRATION_MASK = "99/99";
|
|
38
|
+
const DEFAULT_CVV_MASK = "999";
|
|
39
|
+
function countDigitSlots(mask) {
|
|
40
|
+
return mask.split("").filter((char) => char === "9").length;
|
|
41
|
+
}
|
|
42
|
+
/** Slot capacity of a (possibly pipe-delimited) mask — the widest alternative's count (F13). */
|
|
43
|
+
function maskCapacity(mask) {
|
|
44
|
+
return Math.max(...mask.split("|").map(countDigitSlots));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The narrowest alternative's slot count — `maskCapacity`'s floor, and the length at which a
|
|
48
|
+
* security code counts as fully typed (T173).
|
|
49
|
+
*
|
|
50
|
+
* The ceiling is the right answer for truncation (`maskDigits` must let the widest alternative's
|
|
51
|
+
* digits through) and the wrong one for masking: one method id can cover a 3-and-4-digit family with
|
|
52
|
+
* a single `'999|9999'` mask, and a shopper holding the 3-digit card of that family never reaches a
|
|
53
|
+
* fourth digit — so a ceiling-derived length leaves their complete, valid code legible for the rest
|
|
54
|
+
* of the flow. `SavedCardCvvField` errs on the same side for the same reason. A malformed
|
|
55
|
+
* alternative with no slots (`'999|'`) floors to zero, which masks from the first digit: early,
|
|
56
|
+
* never late.
|
|
57
|
+
*/
|
|
58
|
+
function narrowestMaskCapacity(mask) {
|
|
59
|
+
return Math.min(...mask.split("|").map(countDigitSlots));
|
|
60
|
+
}
|
|
61
|
+
function toDigits(value) {
|
|
62
|
+
return value.replace(/\D/g, "");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Strips non-digits, truncates to the mask's slot capacity, then applies it progressively. A
|
|
66
|
+
* pipe-delimited mask (`applyMask` already selects among the alternatives internally) needs the
|
|
67
|
+
* *widest* alternative's capacity here (F13) — counting `9`s across the whole pipe-joined string, as
|
|
68
|
+
* before this fix, sums every alternative's slots instead of taking the largest one, letting far more
|
|
69
|
+
* digits through than any single alternative can hold. Mirrors `document-field.tsx`'s own
|
|
70
|
+
* `Math.max(...mask.split('|').map(countSlots))` for the identical reason.
|
|
71
|
+
*/
|
|
72
|
+
function maskDigits(rawValue, mask) {
|
|
73
|
+
const digitsOnly = toDigits(rawValue);
|
|
74
|
+
return applyMask(digitsOnly.slice(0, maskCapacity(mask)), mask);
|
|
75
|
+
}
|
|
76
|
+
/** A malformed store-configured regex is swallowed (treated as no match), never thrown. */
|
|
77
|
+
function safeTest(pattern, value) {
|
|
78
|
+
try {
|
|
79
|
+
return new RegExp(pattern).test(value);
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Relaxes a store's `numberRegex` so it matches on a BIN-length prefix instead of only once the
|
|
86
|
+
* shopper has typed the network's full PAN length (F1, specs/card-entry.md Decision 1). T1's own
|
|
87
|
+
* regexes are full-length anchored — the real Visa validator is `^4[0-9]{12}(?:[0-9]{3})?$` (13 or
|
|
88
|
+
* 16 digits, exactly) — so testing the *whole* typed digit string against it as-is cannot resolve
|
|
89
|
+
* before the last keystroke, and resolves intermittently for lengths in between (13 matches, 14–15
|
|
90
|
+
* don't, 16 matches again): the badge, and every field gated on `detectedMethod`, flicker on, off,
|
|
91
|
+
* and on again while the shopper is still typing. Dropping the trailing `$` and widening every
|
|
92
|
+
* bounded repeat quantifier to also accept fewer repetitions (`{12}` → `{0,12}`, `{n,m}` → `{0,m}`,
|
|
93
|
+
* `{n,}` → `{0,}`) keeps every *fixed* discriminating character exactly as strict as the store
|
|
94
|
+
* configured (Mastercard's `[1-5]` second digit still requires that literal digit once typed), while
|
|
95
|
+
* letting the length-representing tail resolve as soon as it's admissible rather than only once it's
|
|
96
|
+
* complete. Live detection only — the unmodified regex is still what `validateDraft` re-tests at
|
|
97
|
+
* submission time.
|
|
98
|
+
*/
|
|
99
|
+
function relaxForLiveDetection(pattern) {
|
|
100
|
+
return pattern.replace(/\$$/, "").replace(/\{(\d+)(,)?(\d+)?\}/g, (_match, min, hasComma, max) => hasComma && max === void 0 ? "{0,}" : `{0,${max ?? min}}`);
|
|
101
|
+
}
|
|
102
|
+
/** Every method whose *relaxed* (BIN-prefix) transform of `validator.numberRegex` admits `digits`. */
|
|
103
|
+
function relaxedCandidates(methods, digits) {
|
|
104
|
+
return methods.filter((method) => method.validator.numberRegex && safeTest(relaxForLiveDetection(method.validator.numberRegex), digits));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* First method whose *exact*, unrelaxed `validator.numberRegex` matches `digits` in full — the same
|
|
108
|
+
* test `validateDraft` re-runs at submission time.
|
|
109
|
+
*/
|
|
110
|
+
function exactMatch(methods, digits) {
|
|
111
|
+
return methods.find((method) => method.validator.numberRegex && safeTest(method.validator.numberRegex, digits)) ?? null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* First method whose `validator.numberRegex` admits `digits` as a BIN-level prefix, or `null`.
|
|
115
|
+
*
|
|
116
|
+
* N1: relaxing every store regex into a pure prefix test (F1, above) means two networks whose fixed
|
|
117
|
+
* prefixes *nest* — one network's whole discriminating prefix is itself a prefix of a different,
|
|
118
|
+
* longer network's real PAN — both keep "relax-matching" once the shorter network's fixed prefix is
|
|
119
|
+
* satisfied, including at the longer network's own full, syntactically valid length, where the exact,
|
|
120
|
+
* unrelaxed regex alone correctly discriminates by length. Left as pure first-match-wins over the
|
|
121
|
+
* relaxed test, the answer at full PAN length silently depends on which order the store's `methods`
|
|
122
|
+
* happen to arrive in — something this component does not control.
|
|
123
|
+
*
|
|
124
|
+
* The fix tries the **exact** regex first, across every method in `methods` order, and only falls back
|
|
125
|
+
* to the relaxed (prefix) match for input too short to satisfy any exact pattern yet. This restores the
|
|
126
|
+
* exact regex's authority the instant a full PAN is present, which resolves the nesting-prefix case
|
|
127
|
+
* above deterministically regardless of order. **It is not, however, order-independent in general**
|
|
128
|
+
* (T22, V2): two configured networks' exact patterns can both match the very same full-length input —
|
|
129
|
+
* e.g. a real Visa regex and an Elo BIN range both match `4011788888888888` (Elo is issued on
|
|
130
|
+
* Visa-range BINs) — and `exactMatch` below is a plain `methods.find(...)`, so **the first exact match
|
|
131
|
+
* in `methods` order wins the tie**. This is pre-existing (pre-N1 code used the same `find` over exact
|
|
132
|
+
* regexes) and is not introduced or fixed by N1; N1 only shrank how much of the input space stays
|
|
133
|
+
* order-dependent. A caller that needs a specific tie winner must order `methods` by specificity itself
|
|
134
|
+
* before calling — this function does not infer specificity from a regex. See also `resolveNumberMask`
|
|
135
|
+
* below: this function alone cannot help if the field's own mask capacity has already been capped
|
|
136
|
+
* narrower than the digit count an exact match needs to resolve, and see `resolveNetworkMethod` below
|
|
137
|
+
* `resolveNumberMask` (T22, V3/V4) for this component's own order-independent wrapper around the
|
|
138
|
+
* *relaxed*-fallback branch, which this component's own call sites use instead of calling this function
|
|
139
|
+
* directly.
|
|
140
|
+
*/
|
|
141
|
+
function detectNetworkMethod(methods, digits) {
|
|
142
|
+
if (!digits) return null;
|
|
143
|
+
return exactMatch(methods, digits) ?? relaxedCandidates(methods, digits)[0] ?? null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The number mask to apply for the currently typed `digits` (N1, part 2 — the mechanism Part 1 alone
|
|
147
|
+
* cannot fix). Masking with only the single method `detectNetworkMethod` currently resolves to can cap
|
|
148
|
+
* the field's capacity below what a *different*, still-relaxed-matching method would need once more
|
|
149
|
+
* digits arrive — e.g. a 14-digit network's own mask, while the shopper is mid-typing a 19-digit
|
|
150
|
+
* network whose fixed prefix nests inside the shorter one's. Once that happens, every keystroke past 14
|
|
151
|
+
* truncates straight back down to 14 digits, forever — the shopper can never reach the length at which
|
|
152
|
+
* the exact regex (Part 1, above) would resolve correctly, trading a cosmetic mis-mask for a hard block.
|
|
153
|
+
*
|
|
154
|
+
* Never resolves narrower than the widest `numberMask` among every method whose *relaxed* regex still
|
|
155
|
+
* matches, so the field's capacity always keeps pace with the longest network still in contention; only
|
|
156
|
+
* once a network drops out of contention (or an exact match resolves) does the mask narrow back down.
|
|
157
|
+
*/
|
|
158
|
+
function resolveNumberMask(methods, digits) {
|
|
159
|
+
const candidateMasks = relaxedCandidates(methods, digits).map((method) => method.validator.numberMask).filter((mask) => Boolean(mask));
|
|
160
|
+
if (candidateMasks.length === 0) return DEFAULT_NUMBER_MASK;
|
|
161
|
+
return candidateMasks.reduce((widest, mask) => maskCapacity(mask) > maskCapacity(widest) ? mask : widest);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The method this component treats as "detected" at the current `digits` — the single source both the
|
|
165
|
+
* render-scoped badge/validator and `handleNumberChange`'s dispatch decision read from, so the two can
|
|
166
|
+
* never disagree (FR-2). See the banner above for why this is not `detectNetworkMethod(methods, digits)`
|
|
167
|
+
* called directly: the exact-match branch here always uses `methods` in the store's own configured
|
|
168
|
+
* order (T22-2 — ties are the store's call, never a derived signal), and the relaxed-fallback branch
|
|
169
|
+
* only resolves when it is unambiguous, rather than silently favoring whichever candidate array order
|
|
170
|
+
* happens to put first (T22-1).
|
|
171
|
+
*/
|
|
172
|
+
function resolveNetworkMethod(methods, digits) {
|
|
173
|
+
if (!digits) return null;
|
|
174
|
+
const exact = exactMatch(methods, digits);
|
|
175
|
+
if (exact) return exact;
|
|
176
|
+
const candidates = relaxedCandidates(methods, digits);
|
|
177
|
+
return candidates.length === 1 ? candidates[0] ?? null : null;
|
|
178
|
+
}
|
|
179
|
+
/** The length this component slices its BIN to — the same eight digits `gateway.ts` sends as the
|
|
180
|
+
* payment body's BIN. */
|
|
181
|
+
const BIN_LENGTH = 8;
|
|
182
|
+
/**
|
|
183
|
+
* The BIN a selection carries: the first `BIN_LENGTH` of `digits`, or `null` while `digits` holds
|
|
184
|
+
* fewer — no selection is sent for a partial BIN. Callers pass the masked field's own digits, so "the
|
|
185
|
+
* gate is open" and "the BIN is complete" are one fact. Exported for direct unit testing; the package
|
|
186
|
+
* barrel (`src/components.ts`) does not re-export it.
|
|
187
|
+
*/
|
|
188
|
+
function resolveCompleteBin(digits) {
|
|
189
|
+
return digits.length >= BIN_LENGTH ? digits.slice(0, BIN_LENGTH) : null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Whether the N3/V4 effect should re-publish `detectedMethod` now that `draft.path` is `'new-card'`.
|
|
193
|
+
*
|
|
194
|
+
* - `previousPath === 'saved-card'`: only a genuine *return* from the saved-card detour re-dispatches —
|
|
195
|
+
* the very first activation transitions `null` → `'new-card'` too, and `handleNumberChange`'s own
|
|
196
|
+
* dispatch already covers that first selection; re-dispatching here as well would double-count it.
|
|
197
|
+
* - `revealedByCardMode` (T191, FR-12): the other return — the card payment was collapsed on the
|
|
198
|
+
* previous commit and card mode is what brought it back. Omitted means it was not. Both returns share
|
|
199
|
+
* this one guard and the effect's one send, so they never both send for one commit.
|
|
200
|
+
* - `detectedMethod !== null`: nothing to (re-)publish yet.
|
|
201
|
+
* - `lastDispatched === null`: `handleNumberChange` has not already dispatched this exact commit (V4) —
|
|
202
|
+
* otherwise this would double-post the identical `{ methodId, bin }` pair.
|
|
203
|
+
*/
|
|
204
|
+
function shouldRedispatchOnReturnToNewCard({ previousPath, revealedByCardMode = false, detectedMethod, lastDispatched }) {
|
|
205
|
+
return (previousPath === "saved-card" || revealedByCardMode) && detectedMethod !== null && lastDispatched === null;
|
|
206
|
+
}
|
|
207
|
+
const NETWORK_ICON_BY_KEY = {
|
|
208
|
+
visa: "visa",
|
|
209
|
+
mastercard: "mastercard",
|
|
210
|
+
amex: "amex",
|
|
211
|
+
americanexpress: "amex",
|
|
212
|
+
diners: "diners",
|
|
213
|
+
dinersclub: "diners",
|
|
214
|
+
elo: "elo-card"
|
|
215
|
+
};
|
|
216
|
+
function networkIconName(name) {
|
|
217
|
+
const key = name.toLowerCase().replace(/[^a-z]/g, "");
|
|
218
|
+
return NETWORK_ICON_BY_KEY[key] ?? null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Standard Luhn checksum. Rejects non-digit strings and anything shorter than 12 digits — no real
|
|
222
|
+
* card network issues a PAN shorter than that.
|
|
223
|
+
*/
|
|
224
|
+
function isValidLuhn(digits) {
|
|
225
|
+
if (digits.length < 12 || !/^\d+$/.test(digits)) return false;
|
|
226
|
+
let sum = 0;
|
|
227
|
+
let double = false;
|
|
228
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
229
|
+
let digit = Number(digits[i]);
|
|
230
|
+
if (double) {
|
|
231
|
+
digit *= 2;
|
|
232
|
+
if (digit > 9) digit -= 9;
|
|
233
|
+
}
|
|
234
|
+
sum += digit;
|
|
235
|
+
double = !double;
|
|
236
|
+
}
|
|
237
|
+
return sum % 10 === 0;
|
|
238
|
+
}
|
|
239
|
+
const EXPIRATION_FORMAT = /^(0[1-9]|1[0-2])\/(\d{2})$/;
|
|
240
|
+
/** `MM/YY` structural check: month 01–12, two-digit year. */
|
|
241
|
+
function isExpirationFormatValid(value) {
|
|
242
|
+
return EXPIRATION_FORMAT.test(value);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* True when the printed `MM/YY` has not yet ended — cards are valid through the end of their
|
|
246
|
+
* printed month. `now` is injectable for deterministic tests.
|
|
247
|
+
*/
|
|
248
|
+
function isExpirationInFuture(value, now = /* @__PURE__ */ new Date()) {
|
|
249
|
+
const match = EXPIRATION_FORMAT.exec(value);
|
|
250
|
+
if (!match) return false;
|
|
251
|
+
const month = Number(match[1]);
|
|
252
|
+
const year = 2e3 + Number(match[2]);
|
|
253
|
+
return new Date(year, month, 0, 23, 59, 59, 999).getTime() >= now.getTime();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Normalizes a 4-digit-year expiration (`MM/YYYY`, e.g. from a browser/password-manager `cc-exp`
|
|
257
|
+
* autofill, or the equivalent pasted digits) down to `MM/YY` before masking (F3) — mirrors
|
|
258
|
+
* `gateway.ts`'s own `toDueDate` two-slice scheme (month = first two digits, year = last two).
|
|
259
|
+
* Without this, the mask's blind 4-digit slot cap truncated `122030` (from an autofilled `12/2030`)
|
|
260
|
+
* to `1220` — keeping the *first* two digits of the year instead of the last two — silently turning
|
|
261
|
+
* a card valid through 2030 into one that reads as already expired in 2020.
|
|
262
|
+
*
|
|
263
|
+
* T172, Decision 5: the recombination applies to a genuine six-digit shape **only**. Applied to any
|
|
264
|
+
* count above four — F3's original rule — it also claims the far more common overflow case: the
|
|
265
|
+
* field already holds a complete `MM/YY` and the shopper types one more digit, so the raw value is
|
|
266
|
+
* five digits (`12/25` + `9` → `12259`). Recombining that yields `12` + `59` = `12/59`: the year's
|
|
267
|
+
* first digit is dropped, the second slides down a place, and the new digit takes the tens place — a
|
|
268
|
+
* value the shopper never typed as a sequence, with no keystroke ever visibly refused.
|
|
269
|
+
*
|
|
270
|
+
* Digit count alone still separates the three real input shapes (typed digit-by-digit ≤ 4,
|
|
271
|
+
* autofill/paste of `MM/YYYY` = 6, accidental overflow = anything else). Fix round 1 (review finding
|
|
272
|
+
* #1): every other length refuses the keystroke by returning `previousDigits` — whatever the field
|
|
273
|
+
* already held — rather than `digits.slice(0, 4)`. Slicing the *new* raw string only reconstructs the
|
|
274
|
+
* original value when the intruding digit lands at the very end; caught anywhere else (e.g. the caret
|
|
275
|
+
* placed right after the `/`), the first four characters of the new string mix a surviving old digit
|
|
276
|
+
* with the intruding one, silently producing a date the shopper never typed or saw form (`12/30`,
|
|
277
|
+
* caret after the `/`, types `9` → raw `12930` → `slice(0, 4)` = `1293` → `12/93`). Returning
|
|
278
|
+
* `previousDigits` makes the refusal caret-position independent: the displayed value is provably
|
|
279
|
+
* unchanged, not merely unchanged for the one caret position a naive slice happens to get right. No
|
|
280
|
+
* known browser or password manager delivers `MM/YYYY` in anything but six digits; a future variant is
|
|
281
|
+
* a deliberate, reviewed addition here rather than a silent gap.
|
|
282
|
+
*/
|
|
283
|
+
function normalizeExpirationDigits(digits, previousDigits) {
|
|
284
|
+
if (digits.length <= 4) return digits;
|
|
285
|
+
if (digits.length === 6) return `${digits.slice(0, 2)}${digits.slice(-2)}`;
|
|
286
|
+
return previousDigits;
|
|
287
|
+
}
|
|
288
|
+
function isValidCvv(value, validator) {
|
|
289
|
+
if (validator.cvvRegex) return safeTest(validator.cvvRegex, value);
|
|
290
|
+
return /^\d{3,4}$/.test(value);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Whether network detection could ever resolve to `method`. A method with no `numberRegex` never can
|
|
294
|
+
* (`resolveNetworkMethod`/`detectNetworkMethod` both require one): a promissory line of credit, a store
|
|
295
|
+
* credit balance, or a card-group method configured without a pattern. Not the section's card
|
|
296
|
+
* expression, `isCardMethod`, which counts that last one as a card (T191, Decision 1 and FR-14).
|
|
297
|
+
*
|
|
298
|
+
* Its one caller is `validateDraft`'s own `networkDetectionPossible` gate below (the F7/N2
|
|
299
|
+
* validation-message fix) — a single call site reads as dead weight here, and it is not.
|
|
300
|
+
*/
|
|
301
|
+
function isCardShapedMethod(method) {
|
|
302
|
+
return Boolean(method.validator.numberRegex);
|
|
303
|
+
}
|
|
304
|
+
function validateDraft(draft, validator, methods, messages) {
|
|
305
|
+
const errors = {};
|
|
306
|
+
const digits = toDigits(draft.number);
|
|
307
|
+
const networkDetectionPossible = methods.some(isCardShapedMethod);
|
|
308
|
+
if (!digits) errors.number = messages.numberRequired;
|
|
309
|
+
else if (!isValidLuhn(digits)) errors.number = messages.numberInvalid;
|
|
310
|
+
else if (networkDetectionPossible && !(validator.numberRegex && safeTest(validator.numberRegex, digits))) errors.number = messages.numberInvalid;
|
|
311
|
+
if (validator.holderNameRequired && !draft.holderName.trim()) errors.holderName = messages.holderNameRequired;
|
|
312
|
+
if (validator.expirationRequired) {
|
|
313
|
+
if (!draft.expiration.trim()) errors.expiration = messages.expirationRequired;
|
|
314
|
+
else if (!isExpirationFormatValid(draft.expiration)) errors.expiration = messages.expirationInvalid;
|
|
315
|
+
else if (!isExpirationInFuture(draft.expiration)) errors.expiration = messages.expirationExpired;
|
|
316
|
+
}
|
|
317
|
+
if (validator.cvvRequired) {
|
|
318
|
+
if (!draft.cvv.trim()) errors.cvv = messages.cvvRequired;
|
|
319
|
+
else if (!isValidCvv(draft.cvv, validator)) errors.cvv = messages.cvvInvalid;
|
|
320
|
+
}
|
|
321
|
+
return errors;
|
|
322
|
+
}
|
|
323
|
+
const CARD_ENTRY_INVALID_DATA_CODE = "INVALID_CARD_DATA";
|
|
324
|
+
const INVALID_CARD_DATA_ERROR = {
|
|
325
|
+
code: CARD_ENTRY_INVALID_DATA_CODE,
|
|
326
|
+
message: "Enter valid card details before placing the order."
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* The submission and pre-flight refusal for a card form the shopper cannot currently see (T74,
|
|
330
|
+
* Decision 5), and for a visible form's valid card under a persisted method that cannot take it: one
|
|
331
|
+
* this section does not serve, one that is not a card (T191, FR-13), or another card network than the
|
|
332
|
+
* typed number's (T191 fix round 1, finding R1). Distinct from `INVALID_CARD_DATA`: the card may be
|
|
333
|
+
* perfectly valid — the section is simply not on a method this form can pay it with. A fourth
|
|
334
|
+
* client-synthesized `CardCaptureError` in `matter/`, beside `CARD_ENTRY_INVALID_DATA_CODE`.
|
|
335
|
+
*/
|
|
336
|
+
const CARD_ENTRY_COLLAPSED_CODE = "CARD_ENTRY_COLLAPSED";
|
|
337
|
+
const CARD_ENTRY_COLLAPSED_ERROR = {
|
|
338
|
+
code: CARD_ENTRY_COLLAPSED_CODE,
|
|
339
|
+
message: "This card is not available for the currently selected payment method."
|
|
340
|
+
};
|
|
341
|
+
const CardEntryDefinition = defineComponent({
|
|
342
|
+
name: "CardEntry",
|
|
343
|
+
importPath: "@vtex/faststore/components",
|
|
344
|
+
component: CardEntry,
|
|
345
|
+
requiresContext: "card-capture",
|
|
346
|
+
editor: {
|
|
347
|
+
label: "Card Entry",
|
|
348
|
+
category: "commerce"
|
|
349
|
+
},
|
|
350
|
+
schema: {
|
|
351
|
+
legend: {
|
|
352
|
+
type: "string",
|
|
353
|
+
default: "Card details",
|
|
354
|
+
label: "Legend"
|
|
355
|
+
},
|
|
356
|
+
numberLabel: {
|
|
357
|
+
type: "string",
|
|
358
|
+
default: "Card number",
|
|
359
|
+
label: "Number label"
|
|
360
|
+
},
|
|
361
|
+
numberPlaceholder: {
|
|
362
|
+
type: "string",
|
|
363
|
+
label: "Number placeholder"
|
|
364
|
+
},
|
|
365
|
+
holderNameLabel: {
|
|
366
|
+
type: "string",
|
|
367
|
+
default: "Cardholder name",
|
|
368
|
+
label: "Holder name label"
|
|
369
|
+
},
|
|
370
|
+
holderNamePlaceholder: {
|
|
371
|
+
type: "string",
|
|
372
|
+
label: "Holder name placeholder"
|
|
373
|
+
},
|
|
374
|
+
expirationLabel: {
|
|
375
|
+
type: "string",
|
|
376
|
+
default: "Expiration date",
|
|
377
|
+
label: "Expiration label"
|
|
378
|
+
},
|
|
379
|
+
expirationPlaceholder: {
|
|
380
|
+
type: "string",
|
|
381
|
+
default: "MM/YY",
|
|
382
|
+
label: "Expiration placeholder"
|
|
383
|
+
},
|
|
384
|
+
cvvLabel: {
|
|
385
|
+
type: "string",
|
|
386
|
+
default: "Security code",
|
|
387
|
+
label: "CVV label"
|
|
388
|
+
},
|
|
389
|
+
cvvPlaceholder: {
|
|
390
|
+
type: "string",
|
|
391
|
+
label: "CVV placeholder"
|
|
392
|
+
},
|
|
393
|
+
cvvDescription: {
|
|
394
|
+
type: "string",
|
|
395
|
+
default: "The 3-4 digit number on your card",
|
|
396
|
+
label: "CVV description"
|
|
397
|
+
},
|
|
398
|
+
numberRequiredMessage: {
|
|
399
|
+
type: "string",
|
|
400
|
+
default: "Card number is required",
|
|
401
|
+
label: "Number required message"
|
|
402
|
+
},
|
|
403
|
+
numberInvalidMessage: {
|
|
404
|
+
type: "string",
|
|
405
|
+
default: "Enter a valid card number",
|
|
406
|
+
label: "Number invalid message"
|
|
407
|
+
},
|
|
408
|
+
holderNameRequiredMessage: {
|
|
409
|
+
type: "string",
|
|
410
|
+
default: "Cardholder name is required",
|
|
411
|
+
label: "Holder name required message"
|
|
412
|
+
},
|
|
413
|
+
expirationRequiredMessage: {
|
|
414
|
+
type: "string",
|
|
415
|
+
default: "Expiration date is required",
|
|
416
|
+
label: "Expiration required message"
|
|
417
|
+
},
|
|
418
|
+
expirationInvalidMessage: {
|
|
419
|
+
type: "string",
|
|
420
|
+
default: "Enter a valid expiration date",
|
|
421
|
+
label: "Expiration invalid message"
|
|
422
|
+
},
|
|
423
|
+
expirationExpiredMessage: {
|
|
424
|
+
type: "string",
|
|
425
|
+
default: "This card has expired",
|
|
426
|
+
label: "Expiration expired message"
|
|
427
|
+
},
|
|
428
|
+
cvvRequiredMessage: {
|
|
429
|
+
type: "string",
|
|
430
|
+
default: "Security code is required",
|
|
431
|
+
label: "CVV required message"
|
|
432
|
+
},
|
|
433
|
+
cvvInvalidMessage: {
|
|
434
|
+
type: "string",
|
|
435
|
+
default: "Enter a valid security code",
|
|
436
|
+
label: "CVV invalid message"
|
|
437
|
+
},
|
|
438
|
+
disabled: {
|
|
439
|
+
type: "boolean",
|
|
440
|
+
default: false,
|
|
441
|
+
label: "Disabled"
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
guideline: "New-card entry: masked number with live network detection, holder name, expiration, and CVV, each shown and required per the detected network's validator config from the 'card-capture' context. Must be placed inside a card-capture provider: PaymentSectionRoot or PaymentSectionController at checkout. Owns the card values itself — it builds the card and hands it only to the submission target its provider supplies; no other component ever needs the card number or CVV. Card number and CVV are never schema props, fragment fields, or action payloads — do not add one. See card-entry.md."
|
|
445
|
+
});
|
|
446
|
+
function CardEntry({ legend = "Card details", numberLabel = "Card number", numberPlaceholder, holderNameLabel = "Cardholder name", holderNamePlaceholder, expirationLabel = "Expiration date", expirationPlaceholder = "MM/YY", cvvLabel = "Security code", cvvPlaceholder, cvvDescription = "The 3-4 digit number on your card", numberRequiredMessage = "Card number is required", numberInvalidMessage = "Enter a valid card number", holderNameRequiredMessage = "Cardholder name is required", expirationRequiredMessage = "Expiration date is required", expirationInvalidMessage = "Enter a valid expiration date", expirationExpiredMessage = "This card has expired", cvvRequiredMessage = "Security code is required", cvvInvalidMessage = "Enter a valid security code", disabled = false, className }) {
|
|
447
|
+
const { networks: methods, busy: placing, clearOn, onNetworkChange, selection } = useCardCapture();
|
|
448
|
+
const draftPath = selection?.path ?? null;
|
|
449
|
+
const draftBin = selection?.bin ?? null;
|
|
450
|
+
const collapsed = selection?.collapsed ?? false;
|
|
451
|
+
const cardModeActive = selection?.cardModeActive ?? false;
|
|
452
|
+
const selectedMethodId = selection?.selectedMethodId ?? null;
|
|
453
|
+
const selectionOutOfScope = selection?.selectionOutOfScope ?? false;
|
|
454
|
+
const selectionNonCard = selection?.selectionNonCard ?? false;
|
|
455
|
+
const selectMethod = selection?.selectMethod;
|
|
456
|
+
const validationMessages = useMemo(() => ({
|
|
457
|
+
numberRequired: numberRequiredMessage,
|
|
458
|
+
numberInvalid: numberInvalidMessage,
|
|
459
|
+
holderNameRequired: holderNameRequiredMessage,
|
|
460
|
+
expirationRequired: expirationRequiredMessage,
|
|
461
|
+
expirationInvalid: expirationInvalidMessage,
|
|
462
|
+
expirationExpired: expirationExpiredMessage,
|
|
463
|
+
cvvRequired: cvvRequiredMessage,
|
|
464
|
+
cvvInvalid: cvvInvalidMessage
|
|
465
|
+
}), [
|
|
466
|
+
numberRequiredMessage,
|
|
467
|
+
numberInvalidMessage,
|
|
468
|
+
holderNameRequiredMessage,
|
|
469
|
+
expirationRequiredMessage,
|
|
470
|
+
expirationInvalidMessage,
|
|
471
|
+
expirationExpiredMessage,
|
|
472
|
+
cvvRequiredMessage,
|
|
473
|
+
cvvInvalidMessage
|
|
474
|
+
]);
|
|
475
|
+
const [number, setNumber] = useState("");
|
|
476
|
+
const [holderName, setHolderName] = useState("");
|
|
477
|
+
const [expiration, setExpiration] = useState("");
|
|
478
|
+
const [cvv, setCvv] = useState("");
|
|
479
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
480
|
+
const rootRef = useRef(null);
|
|
481
|
+
const lastDispatchedRef = useRef(null);
|
|
482
|
+
const previousDraftPathRef = useRef(draftPath);
|
|
483
|
+
const reportedNetworkIdRef = useRef(null);
|
|
484
|
+
const previousCollapsedRef = useRef(collapsed);
|
|
485
|
+
const digits = toDigits(number);
|
|
486
|
+
const detectedMethod = useMemo(() => resolveNetworkMethod(methods, digits), [methods, digits]);
|
|
487
|
+
const validator = detectedMethod?.validator ?? DEFAULT_VALIDATOR;
|
|
488
|
+
const selectionOtherNetwork = selectedMethodId !== null && detectedMethod !== null && detectedMethod.id !== selectedMethodId;
|
|
489
|
+
const detectedNetworkIcon = detectedMethod ? networkIconName(detectedMethod.name) : null;
|
|
490
|
+
const cvvComplete = isSecurityCodeComplete(cvv, narrowestMaskCapacity(validator.cvvMask ?? DEFAULT_CVV_MASK));
|
|
491
|
+
const detectedNetworkId = detectedMethod?.id ?? null;
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
if (reportedNetworkIdRef.current === detectedNetworkId) return;
|
|
494
|
+
reportedNetworkIdRef.current = detectedNetworkId;
|
|
495
|
+
onNetworkChange?.(detectedNetworkId);
|
|
496
|
+
}, [detectedNetworkId, onNetworkChange]);
|
|
497
|
+
useEffect(() => {
|
|
498
|
+
const previousPath = previousDraftPathRef.current;
|
|
499
|
+
previousDraftPathRef.current = draftPath;
|
|
500
|
+
const revealedByCardMode = previousCollapsedRef.current && !collapsed && cardModeActive;
|
|
501
|
+
previousCollapsedRef.current = collapsed;
|
|
502
|
+
if (draftPath !== "new-card") {
|
|
503
|
+
lastDispatchedRef.current = null;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (!shouldRedispatchOnReturnToNewCard({
|
|
507
|
+
previousPath,
|
|
508
|
+
revealedByCardMode,
|
|
509
|
+
detectedMethod,
|
|
510
|
+
lastDispatched: lastDispatchedRef.current
|
|
511
|
+
}) || !detectedMethod) return;
|
|
512
|
+
const bin = resolveCompleteBin(digits);
|
|
513
|
+
if (bin === null) return;
|
|
514
|
+
lastDispatchedRef.current = {
|
|
515
|
+
methodId: detectedMethod.id,
|
|
516
|
+
bin
|
|
517
|
+
};
|
|
518
|
+
selectMethod?.({
|
|
519
|
+
methodId: detectedMethod.id,
|
|
520
|
+
bin
|
|
521
|
+
});
|
|
522
|
+
}, [
|
|
523
|
+
draftPath,
|
|
524
|
+
collapsed,
|
|
525
|
+
cardModeActive,
|
|
526
|
+
detectedMethod,
|
|
527
|
+
digits,
|
|
528
|
+
selectMethod
|
|
529
|
+
]);
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
if (collapsed) {
|
|
532
|
+
setCvv("");
|
|
533
|
+
setFieldErrors((prev) => {
|
|
534
|
+
if (!("cvv" in prev)) return prev;
|
|
535
|
+
const next = { ...prev };
|
|
536
|
+
delete next.cvv;
|
|
537
|
+
return next;
|
|
538
|
+
});
|
|
539
|
+
lastDispatchedRef.current = null;
|
|
540
|
+
}
|
|
541
|
+
}, [collapsed]);
|
|
542
|
+
const focusField = useCallback((key) => {
|
|
543
|
+
(rootRef.current?.querySelector(`[data-fs-card-entry-field="${FIELD_ATTR[key]}"] input`))?.focus();
|
|
544
|
+
}, []);
|
|
545
|
+
function clearFieldError(key) {
|
|
546
|
+
setFieldErrors((prev) => {
|
|
547
|
+
if (!(key in prev)) return prev;
|
|
548
|
+
const next = { ...prev };
|
|
549
|
+
delete next[key];
|
|
550
|
+
return next;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function runFieldValidation(key, draftValue) {
|
|
554
|
+
const errors = validateDraft(draftValue, validator, methods, validationMessages);
|
|
555
|
+
setFieldErrors((prev) => {
|
|
556
|
+
const next = { ...prev };
|
|
557
|
+
if (errors[key]) next[key] = errors[key];
|
|
558
|
+
else delete next[key];
|
|
559
|
+
return next;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function handleNumberChange(event) {
|
|
563
|
+
const rawDigits = toDigits(event.target.value);
|
|
564
|
+
const matched = resolveNetworkMethod(methods, rawDigits);
|
|
565
|
+
const masked = maskDigits(rawDigits, resolveNumberMask(methods, rawDigits));
|
|
566
|
+
setNumber(masked);
|
|
567
|
+
clearFieldError("number");
|
|
568
|
+
const nextDigits = toDigits(masked);
|
|
569
|
+
if (nextDigits.length > 0 && draftPath !== "new-card") selection?.selectNewCard();
|
|
570
|
+
const bin = resolveCompleteBin(nextDigits);
|
|
571
|
+
if (matched) {
|
|
572
|
+
const last = lastDispatchedRef.current;
|
|
573
|
+
if (selection && bin !== null && (last?.methodId !== matched.id || last.bin !== bin || draftBin !== last.bin)) {
|
|
574
|
+
selection.setDraftBin(bin);
|
|
575
|
+
lastDispatchedRef.current = {
|
|
576
|
+
methodId: matched.id,
|
|
577
|
+
bin
|
|
578
|
+
};
|
|
579
|
+
selection.selectMethod({
|
|
580
|
+
methodId: matched.id,
|
|
581
|
+
bin
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
} else selection?.setDraftBin(null);
|
|
585
|
+
}
|
|
586
|
+
function handleNumberBlur() {
|
|
587
|
+
runFieldValidation("number", {
|
|
588
|
+
number,
|
|
589
|
+
holderName,
|
|
590
|
+
expiration,
|
|
591
|
+
cvv
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function handleHolderNameChange(event) {
|
|
595
|
+
setHolderName(event.target.value);
|
|
596
|
+
clearFieldError("holderName");
|
|
597
|
+
}
|
|
598
|
+
function handleHolderNameBlur() {
|
|
599
|
+
runFieldValidation("holderName", {
|
|
600
|
+
number,
|
|
601
|
+
holderName,
|
|
602
|
+
expiration,
|
|
603
|
+
cvv
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function handleExpirationChange(event) {
|
|
607
|
+
const digits = normalizeExpirationDigits(toDigits(event.target.value), toDigits(expiration));
|
|
608
|
+
setExpiration(maskDigits(digits, DEFAULT_EXPIRATION_MASK));
|
|
609
|
+
clearFieldError("expiration");
|
|
610
|
+
}
|
|
611
|
+
function handleExpirationBlur() {
|
|
612
|
+
runFieldValidation("expiration", {
|
|
613
|
+
number,
|
|
614
|
+
holderName,
|
|
615
|
+
expiration,
|
|
616
|
+
cvv
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function handleCvvChange(event) {
|
|
620
|
+
setCvv(maskDigits(event.target.value, validator.cvvMask ?? DEFAULT_CVV_MASK));
|
|
621
|
+
clearFieldError("cvv");
|
|
622
|
+
}
|
|
623
|
+
function handleCvvBlur() {
|
|
624
|
+
runFieldValidation("cvv", {
|
|
625
|
+
number,
|
|
626
|
+
holderName,
|
|
627
|
+
expiration,
|
|
628
|
+
cvv
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const checkDraft = useCallback(() => {
|
|
632
|
+
if (collapsed) return [CARD_ENTRY_COLLAPSED_ERROR];
|
|
633
|
+
const errors = validateDraft({
|
|
634
|
+
number,
|
|
635
|
+
holderName,
|
|
636
|
+
expiration,
|
|
637
|
+
cvv
|
|
638
|
+
}, validator, methods, validationMessages);
|
|
639
|
+
if (Object.keys(errors).length === 0) return selectionOutOfScope || selectionNonCard || selectionOtherNetwork ? [CARD_ENTRY_COLLAPSED_ERROR] : [];
|
|
640
|
+
setFieldErrors(errors);
|
|
641
|
+
const firstInvalid = VALIDATION_ORDER.find((key) => errors[key]);
|
|
642
|
+
if (firstInvalid) focusField(firstInvalid);
|
|
643
|
+
return [INVALID_CARD_DATA_ERROR];
|
|
644
|
+
}, [
|
|
645
|
+
collapsed,
|
|
646
|
+
number,
|
|
647
|
+
holderName,
|
|
648
|
+
expiration,
|
|
649
|
+
cvv,
|
|
650
|
+
validator,
|
|
651
|
+
methods,
|
|
652
|
+
focusField,
|
|
653
|
+
validationMessages,
|
|
654
|
+
selectionOutOfScope,
|
|
655
|
+
selectionNonCard,
|
|
656
|
+
selectionOtherNetwork
|
|
657
|
+
]);
|
|
658
|
+
const capture = useCallback(async (send) => {
|
|
659
|
+
const refusals = checkDraft();
|
|
660
|
+
if (refusals.length > 0) return refusals;
|
|
661
|
+
const card = {
|
|
662
|
+
number,
|
|
663
|
+
securityCode: cvv,
|
|
664
|
+
expirationDate: expiration,
|
|
665
|
+
holderName: holderName.trim()
|
|
666
|
+
};
|
|
667
|
+
let succeeded = false;
|
|
668
|
+
try {
|
|
669
|
+
const userErrors = await send(card);
|
|
670
|
+
succeeded = userErrors.length === 0;
|
|
671
|
+
return userErrors;
|
|
672
|
+
} finally {
|
|
673
|
+
if (succeeded || clearOn === "any-outcome") {
|
|
674
|
+
setNumber("");
|
|
675
|
+
setHolderName("");
|
|
676
|
+
setExpiration("");
|
|
677
|
+
setCvv("");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}, [
|
|
681
|
+
checkDraft,
|
|
682
|
+
number,
|
|
683
|
+
holderName,
|
|
684
|
+
expiration,
|
|
685
|
+
cvv,
|
|
686
|
+
clearOn
|
|
687
|
+
]);
|
|
688
|
+
useRegisterCardCapture(capture, checkDraft);
|
|
689
|
+
return /* @__PURE__ */ jsx("div", {
|
|
690
|
+
hidden: collapsed,
|
|
691
|
+
"data-fs-card-entry-collapse-boundary": "",
|
|
692
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
693
|
+
ref: rootRef,
|
|
694
|
+
className,
|
|
695
|
+
...placing || collapsed ? { inert: true } : {},
|
|
696
|
+
"aria-disabled": placing || void 0,
|
|
697
|
+
"data-fs-card-entry": "",
|
|
698
|
+
...placing ? { "data-fs-card-entry-placing": "" } : {},
|
|
699
|
+
...disabled ? { "data-fs-card-entry-disabled": "" } : {},
|
|
700
|
+
...collapsed ? { "data-fs-card-entry-collapsed": "" } : {},
|
|
701
|
+
children: /* @__PURE__ */ jsxs(Fieldset.Root, {
|
|
702
|
+
disabled: disabled || collapsed,
|
|
703
|
+
"data-fs-card-entry-fieldset": "",
|
|
704
|
+
children: [
|
|
705
|
+
/* @__PURE__ */ jsx(Fieldset.Legend, { children: legend }),
|
|
706
|
+
/* @__PURE__ */ jsx("div", {
|
|
707
|
+
"data-fs-card-entry-field": "number",
|
|
708
|
+
children: /* @__PURE__ */ jsxs(Field.Root, {
|
|
709
|
+
invalid: Boolean(fieldErrors.number),
|
|
710
|
+
disabled,
|
|
711
|
+
"data-fs-card-entry-number": "",
|
|
712
|
+
children: [
|
|
713
|
+
/* @__PURE__ */ jsx(Field.Label, { children: numberLabel }),
|
|
714
|
+
/* @__PURE__ */ jsxs("div", {
|
|
715
|
+
"data-fs-card-entry-number-control": "",
|
|
716
|
+
children: [/* @__PURE__ */ jsx(Field.Control, {
|
|
717
|
+
name: "cardNumber",
|
|
718
|
+
inputMode: "numeric",
|
|
719
|
+
autoComplete: "cc-number",
|
|
720
|
+
placeholder: numberPlaceholder,
|
|
721
|
+
value: number,
|
|
722
|
+
onChange: handleNumberChange,
|
|
723
|
+
onBlur: handleNumberBlur,
|
|
724
|
+
disabled
|
|
725
|
+
}), detectedMethod ? /* @__PURE__ */ jsx("div", {
|
|
726
|
+
"data-fs-card-entry-networks": "",
|
|
727
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
728
|
+
"data-fs-card-entry-network": "",
|
|
729
|
+
"data-fs-card-entry-network-id": detectedMethod.id,
|
|
730
|
+
children: detectedNetworkIcon ? /* @__PURE__ */ jsx(Icon, {
|
|
731
|
+
name: detectedNetworkIcon,
|
|
732
|
+
"data-fs-card-entry-network-icon": ""
|
|
733
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
734
|
+
"data-fs-card-entry-network-fallback": "",
|
|
735
|
+
children: detectedMethod.name
|
|
736
|
+
})
|
|
737
|
+
})
|
|
738
|
+
}) : null]
|
|
739
|
+
}),
|
|
740
|
+
fieldErrors.number ? /* @__PURE__ */ jsx(Field.Error, {
|
|
741
|
+
match: true,
|
|
742
|
+
children: fieldErrors.number
|
|
743
|
+
}) : null
|
|
744
|
+
]
|
|
745
|
+
})
|
|
746
|
+
}),
|
|
747
|
+
validator.holderNameRequired ? /* @__PURE__ */ jsx("div", {
|
|
748
|
+
"data-fs-card-entry-field": "holder-name",
|
|
749
|
+
children: /* @__PURE__ */ jsxs(Field.Root, {
|
|
750
|
+
invalid: Boolean(fieldErrors.holderName),
|
|
751
|
+
disabled,
|
|
752
|
+
children: [
|
|
753
|
+
/* @__PURE__ */ jsx(Field.Label, { children: holderNameLabel }),
|
|
754
|
+
/* @__PURE__ */ jsx(Field.Control, {
|
|
755
|
+
name: "holderName",
|
|
756
|
+
autoComplete: "cc-name",
|
|
757
|
+
placeholder: holderNamePlaceholder,
|
|
758
|
+
value: holderName,
|
|
759
|
+
onChange: handleHolderNameChange,
|
|
760
|
+
onBlur: handleHolderNameBlur,
|
|
761
|
+
disabled
|
|
762
|
+
}),
|
|
763
|
+
fieldErrors.holderName ? /* @__PURE__ */ jsx(Field.Error, {
|
|
764
|
+
match: true,
|
|
765
|
+
children: fieldErrors.holderName
|
|
766
|
+
}) : null
|
|
767
|
+
]
|
|
768
|
+
})
|
|
769
|
+
}) : null,
|
|
770
|
+
validator.expirationRequired ? /* @__PURE__ */ jsx("div", {
|
|
771
|
+
"data-fs-card-entry-field": "expiration",
|
|
772
|
+
children: /* @__PURE__ */ jsxs(Field.Root, {
|
|
773
|
+
invalid: Boolean(fieldErrors.expiration),
|
|
774
|
+
disabled,
|
|
775
|
+
children: [
|
|
776
|
+
/* @__PURE__ */ jsx(Field.Label, { children: expirationLabel }),
|
|
777
|
+
/* @__PURE__ */ jsx(Field.Control, {
|
|
778
|
+
name: "expiration",
|
|
779
|
+
inputMode: "numeric",
|
|
780
|
+
autoComplete: "cc-exp",
|
|
781
|
+
placeholder: expirationPlaceholder,
|
|
782
|
+
value: expiration,
|
|
783
|
+
onChange: handleExpirationChange,
|
|
784
|
+
onBlur: handleExpirationBlur,
|
|
785
|
+
disabled
|
|
786
|
+
}),
|
|
787
|
+
fieldErrors.expiration ? /* @__PURE__ */ jsx(Field.Error, {
|
|
788
|
+
match: true,
|
|
789
|
+
children: fieldErrors.expiration
|
|
790
|
+
}) : null
|
|
791
|
+
]
|
|
792
|
+
})
|
|
793
|
+
}) : null,
|
|
794
|
+
validator.cvvRequired ? /* @__PURE__ */ jsx("div", {
|
|
795
|
+
"data-fs-card-entry-field": "cvv",
|
|
796
|
+
children: /* @__PURE__ */ jsxs(Field.Root, {
|
|
797
|
+
invalid: Boolean(fieldErrors.cvv),
|
|
798
|
+
disabled,
|
|
799
|
+
children: [
|
|
800
|
+
/* @__PURE__ */ jsx(Field.Label, { children: cvvLabel }),
|
|
801
|
+
/* @__PURE__ */ jsx(Field.Control, {
|
|
802
|
+
name: "cvv",
|
|
803
|
+
type: "text",
|
|
804
|
+
inputMode: "numeric",
|
|
805
|
+
autoComplete: "cc-csc",
|
|
806
|
+
placeholder: cvvPlaceholder,
|
|
807
|
+
value: cvv,
|
|
808
|
+
onChange: handleCvvChange,
|
|
809
|
+
onBlur: handleCvvBlur,
|
|
810
|
+
disabled,
|
|
811
|
+
style: cvvComplete ? SECURITY_CODE_MASK_STYLE : void 0
|
|
812
|
+
}),
|
|
813
|
+
cvvDescription ? /* @__PURE__ */ jsx(Field.Description, {
|
|
814
|
+
"data-fs-card-entry-cvv-description": "",
|
|
815
|
+
children: cvvDescription
|
|
816
|
+
}) : null,
|
|
817
|
+
fieldErrors.cvv ? /* @__PURE__ */ jsx(Field.Error, {
|
|
818
|
+
match: true,
|
|
819
|
+
children: fieldErrors.cvv
|
|
820
|
+
}) : null
|
|
821
|
+
]
|
|
822
|
+
})
|
|
823
|
+
}) : null
|
|
824
|
+
]
|
|
825
|
+
})
|
|
826
|
+
})
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
export { CARD_ENTRY_COLLAPSED_CODE, CARD_ENTRY_INVALID_DATA_CODE, CardEntry, CardEntryDefinition, NETWORK_ICON_BY_KEY, detectNetworkMethod, isExpirationFormatValid, isExpirationInFuture, isValidLuhn, resolveCompleteBin, resolveNetworkMethod, shouldRedispatchOnReturnToNewCard };
|
|
831
|
+
|
|
832
|
+
//# sourceMappingURL=card-entry.js.map
|