@vtex/faststore 0.0.4 → 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 +67 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1595 -481
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +31 -7
- package/dist/components.js +35 -13
- package/dist/content-types.json +73 -0
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/account-access-gate.md +153 -0
- package/dist/docs/account-entry-point.md +1 -2
- package/dist/docs/account-menu.md +164 -0
- 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/buy-button.md +1 -1
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/cart.md +1 -1
- 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/session.md +1 -1
- package/dist/docs/shipping-address.md +110 -258
- package/dist/docs/sign-in-redirect.md +1 -1
- 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/account-access-gate/account-access-gate.d.ts +86 -0
- package/dist/matter/account-access-gate/account-access-gate.js +283 -0
- package/dist/matter/account-access-gate/account-access-gate.js.map +1 -0
- package/dist/matter/account-access-gate/sign-out-intent.d.ts +17 -0
- package/dist/matter/account-access-gate/sign-out-intent.js +62 -0
- package/dist/matter/account-access-gate/sign-out-intent.js.map +1 -0
- package/dist/matter/account-entry-point/account-entry-point.js +2 -2
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +88 -0
- package/dist/matter/account-menu/account-menu.js +246 -0
- package/dist/matter/account-menu/account-menu.js.map +1 -0
- 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/cart/cart.d.ts +1 -1
- package/dist/matter/cart/cart.js +2 -2
- package/dist/matter/cart/cart.js.map +1 -1
- 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/delivery-options/delivery-options.js.map +1 -1
- 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 +5 -5
- 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 +481 -792
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.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/footer-social/footer-social.js +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/incentives/incentives.js +1 -1
- 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/cart-action-provider.d.ts +1 -1
- package/dist/sdk/actions/cart-action-provider.js +1 -1
- package/dist/sdk/actions/cart-action-provider.js.map +1 -1
- 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/session-action-provider.d.ts +1 -1
- package/dist/sdk/actions/session-action-provider.js +1 -1
- package/dist/sdk/actions/session-action-provider.js.map +1 -1
- package/dist/sdk/actions/shipping-action-provider.d.ts +1 -1
- package/dist/sdk/actions/shipping-action-provider.js +1 -1
- package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
- 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/cache/storefront-cache.js +1 -1
- package/dist/sdk/cache/storefront-cache.js.map +1 -1
- package/dist/sdk/cart/operations.d.ts +4 -4
- package/dist/sdk/cart/operations.js +1 -1
- package/dist/sdk/cart/operations.js.map +1 -1
- package/dist/sdk/cart/optimistic.js +1 -1
- package/dist/sdk/cart/optimistic.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.d.ts +1 -1
- package/dist/sdk/cart/use-cart-actions.js +4 -4
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
- package/dist/sdk/cart/write-gate.js +1 -1
- package/dist/sdk/cart/write-gate.js.map +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 +38 -2
- 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.d.ts +1 -1
- package/dist/sdk/data/reactive-data-provider.js +8 -4
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.d.ts +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +13 -5
- 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 +6 -2
- 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/gql.js +2 -1
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.js +85 -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/operations.js.map +1 -1
- package/dist/sdk/identity/use-identity-actions.d.ts +1 -1
- 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/session/auth-operations.d.ts +1 -1
- package/dist/sdk/session/auth-operations.js.map +1 -1
- package/dist/sdk/session/use-session-actions.d.ts +1 -1
- package/dist/sdk/session/use-session-actions.js.map +1 -1
- package/dist/sdk/session/use-session-revalidation.js +65 -0
- package/dist/sdk/session/use-session-revalidation.js.map +1 -0
- package/dist/sdk/shipping/option-select-operations.d.ts +1 -1
- package/dist/sdk/shipping/option-select-operations.js.map +1 -1
- package/dist/sdk/shipping/request-timeout.js +1 -1
- package/dist/sdk/shipping/request-timeout.js.map +1 -1
- package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
- package/dist/sdk/shipping/shipping-operations.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-actions.d.ts +2 -2
- package/dist/sdk/shipping/use-shipping-actions.js +1 -1
- package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
- package/dist/sdk/state/cart.d.ts +3 -4
- package/dist/sdk/state/cart.js +6 -7
- package/dist/sdk/state/cart.js.map +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/safe-href.js +3 -3
- package/dist/utils/safe-href.js.map +1 -1
- 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,157 @@
|
|
|
1
|
+
# PlaceOrder
|
|
2
|
+
|
|
3
|
+
**Category:** commerce
|
|
4
|
+
**Purpose:** The checkout's place-order CTA. Runs the three-call payment sequence (start transaction, the holder's direct-to-gateway submission, gateway callback), blocks interaction while it runs, and renders one inline outcome — an error message or a success confirmation — once it settles.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
A PlaceOrder block is present when you see:
|
|
11
|
+
|
|
12
|
+
- A **primary CTA button** at the bottom of a payment/checkout section with a label like "Place order", "Finalizar compra", "Pay now", or similar.
|
|
13
|
+
- The button may appear **greyed out with a spinner or busy indicator** and a label like "Placing order…" — that is still the same `PlaceOrder`, mid-sequence.
|
|
14
|
+
- A **confirmation line or banner below the button** reading something like "Order placed successfully" once the button has been pressed and the order succeeded — this is `PlaceOrder`'s own inline success state, not a separate order-confirmation page.
|
|
15
|
+
- An **inline error line below the button** (e.g. "Enter your payment details before placing the order.") is also part of this same block, not a separate error component.
|
|
16
|
+
|
|
17
|
+
The image is the source of truth for layout.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- The final CTA in a credit-card checkout/payment section, once a method (and, for a new card, its fields) has been entered.
|
|
24
|
+
|
|
25
|
+
## When NOT to use
|
|
26
|
+
|
|
27
|
+
- Product-page purchase CTA (add to cart) — use [[buy-button]] instead. `PlaceOrder` is checkout-only and drives the payment sequence, not a cart write.
|
|
28
|
+
- Do **not** nest it inside the card-entry form. It must be a **sibling** of card entry and saved cards under the same payment root — nesting it inside the card form would need a second CTA for the saved-card path, which is exactly the mistake this component's placement avoids.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Composition
|
|
33
|
+
|
|
34
|
+
`PlaceOrder` is a single, self-contained component — no Root/leaf split. It must be placed as a child of `PaymentSectionRoot` or `PaymentSectionController`, as a **sibling** of `CardEntry` / `SavedCards`, never nested inside either:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
PaymentSectionRoot (or PaymentSectionController)
|
|
38
|
+
├── PaymentMethodList
|
|
39
|
+
├── CardEntry (new-card path)
|
|
40
|
+
├── SavedCards (saved-card path)
|
|
41
|
+
└── PlaceOrder ← sibling, not a child of CardEntry/SavedCards
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Child order reflects visual order. `PlaceOrder` composes under **either** payment root with no change — both provide the same `payment` context and the same submission channel (ADR-0009).
|
|
45
|
+
|
|
46
|
+
`PlaceOrder` never reads or holds a card field. It calls `usePlaceOrder`, the T13 orchestration hook, which resolves a continuation that `CardEntry`/`SavedCards` registered on a submission channel by the shopper's entry path — this component only knows there is a hook to call, a loading state to block on, and an outcome to render. It also reads `usePayment().draft.billingAddressDifferent` (T11) and forwards it into `usePlaceOrder`, so the gateway descriptor carries whether the shopper picked a different billing address — the one other fact this component touches, and still not a card field.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Components
|
|
51
|
+
|
|
52
|
+
### PlaceOrder
|
|
53
|
+
|
|
54
|
+
No sub-components. Renders a button and, once the sequence has settled, one inline outcome region (error or success).
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Notes |
|
|
57
|
+
|------|------|---------|-------|
|
|
58
|
+
| `label` | string | `"Place order"` | Button label while idle. |
|
|
59
|
+
| `placingLabel` | string | `"Placing order..."` | Button label while the sequence is in flight. |
|
|
60
|
+
| `resumeLabel` | string | `"Complete your order"` | Button label while a surviving attempt offers a resume — the card already left the browser (or its fate is unknown) and the next click finalizes it instead of running the full sequence again. |
|
|
61
|
+
| `successMessage` | string | `"Order placed successfully."` | Shown once the sequence settles with no errors. |
|
|
62
|
+
| `missingSubmissionMessage` | string | `"Enter your payment details before placing the order."` | Shown when `placeOrder()` is invoked with no registered submission for the current path (e.g. the shopper never finished entering a card). |
|
|
63
|
+
| `missingMethodMessage` | string | `"Choose a payment method before placing the order."` | Shown when no payment method is selected yet. |
|
|
64
|
+
| `invalidTransactionMessage` | string | `"Unable to start the payment. Please try again."` | Shown when `startTransaction` resolves incomplete transaction data. |
|
|
65
|
+
| `networkErrorMessage` | string | `"Unable to complete the request. Please try again."` | Shown for an infrastructure-level failure reaching the payment gateway action. |
|
|
66
|
+
| `missingDocumentMessage` | string | `"Enter your document before placing the order."` | Shown when the selected method requires a cardholder document and none is set. |
|
|
67
|
+
| `invalidDocumentMessage` | string | `"Enter a valid document before placing the order."` | Shown when the typed cardholder document fails validation. |
|
|
68
|
+
| `unavailableMethodMessage` | string | `"Choose another payment method before placing the order."` | Shown instead of the two document messages above when the selected method calls for a cardholder document the page is not showing — a method the card form cannot pay with, or one outside the payment root's `group` — so the shopper has to choose another payment method. |
|
|
69
|
+
| `unsavedBillingAddressMessage` | string | `"Save your billing address before placing the order."` | Shown when the shopper declined "same as delivery" but never saved a custom billing address. |
|
|
70
|
+
| `invalidCardDataMessage` | string | `"Enter valid card details before placing the order."` | Shown when the new-card draft itself fails validation before ever reaching the gateway. |
|
|
71
|
+
| `invalidSecurityCodeMessage` | string | `"Enter a valid security code before placing the order."` | Shown when a saved-card submission is refused for a missing or invalid CVV. |
|
|
72
|
+
| `gatewayErrorMessage` | string | `"Unable to complete the payment. Please try again."` | Shown when the payment gateway itself refuses the submitted card. |
|
|
73
|
+
| `inFlightMessage` | string | `"This order is already being placed. Please wait."` | Shown when a click lands on an order-placement attempt whose own sequence is still running — a transaction this instance did not open, and which nothing here may finalize or re-run. |
|
|
74
|
+
| `unconfirmedMessage` | string | `"We could not confirm your order yet. Your payment may still be processing, so please do not pay again."` | Shown when the payment was handed to the gateway but the order could not be confirmed, so it may still be processing. Renders in its own `role="status"` node, never in the error alert. |
|
|
75
|
+
| `successHref` | string | *(none)* | Route **template** navigated to after a successful placement. The literal `{orderGroup}` is replaced with the placed order's group, percent-encoded. **Opt-in and success-gated**: no default is provided on purpose, so an absent prop keeps today's behavior byte-for-byte, and the navigation is requested only once an attempt settles with no errors and a non-null order group — never on a refusal. |
|
|
76
|
+
|
|
77
|
+
Every message prop above is resolved by the `code` of the client-synthesized `PaymentUserError` this CTA renders — in its alert, or in its status node for `unconfirmedMessage` — never by which component produced the error. A `PaymentUserError` whose code this component does not author (a server-returned code) renders that error's own `message` unchanged.
|
|
78
|
+
|
|
79
|
+
**Data:** declares its own thin `fragment PlaceOrderData on Query` selecting `payment.selectedMethodId`, `payment.selectedInstallment`, and `payment.methods { id groupName }` — the exact slice the orchestration hook needs, no more. **Actions:** declares `actions: { payment: ['startTransaction', 'gatewayCallback'], analytics: ['track'] }`. Must be inside a payment root (`requiresContext: 'payment'`).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Canonical schema example
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"payment-section": {
|
|
88
|
+
"type": "PaymentSectionRoot",
|
|
89
|
+
"children": ["payment-method-list", "card-entry", "place-order"]
|
|
90
|
+
},
|
|
91
|
+
"payment-method-list": { "type": "PaymentMethodList" },
|
|
92
|
+
"card-entry": { "type": "CardEntry" },
|
|
93
|
+
"place-order": {
|
|
94
|
+
"type": "PlaceOrder",
|
|
95
|
+
"props": {
|
|
96
|
+
"label": "Place order",
|
|
97
|
+
"placingLabel": "Placing order...",
|
|
98
|
+
"successMessage": "Order placed successfully.",
|
|
99
|
+
"successHref": "/checkout/order-placed/{orderGroup}"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`successHref` is shown here for reference only — it is opt-in (see the props table above), and no store composes it today. It must be a path on the store's own origin (root-relative, like the example above, or an absolute URL on the same origin) — an absolute URL on a different origin, a protocol-relative URL, or a non-http(s) scheme is refused rather than navigated to.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Common mistakes
|
|
110
|
+
|
|
111
|
+
- **Nesting `PlaceOrder` inside `CardEntry` or `SavedCards`.** It must be a sibling of both under the same payment root, so one CTA serves both the new-card and saved-card paths.
|
|
112
|
+
- **Placing `PlaceOrder` outside any `PaymentSectionRoot`/`PaymentSectionController`.** It throws at render — there is no payment context or submission channel to resolve.
|
|
113
|
+
- **Confusing it with `BuyButtonButton`.** `BuyButtonButton` adds a product to the cart on a PDP; `PlaceOrder` runs the payment sequence in checkout. They are never composed together.
|
|
114
|
+
- **Expecting a redirect or a 3D Secure challenge.** v1 is the direct-API mode only — the shopper stays on the page and the outcome renders inline. The redirect mode is phase 2.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Known limitations
|
|
119
|
+
|
|
120
|
+
- **A remount reads the surviving attempt instead of losing it — but the attempt itself does not survive a page reload.** T29 (`specs/make-an-order-attempt-survive-a.md`) gave the place-order sequence an identity-and-phase record on the payment context, above this component's own mount, alongside `placing`. Closing and reopening a `Dialog`/`Drawer` popup with no `keepMounted` no longer resets it: a settled attempt renders disabled with no further calls (`succeeded` is derived from the record, not only from this instance's own `attempted`/`userErrors`), and an attempt whose card already reached the gateway before the remount offers a resume (`resumeLabel`) that finalizes the same transaction — never a fresh `startTransaction` or a second card submission. This closes the case this bullet used to attribute to "a settled-order read model that outlives the overlay (T16/T17)": both are merged, and the limitation still stood, because neither one ever touched this component's own mount-scoped guard, which is what actually caused the duplicate-order risk. What is still not covered: a full page reload loses the attempt (no `sessionStorage` or equivalent persists it today — T29's own OQ-3), and the direct-to-gateway POST's abort budget (30s, `sdk/payment/gateway.ts`) is an unconfirmed placeholder pending a number from payments (OQ-7).
|
|
121
|
+
- **Sibling placement (never nested inside `CardEntry`/`SavedCards`) is a convention, not something the compiler enforces.** `requiresContext: 'payment'` only fails a build with no payment-providing ancestor at any depth; it cannot detect that this component was nested one level too deep. Getting this wrong compiles clean and still leaves the other entry path with no CTA at all (see `specs/place-order.md`'s Open items).
|
|
122
|
+
- **`successHref` navigates with `router.replace`, not `router.push`.** The payment step is removed from the history stack, so the Back button from the confirmation page returns to whatever preceded payment, not to the payment step itself — deliberate (placing an order is terminal, and a live place-order CTA one Back press away risks a duplicate transaction), but a divergence from this package's other redirect-on-success component, `CartRedirectOnAdd`, which uses `push`.
|
|
123
|
+
- **A `successHref` authored without the literal `{orderGroup}` placeholder fails closed.** The component refuses to navigate rather than sending every shopper to the same unresolvable URL, and logs a `console.warn` outside production. This is a store-authoring mistake to fix in the template, not a runtime condition to handle.
|
|
124
|
+
- **A `successHref` that resolves off the store's own origin also fails closed.** An absolute URL on a different origin, a protocol-relative URL (`//host/...`), or a non-http(s) scheme is refused rather than navigated to, with the same dev-only `console.warn`. This keeps a store schema from turning the CTA into a redirect off the storefront right after a real purchase.
|
|
125
|
+
- **`successHref` is inert until a store authors it.** As of this writing no store composes it — see `specs/navigate-the-shopper-to-the-order.md` for the follow-up that wires Reserva's payment page to it, gated on the confirmation page's read path being fixed first.
|
|
126
|
+
|
|
127
|
+
## Styling
|
|
128
|
+
|
|
129
|
+
| Component | What you style | Plausible classes |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `PlaceOrder` (root `div`) | Layout wrapper stacking the button and the outcome region | `flex flex-col gap-[8px] items-start` |
|
|
132
|
+
| Button (`data-fs-place-order-button`) | The CTA itself — full-width pill, disabled/busy states | `inline-flex items-center justify-center w-full h-[44px] px-[20px] bg-[#C8102E] text-white rounded-[4px] text-[14px] font-bold uppercase cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed` |
|
|
133
|
+
| Error (`data-fs-place-order-error`) | Inline error line, `role="alert"` | `text-[13px] text-[#C0392B]` |
|
|
134
|
+
| Unconfirmed (`data-fs-place-order-unconfirmed`) | Inline notice that the order could not be confirmed yet, `role="status"` | `text-[13px] text-[#8A6D3B]` |
|
|
135
|
+
| Success (`data-fs-place-order-success`) | Inline success line, `role="status"` | `text-[13px] text-[#1E8E3E]` |
|
|
136
|
+
| Success order id (`data-fs-place-order-success-order-id`) | The placed order's id, inside the success line | `font-mono text-[12px] text-[#666666]` |
|
|
137
|
+
|
|
138
|
+
Always include `disabled:` styling on the button — it disables automatically while the sequence runs and again once it succeeds.
|
|
139
|
+
|
|
140
|
+
Styled example:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"place-order": {
|
|
145
|
+
"type": "PlaceOrder",
|
|
146
|
+
"style": ["flex", "flex-col", "gap-[8px]", "items-start"],
|
|
147
|
+
"props": { "label": "Place order" }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Related
|
|
155
|
+
|
|
156
|
+
- [[payment]] — `PaymentSectionRoot` / `PaymentSectionController`, the roots `PlaceOrder` must compose under.
|
|
157
|
+
- [[buy-button]] — the PDP purchase CTA and this component's named precedent for the blocking loading-state pattern (disabled + `aria-busy` + a `-pending` hook).
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# SaveCard
|
|
2
|
+
|
|
3
|
+
**Category:** commerce
|
|
4
|
+
**Purpose:** Opt-in control for authenticated shoppers to save the card they are currently entering for a future purchase, with a custom nickname. Off by default. Checking it also grants the account's data-storage consent; unchecking it never revokes that consent.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
Use these signals to detect that a SaveCard block is present. Position on the page is irrelevant for
|
|
11
|
+
detection — it can appear anywhere inside a payment section. The image is the source of truth for
|
|
12
|
+
layout.
|
|
13
|
+
|
|
14
|
+
A SaveCard block contains any combination of:
|
|
15
|
+
|
|
16
|
+
- A single **checkbox with a label reading something like "Save this card for next time"**, sitting
|
|
17
|
+
inside (or directly beside) a credit-card entry form.
|
|
18
|
+
- Immediately below or beside the checkbox, **small, secondary-styled helper text** mentioning storing
|
|
19
|
+
or saving personal data — present only when the account has not already granted that consent.
|
|
20
|
+
- When the checkbox is shown checked in the image, a **second text input labeled something like "Card
|
|
21
|
+
nickname" / "Name this card"** appears directly below it.
|
|
22
|
+
- This block always sits **inside a checkout payment section**, near card-number fields or a saved-cards
|
|
23
|
+
list — never on a product page, cart page, or as a standalone marketing checkbox.
|
|
24
|
+
|
|
25
|
+
If the image shows a single opt-in checkbox near card-entry fields, with an optional name/nickname text
|
|
26
|
+
field conditionally shown, use `SaveCard`. If the checkbox instead controls a newsletter or general
|
|
27
|
+
data-sharing preference with no relation to a card, that is `NewsletterOptInControl` /
|
|
28
|
+
`SavePersonalDataOptInControl` (`ClientPreferencesRoot`'s Controls), not this component.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## When to use
|
|
33
|
+
|
|
34
|
+
- Inside a checkout's payment section, as a sibling of the card-entry fields (and/or the saved-cards
|
|
35
|
+
list), so a logged-in shopper can opt in to saving the card they are currently using.
|
|
36
|
+
|
|
37
|
+
## When NOT to use
|
|
38
|
+
|
|
39
|
+
- A general "share my data with this store" consent, unrelated to a specific card → use
|
|
40
|
+
`SavePersonalDataOptInControl` (under `ClientPreferencesRoot`) instead. Different data ownership,
|
|
41
|
+
different section, and — critically — that surface's own `$data` read has a live defect (always reads
|
|
42
|
+
as unset); do not use it as a reference implementation for a new read.
|
|
43
|
+
- A newsletter opt-in → use `NewsletterOptInControl` instead.
|
|
44
|
+
- The list of already-saved cards → use `SavedCards` / `SavedCardOption` instead. `SaveCard` only offers
|
|
45
|
+
to save the card currently being entered; it never lists or selects an existing saved card.
|
|
46
|
+
- Outside a payment section, or outside an identity/session context → `SaveCard` requires **both** a
|
|
47
|
+
`PaymentSectionRoot`/`PaymentSectionController` ancestor and a `SessionRoot` ancestor. Placing it
|
|
48
|
+
without either is a build/runtime error, not a silent no-op.
|
|
49
|
+
- For a guest checkout flow with no login step at all → `SaveCard` self-gates to render nothing for
|
|
50
|
+
anyone who is not logged in; do not wrap it in an extra visibility condition in the schema.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Composition
|
|
55
|
+
|
|
56
|
+
`SaveCard` is a single leaf — no root, no required children, no sub-components:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
PaymentSectionRoot (or PaymentSectionController)
|
|
60
|
+
├── CardEntry (sibling — new-card fields)
|
|
61
|
+
├── SavedCards (sibling — saved-card picker)
|
|
62
|
+
├── Installments (sibling — installment selector)
|
|
63
|
+
└── SaveCard (sibling — this component)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`SaveCard` must be a descendant of `PaymentSectionRoot` or `PaymentSectionController` (it reads and
|
|
67
|
+
writes the shopper's in-progress card draft) **and** must also have a `SessionRoot` ancestor somewhere
|
|
68
|
+
in the same composed page/layout tree (it reads the identity session state to self-gate). Place it as a
|
|
69
|
+
**sibling** of `CardEntry` and `SavedCards`, never nested inside either — one `SaveCard` instance covers
|
|
70
|
+
whichever entry path the shopper is on, the same way `Installments` does.
|
|
71
|
+
|
|
72
|
+
It self-gates to render nothing while the session is loading, for a guest, or for an
|
|
73
|
+
identified-but-not-logged-in shopper — safe to include speculatively inside any payment section, but for
|
|
74
|
+
image-to-schema work only include it when the image actually shows the checkbox described above.
|
|
75
|
+
|
|
76
|
+
It also renders nothing while the card payment is collapsed — the persisted selection is a method that
|
|
77
|
+
is not a card, or one the section does not serve, and a composed `PaymentMethodList` can bring the card
|
|
78
|
+
form back (see [[payment]]). The save-card flag and nickname stay on the draft, so the control comes back
|
|
79
|
+
as the shopper left it.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Components
|
|
84
|
+
|
|
85
|
+
### SaveCard
|
|
86
|
+
|
|
87
|
+
| Prop | Type | Default | Notes |
|
|
88
|
+
|------|------|---------|-------|
|
|
89
|
+
| `label` | string | `"Save this card for next time"` | The toggle's own label text. |
|
|
90
|
+
| `nicknameLabel` | string | `"Card nickname"` | Label for the nickname field; rendered only while checked. |
|
|
91
|
+
| `nicknamePlaceholder` | string | — | Optional placeholder for the nickname field. |
|
|
92
|
+
| `consentNoticeLabel` | string | `"Checking this also lets us store your personal data for future purchases."` | Shown beside the toggle while the account has not already granted the data-storage consent. |
|
|
93
|
+
| `disabled` | boolean | `false` | Disables both the toggle and the nickname field. |
|
|
94
|
+
|
|
95
|
+
**Data:** declares its own `fragment SaveCardData on Query` (selects `Query.clientPreferences.savePersonalData`
|
|
96
|
+
only — a single boolean, not a card field) — a small, thin fragment the compiler merges into the page
|
|
97
|
+
query alongside `PaymentSectionRoot`'s own selection; no explicit `dataSources` entry needed.
|
|
98
|
+
`fetchStrategy: 'reactive'` — the consent notice updates live from the cache, no refetch.
|
|
99
|
+
|
|
100
|
+
**The toggle never seeds from the standing consent.** It always starts unchecked, even when the account
|
|
101
|
+
has already granted `savePersonalData` elsewhere — saving *this* card is always a fresh, per-transaction
|
|
102
|
+
choice.
|
|
103
|
+
|
|
104
|
+
**Checking the toggle** records the shopper's intent for this card *and* grants the account's
|
|
105
|
+
data-storage consent in the same action; the nickname field appears immediately below. **Unchecking**
|
|
106
|
+
withdraws only this transaction's intent — it never revokes a consent an unrelated control may have
|
|
107
|
+
granted. On a rejected consent write, the first error message renders inline.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Canonical schema example
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"payment-section": {
|
|
116
|
+
"type": "PaymentSectionRoot",
|
|
117
|
+
"children": ["card-entry", "saved-cards", "installments", "save-card"]
|
|
118
|
+
},
|
|
119
|
+
"save-card": {
|
|
120
|
+
"type": "SaveCard",
|
|
121
|
+
"props": { "label": "Save this card for next time" },
|
|
122
|
+
"style": ["flex", "flex-col", "gap-2"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Common mistakes
|
|
130
|
+
|
|
131
|
+
- **Inventing `SaveCardControl` or `SaveCardToggle`.** The component is `SaveCard`.
|
|
132
|
+
- **Nesting `SaveCard` inside `CardEntry` or `SavedCards`.** It must be a sibling of both under the
|
|
133
|
+
same `PaymentSectionRoot`, exactly like `Installments`.
|
|
134
|
+
- **Placing `SaveCard` without a `SessionRoot` ancestor somewhere in the page.** It requires session
|
|
135
|
+
context to self-gate and throws without one.
|
|
136
|
+
- **Expecting the checkbox to start checked because the shopper already granted data-storage consent
|
|
137
|
+
elsewhere.** It always starts unchecked — the standing consent and the per-transaction save intent
|
|
138
|
+
are independent by design.
|
|
139
|
+
- **Expecting unchecking to revoke a previously granted consent.** It deliberately does not; see
|
|
140
|
+
`specs/save-card-control.md` Decision 1 for the full rationale.
|
|
141
|
+
- **Using `SaveCard` to render a general privacy/data-sharing consent form.** It is scoped to a single
|
|
142
|
+
boolean and a nickname for the card currently being entered — use `ClientPreferencesRoot`'s Controls
|
|
143
|
+
for anything broader.
|
|
144
|
+
- **Expecting a full card number or CVV field.** The fragment structurally selects neither, and no prop
|
|
145
|
+
accepts one — this component never touches sensitive card data.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Styling
|
|
150
|
+
|
|
151
|
+
Single DOM root wrapping the toggle field, an optional nickname field, and an optional inline error.
|
|
152
|
+
|
|
153
|
+
| Component | What you style | Plausible classes |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `SaveCard` (root `div`) | Stacks the toggle field, nickname field, and error | `flex flex-col gap-[8px]` |
|
|
156
|
+
| Consent notice (`data-fs-save-card-consent-notice`) | De-emphasized helper text | `text-[12px] text-[#666666]` |
|
|
157
|
+
| Nickname field wrapper (`data-fs-save-card-nickname-field`) | Spacing above the field | `mt-[8px]` |
|
|
158
|
+
| Inline error (`data-fs-save-card-error`) | Warning-toned alert text | `text-[12px] font-medium text-[#B00020]` |
|
|
159
|
+
| Placing (`data-fs-save-card-placing`) | Present while an order-placement sequence is in flight (T28) — distinct from `data-fs-save-card-pending`, which this component already emits for its own local consent dispatch; the root is also `inert` and `aria-disabled` while placing | `opacity-60 cursor-not-allowed` |
|
|
160
|
+
|
|
161
|
+
Styled example:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"save-card": {
|
|
166
|
+
"type": "SaveCard",
|
|
167
|
+
"style": ["flex", "flex-col", "gap-2"]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
There is no per-field styling hook beyond the particles' own `data-fs-checkbox-*` / `data-fs-field-*`
|
|
173
|
+
attributes (`Checkbox.Root`/`Checkbox.Indicator`, `Field.Root`/`Field.Label`/`Field.Control`) — style
|
|
174
|
+
the checked state via Base UI's own `data-checked` on `Checkbox.Root`, per the `style-components` skill.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Related
|
|
179
|
+
|
|
180
|
+
- [[saved-cards]] — the list of already-saved cards, and the CVV re-entry for one; `SaveCard` only
|
|
181
|
+
offers to save the card currently being entered, never lists an existing one.
|
|
182
|
+
- [[installments]] — the closest structural sibling: a single leaf requiring `PaymentSectionRoot`, no
|
|
183
|
+
Root/leaf split, sibling placement under the same section.
|
|
184
|
+
- [[client-preferences]] — owns the broader `savePersonalData` / `optinNewsLetter` consent surface;
|
|
185
|
+
`SaveCard` reads the same underlying field but through its own fragment, never by composing under
|
|
186
|
+
`ClientPreferencesRoot` (whose own read of that field has a live, unrelated defect).
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# SavedCards
|
|
2
|
+
|
|
3
|
+
**Category:** commerce
|
|
4
|
+
**Purpose:** Renders the checkout payment step's saved-card picker — a radio list of a shopper's stored cards (network, last four digits, label), with expired cards shown but disabled, and a CVV field that appears once a card is selected.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
Use these signals to detect that a SavedCards block is present. Position on the page is irrelevant for detection — it can appear anywhere in a payment/checkout step. The image is the source of truth for layout.
|
|
11
|
+
|
|
12
|
+
A SavedCards block contains any combination of:
|
|
13
|
+
|
|
14
|
+
- A **list of radio rows**, each showing a **card network name or logo** (Visa, Mastercard, …) plus a **masked number** like `•••• 4242` or `**** 4242`.
|
|
15
|
+
- A **custom label** next to a row, e.g. *"Meu cartão"* / *"Personal card"* — shopper-assigned, not always present.
|
|
16
|
+
- A row rendered **visually muted or with a "expired" tag**, still present in the list but not clickable.
|
|
17
|
+
- A **short numeric input appearing next to or below the selected row**, typically labeled *"CVV"* / *"Security code"* / *"CVC"* — this appears only once a row is checked. Its digits render **obscured** (dots), same visual family as a password field, even though it is not one under the hood (see Components below, `SavedCardCvvField`, for why).
|
|
18
|
+
- A **"pay with a new card" link or button** rendered as a sibling entry, not a row inside this list — that sibling is a different component (`CardEntry`), not part of this block.
|
|
19
|
+
|
|
20
|
+
If the image shows a **radio list of masked card numbers with network names**, use `SavedCards`. Do not use it for a single card-entry form (no radio choice) — that is `CardEntry`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## When to use
|
|
25
|
+
|
|
26
|
+
- The checkout/payment step, for an authenticated shopper with one or more stored cards.
|
|
27
|
+
- Anywhere a shopper picks among previously saved payment methods before completing a purchase.
|
|
28
|
+
|
|
29
|
+
## When NOT to use
|
|
30
|
+
|
|
31
|
+
- A blank new-card entry form (number, expiry, holder name fields) → use `CardEntry` instead; `SavedCards` never renders those fields.
|
|
32
|
+
- A payment-method picker across networks/methods (Visa vs. Pix vs. Cash) → use `PaymentMethodList`, the sibling method-level picker under the same `PaymentSectionRoot`. `SavedCards` only distinguishes stored *cards*, not payment *methods*.
|
|
33
|
+
- An address book / saved shipping addresses list → use the equivalent shipping-address components; visually similar (a radio list of saved records) but a different domain.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Composition
|
|
38
|
+
|
|
39
|
+
`SavedCards` is the list root and must be a descendant of `PaymentSectionRoot` or `PaymentSectionController`. `SavedCardOption` is the per-card row template placed directly inside it. `SavedCardCvvField` is placed inside `SavedCardOption` so the CVV appears once that row is selected.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
PaymentSectionRoot (or PaymentSectionController)
|
|
43
|
+
├── PaymentMethodList (sibling — network/method picker)
|
|
44
|
+
├── SavedCards
|
|
45
|
+
│ └── SavedCardOption (per-card template, repeated once per stored card)
|
|
46
|
+
│ └── SavedCardCvvField (appears only for the selected, CVV-required row)
|
|
47
|
+
└── CardEntry (sibling — "use a new card", not a child of SavedCards)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Child order reflects visual order. `SavedCardOption` and `SavedCardCvvField` are the only two valid parent/child pairs this block defines; everything else (`PaymentMethodList`, `CardEntry`) is a sibling under the same `PaymentSectionRoot`, never nested inside `SavedCards`.
|
|
51
|
+
|
|
52
|
+
**Composition rules:**
|
|
53
|
+
|
|
54
|
+
- `SavedCards` must be inside `PaymentSectionRoot` or `PaymentSectionController`. Outside either, it throws.
|
|
55
|
+
- `SavedCardOption` must be a direct child of `SavedCards`. It renders `null` when its row's data has not resolved.
|
|
56
|
+
- `SavedCardCvvField` must be inside `SavedCardOption`. Outside it, it throws. Omitting it means a shopper can select that card but never submit a payment with it — always include it.
|
|
57
|
+
- `CardEntry` ("use a new card") is a **sibling** of `SavedCards`, never a child or a row inside it — there is no "toggle" prop that switches this list into new-card mode.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Components
|
|
62
|
+
|
|
63
|
+
### SavedCards
|
|
64
|
+
|
|
65
|
+
List root. Renders a radio group of the shopper's saved cards. Renders its container even when the list is empty or still loading (a hooked, not zero-height, region). It renders nothing at all while the card payment is collapsed — the persisted selection is a method that is not a card, or one the section does not serve, and a composed `PaymentMethodList` can bring the card form back (see [[payment]]). Its rows' CVV fields unmount with it, which clears any code typed there. To reach a saved card from a method that is not a card, the shopper chooses the card option first, which brings the list back.
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Notes |
|
|
68
|
+
|------|------|---------|-------|
|
|
69
|
+
| `legend` | string | `"Saved cards"` | The radio group's accessible name, rendered as a visible fieldset legend above the rows (T38). An authored empty or whitespace-only string falls back to this default rather than rendering a nameless group. |
|
|
70
|
+
|
|
71
|
+
Which cards exist is always real data (`Query.payment.savedCards`) — never author them as props.
|
|
72
|
+
|
|
73
|
+
**Data:** declares `fragment SavedCardsListData on Query`. The compiler merges it into the page query — no explicit `dataSources` entry needed.
|
|
74
|
+
|
|
75
|
+
### SavedCardOption
|
|
76
|
+
|
|
77
|
+
One saved card as a radio row: network name, masked last four (`•••• 4242`), and the shopper's own label when set. Disabled (not selectable) when the card is expired — the row still renders so the shopper knows the card exists.
|
|
78
|
+
|
|
79
|
+
No props beyond standard container attributes (`className`, `children`). Must be a direct child of `SavedCards`.
|
|
80
|
+
|
|
81
|
+
**Data:** declares `fragment SavedCardOptionData on Query`, independently resolved per row (a write to one saved card only re-renders that row, not the whole list).
|
|
82
|
+
|
|
83
|
+
### SavedCardCvvField
|
|
84
|
+
|
|
85
|
+
The CVV re-entry field for its row. Renders nothing until that specific row is selected and the card requires a CVV; then renders a numeric input (digits only, capped at 4 characters) whose digits render **obscured** (dots) via an inline `-webkit-text-security: disc` style once the code is fully typed, with a helper line underneath it (`securityCodeDescription`, defaulting to "The 3-4 digit number on your card"; an empty string renders nothing). Wires the direct-to-gateway payment submission for the saved-card path the moment it is composed — the CVV is never sent to the storefront's own backend. A blank or under-three-digit code refuses before the place-order sequence opens a transaction, moving focus back to this input; the message reaches the shopper through the place-order CTA's own alert, not an inline field error here.
|
|
86
|
+
|
|
87
|
+
The mask engages on the third digit and stays on for a fourth. Three, because a saved card carries no per-network security-code length — the row's data has a display network name, not a `cvvMask` — so there is no signal here that tells a 3-digit network apart from Amex's 4, and the field's own accept-either floor is the only length available. Masking at the floor never leaves a complete code legible on either kind of card; a 4-digit code is simply already masked when its last digit arrives. Below three digits the code renders in the clear, so a shopper can read back what they just typed, and backspacing below three reveals what is left. Same rule and same shared predicate as `CardEntry`'s CVV field, which resolves its length from the detected method instead.
|
|
88
|
+
|
|
89
|
+
`type="password"` was tried first and reverted: a real Chrome browser offered to save the security code to its password manager (synced to the signed-in account) the instant the field carried that type, which PCI-DSS forbids for a value that must not be retained past authorization. The input stays `type="text"`, and no caller-passed `type` can override that. The CSS property above obscures the digits without tripping that heuristic, since it never touches `type`; a caller-passed `style` cannot remove it either — once the code is complete the mask always merges in last, so no caller can hold it off past that point. Supported in Chromium and WebKit (and, per T70's spec, Firefox 114+ — unverified on this branch); an engine without it renders the digits in the clear, same as before this mechanism existed — not a regression. The declaration is an inline style, which outranks a class-based rule from a compound swatch at normal importance — but not one that reaches for Tailwind's `!` important modifier: swatch tokens reach `className` verbatim with no validation, so a store that authors an arbitrary-property utility ending in `!` can still unmask the field. Nothing in this package closes that hole; it is accepted rather than fixed (masking spec, OQ-1).
|
|
90
|
+
|
|
91
|
+
No props beyond standard input attributes (`className`), `securityCodeLabel` (accessible name), and `securityCodeDescription` (helper text). Must be inside `SavedCardOption`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Canonical schema example
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"saved-cards": {
|
|
100
|
+
"type": "SavedCards",
|
|
101
|
+
"children": ["saved-card-option"]
|
|
102
|
+
},
|
|
103
|
+
"saved-card-option": {
|
|
104
|
+
"type": "SavedCardOption",
|
|
105
|
+
"children": ["saved-card-cvv"]
|
|
106
|
+
},
|
|
107
|
+
"saved-card-cvv": { "type": "SavedCardCvvField" }
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### In context (inside a payment section, alongside the method picker and "use a new card")
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"payment-section": {
|
|
116
|
+
"type": "PaymentSectionRoot",
|
|
117
|
+
"children": ["payment-method-list", "saved-cards", "card-entry"]
|
|
118
|
+
},
|
|
119
|
+
"payment-method-list": { "type": "PaymentMethodList" },
|
|
120
|
+
"saved-cards": {
|
|
121
|
+
"type": "SavedCards",
|
|
122
|
+
"children": ["saved-card-option"]
|
|
123
|
+
},
|
|
124
|
+
"saved-card-option": {
|
|
125
|
+
"type": "SavedCardOption",
|
|
126
|
+
"children": ["saved-card-cvv"]
|
|
127
|
+
},
|
|
128
|
+
"saved-card-cvv": { "type": "SavedCardCvvField" },
|
|
129
|
+
"card-entry": { "type": "CardEntry" }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Common mistakes
|
|
136
|
+
|
|
137
|
+
- **Nesting `CardEntry` ("use a new card") inside `SavedCards`.** It is a sibling under the same `PaymentSectionRoot`, never a row or a child of this list.
|
|
138
|
+
- **Inventing a `mode` or `showNewCard` prop on `SavedCards` to switch to new-card entry.** There is no such prop — "use a new card" is a separate component composed alongside this one, not a display mode of it.
|
|
139
|
+
- **Omitting `SavedCardCvvField` inside `SavedCardOption`.** Without it, a shopper can select a saved card but the place-order flow has nothing to submit with — always include it.
|
|
140
|
+
- **Placing `SavedCardOption` or `SavedCardCvvField` outside their required ancestor.** `SavedCardOption` must be inside `SavedCards`; `SavedCardCvvField` must be inside `SavedCardOption`. Only `SavedCardCvvField` throws when misplaced. A misplaced `SavedCardOption` does **not** throw — it silently renders `null` (no ancestry check of its own; it just has no resolved row data to show), so a page can ship with a silently empty saved-cards list. This is an authoring convention, not a compiler or runtime guarantee: `requiresContext` is satisfied by *any* ancestor providing that context at *any* depth, not specifically a direct parent, and there is no mechanism that forbids a given ancestor either.
|
|
141
|
+
- **Passing a `savedCards` prop, a card list, or a `cvv` value as an authored prop.** Every field here is real data from `Query.payment.savedCards` or shopper-typed local state — never a schema-bindable prop. Neither component in this block declares a `cvv`, `cardNumber`, or `bin` prop of any kind.
|
|
142
|
+
- **Using `SavedCards` for the network/method picker.** That is `PaymentMethodList` — a sibling under the same root, distinguishing payment *methods* (Visa vs. Pix), not stored *cards*.
|
|
143
|
+
- **Wrapping `SavedCards` in a page with no `PaymentSectionRoot` ancestor.** It requires the payment context; use only inside a composed payment section.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Known limitations
|
|
148
|
+
|
|
149
|
+
- **A saved card with `cvvRequired: false` can be selected but never paid with — open item, not fixed in this pass.** `SavedCardCvvField` renders nothing when `cvvRequired` is `false`, and it is the only component that registers the `'saved-card'` submission continuation. For such a card, today: the row renders selected (`data-fs-saved-card-option-selected` present, `aria-checked="true"`), but `usePaymentSubmission().resolve('saved-card')` returns `null`, so `usePlaceOrder` resolves `MISSING_PAYMENT_SUBMISSION` and Place Order fails — with no CVV field ever offered and no way for the shopper to proceed. This is data-driven (`SavedCard.cvvRequired` passes through the account's own `useCvvForAuthorization`), not an authoring mistake, and it is deliberately left open rather than guessed at: the fix depends on whether the payment gateway's direct-to-gateway POST accepts `fields.validationCode: ''` for a `useCvvForAuthorization: false` account, or whether that account type needs the legacy's separate no-CVV mutation instead — a payments question this repository cannot answer on its own. See `specs/saved-cards.md`'s Open Questions and Assumptions for the full record.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Styling
|
|
154
|
+
|
|
155
|
+
Style `SavedCards` as a vertical stack of rows, each row as a horizontal line (radio indicator, network, last four, label), and the CVV field as a small, short numeric input that appears inline with the selected row.
|
|
156
|
+
|
|
157
|
+
| Component | What you style | Plausible classes |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `SavedCards` | Stacks the rows | `flex flex-col gap-[8px] w-full` |
|
|
160
|
+
| Fieldset (`data-fs-saved-cards-fieldset`, native `<fieldset>`) | Wraps the legend and the radio rows (T38) — gives the group its visible, accessible name | usually left unstyled; a merchant border/padding choice |
|
|
161
|
+
| Legend (`data-fs-fieldset-legend`, native `<legend>`) | The group's visible name, from `legend` | `text-[14px] font-medium text-[#1A1A1A]` |
|
|
162
|
+
| `SavedCardOption` | One row — indicator, network, last four, label in a line; muted/dashed when expired | `flex items-center gap-[8px] p-[12px] border border-[#E0E0E0] rounded-[8px] data-[fs-saved-card-option-expired]:opacity-50 data-[fs-saved-card-option-selected]:border-[#000000]` |
|
|
163
|
+
| `SavedCardCvvField` | The short CVV input | `w-[64px] text-[14px] px-[8px] py-[6px] border border-[#CCCCCC] rounded-[4px]` |
|
|
164
|
+
| CVV helper text (`data-fs-saved-card-cvv-description`) | Small muted line next to/under the CVV input | `text-[12px] text-[#666666]` |
|
|
165
|
+
| Placing (`data-fs-saved-cards-placing`) | Present on `SavedCards`' own root while an order-placement sequence is in flight (T28); the whole list — including every `SavedCardOption` row and its `SavedCardCvvField` — is `inert` and `aria-disabled` at that point, transitively, with no attribute of their own | `opacity-60 cursor-not-allowed` |
|
|
166
|
+
|
|
167
|
+
Styled example:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"saved-cards": {
|
|
172
|
+
"type": "SavedCards",
|
|
173
|
+
"children": ["saved-card-option"],
|
|
174
|
+
"style": ["flex", "flex-col", "gap-[8px]", "w-full"]
|
|
175
|
+
},
|
|
176
|
+
"saved-card-option": {
|
|
177
|
+
"type": "SavedCardOption",
|
|
178
|
+
"children": ["saved-card-cvv"],
|
|
179
|
+
"style": [
|
|
180
|
+
"flex", "items-center", "gap-[8px]", "p-[12px]",
|
|
181
|
+
"border", "border-[#E0E0E0]", "rounded-[8px]",
|
|
182
|
+
"data-[fs-saved-card-option-expired]:opacity-50",
|
|
183
|
+
"data-[fs-saved-card-option-selected]:border-[#000000]"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
"saved-card-cvv": {
|
|
187
|
+
"type": "SavedCardCvvField",
|
|
188
|
+
"style": ["w-[64px]", "text-[14px]", "px-[8px]", "py-[6px]", "border", "border-[#CCCCCC]", "rounded-[4px]"]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Accessibility Contract
|
|
196
|
+
|
|
197
|
+
**What the platform promises:** `SavedCards`' radio group carries a visible, programmatic accessible
|
|
198
|
+
name (`legend`, default `"Saved cards"`) — a screen-reader user hears these rows are a choice between
|
|
199
|
+
stored cards, not payment methods. An authored empty or whitespace-only `legend` falls back to the
|
|
200
|
+
default rather than shipping a nameless group. Each row keeps its own accessible name from its network
|
|
201
|
+
and masked last four; the CVV re-entry field carries its own `securityCodeLabel` as an `aria-label`
|
|
202
|
+
and, when `securityCodeDescription` is set, an `aria-describedby` association to the helper text. The
|
|
203
|
+
group is a native `<fieldset>`/`<legend>` pair, not only `aria-label`. Keyboard: the group is one Tab
|
|
204
|
+
stop — Tab lands on the checked row, and the arrow keys move the checked row and change the selection.
|
|
205
|
+
A default `:focus-visible` outline is painted from the platform's own stylesheet.
|
|
206
|
+
|
|
207
|
+
**What the platform does not promise:** colour contrast. `SavedCards` (and every other payment
|
|
208
|
+
component) exposes `data-fs-*` hooks so a merchant can author contrast-compliant swatches, but no
|
|
209
|
+
component in this package measures or guarantees any contrast ratio — that is the merchant's theme, not
|
|
210
|
+
this platform.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Related
|
|
215
|
+
|
|
216
|
+
- [[payment]] — `PaymentSectionRoot` / `PaymentMethodList`: the required ancestor and the sibling network/method picker. `SavedCards` cannot render outside a `PaymentSectionRoot` (or `PaymentSectionController`).
|
|
217
|
+
- `CardEntry` — the "use a new card" entry point (new-card capture, a different task's deliverable). Always a sibling of `SavedCards`, never nested inside it.
|
|
218
|
+
- [[shipping-address]] — its saved-address list is the closest visual precedent (a radio list of stored records with an expired/non-selectable-style state), from a different domain (addresses, not cards).
|