@vtex/faststore 0.0.8 → 0.0.10
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 +59 -0
- package/dist/actions.json +42 -0
- package/dist/catalog.json +731 -16
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/components.d.ts +9 -4
- package/dist/components.js +11 -6
- package/dist/content-types.json +300 -0
- package/dist/core/dist/define-component.d.ts +6 -1
- package/dist/docs/account-entry-point.md +4 -4
- package/dist/docs/account-menu.md +2 -2
- package/dist/docs/account-saved-cards.md +301 -0
- package/dist/docs/binding-switcher.md +154 -0
- package/dist/docs/card-entry.md +3 -3
- package/dist/docs/card-summary.md +175 -0
- package/dist/docs/cart-destination.md +238 -0
- package/dist/docs/cart-item.md +21 -3
- package/dist/docs/cart-totalizer.md +1 -1
- package/dist/docs/cart.md +118 -11
- package/dist/docs/footer-social.md +1 -1
- package/dist/docs/free-shipping-progress.md +13 -9
- package/dist/docs/incentives.md +2 -2
- package/dist/docs/logo.md +1 -1
- package/dist/docs/payment-method-badges.md +2 -2
- package/dist/docs/payment-methods.md +1 -1
- package/dist/docs/product-information.md +2 -2
- package/dist/docs/product-shelf.md +7 -0
- package/dist/docs/select.md +3 -4
- package/dist/docs/sign-in-redirect.md +1 -1
- package/dist/docs/toast.md +30 -12
- package/dist/index.d.ts +2 -1
- package/dist/internal/client.d.ts +4 -1
- package/dist/internal/client.js +4 -1
- package/dist/internal/server.d.ts +4 -2
- package/dist/internal/server.js +4 -2
- package/dist/matter/account-saved-cards/account-saved-cards.d.ts +383 -0
- package/dist/matter/account-saved-cards/account-saved-cards.js +956 -0
- package/dist/matter/account-saved-cards/account-saved-cards.js.map +1 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -1
- package/dist/matter/cart/cart-namespace.d.ts +5 -1
- package/dist/matter/cart/cart-namespace.js +5 -1
- package/dist/matter/cart/cart-namespace.js.map +1 -1
- package/dist/matter/cart/cart.d.ts +34 -4
- package/dist/matter/cart/cart.js +238 -28
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/cart-destination/cart-destination.d.ts +222 -0
- package/dist/matter/cart-destination/cart-destination.js +613 -0
- package/dist/matter/cart-destination/cart-destination.js.map +1 -0
- package/dist/matter/cart-item/cart-item.d.ts +4 -2
- package/dist/matter/cart-item/cart-item.js +75 -16
- package/dist/matter/cart-item/cart-item.js.map +1 -1
- package/dist/matter/cart-totalizer/cart-totalizer.js +3 -2
- package/dist/matter/cart-totalizer/cart-totalizer.js.map +1 -1
- package/dist/matter/free-shipping-progress/free-shipping-progress.d.ts +6 -3
- package/dist/matter/free-shipping-progress/free-shipping-progress.js +76 -33
- package/dist/matter/free-shipping-progress/free-shipping-progress.js.map +1 -1
- package/dist/matter/pricing/pricing.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +7 -0
- package/dist/matter/product-shelf/product-shelf.js +115 -1
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.d.ts +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.js +6 -2
- package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
- package/dist/particles/binding-switcher/binding-switcher.d.ts +38 -0
- package/dist/particles/binding-switcher/binding-switcher.js +183 -0
- package/dist/particles/binding-switcher/binding-switcher.js.map +1 -0
- package/dist/particles/card-summary/card-network.d.ts +17 -0
- package/dist/particles/card-summary/card-network.js +25 -0
- package/dist/particles/card-summary/card-network.js.map +1 -0
- package/dist/particles/card-summary/card-summary.d.ts +83 -0
- package/dist/particles/card-summary/card-summary.js +188 -0
- package/dist/particles/card-summary/card-summary.js.map +1 -0
- package/dist/particles/footer-social/footer-social.d.ts +1 -1
- package/dist/particles/footer-social/footer-social.js +1 -1
- package/dist/particles/footer-social/footer-social.js.map +1 -1
- package/dist/particles/incentives/incentives.d.ts +1 -1
- package/dist/particles/incentives/incentives.js +1 -1
- package/dist/particles/incentives/incentives.js.map +1 -1
- package/dist/particles/payment-methods/payment-methods.d.ts +1 -1
- package/dist/particles/payment-methods/payment-methods.js +1 -1
- package/dist/particles/payment-methods/payment-methods.js.map +1 -1
- package/dist/particles/toast/toast.d.ts +20 -6
- package/dist/particles/toast/toast.js +49 -7
- package/dist/particles/toast/toast.js.map +1 -1
- package/dist/schema.graphql +298 -34
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/locale-action-provider.d.ts +16 -0
- package/dist/sdk/actions/locale-action-provider.js +20 -0
- package/dist/sdk/actions/locale-action-provider.js.map +1 -0
- package/dist/sdk/actions/saved-card-action-provider.d.ts +12 -0
- package/dist/sdk/actions/saved-card-action-provider.js +16 -0
- package/dist/sdk/actions/saved-card-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/cache/storefront-cache.js +2 -1
- package/dist/sdk/cache/storefront-cache.js.map +1 -1
- package/dist/sdk/cart/operations.d.ts +2 -1
- package/dist/sdk/cart/operations.js +55 -1
- package/dist/sdk/cart/operations.js.map +1 -1
- package/dist/sdk/cart/optimistic.d.ts +30 -7
- package/dist/sdk/cart/optimistic.js +78 -41
- package/dist/sdk/cart/optimistic.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.d.ts +7 -0
- package/dist/sdk/cart/use-cart-actions.js +61 -15
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-sync-sales-channel.js +70 -0
- package/dist/sdk/cart/use-cart-sync-sales-channel.js.map +1 -0
- package/dist/sdk/generated/gql.js +15 -11
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.d.ts +177 -46
- package/dist/sdk/generated/graphql.js +2314 -93
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +19 -0
- package/dist/sdk/i18n/messages/en-US.json +19 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +19 -0
- package/dist/sdk/i18n/messages/es-ES.js +19 -0
- package/dist/sdk/i18n/messages/es-ES.json +19 -0
- package/dist/sdk/i18n/messages/pt-BR.js +19 -0
- package/dist/sdk/i18n/messages/pt-BR.json +19 -0
- package/dist/sdk/i18n/provider.js +1 -1
- package/dist/sdk/i18n/provider.js.map +1 -1
- package/dist/sdk/locale/binding-switch-provider.d.ts +13 -0
- package/dist/sdk/locale/binding-switch-provider.js +20 -0
- package/dist/sdk/locale/binding-switch-provider.js.map +1 -0
- package/dist/sdk/locale/use-locale-actions.d.ts +16 -0
- package/dist/sdk/locale/use-locale-actions.js +70 -0
- package/dist/sdk/locale/use-locale-actions.js.map +1 -0
- package/dist/sdk/navigation/binding-switch.d.ts +39 -0
- package/dist/sdk/navigation/binding-switch.js +129 -0
- package/dist/sdk/navigation/binding-switch.js.map +1 -0
- package/dist/sdk/navigation/decode-route-params.d.ts +11 -0
- package/dist/sdk/navigation/decode-route-params.js +32 -0
- package/dist/sdk/navigation/decode-route-params.js.map +1 -0
- package/dist/sdk/saved-cards/operations.d.ts +16 -0
- package/dist/sdk/saved-cards/operations.js +41 -0
- package/dist/sdk/saved-cards/operations.js.map +1 -0
- package/dist/sdk/saved-cards/use-saved-card-actions.d.ts +26 -0
- package/dist/sdk/saved-cards/use-saved-card-actions.js +86 -0
- package/dist/sdk/saved-cards/use-saved-card-actions.js.map +1 -0
- package/dist/sdk/seo/page-alternates.d.ts +5 -3
- package/dist/sdk/seo/page-alternates.js +7 -12
- package/dist/sdk/seo/page-alternates.js.map +1 -1
- package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
- package/dist/sdk/shipping/shipping-operations.js +13 -0
- package/dist/sdk/shipping/shipping-operations.js.map +1 -1
- package/dist/sdk/state/cart.d.ts +9 -2
- package/dist/sdk/state/cart.js +9 -2
- package/dist/sdk/state/cart.js.map +1 -1
- package/package.json +2 -2
- package/src/styles/tailwind.css +1 -2
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Account Saved Cards
|
|
2
|
+
|
|
3
|
+
**Category:** identity
|
|
4
|
+
**Purpose:** The signed-in shopper's saved cards in their account: a list of card summaries, or a statement that there are none, with loading and failure states, and a confirmed removal for each card. Needs no cart.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
- A section heading such as "Saved cards", "Payment methods", or "My cards" inside an account page.
|
|
11
|
+
- A vertical list of rows, each a card-network logo, a network name, and `•••• 1234`, with a line such as "Expires 05/28" under it.
|
|
12
|
+
- A dimmed row reading "Expired 05/24" or "This card can't be used".
|
|
13
|
+
- A "Remove" link or button in each row, opening a confirmation such as "Remove this card?" with Cancel and "Remove card".
|
|
14
|
+
- Or a single sentence such as "You have no saved cards yet." where the list would be.
|
|
15
|
+
- Usually beside the account's side navigation, never inside a checkout step.
|
|
16
|
+
|
|
17
|
+
The image is the source of truth for layout.
|
|
18
|
+
|
|
19
|
+
## When to use
|
|
20
|
+
|
|
21
|
+
- Showing the shopper the cards saved to their account, outside any purchase: the account's Payments page, or a block inside another account page.
|
|
22
|
+
|
|
23
|
+
## When NOT to use
|
|
24
|
+
|
|
25
|
+
- Picking a saved card to pay with during checkout → [[saved-cards]] (`SavedCards`, cart-scoped, with CVV entry).
|
|
26
|
+
- Describing one card on its own → [[card-summary]].
|
|
27
|
+
- The store's accepted payment methods as a strip of logos → [[payment-methods]].
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Composition
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
AccountSavedCardsRoot ← requires 'session'; provides 'account-saved-cards'; declares savedCard.remove
|
|
35
|
+
├── AccountSavedCardsHeading
|
|
36
|
+
├── AccountSavedCardsRemoveFeedback
|
|
37
|
+
├── AccountSavedCardsLoading
|
|
38
|
+
├── AccountSavedCardsError
|
|
39
|
+
├── AccountSavedCardsEmpty
|
|
40
|
+
└── AccountSavedCardsList ← provides 'account-saved-card'
|
|
41
|
+
└── AccountSavedCardItem ← one row per card; provides 'card-summary', 'account-saved-card-data'
|
|
42
|
+
├── CardSummary (default when the row has no children)
|
|
43
|
+
├── AccountSavedCardExpiry (default when the row has no children)
|
|
44
|
+
└── AccountSavedCardRemove (default when the row has no children) ← provides 'account-saved-card-remove'
|
|
45
|
+
└── AlertDialogRoot
|
|
46
|
+
├── AccountSavedCardRemoveTrigger
|
|
47
|
+
└── AlertDialogPortal > AlertDialogBackdrop + AlertDialogViewport > AlertDialogPopup
|
|
48
|
+
├── AccountSavedCardRemoveTitle
|
|
49
|
+
├── AccountSavedCardRemoveDescription
|
|
50
|
+
├── AccountSavedCardRemoveCancel
|
|
51
|
+
└── AccountSavedCardRemoveConfirm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`AccountSavedCardRemove` with no children renders the prompt shown under it.
|
|
55
|
+
|
|
56
|
+
The Root renders nothing unless the session is `loggedIn`, and no chrome: no navigation, no page title, no landmark. It derives one state and exposes it on `data-fs-account-saved-cards-state`:
|
|
57
|
+
|
|
58
|
+
| State | When | Renders |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `loading` | No cards yet and the read is in flight | `AccountSavedCardsLoading` |
|
|
61
|
+
| `error` | No cards and the read failed | `AccountSavedCardsError` |
|
|
62
|
+
| `empty` | The read resolved with no card | `AccountSavedCardsEmpty` |
|
|
63
|
+
| `ready` | The read resolved with cards | `AccountSavedCardsList` |
|
|
64
|
+
| `idle` | No cards and nothing in flight | Nothing but the heading |
|
|
65
|
+
|
|
66
|
+
The heading and the removal feedback render in every state. Every part except the Item must be inside the Root; the Item must be inside the List; the CardSummary parts, `AccountSavedCardExpiry`, and `AccountSavedCardRemove` inside the Item; and the five prompt parts inside `AccountSavedCardRemove`. A part outside its provider fails the build with a `ContextConstraintError`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Components
|
|
71
|
+
|
|
72
|
+
### AccountSavedCardsRoot
|
|
73
|
+
|
|
74
|
+
Reads the card ids (`customerSavedCards`, reactive) and publishes the section state. No schema props.
|
|
75
|
+
|
|
76
|
+
It also holds the section's removal: which card is being removed (one at a time) and how the last removal ended. It declares `actions: { savedCard: ['remove'] }`, so every remove control inside reaches the action, including the one a bare Item renders.
|
|
77
|
+
|
|
78
|
+
### AccountSavedCardsHeading
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Notes |
|
|
81
|
+
|------|------|---------|-------|
|
|
82
|
+
| `as` | `'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6'` | `h2` | No `h1`: the page title belongs to the page shell. |
|
|
83
|
+
|
|
84
|
+
### AccountSavedCardsLoading
|
|
85
|
+
|
|
86
|
+
| Prop | Type | Default | Notes |
|
|
87
|
+
|------|------|---------|-------|
|
|
88
|
+
| `skeletonCount` | number | `2` | Placeholder rows (`data-fs-account-saved-cards-loading-row`), hidden from assistive technology. |
|
|
89
|
+
|
|
90
|
+
A polite live region (`data-fs-account-saved-cards-loading-announcement`) stays mounted and announces the wait.
|
|
91
|
+
|
|
92
|
+
### AccountSavedCardsError
|
|
93
|
+
|
|
94
|
+
An alert. An expired session (`UNAUTHORIZED`) says so and links to sign-in (`data-fs-account-saved-cards-sign-in`), returning the shopper to the current page; any other failure says the cards could not be loaded. `data-fs-account-saved-cards-error-code` is `unauthorized` or `failed`. The server's message never renders. Children replace the message for every failure and drop the link.
|
|
95
|
+
|
|
96
|
+
### AccountSavedCardsEmpty
|
|
97
|
+
|
|
98
|
+
The statement that no card is saved. Renders only once the read resolved with none.
|
|
99
|
+
|
|
100
|
+
### AccountSavedCardsList
|
|
101
|
+
|
|
102
|
+
A `<ul>` repeating its child once per card.
|
|
103
|
+
|
|
104
|
+
### AccountSavedCardItem
|
|
105
|
+
|
|
106
|
+
A `<li>` that provides its card to the CardSummary parts, `AccountSavedCardExpiry`, and `AccountSavedCardRemove`. With no children it renders `CardSummary`'s default (mark, network name, mask, last four digits), then `AccountSavedCardExpiry`, then `AccountSavedCardRemove`. It shows no label: the account's read does not return one.
|
|
107
|
+
|
|
108
|
+
An expired card's row carries `data-fs-account-saved-card-expired`, and an unusable one's `data-fs-account-saved-card-unusable`, so a swatch can dim it. A working card's row carries neither. While its card is being removed, the row carries `data-fs-account-saved-card-removing`. Compose the three parts as children to rearrange the row or to reword it from the CMS. Leave `AccountSavedCardRemove` out only for a read-only list.
|
|
109
|
+
|
|
110
|
+
### AccountSavedCardExpiry
|
|
111
|
+
|
|
112
|
+
The line that says when the card expires, from the API's `status` and `expiry`. No schema props and no children.
|
|
113
|
+
|
|
114
|
+
| Card | Line | `data-fs-account-saved-card-expiry-status` |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Working, with a date | "Expires 05/28" | `usable` |
|
|
117
|
+
| Expired, with a date | "Expired 05/24" | `expired` |
|
|
118
|
+
| Expired, no date | "Expired" | `expired` |
|
|
119
|
+
| Unusable (any other status) | "This card can't be used", never a date | `unusable` |
|
|
120
|
+
| Working, no date | Nothing | — |
|
|
121
|
+
|
|
122
|
+
The label is in `data-fs-account-saved-card-expiry-label`, the `MM/YY` date in `data-fs-account-saved-card-expiry-date` (a `<time>`). The short date is hidden from assistive technology; a visually hidden span reads the month and year in full ("May 2028") in the store's locale. A month outside 1–12 or a year outside four digits counts as no date.
|
|
123
|
+
|
|
124
|
+
### AccountSavedCardRemove
|
|
125
|
+
|
|
126
|
+
The controller for one row's removal prompt. It owns the prompt's open state and passes it to its single `AlertDialogRoot` child; the schema never sets `open`. With no children it renders the default prompt. No schema props.
|
|
127
|
+
|
|
128
|
+
Opening the prompt dispatches nothing. Confirm dispatches `savedCard.remove` with the row's card id. The prompt then stays open until the server answers, and Escape and Cancel cannot close it meanwhile. On success the row leaves the list, and the prompt goes with it. On failure the prompt closes, focus returns to the trigger, and the row stays. An expired or unusable card is removed like any other.
|
|
129
|
+
|
|
130
|
+
### AccountSavedCardRemoveTrigger
|
|
131
|
+
|
|
132
|
+
The row's control that opens the prompt. Its visible text is short ("Remove"); a visually hidden span adds the card ("Visa ending in 1234"), so each trigger has its own accessible name. Disabled while any removal in the section is pending.
|
|
133
|
+
|
|
134
|
+
### AccountSavedCardRemoveTitle, AccountSavedCardRemoveDescription
|
|
135
|
+
|
|
136
|
+
The prompt's title and accessible description. With no children, the description renders the card's `CardSummary` before its text, so the prompt names the card it removes.
|
|
137
|
+
|
|
138
|
+
### AccountSavedCardRemoveCancel
|
|
139
|
+
|
|
140
|
+
Closes the prompt and keeps the card. Opened with a mouse or the keyboard, the prompt focuses it first, so the safe action is the default; opened by touch, the popup takes focus itself. Disabled while the removal is pending.
|
|
141
|
+
|
|
142
|
+
### AccountSavedCardRemoveConfirm
|
|
143
|
+
|
|
144
|
+
Removes the card. While the removal is pending it carries `aria-disabled="true"`, `aria-busy="true"`, and `data-fs-account-saved-card-remove-confirm-pending`, and shows its pending text ("Removing…"). It stays focusable, so focus does not leave the prompt.
|
|
145
|
+
|
|
146
|
+
### AccountSavedCardsRemoveFeedback
|
|
147
|
+
|
|
148
|
+
Tells the shopper how the last removal ended. Place it once in the Root, outside the list, because a removed card's row unmounts. No schema props and no children.
|
|
149
|
+
|
|
150
|
+
| Outcome | Renders | `data-fs-account-saved-cards-remove-feedback-outcome` |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| Removed | "The card was removed." in a polite status region, which takes focus | `removed` |
|
|
153
|
+
| Session expired (`UNAUTHORIZED`) | An alert saying so, with a sign-in link (`data-fs-account-saved-cards-remove-feedback-sign-in`) that returns to the page | `unauthorized` |
|
|
154
|
+
| Any other failure | An alert: "We couldn't remove the card. Please try again." | `failed` |
|
|
155
|
+
| None yet, or a new removal started | Nothing | — |
|
|
156
|
+
|
|
157
|
+
The status region stays mounted so screen readers announce it. No message disappears on its own, and the server's message never renders.
|
|
158
|
+
|
|
159
|
+
## Text
|
|
160
|
+
|
|
161
|
+
Every text resolves in this order: `children`, then the part's CMS entry (`$content`, a blank field counts as unset), then the catalog message in the store's locale.
|
|
162
|
+
|
|
163
|
+
| Part | CMS fields | Catalog ids |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `AccountSavedCardsHeading` | `text` | `fs/saved-cards.heading` |
|
|
166
|
+
| `AccountSavedCardsEmpty` | `text` | `fs/saved-cards.empty` |
|
|
167
|
+
| `AccountSavedCardsLoading` | `text` | `fs/saved-cards.loading` |
|
|
168
|
+
| `AccountSavedCardsError` | `text`, `sessionExpiredText`, `signInLabel` | `fs/saved-cards.load-failed`, `fs/saved-cards.session-expired`, `fs/saved-cards.sign-in` |
|
|
169
|
+
| `AccountSavedCardExpiry` | `expiresLabel`, `expiredLabel`, `expiredText`, `unusableText` | `fs/saved-cards.expires`, `fs/saved-cards.expired`, `fs/saved-cards.expired-no-date`, `fs/saved-cards.unusable` |
|
|
170
|
+
| `AccountSavedCardRemoveTrigger` | `text` | `fs/saved-cards.remove.trigger` |
|
|
171
|
+
| `AccountSavedCardRemoveTitle` | `text` | `fs/saved-cards.remove.title` |
|
|
172
|
+
| `AccountSavedCardRemoveDescription` | `text` | `fs/saved-cards.remove.description` |
|
|
173
|
+
| `AccountSavedCardRemoveCancel` | `text` | `fs/saved-cards.remove.cancel` |
|
|
174
|
+
| `AccountSavedCardRemoveConfirm` | `text`, `pendingText` | `fs/saved-cards.remove.confirm`, `fs/saved-cards.remove.pending` |
|
|
175
|
+
| `AccountSavedCardsRemoveFeedback` | `successText`, `failureText`, `sessionExpiredText`, `signInLabel` | `fs/saved-cards.remove.success`, `fs/saved-cards.remove.failed`, `fs/saved-cards.session-expired`, `fs/saved-cards.sign-in` |
|
|
176
|
+
|
|
177
|
+
`AccountSavedCardExpiry` and `AccountSavedCardsRemoveFeedback` take no children. `expiresLabel` and `expiredLabel` precede the date, which the component always places; `expiredText` and `unusableText` stand alone. The trigger's hidden card name follows its text whatever the source. Children on the Confirm replace only its idle label. Only a part placed in the schema reads its CMS entry: the expiry line and the prompt an Item renders by default always show the catalog wording.
|
|
178
|
+
|
|
179
|
+
## Canonical schema example
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"saved-cards": {
|
|
184
|
+
"type": "AccountSavedCardsRoot",
|
|
185
|
+
"children": [
|
|
186
|
+
"saved-cards-heading",
|
|
187
|
+
"saved-cards-remove-feedback",
|
|
188
|
+
"saved-cards-loading",
|
|
189
|
+
"saved-cards-error",
|
|
190
|
+
"saved-cards-empty",
|
|
191
|
+
"saved-cards-list"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"saved-cards-heading": { "type": "AccountSavedCardsHeading" },
|
|
195
|
+
"saved-cards-remove-feedback": { "type": "AccountSavedCardsRemoveFeedback" },
|
|
196
|
+
"saved-cards-loading": { "type": "AccountSavedCardsLoading" },
|
|
197
|
+
"saved-cards-error": { "type": "AccountSavedCardsError" },
|
|
198
|
+
"saved-cards-empty": { "type": "AccountSavedCardsEmpty" },
|
|
199
|
+
"saved-cards-list": { "type": "AccountSavedCardsList", "children": ["saved-card-item"] },
|
|
200
|
+
"saved-card-item": { "type": "AccountSavedCardItem" }
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Nested inside another account page, with a lower heading:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"saved-cards-heading": { "type": "AccountSavedCardsHeading", "props": { "as": "h3" } }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With the row composed, so the expiry line and the removal prompt can be reworded from the CMS:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"saved-card-item": {
|
|
217
|
+
"type": "AccountSavedCardItem",
|
|
218
|
+
"children": ["saved-card-summary", "saved-card-expiry", "saved-card-remove"]
|
|
219
|
+
},
|
|
220
|
+
"saved-card-summary": { "type": "CardSummary" },
|
|
221
|
+
"saved-card-expiry": { "type": "AccountSavedCardExpiry" },
|
|
222
|
+
"saved-card-remove": { "type": "AccountSavedCardRemove", "children": ["saved-card-remove-dialog"] },
|
|
223
|
+
"saved-card-remove-dialog": {
|
|
224
|
+
"type": "AlertDialogRoot",
|
|
225
|
+
"children": ["saved-card-remove-trigger", "saved-card-remove-portal"]
|
|
226
|
+
},
|
|
227
|
+
"saved-card-remove-trigger": { "type": "AccountSavedCardRemoveTrigger" },
|
|
228
|
+
"saved-card-remove-portal": {
|
|
229
|
+
"type": "AlertDialogPortal",
|
|
230
|
+
"children": ["saved-card-remove-backdrop", "saved-card-remove-viewport"]
|
|
231
|
+
},
|
|
232
|
+
"saved-card-remove-backdrop": { "type": "AlertDialogBackdrop" },
|
|
233
|
+
"saved-card-remove-viewport": { "type": "AlertDialogViewport", "children": ["saved-card-remove-popup"] },
|
|
234
|
+
"saved-card-remove-popup": {
|
|
235
|
+
"type": "AlertDialogPopup",
|
|
236
|
+
"children": [
|
|
237
|
+
"saved-card-remove-title",
|
|
238
|
+
"saved-card-remove-description",
|
|
239
|
+
"saved-card-remove-cancel",
|
|
240
|
+
"saved-card-remove-confirm"
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
"saved-card-remove-title": { "type": "AccountSavedCardRemoveTitle" },
|
|
244
|
+
"saved-card-remove-description": { "type": "AccountSavedCardRemoveDescription" },
|
|
245
|
+
"saved-card-remove-cancel": { "type": "AccountSavedCardRemoveCancel" },
|
|
246
|
+
"saved-card-remove-confirm": { "type": "AccountSavedCardRemoveConfirm" }
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Common mistakes
|
|
251
|
+
|
|
252
|
+
- **`AccountSavedCards`, `SavedCardsList`, `MyCards`.** The Root is `AccountSavedCardsRoot`; the list is `AccountSavedCardsList`.
|
|
253
|
+
- **Using `SavedCards` or `SavedCardOption` in the account.** Those are the checkout's cart-scoped picker and need a payment section.
|
|
254
|
+
- **Placing `AccountSavedCardItem` directly in the Root.** It must be inside `AccountSavedCardsList`.
|
|
255
|
+
- **Composing `AccountSavedCardsEmpty` as the only state part.** Compose Loading and Error too, or a slow or failed read shows nothing under the heading.
|
|
256
|
+
- **Setting `as: "h1"`.** Not accepted; the page title belongs to the page shell.
|
|
257
|
+
- **Passing `text` as a schema prop.** Text comes from children, the CMS entry, or the catalog.
|
|
258
|
+
- **Placing the Root outside the `my-account` layout.** It requires `session`; without a `SessionRoot` the build fails.
|
|
259
|
+
- **Giving the Item custom children without `AccountSavedCardExpiry`.** The row then hides when the card expires and whether it can be used.
|
|
260
|
+
- **Expecting CMS wording on an Item with no children.** Its default expiry line and removal prompt are not in the schema, so they read only the catalog; place `AccountSavedCardExpiry` and the `AccountSavedCardRemove` composition explicitly.
|
|
261
|
+
- **Giving the Item custom children without `AccountSavedCardRemove`.** The shopper can no longer remove the card. Do it only for a read-only list.
|
|
262
|
+
- **Leaving out `AccountSavedCardsRemoveFeedback`.** A removal then ends with no message: a success only drops the row, and a failure says nothing.
|
|
263
|
+
- **Placing the feedback inside the Item.** A removed card's row unmounts, taking the message with it.
|
|
264
|
+
- **Setting `open` on the `AlertDialogRoot` inside `AccountSavedCardRemove`.** The controller owns it.
|
|
265
|
+
- **Adding a plain button as the confirm control.** Only `AccountSavedCardRemoveConfirm` dispatches the removal.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Styling
|
|
270
|
+
|
|
271
|
+
| Component | What you style | Plausible classes |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `AccountSavedCardsRoot` | Section spacing | `flex flex-col gap-[16px]` |
|
|
274
|
+
| `AccountSavedCardsHeading` | Heading typography | `text-[20px] font-semibold text-[#1F1F1F]` |
|
|
275
|
+
| `AccountSavedCardsList` | Row stack | `flex flex-col gap-[8px]` |
|
|
276
|
+
| `AccountSavedCardItem` | Row box; dim expired and unusable rows | `rounded-[8px] border border-[#E5E5E5] p-[12px] data-[fs-account-saved-card-expired]:opacity-60 data-[fs-account-saved-card-unusable]:opacity-60` |
|
|
277
|
+
| `AccountSavedCardExpiry` | Line typography; color by state | `mt-[4px] text-[13px] text-[#6B6B6B] data-[fs-account-saved-card-expiry-status=expired]:text-[#B42318] data-[fs-account-saved-card-expiry-status=unusable]:text-[#B42318]` |
|
|
278
|
+
| `AccountSavedCardsEmpty` | Statement typography | `text-[14px] text-[#6B6B6B]` |
|
|
279
|
+
| `AccountSavedCardsLoading` | Placeholder stack; rows via `[&_[data-fs-account-saved-cards-loading-row]]` | `flex flex-col gap-[8px] [&_[data-fs-account-saved-cards-loading-row]]:h-[48px] [&_[data-fs-account-saved-cards-loading-row]]:rounded-[8px] [&_[data-fs-account-saved-cards-loading-row]]:bg-[#F0F0F0]` |
|
|
280
|
+
| `AccountSavedCardsError` | Alert box and link | `rounded-[8px] bg-[#FDECEC] p-[12px] text-[14px] text-[#B42318] [&_a]:underline` |
|
|
281
|
+
| `AccountSavedCardItem` (removing) | Fade the row while it is removed | `data-[fs-account-saved-card-removing]:opacity-60` |
|
|
282
|
+
| `AccountSavedCardRemoveTrigger` | Link-like control | `mt-[8px] text-[13px] text-[#B42318] underline disabled:opacity-50` |
|
|
283
|
+
| `AlertDialogPopup` | Prompt box (style the AlertDialog parts directly) | `w-full max-w-[420px] rounded-[12px] bg-white p-[24px] shadow-xl` |
|
|
284
|
+
| `AccountSavedCardRemoveConfirm` | Destructive button; dim while pending | `rounded-[6px] bg-[#B42318] px-[16px] py-[8px] text-white aria-disabled:opacity-70` |
|
|
285
|
+
| `AccountSavedCardsRemoveFeedback` | Message typography; alert color | `text-[14px] [&_[role=alert]]:text-[#B42318] [&_a]:underline` |
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"saved-cards": { "type": "AccountSavedCardsRoot", "style": ["flex", "flex-col", "gap-[16px]"] },
|
|
290
|
+
"saved-card-item": { "type": "AccountSavedCardItem", "style": ["rounded-[8px]", "border", "border-[#E5E5E5]", "p-[12px]"] }
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Related
|
|
297
|
+
|
|
298
|
+
- [[card-summary]]: the card description each row renders.
|
|
299
|
+
- [[alert-dialog]]: the overlay the removal prompt is built on.
|
|
300
|
+
- [[saved-cards]]: the checkout's saved-card picker; cart-scoped.
|
|
301
|
+
- [[my-account-profile]]: the account's other session-scoped matter.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# BindingSwitcher
|
|
2
|
+
|
|
3
|
+
**Category:** action
|
|
4
|
+
**Purpose:** Lists the store's bindings and starts a binding switch (locale, currency, and sales channel) through `locale.set`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
A closed dropdown whose visible value is a language name, a chevron, and sometimes one short sentence in a status line. The image is the source of truth for layout.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
A store that declares two or more bindings and wants the shopper to switch the whole binding. Pass `bindings` as `$data.Global.localization.bindings`. With fewer than two counted bindings the component renders nothing.
|
|
17
|
+
|
|
18
|
+
## When NOT to use
|
|
19
|
+
|
|
20
|
+
- A country list, a currency picker, or a CMS language field.
|
|
21
|
+
- A store with one binding. It renders nothing and should not reserve an empty control.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Composition
|
|
26
|
+
|
|
27
|
+
The schema tree is one node. `BindingSwitcher` has no children. The `Select` parts are internal and are not schema children.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
BindingSwitcher
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Components
|
|
36
|
+
|
|
37
|
+
### BindingSwitcher
|
|
38
|
+
|
|
39
|
+
Renders a host `div` (`data-fs-binding-switcher`). No fragment. Declares `actions: { locale: ['set'] }`. Choosing another binding calls `$actions.locale.set({ bindingId })`.
|
|
40
|
+
|
|
41
|
+
| Prop | Type | Default | Notes |
|
|
42
|
+
|------|------|---------|-------|
|
|
43
|
+
| `bindings` | object | — | `foundation.json` `localization.bindings`, keyed by binding id. No library default. The store passes `$data.Global.localization.bindings`. |
|
|
44
|
+
| `label` | string | `"Language"` | Accessible name, rendered as the select label. |
|
|
45
|
+
| `notice` | string | `"This page isn't available in the selected language."` | Shown when the page loaded with `fs_notice=no-equivalent`. Inline `<output>` (implicit `status` role). |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Canonical schema example
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"binding-switcher": {
|
|
54
|
+
"type": "BindingSwitcher",
|
|
55
|
+
"props": {
|
|
56
|
+
"bindings": "$data.Global.localization.bindings"
|
|
57
|
+
},
|
|
58
|
+
"style": [
|
|
59
|
+
"flex",
|
|
60
|
+
"flex-col",
|
|
61
|
+
"gap-2",
|
|
62
|
+
"text-[14px]",
|
|
63
|
+
"[&_[data-fs-binding-switcher-notice]]:text-[13px]",
|
|
64
|
+
"[&_[data-fs-select-trigger]]:inline-flex",
|
|
65
|
+
"[&_[data-fs-select-trigger]]:items-center",
|
|
66
|
+
"[&_[data-fs-select-trigger]]:gap-2",
|
|
67
|
+
"[&_[data-fs-select-trigger]]:rounded-md",
|
|
68
|
+
"[&_[data-fs-select-trigger]]:border",
|
|
69
|
+
"[&_[data-fs-select-trigger]]:border-neutral-300",
|
|
70
|
+
"[&_[data-fs-select-trigger]]:bg-white",
|
|
71
|
+
"[&_[data-fs-select-trigger]]:px-3",
|
|
72
|
+
"[&_[data-fs-select-trigger]]:py-1.5",
|
|
73
|
+
"[&_[data-fs-select-positioner]]:z-50",
|
|
74
|
+
"[&_[data-fs-select-popup]]:min-w-[var(--anchor-width)]",
|
|
75
|
+
"[&_[data-fs-select-popup]]:overflow-hidden",
|
|
76
|
+
"[&_[data-fs-select-popup]]:rounded-md",
|
|
77
|
+
"[&_[data-fs-select-popup]]:border",
|
|
78
|
+
"[&_[data-fs-select-popup]]:border-neutral-200",
|
|
79
|
+
"[&_[data-fs-select-popup]]:bg-white",
|
|
80
|
+
"[&_[data-fs-select-popup]]:shadow-lg",
|
|
81
|
+
"[&_[data-fs-select-list]]:py-1",
|
|
82
|
+
"[&_[data-fs-select-item]]:cursor-pointer",
|
|
83
|
+
"[&_[data-fs-select-item]]:px-3",
|
|
84
|
+
"[&_[data-fs-select-item]]:py-2",
|
|
85
|
+
"[&_[data-fs-select-item][data-highlighted]]:bg-neutral-100"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Common mistakes
|
|
94
|
+
|
|
95
|
+
- **LocaleSelector** or **LanguagePicker.** The name is `BindingSwitcher`.
|
|
96
|
+
- **A toast for the notice.** The notice is an inline `<output>`.
|
|
97
|
+
- **A default bindings list.** The store passes `$data.Global.localization.bindings`.
|
|
98
|
+
- **Children that rebuild Select.** The schema element has no children.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Styling
|
|
103
|
+
|
|
104
|
+
The host stacks the notice above the control. The popup stays in the host and opens below the trigger, 4px clear of it, so the element `style` reaches `[data-fs-select-popup]`. Give that popup an opaque surface. A parent locked to a fixed height lets the notice paint outside the bar; `min-h-16` on that parent keeps the notice inside it.
|
|
105
|
+
|
|
106
|
+
| Component | What you style | Plausible classes |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `BindingSwitcher` | Stacks the notice and the control. The notice line is `text-[13px]`. The popup is `[data-fs-select-popup]`. | `flex flex-col gap-2 text-[14px] [&_[data-fs-binding-switcher-notice]]:text-[13px] [&_[data-fs-select-popup]]:bg-white [&_[data-fs-select-popup]]:border [&_[data-fs-select-popup]]:border-neutral-200 [&_[data-fs-select-popup]]:rounded-md [&_[data-fs-select-popup]]:shadow-lg` |
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"binding-switcher": {
|
|
113
|
+
"type": "BindingSwitcher",
|
|
114
|
+
"props": {
|
|
115
|
+
"bindings": "$data.Global.localization.bindings"
|
|
116
|
+
},
|
|
117
|
+
"style": [
|
|
118
|
+
"flex",
|
|
119
|
+
"flex-col",
|
|
120
|
+
"gap-2",
|
|
121
|
+
"text-[14px]",
|
|
122
|
+
"[&_[data-fs-binding-switcher-notice]]:text-[13px]",
|
|
123
|
+
"[&_[data-fs-select-trigger]]:inline-flex",
|
|
124
|
+
"[&_[data-fs-select-trigger]]:items-center",
|
|
125
|
+
"[&_[data-fs-select-trigger]]:gap-2",
|
|
126
|
+
"[&_[data-fs-select-trigger]]:rounded-md",
|
|
127
|
+
"[&_[data-fs-select-trigger]]:border",
|
|
128
|
+
"[&_[data-fs-select-trigger]]:border-neutral-300",
|
|
129
|
+
"[&_[data-fs-select-trigger]]:bg-white",
|
|
130
|
+
"[&_[data-fs-select-trigger]]:px-3",
|
|
131
|
+
"[&_[data-fs-select-trigger]]:py-1.5",
|
|
132
|
+
"[&_[data-fs-select-positioner]]:z-50",
|
|
133
|
+
"[&_[data-fs-select-popup]]:min-w-[var(--anchor-width)]",
|
|
134
|
+
"[&_[data-fs-select-popup]]:overflow-hidden",
|
|
135
|
+
"[&_[data-fs-select-popup]]:rounded-md",
|
|
136
|
+
"[&_[data-fs-select-popup]]:border",
|
|
137
|
+
"[&_[data-fs-select-popup]]:border-neutral-200",
|
|
138
|
+
"[&_[data-fs-select-popup]]:bg-white",
|
|
139
|
+
"[&_[data-fs-select-popup]]:shadow-lg",
|
|
140
|
+
"[&_[data-fs-select-list]]:py-1",
|
|
141
|
+
"[&_[data-fs-select-item]]:cursor-pointer",
|
|
142
|
+
"[&_[data-fs-select-item]]:px-3",
|
|
143
|
+
"[&_[data-fs-select-item]]:py-2",
|
|
144
|
+
"[&_[data-fs-select-item][data-highlighted]]:bg-neutral-100"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Related
|
|
153
|
+
|
|
154
|
+
- [[select]] — the empty dropdown this component composes. `Select` does not know about bindings.
|
package/dist/docs/card-entry.md
CHANGED
|
@@ -90,7 +90,7 @@ In the checkout, the card number, holder name, expiration, and security code are
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
**If the store's `
|
|
93
|
+
**If the store's `foundation.json` already has an `icons` object, extend it — do not replace it.** A store that already sets `"collection"` (or `"aliases"` / `"collections"`) must keep that key alongside the new `include`:
|
|
94
94
|
|
|
95
95
|
```json
|
|
96
96
|
{
|
|
@@ -107,7 +107,7 @@ Omitting `include` entirely is silent too. The network icon renders as an empty
|
|
|
107
107
|
|
|
108
108
|
## Store setup: response headers
|
|
109
109
|
|
|
110
|
-
The card number and security code are typed into `CardEntry`'s raw fields and POSTed from the browser straight to the VTEX payment gateway, so the page that composes it needs a policy that names the vault host the card is POSTed to, forbids framing the page, and narrows what the page may load and call. The generator ships no default: the store declares it in `
|
|
110
|
+
The card number and security code are typed into `CardEntry`'s raw fields and POSTed from the browser straight to the VTEX payment gateway, so the page that composes it needs a policy that names the vault host the card is POSTed to, forbids framing the page, and narrows what the page may load and call. The generator ships no default: the store declares it in `foundation.json` under `responseHeaders`, scoped by `routes` to the page that composes `CardEntry`.
|
|
111
111
|
|
|
112
112
|
```json
|
|
113
113
|
{
|
|
@@ -123,7 +123,7 @@ The card number and security code are typed into `CardEntry`'s raw fields and PO
|
|
|
123
123
|
}
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
- **The vault host in `connect-src` is `https://{account}.vtexvault.com`, for the account the store serves.** That is the `NEXT_PUBLIC_ACCOUNT` the store is built with, which is not always `account
|
|
126
|
+
- **The vault host in `connect-src` is `https://{account}.vtexvault.com`, for the account the store serves.** That is the `NEXT_PUBLIC_ACCOUNT` the store is built with, which is not always `store.account` in `foundation.json`. Write the served account, with no wildcard: `https://*.vtexvault.com` would let a compromised script send the card to any tenant's vault.
|
|
127
127
|
- **A policy that names the legacy payments host (`vtexpayments.com.br`) makes the browser block the card POST.** The order cannot be placed and nothing else on the page reports why. The generated app calls its own GraphQL at the relative `/api/graphql`, so `'self'` covers it; a store built with an absolute `NEXT_PUBLIC_STOREFRONT_API_URL` must add that origin.
|
|
128
128
|
- **A rule's `routes` must name the page that composes `CardEntry`**, as the page's `route` spells it (`/checkout`, `/reserva/checkout/payment`). Renaming that page fails the compile instead of dropping the policy. A page added later that composes `CardEntry` carries nothing until its route is added to the rule.
|
|
129
129
|
- **`script-src 'unsafe-inline'` is a known limit, and the policy does not stop injected script from sending the card off the page.** A static header cannot carry a nonce, so the inline scripts Next.js emits need it, and script injected into the page runs with the same rights. `connect-src` closes only `fetch`, XHR, beacons and WebSocket. The policy still leaves three channels open to any https host: an image request (`img-src https:`, which the page needs for catalog images), a form submission (no `form-action`) and a navigation (CSP cannot restrict one). What today's ruled policy does give is the vault origin pinned, no `'unsafe-eval'`, a page no other site can frame (`frame-ancestors 'none'`) and no `http:` or wildcard source. It does not replace keeping untrusted script off the payment page.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Card Summary
|
|
2
|
+
|
|
3
|
+
**Category:** content
|
|
4
|
+
**Purpose:** A saved card's identity: its network's mark, its label or else its network name, a mask, and its last four digits, as parts the schema arranges.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
- A card-network logo (Visa, Mastercard, American Express, Diners, Elo) beside a short text.
|
|
11
|
+
- A name such as `Visa` or a shopper's own label such as `Company card`.
|
|
12
|
+
- A masking symbol (`••••`, `****`) followed by four digits: `•••• 1234`.
|
|
13
|
+
- Usually one line per card, repeated in a list or inside a selectable row.
|
|
14
|
+
|
|
15
|
+
The image is the source of truth for layout.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
- Describing one saved card wherever it appears: the account's saved-cards list, a removal confirmation, the checkout's saved-card row.
|
|
20
|
+
|
|
21
|
+
## When NOT to use
|
|
22
|
+
|
|
23
|
+
- The store's accepted payment methods as a strip of logos → [[payment-methods]].
|
|
24
|
+
- Typing a new card → the checkout's card entry; this particle never takes a full number or a security code.
|
|
25
|
+
- A card's expiry or usability state → the row that owns the card renders it beside this particle.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Composition
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
<row that provides 'card-summary'> ← e.g. AccountSavedCardItem
|
|
33
|
+
└── CardSummary
|
|
34
|
+
├── CardSummaryMark (default)
|
|
35
|
+
├── CardSummaryName (default)
|
|
36
|
+
├── CardSummaryMask (default)
|
|
37
|
+
└── CardSummaryDigits (default)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`CardSummary` with no children renders the four parts in that order. Composing children replaces the default: any order, any subset. Child order is the visual order and the screen-reader order. The parts can also sit directly in the providing row, without the `CardSummary` wrapper.
|
|
41
|
+
|
|
42
|
+
Every part requires the `card-summary` context, and only a matter row that owns card data provides it in a schema. The row wraps its children in `CardSummaryRoot` in code and declares `'card-summary'` in its own `providesContext`, so the schema composes the parts directly in the row. Context matching is by contract, not by component name. Stories, tests, and hand-written JSX render `CardSummaryRoot` themselves.
|
|
43
|
+
|
|
44
|
+
A part outside any provider fails the build with a `ContextConstraintError`, and throws at runtime outside the compiler.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Components
|
|
49
|
+
|
|
50
|
+
### CardSummaryRoot
|
|
51
|
+
|
|
52
|
+
Code only: it has no catalog entry and is never placed in a schema, because a schema cannot supply the card. Renders no element. Publishes the card to the parts. Keeps only the trailing four digit characters of `lastDigits`; the raw value never reaches a part.
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Notes |
|
|
55
|
+
|------|------|---------|-------|
|
|
56
|
+
| `network` | string | — | The network's display name from the platform, e.g. `Visa`. Data, not a schema prop. |
|
|
57
|
+
| `lastDigits` | string | — | The card's last digits. Only four ever render. Data, not a schema prop. |
|
|
58
|
+
| `label` | string \| null | — | The shopper's own name for the card. Shown in place of the network name when non-blank. Data, not a schema prop. |
|
|
59
|
+
|
|
60
|
+
### CardSummary
|
|
61
|
+
|
|
62
|
+
The wrapper, `<span data-fs-card-summary>`. Carries `data-fs-card-summary-network="<icon>"` when the network has a mark and `data-fs-card-summary-labeled` when a label is present. No props.
|
|
63
|
+
|
|
64
|
+
### CardSummaryMark
|
|
65
|
+
|
|
66
|
+
The network's mark. No props. Decided by the data, never by which other parts are composed:
|
|
67
|
+
|
|
68
|
+
| Card | Renders |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Mapped network, no label | The glyph, decorative (the name says the network). |
|
|
71
|
+
| Mapped network, labelled | The glyph plus the network name as screen-reader text. |
|
|
72
|
+
| Unmapped network, no label | Nothing. |
|
|
73
|
+
| Unmapped network, labelled | The network name as visible text (`data-fs-card-summary-network-name`), never another network's glyph. |
|
|
74
|
+
|
|
75
|
+
So a labelled card still tells a sighted shopper and a screen reader its network, even when the schema drops `CardSummaryName`. An unlabelled card does not: its glyph is decorative and `CardSummaryName` is what announces the network, so the network is announced exactly once in the default composition.
|
|
76
|
+
|
|
77
|
+
### CardSummaryName
|
|
78
|
+
|
|
79
|
+
The label, or else the network name. Nothing when both are blank. Read-only. No props.
|
|
80
|
+
|
|
81
|
+
### CardSummaryMask
|
|
82
|
+
|
|
83
|
+
Decoration for the hidden digits, always `aria-hidden`. Nothing when empty or when the card has no digits.
|
|
84
|
+
|
|
85
|
+
| Prop | Type | Default | Notes |
|
|
86
|
+
|------|------|---------|-------|
|
|
87
|
+
| `mask` | string | `••••` | The masking symbol. An empty string renders nothing. |
|
|
88
|
+
|
|
89
|
+
`children` replace `mask`, e.g. an `Icon`. Keep them inline: the particle sits inside labels and paragraphs.
|
|
90
|
+
|
|
91
|
+
### CardSummaryDigits
|
|
92
|
+
|
|
93
|
+
The last four digits. Screen readers hear "ending in 1234" (message id `fs/card.ending-in`, in en-US, es-ES, and pt-BR); the visible digits are hidden from them. Nothing when the card has no digits. No props.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Networks and the `icons.include` obligation
|
|
98
|
+
|
|
99
|
+
The mark comes from `CARD_NETWORK_ICON_BY_KEY` in `card-network.ts`, the one map every card surface shares. A network name matches after lowercasing and removing every character outside `a-z`: `Visa`, `Mastercard`, `Amex`, `American Express`, `Diners`, `Diners Club`, and `Elo` have marks. Matching is exact, so `Visa Electron` or `Elo Debit` render by name, without a mark. `cardNetworkIcon(name)` returns the icon name or `null`.
|
|
100
|
+
|
|
101
|
+
**Every mapped name must be registered.** The network comes from data, so the compiler cannot see which glyphs a route needs. A store that renders `CardSummaryMark` lists the names under `icons.include` in its `foundation.json`; it is JSON, so it cannot import `CARD_NETWORK_ICON_NAMES`, and the names are written out:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{ "icons": { "include": ["visa", "mastercard", "amex", "diners", "elo-card"] } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A name the store never registered renders an empty `<svg>`: no build error and no runtime error, just a missing mark. All five are base-dialect names, so they resolve in any store whatever its icon collection.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Canonical schema example
|
|
112
|
+
|
|
113
|
+
The account's row provides the context; the schema arranges the parts:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"saved-card-item": { "type": "AccountSavedCardItem", "children": ["card-summary"] },
|
|
118
|
+
"card-summary": { "type": "CardSummary", "style": ["inline-flex", "items-center", "gap-[8px]"] }
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Digits first, with another mask:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"saved-card-item": { "type": "AccountSavedCardItem", "children": ["card-digits", "card-mask", "card-mark", "card-name"] },
|
|
127
|
+
"card-digits": { "type": "CardSummaryDigits" },
|
|
128
|
+
"card-mask": { "type": "CardSummaryMask", "props": { "mask": "****" } },
|
|
129
|
+
"card-mark": { "type": "CardSummaryMark" },
|
|
130
|
+
"card-name": { "type": "CardSummaryName" }
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Common mistakes
|
|
135
|
+
|
|
136
|
+
- **`CardSummaryNumber`, `CardSummaryLastFour`.** The correct name is `CardSummaryDigits`.
|
|
137
|
+
- **`CardSummaryBrand`, `CardSummaryFlag`, `CardSummaryIcon`.** The correct name is `CardSummaryMark`.
|
|
138
|
+
- **`CardSummaryLabel`.** The correct name is `CardSummaryName`; it shows the label or else the network name.
|
|
139
|
+
- **Writing the bullets inside `CardSummaryDigits`.** The mask is its own part, `CardSummaryMask`.
|
|
140
|
+
- **Placing `CardSummaryRoot` in a schema, or passing `network`, `lastDigits`, or `label` as schema props.** The card is data from the row that provides `card-summary`; the Root is not in the catalog.
|
|
141
|
+
- **Composing a part outside a row that provides `card-summary`.** It fails the build.
|
|
142
|
+
- **Dropping `CardSummaryMark` or `CardSummaryDigits` from a custom composition.** The PRD requires both beside a card; only the default composition guarantees them.
|
|
143
|
+
- **Composing `CardSummaryMark` without `CardSummaryName`.** On an unlabelled card the glyph is decorative and the name is what announces the network, so a screen reader hears only "ending in 1234". Keep `CardSummaryName` whenever `CardSummaryMark` is composed.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Styling
|
|
148
|
+
|
|
149
|
+
| Component | What you style | Plausible classes |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `CardSummary` | Lines the parts up | `inline-flex items-center gap-[8px]` |
|
|
152
|
+
| `CardSummaryMark` | Mark box; size the glyph through it | `inline-flex items-center [&_svg]:h-[24px] [&_svg]:w-[32px]` |
|
|
153
|
+
| `CardSummaryName` | Name typography | `text-[14px] font-medium text-[#1F1F1F]` |
|
|
154
|
+
| `CardSummaryMask` | Mask color and spacing | `text-[14px] tracking-[2px] text-[#6B6B6B]` |
|
|
155
|
+
| `CardSummaryDigits` | Digit typography | `text-[14px] tabular-nums text-[#1F1F1F]` |
|
|
156
|
+
|
|
157
|
+
`CardSummaryRoot` renders no element and is not styled.
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"card-summary": { "type": "CardSummary", "style": ["inline-flex", "items-center", "gap-[8px]"], "children": ["card-mark", "card-name", "card-mask", "card-digits"] },
|
|
162
|
+
"card-mark": { "type": "CardSummaryMark", "style": ["inline-flex", "items-center", "[&_svg]:h-[24px]", "[&_svg]:w-[32px]"] },
|
|
163
|
+
"card-name": { "type": "CardSummaryName", "style": ["text-[14px]", "font-medium", "text-[#1F1F1F]"] },
|
|
164
|
+
"card-mask": { "type": "CardSummaryMask", "style": ["text-[14px]", "tracking-[2px]", "text-[#6B6B6B]"] },
|
|
165
|
+
"card-digits": { "type": "CardSummaryDigits", "style": ["text-[14px]", "tabular-nums", "text-[#1F1F1F]"] }
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Related
|
|
172
|
+
|
|
173
|
+
- [[payment-methods]]: the store's accepted payment methods as a strip of logos; not a saved card.
|
|
174
|
+
- [[address-summary]]: the same Root-plus-parts shape, for a postal address.
|
|
175
|
+
- [[icon]]: renders the mark from the base icon dialect.
|