@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,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { useStorefrontContext } from "../../sdk/context/storefront-context.js";
|
|
5
|
+
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
6
|
+
import { resolveAuthoredCopy } from "../../utils/resolve-authored-copy.js";
|
|
7
|
+
import { Fieldset } from "../../particles/fieldset/fieldset.js";
|
|
8
|
+
import { RadioGroup, RadioIndicator, RadioRoot } from "../../particles/radio/radio.js";
|
|
9
|
+
import { substituteTemplate } from "../../utils/substitute-template.js";
|
|
10
|
+
import { findInstallmentOption, resolveInstallmentTiers, useInstallmentsSelection } from "./use-installments-selection.js";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
//#region src/matter/installments/installments.tsx
|
|
13
|
+
/**
|
|
14
|
+
* `Installments` matter (specs/installments.md, T9 of the Credit Card epic;
|
|
15
|
+
* docs/features/credit-card/rfc.md § D4). Lets a shopper pick an installment plan for both a
|
|
16
|
+
* freshly entered card and a selected saved card, rendered as a `RadioGroup` — one row per tier the
|
|
17
|
+
* matched option carries (the API guarantees a 1x tier is present in every option, so one is always
|
|
18
|
+
* shown and selectable).
|
|
19
|
+
*
|
|
20
|
+
* Owns its own `fragment` on `Query.payment` selecting `installmentOptions` and `selectedInstallment`
|
|
21
|
+
* — the two fields `PaymentSectionRoot`'s thin root fragment deliberately excludes (ADR-0011,
|
|
22
|
+
* "decompose by data ownership").
|
|
23
|
+
*
|
|
24
|
+
* Matching, tier resolution, `selectedCount`, state derivation, the BIN-confirmation pending gate, and
|
|
25
|
+
* the request-token-guarded `$actions.payment.setInstallments` dispatch all live in
|
|
26
|
+
* `useInstallmentsSelection()` (T90, specs/render-installments-as-a-radio-group.md § Decision 3) — the
|
|
27
|
+
* shared engine also consumed by `InstallmentsSelect`, so the two surfaces cannot drift apart (mirrors
|
|
28
|
+
* `useDeliveryOptionSelection`). This component is now a thin renderer over that hook: it owns only
|
|
29
|
+
* its own markup (a `RadioGroup` of rows) and its own `role="alert"`, exactly as
|
|
30
|
+
* `useDeliveryOptionSelection`'s callers do. `findInstallmentOption` and `resolveInstallmentTiers` now
|
|
31
|
+
* live in `use-installments-selection.ts` — the hook was their only caller, so this module re-exports
|
|
32
|
+
* both unchanged (T90 finding 4: the previous arrangement, this file importing the hook while the hook
|
|
33
|
+
* imported these two back out of this file, was a genuine two-way ESM cycle) — so their public API and
|
|
34
|
+
* their existing tests are untouched. Render output is byte-identical to before T90 (INV-1); see the
|
|
35
|
+
* hook's own doc comment for the full matching/gating rationale, including the PR #507 findings this
|
|
36
|
+
* behavior must not regress.
|
|
37
|
+
*/
|
|
38
|
+
const fragment = `
|
|
39
|
+
fragment InstallmentsData on Query {
|
|
40
|
+
payment {
|
|
41
|
+
__typename
|
|
42
|
+
id
|
|
43
|
+
installmentOptions {
|
|
44
|
+
__typename
|
|
45
|
+
methodId
|
|
46
|
+
bin
|
|
47
|
+
installments {
|
|
48
|
+
__typename
|
|
49
|
+
count
|
|
50
|
+
value {
|
|
51
|
+
amount
|
|
52
|
+
currencyCode
|
|
53
|
+
}
|
|
54
|
+
interestRate
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
selectedInstallment {
|
|
58
|
+
__typename
|
|
59
|
+
count
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
function formatInstallmentValue(amount, locale, currency) {
|
|
65
|
+
return new Intl.NumberFormat(locale, {
|
|
66
|
+
style: "currency",
|
|
67
|
+
currency
|
|
68
|
+
}).format(amount);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The order form carries `interestRate` in hundredths of a percent (T174, FR-5): `100` is 1%. It is
|
|
72
|
+
* scaled here and nowhere else — the API and the payment gateway body both keep the upstream value.
|
|
73
|
+
*/
|
|
74
|
+
function formatInterestRate(rate, locale) {
|
|
75
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(rate / 100);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Formats one tier's row/option text. Exported (T90,
|
|
79
|
+
* specs/render-installments-as-a-radio-group.md § FR-5) so `InstallmentsSelect` renders each option
|
|
80
|
+
* with the exact same function `Installments` uses, driven by the same four authorable copy props —
|
|
81
|
+
* the two surfaces cannot word a tier differently.
|
|
82
|
+
*/
|
|
83
|
+
function installmentLabel({ tier, locale, installmentTemplate, interestFreeLabel, interestTemplate }) {
|
|
84
|
+
const formattedValue = formatInstallmentValue(tier.value.amount, locale, tier.value.currencyCode);
|
|
85
|
+
const base = substituteTemplate(installmentTemplate, {
|
|
86
|
+
count: String(tier.count),
|
|
87
|
+
value: formattedValue
|
|
88
|
+
});
|
|
89
|
+
const interestSuffix = tier.interestRate === 0 ? interestFreeLabel : substituteTemplate(interestTemplate, { rate: formatInterestRate(tier.interestRate, locale) });
|
|
90
|
+
return interestSuffix ? `${base} ${interestSuffix}` : base;
|
|
91
|
+
}
|
|
92
|
+
const InstallmentsDefinition = defineComponent({
|
|
93
|
+
name: "Installments",
|
|
94
|
+
importPath: "@vtex/faststore/components",
|
|
95
|
+
component: Installments,
|
|
96
|
+
fragment,
|
|
97
|
+
fetchStrategy: "reactive",
|
|
98
|
+
requiresContext: "payment",
|
|
99
|
+
actions: { payment: ["setInstallments"] },
|
|
100
|
+
editor: {
|
|
101
|
+
label: "Installments",
|
|
102
|
+
category: "commerce"
|
|
103
|
+
},
|
|
104
|
+
schema: {
|
|
105
|
+
installmentTemplate: {
|
|
106
|
+
type: "string",
|
|
107
|
+
default: "{count}x of {value}",
|
|
108
|
+
label: "Installment template"
|
|
109
|
+
},
|
|
110
|
+
interestFreeLabel: {
|
|
111
|
+
type: "string",
|
|
112
|
+
default: "interest-free",
|
|
113
|
+
label: "Interest-free label"
|
|
114
|
+
},
|
|
115
|
+
interestTemplate: {
|
|
116
|
+
type: "string",
|
|
117
|
+
default: "{rate}% interest",
|
|
118
|
+
label: "Interest template"
|
|
119
|
+
},
|
|
120
|
+
legend: {
|
|
121
|
+
type: "string",
|
|
122
|
+
default: "Installments",
|
|
123
|
+
label: "Legend"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
guideline: "Installment selector for checkout: shows count, per-installment value, and interest for the card currently being entered (matched by BIN) or the saved card currently selected (matched by the persisted BIN, falling back to method), rendering exactly the tiers the matched option returns (the API guarantees a 1x tier is present in every option, so one is always shown and selectable — see specs/installments.md). Must be inside a PaymentSectionRoot or PaymentSectionController — place it as a sibling of CardEntry and SavedCards, never nested inside either, so one instance serves both entry paths. Picking a row dispatches $actions.payment.setInstallments and the order total updates elsewhere from the normalized cache with no wiring here. Renders its own rows from real data — no children."
|
|
127
|
+
});
|
|
128
|
+
function Installments({ installmentTemplate = "{count}x of {value}", interestFreeLabel = "interest-free", interestTemplate = "{rate}% interest", legend, $data, $actions: $actionsProp, className, ...props }) {
|
|
129
|
+
const $actions = useActionsOverride($actionsProp);
|
|
130
|
+
const { locale } = useStorefrontContext();
|
|
131
|
+
const { tiers, selectedCount, state, isPending, selectionError, selectInstallment, placing, cardPaymentCollapsed } = useInstallmentsSelection({
|
|
132
|
+
$data,
|
|
133
|
+
$actions
|
|
134
|
+
});
|
|
135
|
+
if (cardPaymentCollapsed) return null;
|
|
136
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
137
|
+
...props,
|
|
138
|
+
className,
|
|
139
|
+
inert: placing,
|
|
140
|
+
"aria-disabled": placing || void 0,
|
|
141
|
+
"data-fs-installments": "",
|
|
142
|
+
"data-fs-installments-state": state,
|
|
143
|
+
"aria-busy": isPending || void 0,
|
|
144
|
+
...placing ? { "data-fs-installments-placing": "" } : {},
|
|
145
|
+
children: [selectionError ? /* @__PURE__ */ jsx("div", {
|
|
146
|
+
role: "alert",
|
|
147
|
+
"data-fs-installments-error": "",
|
|
148
|
+
children: selectionError
|
|
149
|
+
}) : null, tiers.length > 0 ? /* @__PURE__ */ jsxs(Fieldset.Root, {
|
|
150
|
+
"data-fs-installments-fieldset": "",
|
|
151
|
+
children: [/* @__PURE__ */ jsx(Fieldset.Legend, { children: resolveAuthoredCopy(legend, "Installments") }), /* @__PURE__ */ jsx(RadioGroup, {
|
|
152
|
+
value: String(selectedCount),
|
|
153
|
+
onValueChange: (value) => selectInstallment(Number(value)),
|
|
154
|
+
disabled: isPending,
|
|
155
|
+
children: tiers.map((tier) => /* @__PURE__ */ jsxs(RadioRoot, {
|
|
156
|
+
value: String(tier.count),
|
|
157
|
+
disabled: isPending,
|
|
158
|
+
children: [/* @__PURE__ */ jsx(RadioIndicator, { "data-fs-installments-option-indicator": "" }), /* @__PURE__ */ jsx("span", {
|
|
159
|
+
"data-fs-installments-option-label": "",
|
|
160
|
+
children: installmentLabel({
|
|
161
|
+
tier,
|
|
162
|
+
locale,
|
|
163
|
+
installmentTemplate,
|
|
164
|
+
interestFreeLabel,
|
|
165
|
+
interestTemplate
|
|
166
|
+
})
|
|
167
|
+
})]
|
|
168
|
+
}, tier.count))
|
|
169
|
+
})]
|
|
170
|
+
}) : null]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { Installments, InstallmentsDefinition, findInstallmentOption, installmentLabel, resolveInstallmentTiers };
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=installments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installments.js","names":[],"sources":["../../../src/matter/installments/installments.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef } from 'react';\nimport { Fieldset } from '../../particles/fieldset/fieldset.js';\nimport { RadioGroup, RadioIndicator, RadioRoot } from '../../particles/radio/radio.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useStorefrontContext } from '../../sdk/context/storefront-context.js';\nimport { resolveAuthoredCopy } from '../../utils/resolve-authored-copy.js';\nimport { substituteTemplate } from '../../utils/substitute-template.js';\nimport { useInstallmentsSelection } from './use-installments-selection.js';\n\n/**\n * `Installments` matter (specs/installments.md, T9 of the Credit Card epic;\n * docs/features/credit-card/rfc.md § D4). Lets a shopper pick an installment plan for both a\n * freshly entered card and a selected saved card, rendered as a `RadioGroup` — one row per tier the\n * matched option carries (the API guarantees a 1x tier is present in every option, so one is always\n * shown and selectable).\n *\n * Owns its own `fragment` on `Query.payment` selecting `installmentOptions` and `selectedInstallment`\n * — the two fields `PaymentSectionRoot`'s thin root fragment deliberately excludes (ADR-0011,\n * \"decompose by data ownership\").\n *\n * Matching, tier resolution, `selectedCount`, state derivation, the BIN-confirmation pending gate, and\n * the request-token-guarded `$actions.payment.setInstallments` dispatch all live in\n * `useInstallmentsSelection()` (T90, specs/render-installments-as-a-radio-group.md § Decision 3) — the\n * shared engine also consumed by `InstallmentsSelect`, so the two surfaces cannot drift apart (mirrors\n * `useDeliveryOptionSelection`). This component is now a thin renderer over that hook: it owns only\n * its own markup (a `RadioGroup` of rows) and its own `role=\"alert\"`, exactly as\n * `useDeliveryOptionSelection`'s callers do. `findInstallmentOption` and `resolveInstallmentTiers` now\n * live in `use-installments-selection.ts` — the hook was their only caller, so this module re-exports\n * both unchanged (T90 finding 4: the previous arrangement, this file importing the hook while the hook\n * imported these two back out of this file, was a genuine two-way ESM cycle) — so their public API and\n * their existing tests are untouched. Render output is byte-identical to before T90 (INV-1); see the\n * hook's own doc comment for the full matching/gating rationale, including the PR #507 findings this\n * behavior must not regress.\n */\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment — raw template literal, matching `PaymentSectionRoot` / `PaymentSummary`'s\n// convention. The codegen `documents` glob does not scan `src/matter/payment*` / `src/matter/installments*`;\n// `data-component-contract.test.ts` validates this fragment against the real schema.\n//\n// Selects `payment { id }` so Apollo's InMemoryCache normalizes `Payment` by id — required for\n// `paymentInstallmentSelect`'s returned `payment` to update the same cache entity this fragment\n// reads (installments.test.tsx's \"selects payment.id so a setInstallments echo re-normalizes the\n// entity this fragment reads\" asserts this against the parsed fragment AST). `total` is\n// deliberately not selected: this component never renders or computes against it\n// (specs/installments.md § Decision 3).\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment InstallmentsData on Query {\n payment {\n __typename\n id\n installmentOptions {\n __typename\n methodId\n bin\n installments {\n __typename\n count\n value {\n amount\n currencyCode\n }\n interestRate\n }\n }\n selectedInstallment {\n __typename\n count\n }\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type InstallmentMoney = Readonly<{ amount: number; currencyCode: string }>;\n\nexport type InstallmentTier = Readonly<{\n count: number;\n value: InstallmentMoney;\n interestRate: number;\n}>;\n\nexport type InstallmentOption = Readonly<{\n methodId: string;\n bin: string | null;\n installments: readonly InstallmentTier[];\n}>;\n\n/**\n * `$data` forwarded by the reactive runtime — the unwrapped value of the `payment` top-level field\n * on `InstallmentsData on Query` (arity-1 fragment, ADR-0015), not a `{ payment: … }` wrapper.\n * Every field optional, mirroring `PaymentSummaryData` / `PaymentSectionData`: `$data` arrives\n * partial while the entity resolves.\n */\nexport type InstallmentsData = {\n installmentOptions?: readonly InstallmentOption[];\n selectedInstallment?: { count: number } | null;\n} | null;\n\nexport type InstallmentsProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {\n /** Placeholders `{count}` / `{value}`, matching `PaymentSummary`'s / `PricingInstallment`'s convention. */\n installmentTemplate?: string;\n /** Appended when a tier's `interestRate` is `0`. */\n interestFreeLabel?: string;\n /**\n * Placeholder `{rate}`; appended instead of `interestFreeLabel` when a tier's `interestRate` is above `0`.\n * `{rate}` is the order form's `interestRate` (hundredths of a percent) divided by 100: `100` renders `1`.\n */\n interestTemplate?: string;\n /**\n * The radio group's accessible name, rendered as a visible `Fieldset.Legend` (T38, Decision 1).\n * Resolved through `resolveAuthoredCopy`: an authored empty or whitespace-only string falls back to\n * the default rather than rendering a nameless group.\n */\n legend?: string;\n /** `InstallmentsData` slice forwarded by the reactive runtime. */\n $data?: InstallmentsData;\n /** Compiler-injected action surface; carries `payment.setInstallments`. */\n $actions?: ComponentActions;\n};\n\n// ---------------------------------------------------------------------------\n// Resolution — `findInstallmentOption` and `resolveInstallmentTiers` now live in\n// `use-installments-selection.ts` (T90 finding 4); re-exported here, unchanged, because\n// `installments.test.tsx` and the package barrel (`src/index.ts`) both import them from this path.\n// ---------------------------------------------------------------------------\n\nexport { findInstallmentOption, resolveInstallmentTiers } from './use-installments-selection.js';\n\nfunction formatInstallmentValue(amount: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);\n}\n\n/**\n * The order form carries `interestRate` in hundredths of a percent (T174, FR-5): `100` is 1%. It is\n * scaled here and nowhere else — the API and the payment gateway body both keep the upstream value.\n */\nfunction formatInterestRate(rate: number, locale: string): string {\n return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(rate / 100);\n}\n\n/**\n * Formats one tier's row/option text. Exported (T90,\n * specs/render-installments-as-a-radio-group.md § FR-5) so `InstallmentsSelect` renders each option\n * with the exact same function `Installments` uses, driven by the same four authorable copy props —\n * the two surfaces cannot word a tier differently.\n */\nexport function installmentLabel({\n tier,\n locale,\n installmentTemplate,\n interestFreeLabel,\n interestTemplate,\n}: {\n tier: InstallmentTier;\n locale: string;\n installmentTemplate: string;\n interestFreeLabel: string;\n interestTemplate: string;\n}): string {\n const formattedValue = formatInstallmentValue(tier.value.amount, locale, tier.value.currencyCode);\n const base = substituteTemplate(installmentTemplate, { count: String(tier.count), value: formattedValue });\n const interestSuffix =\n tier.interestRate === 0\n ? interestFreeLabel\n : substituteTemplate(interestTemplate, { rate: formatInterestRate(tier.interestRate, locale) });\n return interestSuffix ? `${base} ${interestSuffix}` : base;\n}\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const InstallmentsDefinition = defineComponent<InstallmentsProps>({\n name: 'Installments',\n importPath: '@vtex/faststore/components',\n component: Installments,\n fragment,\n fetchStrategy: 'reactive',\n requiresContext: 'payment',\n actions: { payment: ['setInstallments'] },\n editor: { label: 'Installments', category: 'commerce' },\n schema: {\n installmentTemplate: { type: 'string', default: '{count}x of {value}', label: 'Installment template' },\n interestFreeLabel: { type: 'string', default: 'interest-free', label: 'Interest-free label' },\n interestTemplate: { type: 'string', default: '{rate}% interest', label: 'Interest template' },\n // T38: an inline literal, never an identifier — the native catalog builder extracts `schema`\n // statically and silently drops an identifier-valued default (INV-8).\n legend: { type: 'string', default: 'Installments', label: 'Legend' },\n },\n guideline:\n 'Installment selector for checkout: shows count, per-installment value, and interest for the card currently being entered (matched by BIN) or the saved card currently selected (matched by the persisted BIN, falling back to method), rendering exactly the tiers the matched option returns (the API guarantees a 1x tier is present in every option, so one is always shown and selectable — see specs/installments.md). Must be inside a PaymentSectionRoot or PaymentSectionController — place it as a sibling of CardEntry and SavedCards, never nested inside either, so one instance serves both entry paths. Picking a row dispatches $actions.payment.setInstallments and the order total updates elsewhere from the normalized cache with no wiring here. Renders its own rows from real data — no children.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nexport function Installments({\n installmentTemplate = '{count}x of {value}',\n interestFreeLabel = 'interest-free',\n interestTemplate = '{rate}% interest',\n legend,\n $data,\n $actions: $actionsProp,\n className,\n ...props\n}: InstallmentsProps) {\n const $actions = useActionsOverride($actionsProp);\n // T192: the formatting locale is the storefront's, never an authored prop (RFC localization § 10).\n const { locale } = useStorefrontContext();\n const { tiers, selectedCount, state, isPending, selectionError, selectInstallment, placing, cardPaymentCollapsed } =\n useInstallmentsSelection({\n $data,\n $actions,\n });\n\n // T191 (FR-7): nothing renders while the card payment is collapsed. Returned after the last hook, so\n // this instance stays mounted and keeps its local state.\n if (cardPaymentCollapsed) return null;\n\n return (\n <div\n {...props}\n className={className}\n inert={placing}\n aria-disabled={placing || undefined}\n data-fs-installments=\"\"\n data-fs-installments-state={state}\n aria-busy={isPending || undefined}\n {...(placing ? { 'data-fs-installments-placing': '' } : {})}\n >\n {selectionError ? (\n <div role=\"alert\" data-fs-installments-error=\"\">\n {selectionError}\n </div>\n ) : null}\n {tiers.length > 0 ? (\n // T38, Decision 1: Fieldset.Root + Fieldset.Legend, exactly what card-entry.tsx:1101-1112\n // already ships — Base UI propagates the legend's id as aria-labelledby onto the descendant\n // RadioGroup, so this one wrapper names both the fieldset and the radiogroup (measured).\n <Fieldset.Root data-fs-installments-fieldset=\"\">\n <Fieldset.Legend>{resolveAuthoredCopy(legend, 'Installments')}</Fieldset.Legend>\n <RadioGroup\n value={String(selectedCount)}\n onValueChange={(value) => selectInstallment(Number(value))}\n disabled={isPending}\n >\n {tiers.map((tier) => (\n <RadioRoot key={tier.count} value={String(tier.count)} disabled={isPending}>\n <RadioIndicator data-fs-installments-option-indicator=\"\" />\n <span data-fs-installments-option-label=\"\">\n {installmentLabel({ tier, locale, installmentTemplate, interestFreeLabel, interestTemplate })}\n </span>\n </RadioRoot>\n ))}\n </RadioGroup>\n </Fieldset.Root>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAM,WAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;AAsF/B,SAAS,uBAAuB,QAAgB,QAAgB,UAA0B;CACxF,OAAO,IAAI,KAAK,aAAa,QAAQ;EAAE,OAAO;EAAY;CAAS,CAAC,CAAC,CAAC,OAAO,MAAM;AACrF;;;;;AAMA,SAAS,mBAAmB,MAAc,QAAwB;CAChE,OAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO,GAAG;AACtF;;;;;;;AAQA,SAAgB,iBAAiB,EAC/B,MACA,QACA,qBACA,mBACA,oBAOS;CACT,MAAM,iBAAiB,uBAAuB,KAAK,MAAM,QAAQ,QAAQ,KAAK,MAAM,YAAY;CAChG,MAAM,OAAO,mBAAmB,qBAAqB;EAAE,OAAO,OAAO,KAAK,KAAK;EAAG,OAAO;CAAe,CAAC;CACzG,MAAM,iBACJ,KAAK,iBAAiB,IAClB,oBACA,mBAAmB,kBAAkB,EAAE,MAAM,mBAAmB,KAAK,cAAc,MAAM,EAAE,CAAC;CAClG,OAAO,iBAAiB,GAAG,KAAK,GAAG,mBAAmB;AACxD;AAMA,MAAa,yBAAyB,gBAAmC;CACvE,MAAM;CACN,YAAY;CACZ,WAAW;CACX;CACA,eAAe;CACf,iBAAiB;CACjB,SAAS,EAAE,SAAS,CAAC,iBAAiB,EAAE;CACxC,QAAQ;EAAE,OAAO;EAAgB,UAAU;CAAW;CACtD,QAAQ;EACN,qBAAqB;GAAE,MAAM;GAAU,SAAS;GAAuB,OAAO;EAAuB;EACrG,mBAAmB;GAAE,MAAM;GAAU,SAAS;GAAiB,OAAO;EAAsB;EAC5F,kBAAkB;GAAE,MAAM;GAAU,SAAS;GAAoB,OAAO;EAAoB;EAG5F,QAAQ;GAAE,MAAM;GAAU,SAAS;GAAgB,OAAO;EAAS;CACrE;CACA,WACE;AACJ,CAAC;AAMD,SAAgB,aAAa,EAC3B,sBAAsB,uBACtB,oBAAoB,iBACpB,mBAAmB,oBACnB,QACA,OACA,UAAU,cACV,WACA,GAAG,SACiB;CACpB,MAAM,WAAW,mBAAmB,YAAY;CAEhD,MAAM,EAAE,WAAW,qBAAqB;CACxC,MAAM,EAAE,OAAO,eAAe,OAAO,WAAW,gBAAgB,mBAAmB,SAAS,yBAC1F,yBAAyB;EACvB;EACA;CACF,CAAC;CAIH,IAAI,sBAAsB,OAAO;CAEjC,OACE,qBAAC,OAAD;EACE,GAAI;EACO;EACX,OAAO;EACP,iBAAe,WAAW,KAAA;EAC1B,wBAAqB;EACrB,8BAA4B;EAC5B,aAAW,aAAa,KAAA;EACxB,GAAK,UAAU,EAAE,gCAAgC,GAAG,IAAI,CAAC;EAR3D,UAAA,CAUG,iBACC,oBAAC,OAAD;GAAK,MAAK;GAAQ,8BAA2B;GAC1C,UAAA;EACE,CAAA,IACH,MACH,MAAM,SAAS,IAId,qBAAC,SAAS,MAAV;GAAe,iCAA8B;GAA7C,UAAA,CACE,oBAAC,SAAS,QAAV,EAAA,UAAkB,oBAAoB,QAAQ,cAAc,EAAmB,CAAA,GAC/E,oBAAC,YAAD;IACE,OAAO,OAAO,aAAa;IAC3B,gBAAgB,UAAU,kBAAkB,OAAO,KAAK,CAAC;IACzD,UAAU;IAET,UAAA,MAAM,KAAK,SACV,qBAAC,WAAD;KAA4B,OAAO,OAAO,KAAK,KAAK;KAAG,UAAU;KAAjE,UAAA,CACE,oBAAC,gBAAD,EAAgB,yCAAsC,GAAI,CAAA,GAC1D,oBAAC,QAAD;MAAM,qCAAkC;MACrC,UAAA,iBAAiB;OAAE;OAAM;OAAQ;OAAqB;OAAmB;MAAiB,CAAC;KACxF,CAAA,CACG;IALK,GAAA,KAAK,KAKV,CACZ;GACS,CAAA,CACC;EACb,CAAA,IAAA,IACD;;AAET"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "../../sdk/actions/types.js";
|
|
2
|
+
import "./installments-select.js";
|
|
3
|
+
import { InstallmentOption, InstallmentTier } from "./installments.js";
|
|
4
|
+
//#region src/matter/installments/use-installments-selection.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Bin-first, methodId-fallback lookup — the client mirror of the server's own
|
|
7
|
+
* `findMatchingInstallmentOption` (`apps/storefront-api-core/.../checkout/mappers.ts`). Returns
|
|
8
|
+
* `null` when neither a `bin` nor a `methodId` match exists — never a guessed or nearest-match option
|
|
9
|
+
* (specs/installments.md § Decision 1).
|
|
10
|
+
*/
|
|
11
|
+
export declare function findInstallmentOption(options: readonly InstallmentOption[], { bin, methodId }: {
|
|
12
|
+
bin: string | null;
|
|
13
|
+
methodId: string | null;
|
|
14
|
+
}): InstallmentOption | null;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the matched option's tiers as the API projected them. The API
|
|
17
|
+
* guarantees a `count: 1` tier is present in every `installmentOptions`
|
|
18
|
+
* entry, including those with an empty tier list (`mappers.ts:guaranteeSingleInstallmentTier`),
|
|
19
|
+
* so this function returns it verbatim — no client-side synthesis. A tier
|
|
20
|
+
* invented only on the read side was a dead end: `paymentInstallmentSelect`
|
|
21
|
+
* validates the picked count against the same upstream list the synthesis was
|
|
22
|
+
* compensating for (`index.ts:1675-1688`), so it either dispatched nothing or
|
|
23
|
+
* was rejected `INVALID_INSTALLMENT_COUNT`. That rationale is why neither surface
|
|
24
|
+
* may re-add client-side synthesis — the guarantee belongs server-side, and is
|
|
25
|
+
* now closed by the read/write guarantee. Returns `[]` when no option resolved.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveInstallmentTiers(option: InstallmentOption | null): readonly InstallmentTier[];
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=use-installments-selection.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
3
|
+
import { isCardMethod } from "../payment/use-payment-method-selection.js";
|
|
4
|
+
import { usePayment } from "../payment/payment.js";
|
|
5
|
+
import { useCallback, useRef, useState } from "react";
|
|
6
|
+
//#region src/matter/installments/use-installments-selection.ts
|
|
7
|
+
const EMPTY_TIERS = [];
|
|
8
|
+
/**
|
|
9
|
+
* Bin-first, methodId-fallback lookup — the client mirror of the server's own
|
|
10
|
+
* `findMatchingInstallmentOption` (`apps/storefront-api-core/.../checkout/mappers.ts`). Returns
|
|
11
|
+
* `null` when neither a `bin` nor a `methodId` match exists — never a guessed or nearest-match option
|
|
12
|
+
* (specs/installments.md § Decision 1).
|
|
13
|
+
*/
|
|
14
|
+
function findInstallmentOption(options, { bin, methodId }) {
|
|
15
|
+
if (bin) {
|
|
16
|
+
const matchedByBin = options.find((option) => option.bin === bin);
|
|
17
|
+
if (matchedByBin) return matchedByBin;
|
|
18
|
+
}
|
|
19
|
+
if (methodId) return options.find((option) => option.methodId === methodId) ?? null;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the matched option's tiers as the API projected them. The API
|
|
24
|
+
* guarantees a `count: 1` tier is present in every `installmentOptions`
|
|
25
|
+
* entry, including those with an empty tier list (`mappers.ts:guaranteeSingleInstallmentTier`),
|
|
26
|
+
* so this function returns it verbatim — no client-side synthesis. A tier
|
|
27
|
+
* invented only on the read side was a dead end: `paymentInstallmentSelect`
|
|
28
|
+
* validates the picked count against the same upstream list the synthesis was
|
|
29
|
+
* compensating for (`index.ts:1675-1688`), so it either dispatched nothing or
|
|
30
|
+
* was rejected `INVALID_INSTALLMENT_COUNT`. That rationale is why neither surface
|
|
31
|
+
* may re-add client-side synthesis — the guarantee belongs server-side, and is
|
|
32
|
+
* now closed by the read/write guarantee. Returns `[]` when no option resolved.
|
|
33
|
+
*/
|
|
34
|
+
function resolveInstallmentTiers(option) {
|
|
35
|
+
return option ? option.installments : EMPTY_TIERS;
|
|
36
|
+
}
|
|
37
|
+
const EMPTY_OPTIONS = [];
|
|
38
|
+
const ONE_TIME_COUNT = 1;
|
|
39
|
+
/**
|
|
40
|
+
* Precedence unchanged from `Installments`' own (now-removed) local `installmentsState`: loading,
|
|
41
|
+
* then pending, then empty, then ready.
|
|
42
|
+
*/
|
|
43
|
+
function installmentsSelectionState({ loading, count, binConfirmation }) {
|
|
44
|
+
if (loading) return "loading";
|
|
45
|
+
if (binConfirmation === "pending") return "pending";
|
|
46
|
+
if (count === 0) return "empty";
|
|
47
|
+
return "ready";
|
|
48
|
+
}
|
|
49
|
+
function useInstallmentsSelection({ $data: $dataProp, $actions }) {
|
|
50
|
+
const $data = useDataOverride($dataProp);
|
|
51
|
+
const { draft, selectedMethodId, selectedMethod, selectedBin, binConfirmation, placing, cardPaymentCollapsed } = usePayment();
|
|
52
|
+
const loading = $data == null;
|
|
53
|
+
const tiers = resolveInstallmentTiers(findInstallmentOption($data?.installmentOptions ?? EMPTY_OPTIONS, {
|
|
54
|
+
bin: draft.bin ?? selectedBin ?? null,
|
|
55
|
+
methodId: selectedMethod !== null && isCardMethod(selectedMethod) ? selectedMethodId : null
|
|
56
|
+
}));
|
|
57
|
+
const [selectionError, setSelectionError] = useState(null);
|
|
58
|
+
const [requestedCount, setRequestedCount] = useState(null);
|
|
59
|
+
const selectionToken = useRef(0);
|
|
60
|
+
const isPending = binConfirmation === "pending";
|
|
61
|
+
const selectedCount = requestedCount ?? $data?.selectedInstallment?.count ?? ONE_TIME_COUNT;
|
|
62
|
+
const selectInstallment = useCallback((count) => {
|
|
63
|
+
if (binConfirmation === "pending") return;
|
|
64
|
+
setSelectionError(null);
|
|
65
|
+
const dispatch = $actions?.payment?.setInstallments;
|
|
66
|
+
if (!dispatch) return;
|
|
67
|
+
const token = ++selectionToken.current;
|
|
68
|
+
setRequestedCount(count);
|
|
69
|
+
dispatch(count).then((userErrors) => {
|
|
70
|
+
if (selectionToken.current !== token) return;
|
|
71
|
+
const [firstError] = userErrors;
|
|
72
|
+
if (firstError) setSelectionError(firstError.message);
|
|
73
|
+
}).finally(() => {
|
|
74
|
+
if (selectionToken.current === token) setRequestedCount(null);
|
|
75
|
+
});
|
|
76
|
+
}, [$actions, binConfirmation]);
|
|
77
|
+
return {
|
|
78
|
+
tiers,
|
|
79
|
+
selectedCount,
|
|
80
|
+
state: installmentsSelectionState({
|
|
81
|
+
loading,
|
|
82
|
+
count: tiers.length,
|
|
83
|
+
binConfirmation
|
|
84
|
+
}),
|
|
85
|
+
isPending,
|
|
86
|
+
selectionError,
|
|
87
|
+
selectInstallment,
|
|
88
|
+
placing,
|
|
89
|
+
cardPaymentCollapsed
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { findInstallmentOption, resolveInstallmentTiers, useInstallmentsSelection };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=use-installments-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-installments-selection.js","names":[],"sources":["../../../src/matter/installments/use-installments-selection.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useRef, useState } from 'react';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport { usePayment } from '../payment/payment.js';\nimport { isCardMethod, type PaymentBinConfirmation } from '../payment/use-payment-method-selection.js';\nimport type { InstallmentOption, InstallmentsData, InstallmentTier } from './installments.js';\nimport type { InstallmentsSelectData } from './installments-select.js';\n\n// ---------------------------------------------------------------------------\n// Resolution — moved here from `installments.tsx` (T90 finding 4, review of\n// specs/render-installments-as-a-radio-group.md). This hook was these two functions' only caller, so\n// `installments.tsx` importing the hook while the hook imported the functions back out of\n// `installments.tsx` was a genuine two-way ESM cycle. `installments.tsx` now re-exports both\n// unchanged, so `installments.test.tsx` (zero-diff) and the package barrel (`src/index.ts`) keep\n// importing them from the same path. Mirrors `use-delivery-option-selection.ts`, which owns its own\n// helpers outright and depends on no renderer.\n// ---------------------------------------------------------------------------\n\nconst EMPTY_TIERS: readonly InstallmentTier[] = [];\n\n/**\n * Bin-first, methodId-fallback lookup — the client mirror of the server's own\n * `findMatchingInstallmentOption` (`apps/storefront-api-core/.../checkout/mappers.ts`). Returns\n * `null` when neither a `bin` nor a `methodId` match exists — never a guessed or nearest-match option\n * (specs/installments.md § Decision 1).\n */\nexport function findInstallmentOption(\n options: readonly InstallmentOption[],\n { bin, methodId }: { bin: string | null; methodId: string | null },\n): InstallmentOption | null {\n if (bin) {\n const matchedByBin = options.find((option) => option.bin === bin);\n if (matchedByBin) return matchedByBin;\n }\n if (methodId) {\n return options.find((option) => option.methodId === methodId) ?? null;\n }\n return null;\n}\n\n/**\n * Returns the matched option's tiers as the API projected them. The API\n * guarantees a `count: 1` tier is present in every `installmentOptions`\n * entry, including those with an empty tier list (`mappers.ts:guaranteeSingleInstallmentTier`),\n * so this function returns it verbatim — no client-side synthesis. A tier\n * invented only on the read side was a dead end: `paymentInstallmentSelect`\n * validates the picked count against the same upstream list the synthesis was\n * compensating for (`index.ts:1675-1688`), so it either dispatched nothing or\n * was rejected `INVALID_INSTALLMENT_COUNT`. That rationale is why neither surface\n * may re-add client-side synthesis — the guarantee belongs server-side, and is\n * now closed by the read/write guarantee. Returns `[]` when no option resolved.\n */\nexport function resolveInstallmentTiers(option: InstallmentOption | null): readonly InstallmentTier[] {\n return option ? option.installments : EMPTY_TIERS;\n}\n\n/**\n * Shared selection engine behind `Installments` (the radio surface) and `InstallmentsSelect` (the\n * collapsed dropdown — T90, specs/render-installments-as-a-radio-group.md § Decision 3). Owns the\n * behaviours the spec requires identically across every surface, so they cannot drift apart, mirroring\n * `useDeliveryOptionSelection`:\n *\n * - **Matching.** Bin-first, methodId-fallback lookup (`draft.bin ?? selectedBin`, then\n * `selectedMethodId` when the selected method is a card — T191, FR-9) via `findInstallmentOption` —\n * the client mirror of the server's own `findMatchingInstallmentOption`.\n * - **Tier resolution.** `resolveInstallmentTiers` returns the matched option's own `installments`\n * reference verbatim — no client-side synthesis. A v1 version of this synthesized a `count: 1` row\n * when upstream omitted one; PR #507's review (finding F1) proved that row a dead end —\n * `paymentInstallmentSelect` validates the picked count against the same upstream list the\n * synthesis was compensating for, so a client-invented tier could never actually be selected. The\n * read/write guarantee (`mappers.ts:guaranteeSingleInstallmentTier`) now closes this server-side\n * instead (specs/installments.md § Decision 2).\n * - **`selectedCount`.** Bound to the count of this engine's own latest `setInstallments` dispatch while\n * it is unsettled, and to the persisted `selectedInstallment.count` (defaulting to `1`) otherwise\n * (T174, FR-4, amending INV-3). A pick shows at once, and a refused one falls back to the persisted\n * plan once its dispatch settles.\n * - **State derivation.** `loading` / `pending` / `empty` / `ready`, identical in meaning for both\n * surfaces. `ready` does not guarantee a matched tier: the persisted count can be unreachable in the\n * currently matched option (e.g. the shopper edits the BIN to one offering fewer tiers).\n * - **The pending gate.** Dispatch is held while `binConfirmation === 'pending'` — a tri-state signal\n * from `usePaymentMethodSelection` that compares the persisted `selectedBin` against `draft.bin` and\n * tracks whether a `payment.setMethod` is in flight. The earlier component-local gate (PR #507\n * review, finding F3) wedged permanently because its confirming signal never changed reference for\n * this fragment's own selection set; this gate reads `Payment.selectedBin` from T5's fragment and an\n * in-flight counter instead (specs/epics/credit-card.md, decisions.md:105).\n * - **Dispatch.** Request-token-guarded `$actions.payment.setInstallments`, surfacing the first\n * `userError` inline — exactly like `PaymentMethodList` / `DeliveryOptionsListBody`.\n *\n * Calls `useDataOverride()` and `usePayment()` itself, exactly as `useDeliveryOptionSelection` does.\n * Not exported from the package barrel — an internal engine, matching `use-delivery-option-selection.ts`.\n * Each surface renders its own markup and its own `role=\"alert\"`; label formatting\n * (`installmentLabel`, exported from `installments.tsx`) is deliberately NOT part of this engine — it\n * is a rendering concern each surface applies to the `tiers` this hook returns.\n */\nexport type InstallmentsSelection = {\n /** Tiers of the matched option, in API order. Empty when nothing matched. */\n tiers: readonly InstallmentTier[];\n /** The bound value: the latest unsettled pick's count, else persisted `selectedInstallment.count`, else 1. */\n selectedCount: number;\n /** Presence state, identical in meaning for both surfaces. */\n state: 'loading' | 'pending' | 'empty' | 'ready';\n /** True while `binConfirmation === 'pending'` — disables the control and holds dispatch. */\n isPending: boolean;\n /** First `userError` message from the latest selection, or null. */\n selectionError: string | null;\n /** Request-token-guarded dispatch of `$actions.payment.setInstallments`. No-op while pending. */\n selectInstallment: (count: number) => void;\n /** An order-placement sequence is in flight (T28); `Installments` renders its whole subtree\n * non-interactive while this is true. Read straight off `usePayment()`, never mirrored. */\n placing: boolean;\n /** The card payment is collapsed (T191, FR-7); both surfaces render nothing while this is true. Read\n * straight off `usePayment()`, never mirrored. */\n cardPaymentCollapsed: boolean;\n};\n\nconst EMPTY_OPTIONS: readonly InstallmentOption[] = [];\nconst ONE_TIME_COUNT = 1;\n\n/**\n * Precedence unchanged from `Installments`' own (now-removed) local `installmentsState`: loading,\n * then pending, then empty, then ready.\n */\nfunction installmentsSelectionState({\n loading,\n count,\n binConfirmation,\n}: {\n loading: boolean;\n count: number;\n binConfirmation: PaymentBinConfirmation;\n}): 'loading' | 'pending' | 'empty' | 'ready' {\n if (loading) return 'loading';\n if (binConfirmation === 'pending') return 'pending';\n if (count === 0) return 'empty';\n return 'ready';\n}\n\nexport function useInstallmentsSelection({\n $data: $dataProp,\n $actions,\n}: {\n $data?: InstallmentsData | InstallmentsSelectData;\n $actions?: ComponentActions;\n}): InstallmentsSelection {\n const $data = useDataOverride($dataProp);\n const { draft, selectedMethodId, selectedMethod, selectedBin, binConfirmation, placing, cardPaymentCollapsed } =\n usePayment();\n\n const loading = $data == null;\n const options = $data?.installmentOptions ?? EMPTY_OPTIONS;\n const matchedOption = findInstallmentOption(options, {\n bin: draft.bin ?? selectedBin ?? null,\n // T191 (FR-9, Decision 10): only a card method's own options apply — card mode over a persisted\n // Promissory reads `empty` until a card is recorded, never Promissory's own `1x` tier.\n methodId: selectedMethod !== null && isCardMethod(selectedMethod) ? selectedMethodId : null,\n });\n const tiers = resolveInstallmentTiers(matchedOption);\n\n const [selectionError, setSelectionError] = useState<string | null>(null);\n // T174 (FR-4, Decision 4): the count of the latest `setInstallments` dispatch until it settles.\n const [requestedCount, setRequestedCount] = useState<number | null>(null);\n const selectionToken = useRef(0);\n const isPending = binConfirmation === 'pending';\n const selectedCount = requestedCount ?? $data?.selectedInstallment?.count ?? ONE_TIME_COUNT;\n\n const selectInstallment = useCallback(\n (count: number) => {\n if (binConfirmation === 'pending') return;\n setSelectionError(null);\n const dispatch = $actions?.payment?.setInstallments;\n if (!dispatch) return;\n const token = ++selectionToken.current;\n setRequestedCount(count);\n dispatch(count)\n .then((userErrors) => {\n if (selectionToken.current !== token) return;\n const [firstError] = userErrors;\n if (firstError) setSelectionError(firstError.message);\n })\n .finally(() => {\n // A successful echo already carries this count; a refused pick falls back to the persisted one.\n if (selectionToken.current === token) setRequestedCount(null);\n });\n },\n [$actions, binConfirmation],\n );\n\n return {\n tiers,\n selectedCount,\n state: installmentsSelectionState({ loading, count: tiers.length, binConfirmation }),\n isPending,\n selectionError,\n selectInstallment,\n placing,\n cardPaymentCollapsed,\n };\n}\n"],"mappings":";;;;;;AAoBA,MAAM,cAA0C,CAAC;;;;;;;AAQjD,SAAgB,sBACd,SACA,EAAE,KAAK,YACmB;CAC1B,IAAI,KAAK;EACP,MAAM,eAAe,QAAQ,MAAM,WAAW,OAAO,QAAQ,GAAG;EAChE,IAAI,cAAc,OAAO;CAC3B;CACA,IAAI,UACF,OAAO,QAAQ,MAAM,WAAW,OAAO,aAAa,QAAQ,KAAK;CAEnE,OAAO;AACT;;;;;;;;;;;;;AAcA,SAAgB,wBAAwB,QAA8D;CACpG,OAAO,SAAS,OAAO,eAAe;AACxC;AA6DA,MAAM,gBAA8C,CAAC;AACrD,MAAM,iBAAiB;;;;;AAMvB,SAAS,2BAA2B,EAClC,SACA,OACA,mBAK4C;CAC5C,IAAI,SAAS,OAAO;CACpB,IAAI,oBAAoB,WAAW,OAAO;CAC1C,IAAI,UAAU,GAAG,OAAO;CACxB,OAAO;AACT;AAEA,SAAgB,yBAAyB,EACvC,OAAO,WACP,YAIwB;CACxB,MAAM,QAAQ,gBAAgB,SAAS;CACvC,MAAM,EAAE,OAAO,kBAAkB,gBAAgB,aAAa,iBAAiB,SAAS,yBACtF,WAAW;CAEb,MAAM,UAAU,SAAS;CAQzB,MAAM,QAAQ,wBANQ,sBADN,OAAO,sBAAsB,eACQ;EACnD,KAAK,MAAM,OAAO,eAAe;EAGjC,UAAU,mBAAmB,QAAQ,aAAa,cAAc,IAAI,mBAAmB;CACzF,CACkD,CAAC;CAEnD,MAAM,CAAC,gBAAgB,qBAAqB,SAAwB,IAAI;CAExE,MAAM,CAAC,gBAAgB,qBAAqB,SAAwB,IAAI;CACxE,MAAM,iBAAiB,OAAO,CAAC;CAC/B,MAAM,YAAY,oBAAoB;CACtC,MAAM,gBAAgB,kBAAkB,OAAO,qBAAqB,SAAS;CAE7E,MAAM,oBAAoB,aACvB,UAAkB;EACjB,IAAI,oBAAoB,WAAW;EACnC,kBAAkB,IAAI;EACtB,MAAM,WAAW,UAAU,SAAS;EACpC,IAAI,CAAC,UAAU;EACf,MAAM,QAAQ,EAAE,eAAe;EAC/B,kBAAkB,KAAK;EACvB,SAAS,KAAK,CAAC,CACZ,MAAM,eAAe;GACpB,IAAI,eAAe,YAAY,OAAO;GACtC,MAAM,CAAC,cAAc;GACrB,IAAI,YAAY,kBAAkB,WAAW,OAAO;EACtD,CAAC,CAAC,CACD,cAAc;GAEb,IAAI,eAAe,YAAY,OAAO,kBAAkB,IAAI;EAC9D,CAAC;CACL,GACA,CAAC,UAAU,eAAe,CAC5B;CAEA,OAAO;EACL;EACA;EACA,OAAO,2BAA2B;GAAE;GAAS,OAAO,MAAM;GAAQ;EAAgB,CAAC;EACnF;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -5,16 +5,16 @@ import { Field } from "../../particles/field/field.js";
|
|
|
5
5
|
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
6
6
|
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
7
7
|
import { useSession } from "../session/session.js";
|
|
8
|
+
import { Select } from "../../particles/select/select.js";
|
|
9
|
+
import { DocumentField, isSupportedDocumentType, isValidDocument, maskDocumentValue } from "../../particles/document-field/document-field.js";
|
|
8
10
|
import { Input } from "../../particles/input/input.js";
|
|
9
11
|
import { PhoneField, applyPhoneMask, toE164, validatePhoneNumber } from "../../particles/phone-field/phone-field.js";
|
|
10
|
-
import { DocumentField, isSupportedDocumentType, isValidDocument, maskDocumentValue } from "../../particles/document-field/document-field.js";
|
|
11
|
-
import { Select } from "../../particles/select/select.js";
|
|
12
12
|
import { useToast } from "../../particles/toast/toast.js";
|
|
13
13
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
14
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
15
|
//#region src/matter/my-account-profile/my-account-profile.tsx
|
|
16
16
|
/**
|
|
17
|
-
* The `my-account-profile` compound (
|
|
17
|
+
* The `my-account-profile` compound (see my-account-profile.md; spec archived at the specs-archive tag) — the data root
|
|
18
18
|
* plus form mechanics for the shopper's persistent account profile. `Root` owns the saved profile,
|
|
19
19
|
* the draft, the view↔edit toggle, and the single batched Save; it publishes a context whose
|
|
20
20
|
* registration API is the seam every field Control attaches to.
|
|
@@ -123,12 +123,12 @@ const MyAccountProfileRootDefinition = defineComponent({
|
|
|
123
123
|
},
|
|
124
124
|
saveFailedMessage: {
|
|
125
125
|
type: "string",
|
|
126
|
-
default:
|
|
126
|
+
default: "Unable to save your profile. Please try again.",
|
|
127
127
|
label: "Save failure message"
|
|
128
128
|
},
|
|
129
129
|
savedMessage: {
|
|
130
130
|
type: "string",
|
|
131
|
-
default:
|
|
131
|
+
default: "Your profile has been saved.",
|
|
132
132
|
label: "Save success message"
|
|
133
133
|
}
|
|
134
134
|
},
|