@vtex/faststore 0.0.10 → 0.0.11
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 +42 -0
- package/README.md +0 -1
- package/dist/actions.json +6 -1
- package/dist/catalog.json +789 -1701
- package/dist/cli/index.d.ts +66 -28
- package/dist/cli/index.js +7371 -512
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/server-runtime.js +41 -60
- package/dist/cli/server-runtime.js.map +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +2 -2
- package/dist/components.d.ts +63 -57
- package/dist/components.js +60 -54
- package/dist/content-types.json +5785 -754
- package/dist/core/dist/define-component.d.ts +1 -1
- package/dist/core/dist/define-content-type.d.ts +50 -2
- package/dist/core/dist/define-content-type.js +107 -3
- package/dist/core/dist/define-content-type.js.map +1 -1
- package/dist/core/dist/index.d.ts +2 -2
- package/dist/docs/access-code-field.md +1 -1
- package/dist/docs/access-code-sign-in.md +126 -77
- package/dist/docs/account-address-book.md +210 -95
- package/dist/docs/account-menu.md +14 -15
- package/dist/docs/account-saved-cards.md +44 -39
- package/dist/docs/alert-dialog.md +3 -1
- package/dist/docs/avatar.md +4 -2
- package/dist/docs/billing-address.md +99 -30
- package/dist/docs/binding-switcher.md +5 -2
- package/dist/docs/buy-button.md +7 -8
- package/dist/docs/card-entry.md +39 -26
- package/dist/docs/card-summary.md +18 -8
- package/dist/docs/cardholder-document.md +5 -3
- package/dist/docs/carousel.md +5 -4
- package/dist/docs/cart-destination.md +39 -26
- package/dist/docs/cart-item.md +29 -14
- package/dist/docs/cart-totalizer.md +7 -5
- package/dist/docs/cart.md +135 -41
- package/dist/docs/challenge-return.md +135 -0
- package/dist/docs/client-preferences-fieldset.md +10 -5
- package/dist/docs/client-preferences.md +31 -8
- package/dist/docs/client-profile-fieldset.md +20 -6
- package/dist/docs/client-profile.md +12 -3
- package/dist/docs/copy-text.md +9 -7
- package/dist/docs/document-field.md +1 -1
- package/dist/docs/email-field.md +2 -1
- package/dist/docs/email-identification.md +12 -2
- package/dist/docs/footer-copyright.md +4 -2
- package/dist/docs/form.md +54 -18
- package/dist/docs/free-shipping-progress.md +6 -5
- package/dist/docs/identity-edit.md +8 -2
- package/dist/docs/identity-summary.md +18 -5
- package/dist/docs/installments.md +46 -32
- package/dist/docs/my-account-profile.md +16 -11
- package/dist/docs/navigation-link.md +5 -3
- package/dist/docs/order-cancel.md +3 -1
- package/dist/docs/order-delivery-address.md +7 -5
- package/dist/docs/order-delivery-group.md +1 -3
- package/dist/docs/order-details-back.md +1 -1
- package/dist/docs/order-details-header.md +1 -4
- package/dist/docs/order-invoice-download.md +60 -0
- package/dist/docs/order-list-row.md +1 -1
- package/dist/docs/order-list-search.md +13 -20
- package/dist/docs/order-list-views.md +11 -17
- package/dist/docs/order-list.md +12 -5
- package/dist/docs/order-payment.md +9 -7
- package/dist/docs/order-placed-date.md +1 -1
- package/dist/docs/order-placed-payment-summary.md +28 -15
- package/dist/docs/order-placed.md +49 -14
- package/dist/docs/order-progress.md +1 -1
- package/dist/docs/order-status-badge.md +20 -9
- package/dist/docs/order-summary.md +2 -2
- package/dist/docs/order-tracking-link.md +58 -0
- package/dist/docs/payment-method-badges.md +30 -4
- package/dist/docs/payment-methods.md +1 -1
- package/dist/docs/payment.md +36 -17
- package/dist/docs/phone-field.md +4 -4
- package/dist/docs/place-order.md +84 -36
- package/dist/docs/postal-code-field.md +3 -1
- package/dist/docs/pricing.md +21 -23
- package/dist/docs/product-gallery.md +19 -20
- package/dist/docs/product-information.md +13 -9
- package/dist/docs/product-list.md +7 -6
- package/dist/docs/product-shelf.md +0 -1
- package/dist/docs/promotion.md +4 -2
- package/dist/docs/region.md +23 -15
- package/dist/docs/save-card.md +7 -6
- package/dist/docs/saved-cards.md +65 -36
- package/dist/docs/shipping-address.md +57 -21
- package/dist/docs/shipping-simulator.md +53 -15
- package/dist/docs/sign-in-disclosure.md +176 -0
- package/dist/docs/sign-in-form.md +78 -47
- package/dist/docs/sign-in-redirect.md +4 -2
- package/dist/docs/sign-in.md +173 -0
- package/dist/docs/toast.md +15 -4
- package/dist/docs/variant-selector.md +15 -11
- package/dist/graphql/types.d.ts +4 -14
- package/dist/index.d.ts +2 -2
- package/dist/internal/client.d.ts +1 -3
- package/dist/internal/client.js +1 -1
- package/dist/internal/server.d.ts +7 -4
- package/dist/internal/server.js +7 -4
- package/dist/matter/access-code-sign-in/access-code-sign-in.d.ts +250 -66
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +459 -188
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/account-address-book/account-address-book-delete.d.ts +78 -40
- package/dist/matter/account-address-book/account-address-book-delete.js +86 -50
- package/dist/matter/account-address-book/account-address-book-delete.js.map +1 -1
- package/dist/matter/account-address-book/account-address-book-deliverability.d.ts +21 -4
- package/dist/matter/account-address-book/account-address-book-deliverability.js +45 -17
- package/dist/matter/account-address-book/account-address-book-deliverability.js.map +1 -1
- package/dist/matter/account-address-book/account-address-book-form-parts.d.ts +74 -19
- package/dist/matter/account-address-book/account-address-book-form-parts.js +77 -26
- package/dist/matter/account-address-book/account-address-book-form-parts.js.map +1 -1
- package/dist/matter/account-address-book/account-address-book-profile-notice.d.ts +27 -6
- package/dist/matter/account-address-book/account-address-book-profile-notice.js +55 -28
- package/dist/matter/account-address-book/account-address-book-profile-notice.js.map +1 -1
- package/dist/matter/account-address-book/account-address-book.d.ts +67 -14
- package/dist/matter/account-address-book/account-address-book.js +74 -33
- package/dist/matter/account-address-book/account-address-book.js.map +1 -1
- package/dist/matter/account-address-book/account-address-country-switch.js +3 -3
- package/dist/matter/account-address-book/account-address-country-switch.js.map +1 -1
- package/dist/matter/account-address-book/account-address-country.d.ts +66 -20
- package/dist/matter/account-address-book/account-address-country.js +83 -49
- package/dist/matter/account-address-book/account-address-country.js.map +1 -1
- package/dist/matter/account-address-book/account-address-form-context.d.ts +8 -4
- package/dist/matter/account-address-book/account-address-form-context.js.map +1 -1
- package/dist/matter/account-address-book/account-address-form.d.ts +468 -45
- package/dist/matter/account-address-book/account-address-form.js +518 -155
- package/dist/matter/account-address-book/account-address-form.js.map +1 -1
- package/dist/matter/account-address-book/account-address-record.d.ts +27 -6
- package/dist/matter/account-address-book/account-address-record.js +32 -17
- package/dist/matter/account-address-book/account-address-record.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +34 -17
- package/dist/matter/account-menu/account-menu.js +46 -54
- package/dist/matter/account-menu/account-menu.js.map +1 -1
- package/dist/matter/account-menu-identity/account-menu-identity.js +1 -1
- package/dist/matter/account-saved-cards/account-saved-cards.d.ts +108 -28
- package/dist/matter/account-saved-cards/account-saved-cards.js +177 -122
- package/dist/matter/account-saved-cards/account-saved-cards.js.map +1 -1
- package/dist/matter/billing-address/billing-address.d.ts +156 -74
- package/dist/matter/billing-address/billing-address.js +227 -156
- package/dist/matter/billing-address/billing-address.js.map +1 -1
- package/dist/matter/breadcrumb/breadcrumb.d.ts +21 -3
- package/dist/matter/breadcrumb/breadcrumb.js +24 -9
- package/dist/matter/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/matter/buy-button/buy-button.d.ts +8 -11
- package/dist/matter/buy-button/buy-button.js +10 -19
- package/dist/matter/buy-button/buy-button.js.map +1 -1
- package/dist/matter/card-entry/card-capture.js +12 -4
- package/dist/matter/card-entry/card-capture.js.map +1 -1
- package/dist/matter/card-entry/card-entry.d.ts +123 -32
- package/dist/matter/card-entry/card-entry.js +348 -221
- package/dist/matter/card-entry/card-entry.js.map +1 -1
- package/dist/matter/cart/cart-namespace.d.ts +7 -1
- package/dist/matter/cart/cart-namespace.js +7 -1
- package/dist/matter/cart/cart-namespace.js.map +1 -1
- package/dist/matter/cart/cart.d.ts +402 -61
- package/dist/matter/cart/cart.js +624 -153
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/cart-destination/cart-destination.d.ts +30 -19
- package/dist/matter/cart-destination/cart-destination.js +160 -70
- package/dist/matter/cart-destination/cart-destination.js.map +1 -1
- package/dist/matter/cart-item/cart-item.d.ts +144 -17
- package/dist/matter/cart-item/cart-item.js +166 -49
- package/dist/matter/cart-item/cart-item.js.map +1 -1
- package/dist/matter/cart-item-compositions/cart-item-compositions.js +1 -1
- package/dist/matter/cart-item-compositions/cart-item-compositions.js.map +1 -1
- package/dist/matter/cart-item-customizations/cart-item-customizations.js +1 -1
- package/dist/matter/cart-item-customizations/cart-item-customizations.js.map +1 -1
- package/dist/matter/cart-item-discounts/cart-item-discounts.js +1 -1
- package/dist/matter/cart-item-discounts/cart-item-discounts.js.map +1 -1
- package/dist/matter/cart-item-seller-info/cart-item-seller-info.js +1 -1
- package/dist/matter/cart-item-seller-info/cart-item-seller-info.js.map +1 -1
- package/dist/matter/cart-item-variations/cart-item-variations.js +1 -1
- package/dist/matter/cart-item-variations/cart-item-variations.js.map +1 -1
- package/dist/matter/cart-totalizer/cart-totalizer.d.ts +43 -9
- package/dist/matter/cart-totalizer/cart-totalizer.js +46 -17
- package/dist/matter/cart-totalizer/cart-totalizer.js.map +1 -1
- package/dist/matter/challenge-return/challenge-return.d.ts +62 -0
- package/dist/matter/challenge-return/challenge-return.js +241 -0
- package/dist/matter/challenge-return/challenge-return.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +86 -17
- package/dist/matter/client-preferences/client-preferences-fieldset.js +126 -54
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -1
- package/dist/matter/client-preferences/client-preferences.d.ts +50 -7
- package/dist/matter/client-preferences/client-preferences.js +84 -15
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +44 -9
- package/dist/matter/client-profile/client-profile.js +64 -24
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -22
- package/dist/matter/client-profile-form/client-profile-fieldset.js +161 -73
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options-accordion.d.ts +49 -4
- package/dist/matter/delivery-options/delivery-options-accordion.js +67 -14
- package/dist/matter/delivery-options/delivery-options-accordion.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options-controller.d.ts +69 -9
- package/dist/matter/delivery-options/delivery-options-controller.js +80 -14
- package/dist/matter/delivery-options/delivery-options-controller.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options.d.ts +37 -2
- package/dist/matter/delivery-options/delivery-options.js +75 -12
- package/dist/matter/delivery-options/delivery-options.js.map +1 -1
- package/dist/matter/delivery-options/use-delivery-option-selection.d.ts +1 -0
- package/dist/matter/delivery-options/use-delivery-option-selection.js +1 -1
- package/dist/matter/delivery-options/use-delivery-option-selection.js.map +1 -1
- package/dist/matter/email-identification/email-identification.d.ts +25 -3
- package/dist/matter/email-identification/email-identification.js +49 -12
- package/dist/matter/email-identification/email-identification.js.map +1 -1
- package/dist/matter/free-shipping-progress/free-shipping-progress.d.ts +79 -7
- package/dist/matter/free-shipping-progress/free-shipping-progress.js +77 -20
- package/dist/matter/free-shipping-progress/free-shipping-progress.js.map +1 -1
- package/dist/matter/identity-edit/identity-edit.d.ts +52 -10
- package/dist/matter/identity-edit/identity-edit.js +47 -19
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.d.ts +76 -22
- package/dist/matter/identity-summary/identity-summary.js +95 -45
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +98 -16
- package/dist/matter/installments/installments-select.js +156 -48
- package/dist/matter/installments/installments-select.js.map +1 -1
- package/dist/matter/installments/installments.d.ts +96 -16
- package/dist/matter/installments/installments.js +115 -40
- package/dist/matter/installments/installments.js.map +1 -1
- package/dist/matter/installments/use-installments-selection.d.ts +1 -0
- package/dist/matter/installments/use-installments-selection.js +8 -3
- package/dist/matter/installments/use-installments-selection.js.map +1 -1
- package/dist/matter/my-account-profile/my-account-profile.d.ts +358 -103
- package/dist/matter/my-account-profile/my-account-profile.js +370 -174
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-cancel/order-cancel.d.ts +65 -19
- package/dist/matter/order-cancel/order-cancel.js +118 -51
- package/dist/matter/order-cancel/order-cancel.js.map +1 -1
- package/dist/matter/order-delivery-address/order-delivery-address.d.ts +8 -18
- package/dist/matter/order-delivery-address/order-delivery-address.js +14 -23
- package/dist/matter/order-delivery-address/order-delivery-address.js.map +1 -1
- package/dist/matter/order-delivery-group/order-delivery-group.d.ts +40 -24
- package/dist/matter/order-delivery-group/order-delivery-group.js +64 -74
- package/dist/matter/order-delivery-group/order-delivery-group.js.map +1 -1
- package/dist/matter/order-details-header/order-details-header.d.ts +32 -15
- package/dist/matter/order-details-header/order-details-header.js +66 -58
- package/dist/matter/order-details-header/order-details-header.js.map +1 -1
- package/dist/matter/order-invoice-download/order-invoice-download.d.ts +78 -0
- package/dist/matter/order-invoice-download/order-invoice-download.js +173 -0
- package/dist/matter/order-invoice-download/order-invoice-download.js.map +1 -0
- package/dist/matter/order-list/order-list.d.ts +164 -19
- package/dist/matter/order-list/order-list.js +229 -87
- package/dist/matter/order-list/order-list.js.map +1 -1
- package/dist/matter/order-list-row/order-list-row.d.ts +6 -1
- package/dist/matter/order-list-row/order-list-row.js +3 -2
- package/dist/matter/order-list-row/order-list-row.js.map +1 -1
- package/dist/matter/order-list-search/order-list-search.d.ts +18 -26
- package/dist/matter/order-list-search/order-list-search.js +30 -69
- package/dist/matter/order-list-search/order-list-search.js.map +1 -1
- package/dist/matter/order-list-views/order-list-views.d.ts +15 -22
- package/dist/matter/order-list-views/order-list-views.js +22 -68
- package/dist/matter/order-list-views/order-list-views.js.map +1 -1
- package/dist/matter/order-payment/order-payment.d.ts +33 -10
- package/dist/matter/order-payment/order-payment.js +44 -39
- package/dist/matter/order-payment/order-payment.js.map +1 -1
- package/dist/matter/order-placed/order-placed-deliveries.d.ts +144 -12
- package/dist/matter/order-placed/order-placed-deliveries.js +115 -40
- package/dist/matter/order-placed/order-placed-deliveries.js.map +1 -1
- package/dist/matter/order-placed/order-placed.d.ts +198 -31
- package/dist/matter/order-placed/order-placed.js +230 -139
- package/dist/matter/order-placed/order-placed.js.map +1 -1
- package/dist/matter/order-placed-date/order-placed-date.d.ts +5 -8
- package/dist/matter/order-placed-date/order-placed-date.js +7 -8
- package/dist/matter/order-placed-date/order-placed-date.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +90 -17
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +93 -117
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -1
- package/dist/matter/order-progress/order-progress.d.ts +19 -8
- package/dist/matter/order-progress/order-progress.js +37 -45
- package/dist/matter/order-progress/order-progress.js.map +1 -1
- package/dist/matter/order-summary/order-summary.d.ts +19 -9
- package/dist/matter/order-summary/order-summary.js +38 -44
- package/dist/matter/order-summary/order-summary.js.map +1 -1
- package/dist/matter/order-tracking-link/order-tracking-link.d.ts +43 -0
- package/dist/matter/order-tracking-link/order-tracking-link.js +107 -0
- package/dist/matter/order-tracking-link/order-tracking-link.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.js +17 -10
- package/dist/matter/payment/payment-card-capture.js.map +1 -1
- package/dist/matter/payment/payment-controller.d.ts +4 -2
- package/dist/matter/payment/payment-controller.js +30 -9
- package/dist/matter/payment/payment-controller.js.map +1 -1
- package/dist/matter/payment/payment-method-badges.js +1 -1
- package/dist/matter/payment/payment.d.ts +69 -19
- package/dist/matter/payment/payment.js +101 -35
- package/dist/matter/payment/payment.js.map +1 -1
- package/dist/matter/payment/use-payment-method-selection.d.ts +25 -4
- package/dist/matter/payment/use-payment-method-selection.js +94 -32
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -1
- package/dist/matter/payment/use-payment-submit-gate.d.ts +14 -3
- package/dist/matter/payment/use-payment-submit-gate.js +47 -8
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -1
- package/dist/matter/place-order/place-order.d.ts +171 -57
- package/dist/matter/place-order/place-order.js +376 -185
- package/dist/matter/place-order/place-order.js.map +1 -1
- package/dist/matter/pricing/pricing.d.ts +152 -13
- package/dist/matter/pricing/pricing.js +153 -52
- package/dist/matter/pricing/pricing.js.map +1 -1
- package/dist/matter/product-information/product-information.d.ts +27 -4
- package/dist/matter/product-information/product-information.js +32 -15
- package/dist/matter/product-information/product-information.js.map +1 -1
- package/dist/matter/product-list/product-list.d.ts +24 -6
- package/dist/matter/product-list/product-list.js +32 -26
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +1 -3
- package/dist/matter/product-shelf/product-shelf.js +2 -15
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.d.ts +33 -16
- package/dist/matter/region/region.js +59 -52
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +43 -8
- package/dist/matter/save-card/save-card.js +61 -23
- package/dist/matter/save-card/save-card.js.map +1 -1
- package/dist/matter/saved-cards/saved-cards.d.ts +113 -21
- package/dist/matter/saved-cards/saved-cards.js +349 -77
- package/dist/matter/saved-cards/saved-cards.js.map +1 -1
- package/dist/matter/shipping-address/address-payload.js +25 -0
- package/dist/matter/shipping-address/address-payload.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +71 -71
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/library-country-descriptors.js +136 -0
- package/dist/matter/shipping-address/library-country-descriptors.js.map +1 -0
- package/dist/matter/shipping-address/shipping-address.d.ts +312 -45
- package/dist/matter/shipping-address/shipping-address.js +443 -188
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.d.ts +250 -24
- package/dist/matter/shipping-simulator/shipping-simulator.js +257 -51
- package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
- package/dist/matter/sign-in/sign-in.d.ts +108 -0
- package/dist/matter/sign-in/sign-in.js +285 -0
- package/dist/matter/sign-in/sign-in.js.map +1 -0
- package/dist/matter/sign-in-disclosure/sign-in-disclosure.d.ts +81 -0
- package/dist/matter/sign-in-disclosure/sign-in-disclosure.js +232 -0
- package/dist/matter/sign-in-disclosure/sign-in-disclosure.js.map +1 -0
- package/dist/matter/sign-in-form/sign-in-form.d.ts +93 -62
- package/dist/matter/sign-in-form/sign-in-form.js +134 -144
- package/dist/matter/sign-in-form/sign-in-form.js.map +1 -1
- package/dist/particles/binding-switcher/binding-switcher.d.ts +29 -8
- package/dist/particles/binding-switcher/binding-switcher.js +37 -26
- package/dist/particles/binding-switcher/binding-switcher.js.map +1 -1
- package/dist/particles/card-summary/card-summary.d.ts +38 -8
- package/dist/particles/card-summary/card-summary.js +55 -22
- package/dist/particles/card-summary/card-summary.js.map +1 -1
- package/dist/particles/carousel/carousel.d.ts +99 -11
- package/dist/particles/carousel/carousel.js +103 -35
- package/dist/particles/carousel/carousel.js.map +1 -1
- package/dist/particles/copy-text/copy-text.js +5 -15
- package/dist/particles/copy-text/copy-text.js.map +1 -1
- package/dist/particles/dialog/dialog.d.ts +14 -6
- package/dist/particles/dialog/dialog.js +6 -3
- package/dist/particles/dialog/dialog.js.map +1 -1
- package/dist/particles/drawer/drawer.js +1 -1
- package/dist/particles/drawer/drawer.js.map +1 -1
- package/dist/particles/email-field/email-field.d.ts +2 -1
- package/dist/particles/email-field/email-field.js +2 -1
- package/dist/particles/email-field/email-field.js.map +1 -1
- package/dist/particles/footer-link-group/footer-link-group.js +1 -0
- package/dist/particles/footer-link-group/footer-link-group.js.map +1 -1
- package/dist/particles/form/form.d.ts +85 -13
- package/dist/particles/form/form.js +91 -42
- package/dist/particles/form/form.js.map +1 -1
- package/dist/particles/gallery/gallery.d.ts +129 -12
- package/dist/particles/gallery/gallery.js +121 -34
- package/dist/particles/gallery/gallery.js.map +1 -1
- package/dist/particles/order-details-back/order-details-back.d.ts +5 -3
- package/dist/particles/order-details-back/order-details-back.js +8 -10
- package/dist/particles/order-details-back/order-details-back.js.map +1 -1
- package/dist/particles/order-status-badge/order-status-badge.d.ts +3 -3
- package/dist/particles/order-status-badge/order-status-badge.js +4 -20
- package/dist/particles/order-status-badge/order-status-badge.js.map +1 -1
- package/dist/particles/password-field/password-field.d.ts +1 -1
- package/dist/particles/password-field/password-field.js +2 -4
- package/dist/particles/password-field/password-field.js.map +1 -1
- package/dist/particles/phone-field/phone-field.d.ts +4 -4
- package/dist/particles/phone-field/phone-field.js +8 -11
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/spinner/spinner.d.ts +24 -4
- package/dist/particles/spinner/spinner.js +25 -9
- package/dist/particles/spinner/spinner.js.map +1 -1
- package/dist/particles/switch/switch.d.ts +4 -1
- package/dist/particles/switch/switch.js +3 -1
- package/dist/particles/switch/switch.js.map +1 -1
- package/dist/particles/toast/toast.d.ts +29 -3
- package/dist/particles/toast/toast.js +34 -10
- package/dist/particles/toast/toast.js.map +1 -1
- package/dist/schema.graphql +164 -56
- package/dist/sdk/address/address-fields.js +4 -4
- package/dist/sdk/address/address-fields.js.map +1 -1
- package/dist/sdk/address/constants.js +1 -6
- package/dist/sdk/address/constants.js.map +1 -1
- package/dist/sdk/address/messages.js +44 -44
- package/dist/sdk/address/messages.js.map +1 -1
- package/dist/sdk/address/postal-code.js +15 -2
- package/dist/sdk/address/postal-code.js.map +1 -1
- package/dist/sdk/address/types.d.ts +5 -3
- package/dist/sdk/address/validate-address.js +3 -3
- package/dist/sdk/address/validate-address.js.map +1 -1
- package/dist/sdk/analytics/use-analytics-actions.d.ts +1 -1
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/data/index.js +2 -2
- package/dist/sdk/data/reactive-list-item-data-provider.js +2 -2
- package/dist/sdk/generated/gql.js +1 -1
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.d.ts +20 -5
- package/dist/sdk/generated/graphql.js +6 -0
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/format-error-code.js +23 -0
- package/dist/sdk/i18n/format-error-code.js.map +1 -0
- package/dist/sdk/i18n/message-formatter.d.ts +22 -0
- package/dist/sdk/i18n/message-formatter.js +140 -0
- package/dist/sdk/i18n/message-formatter.js.map +1 -0
- package/dist/sdk/i18n/use-message-formatter.d.ts +8 -0
- package/dist/sdk/i18n/use-message-formatter.js +18 -0
- package/dist/sdk/i18n/use-message-formatter.js.map +1 -0
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/navigation/redirect.d.ts +2 -0
- package/dist/sdk/navigation/redirect.js +19 -1
- package/dist/sdk/navigation/redirect.js.map +1 -1
- package/dist/sdk/navigation/types.d.ts +13 -0
- package/dist/sdk/payment/bin-format.js +20 -0
- package/dist/sdk/payment/bin-format.js.map +1 -0
- package/dist/sdk/payment/challenge-return.js +37 -0
- package/dist/sdk/payment/challenge-return.js.map +1 -0
- package/dist/sdk/payment/confirmation-last-four.js +39 -0
- package/dist/sdk/payment/confirmation-last-four.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +1 -1
- package/dist/sdk/payment/gateway.js +8 -2
- package/dist/sdk/payment/gateway.js.map +1 -1
- package/dist/sdk/payment/operations.d.ts +1 -0
- package/dist/sdk/payment/operations.js +28 -6
- package/dist/sdk/payment/operations.js.map +1 -1
- package/dist/sdk/payment/resumed-purchase.d.ts +1 -0
- package/dist/sdk/payment/resumed-purchase.js +119 -0
- package/dist/sdk/payment/resumed-purchase.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +16 -23
- package/dist/sdk/payment/security-code-mask.js.map +1 -1
- package/dist/sdk/payment/submission-registry.js +6 -2
- package/dist/sdk/payment/submission-registry.js.map +1 -1
- package/dist/sdk/payment/use-payment-actions.d.ts +18 -3
- package/dist/sdk/payment/use-payment-actions.js +23 -3
- package/dist/sdk/payment/use-payment-actions.js.map +1 -1
- package/dist/sdk/payment/use-place-order.d.ts +10 -2
- package/dist/sdk/payment/use-place-order.js +138 -76
- package/dist/sdk/payment/use-place-order.js.map +1 -1
- package/dist/sdk/provider/storefront-provider.d.ts +1 -7
- package/dist/sdk/provider/storefront-provider.js +4 -11
- package/dist/sdk/provider/storefront-provider.js.map +1 -1
- package/dist/sdk/seo/entity-landing.d.ts +24 -0
- package/dist/sdk/seo/entity-landing.js +30 -0
- package/dist/sdk/seo/entity-landing.js.map +1 -0
- package/dist/sdk/seo/page-alternates.d.ts +3 -2
- package/dist/sdk/seo/page-alternates.js +4 -2
- package/dist/sdk/seo/page-alternates.js.map +1 -1
- package/dist/sdk/seo/search-landing.d.ts +42 -0
- package/dist/sdk/seo/search-landing.js +85 -0
- package/dist/sdk/seo/search-landing.js.map +1 -0
- package/dist/sdk/session/mock-session.d.ts +9 -0
- package/dist/sdk/session/mock-session.js +31 -0
- package/dist/sdk/session/mock-session.js.map +1 -0
- package/dist/sdk/session/read-server-session.d.ts +3 -0
- package/dist/sdk/session/read-server-session.js +5 -0
- package/dist/sdk/session/read-server-session.js.map +1 -1
- package/dist/sdk/session/return-path-segment.d.ts +10 -0
- package/dist/sdk/session/return-path-segment.js +15 -0
- package/dist/sdk/session/return-path-segment.js.map +1 -0
- package/dist/sdk/session/use-session-actions.js.map +1 -1
- package/dist/sdk/shipping/use-cart-shipping-simulation.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-simulation.js.map +1 -1
- package/dist/utils/safe-href.d.ts +2 -0
- package/package.json +4 -3
- package/skills/storefront-asset/SKILL.md +2 -2
- package/skills/storefront-asset/references/api-side.md +27 -14
- package/skills/storefront-asset/references/findings.md +5 -2
- package/src/styles/tailwind.css +8 -0
- package/dist/matter/shipping-address/country-descriptors.d.ts +0 -1
- package/dist/message-locales.json +0 -7
- package/dist/sdk/i18n/client.d.ts +0 -4
- package/dist/sdk/i18n/client.js +0 -4
- package/dist/sdk/i18n/locales.d.ts +0 -8
- package/dist/sdk/i18n/locales.js +0 -57
- package/dist/sdk/i18n/locales.js.map +0 -1
- package/dist/sdk/i18n/messages/en-US.js +0 -183
- package/dist/sdk/i18n/messages/en-US.js.map +0 -1
- package/dist/sdk/i18n/messages/en-US.json +0 -178
- package/dist/sdk/i18n/messages/en-US.json.d.ts +0 -182
- package/dist/sdk/i18n/messages/es-ES.js +0 -183
- package/dist/sdk/i18n/messages/es-ES.js.map +0 -1
- package/dist/sdk/i18n/messages/es-ES.json +0 -178
- package/dist/sdk/i18n/messages/pt-BR.js +0 -183
- package/dist/sdk/i18n/messages/pt-BR.js.map +0 -1
- package/dist/sdk/i18n/messages/pt-BR.json +0 -178
- package/dist/sdk/i18n/provider.d.ts +0 -1
- package/dist/sdk/i18n/provider.js +0 -30
- package/dist/sdk/i18n/provider.js.map +0 -1
- package/dist/sdk/i18n/server.d.ts +0 -28
- package/dist/sdk/i18n/server.js +0 -59
- package/dist/sdk/i18n/server.js.map +0 -1
- package/dist/utils/resolve-authored-copy.js +0 -18
- package/dist/utils/resolve-authored-copy.js.map +0 -1
- package/dist/utils/substitute-template.js +0 -33
- package/dist/utils/substitute-template.js.map +0 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import { defineContentType } from "../../core/dist/define-content-type.js";
|
|
3
4
|
import "../../core/dist/index.js";
|
|
4
5
|
import { useStorefrontContext } from "../../sdk/context/storefront-context.js";
|
|
6
|
+
import { useMessageFormatter } from "../../sdk/i18n/use-message-formatter.js";
|
|
5
7
|
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
6
|
-
import {
|
|
8
|
+
import { formatErrorCode } from "../../sdk/i18n/format-error-code.js";
|
|
7
9
|
import { Fieldset } from "../../particles/fieldset/fieldset.js";
|
|
8
10
|
import { RadioGroup, RadioIndicator, RadioRoot } from "../../particles/radio/radio.js";
|
|
9
|
-
import { substituteTemplate } from "../../utils/substitute-template.js";
|
|
10
11
|
import { findInstallmentOption, resolveInstallmentTiers, useInstallmentsSelection } from "./use-installments-selection.js";
|
|
11
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
13
|
//#region src/matter/installments/installments.tsx
|
|
@@ -52,6 +53,7 @@ const fragment = `
|
|
|
52
53
|
currencyCode
|
|
53
54
|
}
|
|
54
55
|
interestRate
|
|
56
|
+
interestFree
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
selectedInstallment {
|
|
@@ -61,6 +63,93 @@ const fragment = `
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
`;
|
|
66
|
+
const InstallmentsContent = defineContentType({
|
|
67
|
+
id: "installments",
|
|
68
|
+
title: "Installments",
|
|
69
|
+
description: "Copy for the installment rows, the group legend and a refused pick.",
|
|
70
|
+
schema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
installmentTemplate: {
|
|
75
|
+
type: "string",
|
|
76
|
+
title: "Installment",
|
|
77
|
+
description: "`value` is the formatted per-installment value.",
|
|
78
|
+
format: "icu-message",
|
|
79
|
+
messageSource: "{count}x of {value}",
|
|
80
|
+
messageArguments: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
count: { type: "integer" },
|
|
84
|
+
value: { type: "string" }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
messageExample: {
|
|
88
|
+
count: 3,
|
|
89
|
+
value: "$35.00"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
interestFreeLabel: {
|
|
93
|
+
type: "string",
|
|
94
|
+
title: "Interest-free label",
|
|
95
|
+
description: "Appended when a tier is interest-free.",
|
|
96
|
+
format: "icu-message",
|
|
97
|
+
messageSource: "interest-free"
|
|
98
|
+
},
|
|
99
|
+
interestTemplate: {
|
|
100
|
+
type: "string",
|
|
101
|
+
title: "Interest",
|
|
102
|
+
description: "Appended when a tier is not interest-free. `rate` is the formatted percentage: an order form rate of 100 is `1`.",
|
|
103
|
+
format: "icu-message",
|
|
104
|
+
messageSource: "{rate}% interest",
|
|
105
|
+
messageArguments: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: { rate: { type: "string" } }
|
|
108
|
+
},
|
|
109
|
+
messageExample: { rate: "1.5" }
|
|
110
|
+
},
|
|
111
|
+
legend: {
|
|
112
|
+
type: "string",
|
|
113
|
+
title: "Legend",
|
|
114
|
+
description: "The radio group's visible accessible name.",
|
|
115
|
+
format: "icu-message",
|
|
116
|
+
messageSource: "Installments"
|
|
117
|
+
},
|
|
118
|
+
invalidInstallmentMessage: {
|
|
119
|
+
type: "string",
|
|
120
|
+
title: "Invalid installment message",
|
|
121
|
+
description: "Shown when the picked installment plan is no longer offered.",
|
|
122
|
+
format: "icu-message",
|
|
123
|
+
messageSource: "This installment plan is no longer available. Choose another one."
|
|
124
|
+
},
|
|
125
|
+
missingMethodMessage: {
|
|
126
|
+
type: "string",
|
|
127
|
+
title: "Missing payment method message",
|
|
128
|
+
description: "Shown when an installment plan is picked before a payment method.",
|
|
129
|
+
format: "icu-message",
|
|
130
|
+
messageSource: "Choose a payment method before choosing an installment plan."
|
|
131
|
+
},
|
|
132
|
+
genericErrorMessage: {
|
|
133
|
+
type: "string",
|
|
134
|
+
title: "Selection failure message",
|
|
135
|
+
description: "Shown when a pick fails: a network failure or a fault the shopper cannot fix.",
|
|
136
|
+
format: "icu-message",
|
|
137
|
+
messageSource: "Unable to select the installment plan. Please try again."
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* The message for each code an installment pick can return. A plan no longer offered and a pick made before a method are ones the shopper can fix; the rest share one message, as does
|
|
144
|
+
* a code this map does not know.
|
|
145
|
+
*/
|
|
146
|
+
const INSTALLMENTS_ERROR_FIELDS = {
|
|
147
|
+
MISSING_CART_CONTEXT: "genericErrorMessage",
|
|
148
|
+
CART_NOT_FOUND: "genericErrorMessage",
|
|
149
|
+
MISSING_PAYMENT_METHOD_SELECTION: "missingMethodMessage",
|
|
150
|
+
INVALID_INSTALLMENT_COUNT: "invalidInstallmentMessage",
|
|
151
|
+
NETWORK_ERROR: "genericErrorMessage"
|
|
152
|
+
};
|
|
64
153
|
function formatInstallmentValue(amount, locale, currency) {
|
|
65
154
|
return new Intl.NumberFormat(locale, {
|
|
66
155
|
style: "currency",
|
|
@@ -77,16 +166,18 @@ function formatInterestRate(rate, locale) {
|
|
|
77
166
|
/**
|
|
78
167
|
* Formats one tier's row/option text. Exported (T90,
|
|
79
168
|
* 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
|
|
81
|
-
*
|
|
169
|
+
* with the exact same function `Installments` uses, driven by the same three message fields — the two
|
|
170
|
+
* surfaces cannot word a tier differently.
|
|
82
171
|
*/
|
|
83
|
-
function installmentLabel({ tier, locale,
|
|
172
|
+
function installmentLabel({ tier, locale, messages }) {
|
|
84
173
|
const formattedValue = formatInstallmentValue(tier.value.amount, locale, tier.value.currencyCode);
|
|
85
|
-
const base =
|
|
86
|
-
count:
|
|
174
|
+
const base = messages.installment({
|
|
175
|
+
count: tier.count,
|
|
87
176
|
value: formattedValue
|
|
88
177
|
});
|
|
89
|
-
|
|
178
|
+
let interestSuffix = "";
|
|
179
|
+
if (tier.interestFree === true) interestSuffix = messages.interestFree();
|
|
180
|
+
else if (tier.interestFree === false && tier.interestRate !== null) interestSuffix = messages.interest({ rate: formatInterestRate(tier.interestRate, locale) });
|
|
90
181
|
return interestSuffix ? `${base} ${interestSuffix}` : base;
|
|
91
182
|
}
|
|
92
183
|
const InstallmentsDefinition = defineComponent({
|
|
@@ -97,42 +188,28 @@ const InstallmentsDefinition = defineComponent({
|
|
|
97
188
|
fetchStrategy: "reactive",
|
|
98
189
|
requiresContext: "payment",
|
|
99
190
|
actions: { payment: ["setInstallments"] },
|
|
191
|
+
content: InstallmentsContent,
|
|
100
192
|
editor: {
|
|
101
193
|
label: "Installments",
|
|
102
194
|
category: "commerce"
|
|
103
195
|
},
|
|
104
|
-
schema: {
|
|
105
|
-
|
|
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."
|
|
196
|
+
schema: {},
|
|
197
|
+
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). Its copy comes from the $content fields installmentTemplate, interestFreeLabel, interestTemplate and legend. 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. Renders nothing until the entry for a new card is valid (length, Luhn, expiration, security code) or a saved card is selected, and nothing when no payment method the store offers carries a tier above 1x. Picking a row dispatches $actions.payment.setInstallments; the mutation returns the cart, whose total is the greater of the order form value and the selected payment value, so a CartTotal elsewhere on the page shows the interest-inclusive total from the normalized cache with no wiring here. Renders its own rows from real data — no children."
|
|
127
198
|
});
|
|
128
|
-
function Installments({
|
|
199
|
+
function Installments({ $content, $locale, $data, $actions: $actionsProp, className, ...props }) {
|
|
129
200
|
const $actions = useActionsOverride($actionsProp);
|
|
130
201
|
const { locale } = useStorefrontContext();
|
|
131
|
-
const {
|
|
202
|
+
const { format } = useMessageFormatter(InstallmentsContent, $content);
|
|
203
|
+
const messages = {
|
|
204
|
+
installment: (args) => format("installmentTemplate", args),
|
|
205
|
+
interestFree: () => format("interestFreeLabel"),
|
|
206
|
+
interest: (args) => format("interestTemplate", args)
|
|
207
|
+
};
|
|
208
|
+
const { tiers, selectedCount, state, isPending, selectionError, selectInstallment, placing, cardPaymentCollapsed, visible } = useInstallmentsSelection({
|
|
132
209
|
$data,
|
|
133
210
|
$actions
|
|
134
211
|
});
|
|
135
|
-
if (cardPaymentCollapsed) return null;
|
|
212
|
+
if (cardPaymentCollapsed || !visible) return null;
|
|
136
213
|
return /* @__PURE__ */ jsxs("div", {
|
|
137
214
|
...props,
|
|
138
215
|
className,
|
|
@@ -145,10 +222,10 @@ function Installments({ installmentTemplate = "{count}x of {value}", interestFre
|
|
|
145
222
|
children: [selectionError ? /* @__PURE__ */ jsx("div", {
|
|
146
223
|
role: "alert",
|
|
147
224
|
"data-fs-installments-error": "",
|
|
148
|
-
children: selectionError
|
|
225
|
+
children: formatErrorCode(InstallmentsContent, INSTALLMENTS_ERROR_FIELDS, selectionError, format, "genericErrorMessage")
|
|
149
226
|
}) : null, tiers.length > 0 ? /* @__PURE__ */ jsxs(Fieldset.Root, {
|
|
150
227
|
"data-fs-installments-fieldset": "",
|
|
151
|
-
children: [/* @__PURE__ */ jsx(Fieldset.Legend, { children:
|
|
228
|
+
children: [/* @__PURE__ */ jsx(Fieldset.Legend, { children: format("legend") }), /* @__PURE__ */ jsx(RadioGroup, {
|
|
152
229
|
value: String(selectedCount),
|
|
153
230
|
onValueChange: (value) => selectInstallment(Number(value)),
|
|
154
231
|
disabled: isPending,
|
|
@@ -160,9 +237,7 @@ function Installments({ installmentTemplate = "{count}x of {value}", interestFre
|
|
|
160
237
|
children: installmentLabel({
|
|
161
238
|
tier,
|
|
162
239
|
locale,
|
|
163
|
-
|
|
164
|
-
interestFreeLabel,
|
|
165
|
-
interestTemplate
|
|
240
|
+
messages
|
|
166
241
|
})
|
|
167
242
|
})]
|
|
168
243
|
}, tier.count))
|
|
@@ -171,6 +246,6 @@ function Installments({ installmentTemplate = "{count}x of {value}", interestFre
|
|
|
171
246
|
});
|
|
172
247
|
}
|
|
173
248
|
//#endregion
|
|
174
|
-
export { Installments, InstallmentsDefinition, findInstallmentOption, installmentLabel, resolveInstallmentTiers };
|
|
249
|
+
export { INSTALLMENTS_ERROR_FIELDS, Installments, InstallmentsContent, InstallmentsDefinition, findInstallmentOption, installmentLabel, resolveInstallmentTiers };
|
|
175
250
|
|
|
176
251
|
//# sourceMappingURL=installments.js.map
|
|
@@ -1 +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":"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"}
|
|
1
|
+
{"version":3,"file":"installments.js","names":[],"sources":["../../../src/matter/installments/installments.tsx"],"sourcesContent":["'use client';\n\nimport {\n type ContentValue,\n defineComponent,\n defineContentType,\n type MessageFieldName,\n type MessageLocaleProp,\n} 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 { formatErrorCode } from '../../sdk/i18n/format-error-code.js';\nimport { useMessageFormatter } from '../../sdk/i18n/use-message-formatter.js';\nimport { type PaymentInstallmentSelectErrorCode, 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). `totalAmount` is\n// deliberately not selected: this component never renders or computes against it\n// (specs/installments.md § Decision 3). `interestFree` is: the server says whether a tier is free, so\n// nothing here recomputes it from the rate (T230).\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 interestFree\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 /** Hundredths of a percent. `null` when the order form did not say what the rate is (T230). */\n interestRate: number | null;\n /** `null` exactly when `interestRate` is `null`. Computed by the API, never by this component. */\n interestFree: boolean | null;\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 const InstallmentsContent = defineContentType({\n id: 'installments',\n title: 'Installments',\n description: 'Copy for the installment rows, the group legend and a refused pick.',\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n installmentTemplate: {\n type: 'string',\n title: 'Installment',\n description: '`value` is the formatted per-installment value.',\n format: 'icu-message',\n messageSource: '{count}x of {value}',\n messageArguments: {\n type: 'object',\n properties: { count: { type: 'integer' }, value: { type: 'string' } },\n },\n messageExample: { count: 3, value: '$35.00' },\n },\n interestFreeLabel: {\n type: 'string',\n title: 'Interest-free label',\n description: 'Appended when a tier is interest-free.',\n format: 'icu-message',\n messageSource: 'interest-free',\n },\n interestTemplate: {\n type: 'string',\n title: 'Interest',\n description:\n 'Appended when a tier is not interest-free. `rate` is the formatted percentage: an order form rate of 100 is `1`.',\n format: 'icu-message',\n messageSource: '{rate}% interest',\n messageArguments: { type: 'object', properties: { rate: { type: 'string' } } },\n messageExample: { rate: '1.5' },\n },\n legend: {\n type: 'string',\n title: 'Legend',\n description: \"The radio group's visible accessible name.\",\n format: 'icu-message',\n messageSource: 'Installments',\n },\n invalidInstallmentMessage: {\n type: 'string',\n title: 'Invalid installment message',\n description: 'Shown when the picked installment plan is no longer offered.',\n format: 'icu-message',\n messageSource: 'This installment plan is no longer available. Choose another one.',\n },\n missingMethodMessage: {\n type: 'string',\n title: 'Missing payment method message',\n description: 'Shown when an installment plan is picked before a payment method.',\n format: 'icu-message',\n messageSource: 'Choose a payment method before choosing an installment plan.',\n },\n genericErrorMessage: {\n type: 'string',\n title: 'Selection failure message',\n description: 'Shown when a pick fails: a network failure or a fault the shopper cannot fix.',\n format: 'icu-message',\n messageSource: 'Unable to select the installment plan. Please try again.',\n },\n },\n },\n});\n\n/**\n * The message for each code an installment pick can return. A plan no longer offered and a pick made before a method are ones the shopper can fix; the rest share one message, as does\n * a code this map does not know.\n */\nexport const INSTALLMENTS_ERROR_FIELDS = {\n MISSING_CART_CONTEXT: 'genericErrorMessage',\n CART_NOT_FOUND: 'genericErrorMessage',\n MISSING_PAYMENT_METHOD_SELECTION: 'missingMethodMessage',\n INVALID_INSTALLMENT_COUNT: 'invalidInstallmentMessage',\n NETWORK_ERROR: 'genericErrorMessage',\n} as const satisfies Record<PaymentInstallmentSelectErrorCode, MessageFieldName<typeof InstallmentsContent>>;\n\nexport type InstallmentsProps = Omit<ComponentPropsWithRef<'div'>, 'children'> &\n MessageLocaleProp & {\n /**\n * Merchant-authored copy: the row template, the interest suffixes, the legend and the error. A tier whose rate\n * the order form did not state (`interestRate` and `interestFree` both `null`) gets no suffix.\n */\n $content?: ContentValue<typeof InstallmentsContent>;\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/** The three tier messages, read from the rendering component's own content type. */\nexport type InstallmentLabelMessages = {\n installment: (args: { count: number; value: string }) => string;\n interestFree: () => string;\n interest: (args: { rate: string }) => string;\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 three message fields — the two\n * surfaces cannot word a tier differently.\n */\nexport function installmentLabel({\n tier,\n locale,\n messages,\n}: {\n tier: InstallmentTier;\n locale: string;\n messages: InstallmentLabelMessages;\n}): string {\n const formattedValue = formatInstallmentValue(tier.value.amount, locale, tier.value.currencyCode);\n const base = messages.installment({ count: tier.count, value: formattedValue });\n // The API decides interest-free; a null rate is unknown, so it claims neither \"interest-free\" nor a rate.\n let interestSuffix = '';\n if (tier.interestFree === true) interestSuffix = messages.interestFree();\n else if (tier.interestFree === false && tier.interestRate !== null) {\n interestSuffix = messages.interest({ rate: formatInterestRate(tier.interestRate, locale) });\n }\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 content: InstallmentsContent,\n editor: { label: 'Installments', category: 'commerce' },\n schema: {},\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). Its copy comes from the $content fields installmentTemplate, interestFreeLabel, interestTemplate and legend. 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. Renders nothing until the entry for a new card is valid (length, Luhn, expiration, security code) or a saved card is selected, and nothing when no payment method the store offers carries a tier above 1x. Picking a row dispatches $actions.payment.setInstallments; the mutation returns the cart, whose total is the greater of the order form value and the selected payment value, so a CartTotal elsewhere on the page shows the interest-inclusive total 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 $content,\n $locale,\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 { format } = useMessageFormatter(InstallmentsContent, $content);\n const messages: InstallmentLabelMessages = {\n installment: (args) => format('installmentTemplate', args),\n interestFree: () => format('interestFreeLabel'),\n interest: (args) => format('interestTemplate', args),\n };\n const {\n tiers,\n selectedCount,\n state,\n isPending,\n selectionError,\n selectInstallment,\n placing,\n cardPaymentCollapsed,\n visible,\n } = useInstallmentsSelection({\n $data,\n $actions,\n });\n\n // T191 (FR-7): nothing renders while the card payment is collapsed. T201: nor while the entry is not\n // valid, or the store offers no installments — the hook's one `visible` decision, so the two surfaces\n // cannot drift. Returned after the last hook, so this instance stays mounted and keeps its local state.\n if (cardPaymentCollapsed || !visible) 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 {formatErrorCode(\n InstallmentsContent,\n INSTALLMENTS_ERROR_FIELDS,\n selectionError,\n format,\n 'genericErrorMessage',\n )}\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>{format('legend')}</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=\"\">{installmentLabel({ tier, locale, messages })}</span>\n </RadioRoot>\n ))}\n </RadioGroup>\n </Fieldset.Root>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -50,10 +50,14 @@ function useInstallmentsSelection({ $data: $dataProp, $actions }) {
|
|
|
50
50
|
const $data = useDataOverride($dataProp);
|
|
51
51
|
const { draft, selectedMethodId, selectedMethod, selectedBin, binConfirmation, placing, cardPaymentCollapsed } = usePayment();
|
|
52
52
|
const loading = $data == null;
|
|
53
|
-
const
|
|
53
|
+
const options = $data?.installmentOptions ?? EMPTY_OPTIONS;
|
|
54
|
+
const tiers = resolveInstallmentTiers(findInstallmentOption(options, {
|
|
54
55
|
bin: draft.bin ?? selectedBin ?? null,
|
|
55
56
|
methodId: selectedMethod !== null && isCardMethod(selectedMethod) ? selectedMethodId : null
|
|
56
57
|
}));
|
|
58
|
+
const gateOpen = draft.path === "saved-card" || draft.entryValid;
|
|
59
|
+
const offersInstallments = options.some((option) => option.installments.some((tier) => tier.count > ONE_TIME_COUNT));
|
|
60
|
+
const visible = gateOpen && (loading || offersInstallments);
|
|
57
61
|
const [selectionError, setSelectionError] = useState(null);
|
|
58
62
|
const [requestedCount, setRequestedCount] = useState(null);
|
|
59
63
|
const selectionToken = useRef(0);
|
|
@@ -69,7 +73,7 @@ function useInstallmentsSelection({ $data: $dataProp, $actions }) {
|
|
|
69
73
|
dispatch(count).then((userErrors) => {
|
|
70
74
|
if (selectionToken.current !== token) return;
|
|
71
75
|
const [firstError] = userErrors;
|
|
72
|
-
if (firstError) setSelectionError(firstError
|
|
76
|
+
if (firstError) setSelectionError(firstError);
|
|
73
77
|
}).finally(() => {
|
|
74
78
|
if (selectionToken.current === token) setRequestedCount(null);
|
|
75
79
|
});
|
|
@@ -86,7 +90,8 @@ function useInstallmentsSelection({ $data: $dataProp, $actions }) {
|
|
|
86
90
|
selectionError,
|
|
87
91
|
selectInstallment,
|
|
88
92
|
placing,
|
|
89
|
-
cardPaymentCollapsed
|
|
93
|
+
cardPaymentCollapsed,
|
|
94
|
+
visible
|
|
90
95
|
};
|
|
91
96
|
}
|
|
92
97
|
//#endregion
|
|
@@ -1 +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"}
|
|
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 type { PaymentUserError } from '../../sdk/payment/operations.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 * - **Visibility.** `visible`, the one decision both surfaces render from (T201, FR-2/FR-3, INV-5):\n * the entry is valid (`draft.entryValid`) or a saved card is the selection (`draft.path`), and the\n * store offers a tier above 1x on some method — or `$data` is still loading, which renders the loading\n * wrapper. A store that offers no installments anywhere shows no selector rather than a one-row group.\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 */\n/** The `PaymentInstallmentSelectErrorCode` values plus the client-minted transport failure. */\nexport type PaymentInstallmentSelectErrorCode =\n | 'MISSING_CART_CONTEXT'\n | 'CART_NOT_FOUND'\n | 'MISSING_PAYMENT_METHOD_SELECTION'\n | 'INVALID_INSTALLMENT_COUNT'\n | 'NETWORK_ERROR';\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` from the latest selection, or null. Each surface words it by `code`. */\n selectionError: PaymentUserError | 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 /** Whether either surface renders at all (T201); both render nothing while this is false. */\n visible: 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 // T201 (FR-2, FR-3): a saved card needs no entry; a new card needs a valid one. Once `$data` has\n // resolved, a store whose every tier is 1x has nothing to choose, however the card is entered.\n const gateOpen = draft.path === 'saved-card' || draft.entryValid;\n const offersInstallments = options.some((option) => option.installments.some((tier) => tier.count > ONE_TIME_COUNT));\n const visible = gateOpen && (loading || offersInstallments);\n\n const [selectionError, setSelectionError] = useState<PaymentUserError | 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);\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 visible,\n };\n}\n"],"mappings":";;;;;;AAqBA,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;AA2EA,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;CACzB,MAAM,UAAU,OAAO,sBAAsB;CAO7C,MAAM,QAAQ,wBANQ,sBAAsB,SAAS;EACnD,KAAK,MAAM,OAAO,eAAe;EAGjC,UAAU,mBAAmB,QAAQ,aAAa,cAAc,IAAI,mBAAmB;CACzF,CACkD,CAAC;CAGnD,MAAM,WAAW,MAAM,SAAS,gBAAgB,MAAM;CACtD,MAAM,qBAAqB,QAAQ,MAAM,WAAW,OAAO,aAAa,MAAM,SAAS,KAAK,QAAQ,cAAc,CAAC;CACnH,MAAM,UAAU,aAAa,WAAW;CAExC,MAAM,CAAC,gBAAgB,qBAAqB,SAAkC,IAAI;CAElF,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,UAAU;EAC9C,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;EACA;CACF;AACF"}
|