@vtex/faststore 0.0.10 → 0.0.12

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.
Files changed (509) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +0 -1
  3. package/dist/_virtual/_rolldown/runtime.js +4 -0
  4. package/dist/actions.json +6 -1
  5. package/dist/catalog.json +789 -1701
  6. package/dist/cli/index.d.ts +66 -28
  7. package/dist/cli/index.js +7371 -512
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/server-runtime.js +41 -60
  10. package/dist/cli/server-runtime.js.map +1 -1
  11. package/dist/client.d.ts +3 -3
  12. package/dist/client.js +2 -2
  13. package/dist/components.d.ts +63 -57
  14. package/dist/components.js +60 -54
  15. package/dist/content-types.json +5785 -754
  16. package/dist/core/dist/define-component.d.ts +1 -1
  17. package/dist/core/dist/define-content-type.d.ts +50 -2
  18. package/dist/core/dist/define-content-type.js +107 -3
  19. package/dist/core/dist/define-content-type.js.map +1 -1
  20. package/dist/core/dist/index.d.ts +2 -2
  21. package/dist/docs/access-code-field.md +1 -1
  22. package/dist/docs/access-code-sign-in.md +126 -77
  23. package/dist/docs/account-address-book.md +210 -95
  24. package/dist/docs/account-menu.md +14 -15
  25. package/dist/docs/account-saved-cards.md +44 -39
  26. package/dist/docs/alert-dialog.md +3 -1
  27. package/dist/docs/avatar.md +4 -2
  28. package/dist/docs/billing-address.md +99 -30
  29. package/dist/docs/binding-switcher.md +5 -2
  30. package/dist/docs/buy-button.md +7 -8
  31. package/dist/docs/card-entry.md +39 -26
  32. package/dist/docs/card-summary.md +18 -8
  33. package/dist/docs/cardholder-document.md +5 -3
  34. package/dist/docs/carousel.md +5 -4
  35. package/dist/docs/cart-destination.md +39 -26
  36. package/dist/docs/cart-item.md +29 -14
  37. package/dist/docs/cart-totalizer.md +7 -5
  38. package/dist/docs/cart.md +135 -41
  39. package/dist/docs/challenge-return.md +135 -0
  40. package/dist/docs/client-preferences-fieldset.md +10 -5
  41. package/dist/docs/client-preferences.md +31 -8
  42. package/dist/docs/client-profile-fieldset.md +20 -6
  43. package/dist/docs/client-profile.md +12 -3
  44. package/dist/docs/copy-text.md +9 -7
  45. package/dist/docs/document-field.md +1 -1
  46. package/dist/docs/email-field.md +2 -1
  47. package/dist/docs/email-identification.md +12 -2
  48. package/dist/docs/footer-copyright.md +4 -2
  49. package/dist/docs/form.md +54 -18
  50. package/dist/docs/free-shipping-progress.md +6 -5
  51. package/dist/docs/identity-edit.md +8 -2
  52. package/dist/docs/identity-summary.md +18 -5
  53. package/dist/docs/installments.md +46 -32
  54. package/dist/docs/my-account-profile.md +16 -11
  55. package/dist/docs/navigation-link.md +5 -3
  56. package/dist/docs/order-cancel.md +3 -1
  57. package/dist/docs/order-delivery-address.md +7 -5
  58. package/dist/docs/order-delivery-group.md +1 -3
  59. package/dist/docs/order-details-back.md +1 -1
  60. package/dist/docs/order-details-header.md +1 -4
  61. package/dist/docs/order-invoice-download.md +60 -0
  62. package/dist/docs/order-list-row.md +1 -1
  63. package/dist/docs/order-list-search.md +13 -20
  64. package/dist/docs/order-list-views.md +11 -17
  65. package/dist/docs/order-list.md +12 -5
  66. package/dist/docs/order-payment.md +9 -7
  67. package/dist/docs/order-placed-date.md +1 -1
  68. package/dist/docs/order-placed-payment-summary.md +28 -15
  69. package/dist/docs/order-placed.md +49 -14
  70. package/dist/docs/order-progress.md +1 -1
  71. package/dist/docs/order-status-badge.md +20 -9
  72. package/dist/docs/order-summary.md +2 -2
  73. package/dist/docs/order-tracking-link.md +58 -0
  74. package/dist/docs/payment-method-badges.md +30 -4
  75. package/dist/docs/payment-methods.md +1 -1
  76. package/dist/docs/payment.md +36 -17
  77. package/dist/docs/phone-field.md +4 -4
  78. package/dist/docs/place-order.md +84 -36
  79. package/dist/docs/postal-code-field.md +3 -1
  80. package/dist/docs/pricing.md +21 -23
  81. package/dist/docs/product-gallery.md +19 -20
  82. package/dist/docs/product-information.md +13 -9
  83. package/dist/docs/product-list.md +7 -6
  84. package/dist/docs/product-shelf.md +0 -1
  85. package/dist/docs/promotion.md +4 -2
  86. package/dist/docs/region.md +23 -15
  87. package/dist/docs/save-card.md +7 -6
  88. package/dist/docs/saved-cards.md +65 -36
  89. package/dist/docs/shipping-address.md +57 -21
  90. package/dist/docs/shipping-simulator.md +53 -15
  91. package/dist/docs/sign-in-disclosure.md +176 -0
  92. package/dist/docs/sign-in-form.md +78 -47
  93. package/dist/docs/sign-in-redirect.md +4 -2
  94. package/dist/docs/sign-in.md +173 -0
  95. package/dist/docs/toast.md +15 -4
  96. package/dist/docs/variant-selector.md +15 -11
  97. package/dist/domain-logic/dist/checkout/rules.js +33 -0
  98. package/dist/domain-logic/dist/checkout/rules.js.map +1 -0
  99. package/dist/graphql/types.d.ts +4 -14
  100. package/dist/index.d.ts +2 -2
  101. package/dist/internal/client.d.ts +1 -3
  102. package/dist/internal/client.js +1 -1
  103. package/dist/internal/server.d.ts +7 -4
  104. package/dist/internal/server.js +7 -4
  105. package/dist/matter/access-code-sign-in/access-code-sign-in.d.ts +250 -66
  106. package/dist/matter/access-code-sign-in/access-code-sign-in.js +459 -188
  107. package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
  108. package/dist/matter/account-address-book/account-address-book-delete.d.ts +78 -40
  109. package/dist/matter/account-address-book/account-address-book-delete.js +86 -50
  110. package/dist/matter/account-address-book/account-address-book-delete.js.map +1 -1
  111. package/dist/matter/account-address-book/account-address-book-deliverability.d.ts +21 -4
  112. package/dist/matter/account-address-book/account-address-book-deliverability.js +45 -17
  113. package/dist/matter/account-address-book/account-address-book-deliverability.js.map +1 -1
  114. package/dist/matter/account-address-book/account-address-book-form-parts.d.ts +74 -19
  115. package/dist/matter/account-address-book/account-address-book-form-parts.js +77 -26
  116. package/dist/matter/account-address-book/account-address-book-form-parts.js.map +1 -1
  117. package/dist/matter/account-address-book/account-address-book-profile-notice.d.ts +27 -6
  118. package/dist/matter/account-address-book/account-address-book-profile-notice.js +55 -28
  119. package/dist/matter/account-address-book/account-address-book-profile-notice.js.map +1 -1
  120. package/dist/matter/account-address-book/account-address-book.d.ts +67 -14
  121. package/dist/matter/account-address-book/account-address-book.js +74 -33
  122. package/dist/matter/account-address-book/account-address-book.js.map +1 -1
  123. package/dist/matter/account-address-book/account-address-country-switch.js +3 -3
  124. package/dist/matter/account-address-book/account-address-country-switch.js.map +1 -1
  125. package/dist/matter/account-address-book/account-address-country.d.ts +66 -20
  126. package/dist/matter/account-address-book/account-address-country.js +83 -49
  127. package/dist/matter/account-address-book/account-address-country.js.map +1 -1
  128. package/dist/matter/account-address-book/account-address-form-context.d.ts +8 -4
  129. package/dist/matter/account-address-book/account-address-form-context.js.map +1 -1
  130. package/dist/matter/account-address-book/account-address-form.d.ts +468 -45
  131. package/dist/matter/account-address-book/account-address-form.js +518 -155
  132. package/dist/matter/account-address-book/account-address-form.js.map +1 -1
  133. package/dist/matter/account-address-book/account-address-record.d.ts +27 -6
  134. package/dist/matter/account-address-book/account-address-record.js +32 -17
  135. package/dist/matter/account-address-book/account-address-record.js.map +1 -1
  136. package/dist/matter/account-menu/account-menu.d.ts +34 -17
  137. package/dist/matter/account-menu/account-menu.js +46 -54
  138. package/dist/matter/account-menu/account-menu.js.map +1 -1
  139. package/dist/matter/account-menu-identity/account-menu-identity.js +1 -1
  140. package/dist/matter/account-saved-cards/account-saved-cards.d.ts +108 -28
  141. package/dist/matter/account-saved-cards/account-saved-cards.js +177 -122
  142. package/dist/matter/account-saved-cards/account-saved-cards.js.map +1 -1
  143. package/dist/matter/billing-address/billing-address.d.ts +156 -74
  144. package/dist/matter/billing-address/billing-address.js +227 -156
  145. package/dist/matter/billing-address/billing-address.js.map +1 -1
  146. package/dist/matter/breadcrumb/breadcrumb.d.ts +21 -3
  147. package/dist/matter/breadcrumb/breadcrumb.js +24 -9
  148. package/dist/matter/breadcrumb/breadcrumb.js.map +1 -1
  149. package/dist/matter/buy-button/buy-button.d.ts +8 -11
  150. package/dist/matter/buy-button/buy-button.js +10 -19
  151. package/dist/matter/buy-button/buy-button.js.map +1 -1
  152. package/dist/matter/card-entry/card-capture.js +12 -4
  153. package/dist/matter/card-entry/card-capture.js.map +1 -1
  154. package/dist/matter/card-entry/card-entry.d.ts +123 -32
  155. package/dist/matter/card-entry/card-entry.js +348 -221
  156. package/dist/matter/card-entry/card-entry.js.map +1 -1
  157. package/dist/matter/cart/cart-namespace.d.ts +7 -1
  158. package/dist/matter/cart/cart-namespace.js +7 -1
  159. package/dist/matter/cart/cart-namespace.js.map +1 -1
  160. package/dist/matter/cart/cart.d.ts +402 -61
  161. package/dist/matter/cart/cart.js +624 -153
  162. package/dist/matter/cart/cart.js.map +1 -1
  163. package/dist/matter/cart-destination/cart-destination.d.ts +30 -19
  164. package/dist/matter/cart-destination/cart-destination.js +160 -70
  165. package/dist/matter/cart-destination/cart-destination.js.map +1 -1
  166. package/dist/matter/cart-item/cart-item.d.ts +144 -17
  167. package/dist/matter/cart-item/cart-item.js +166 -49
  168. package/dist/matter/cart-item/cart-item.js.map +1 -1
  169. package/dist/matter/cart-item-compositions/cart-item-compositions.js +1 -1
  170. package/dist/matter/cart-item-compositions/cart-item-compositions.js.map +1 -1
  171. package/dist/matter/cart-item-customizations/cart-item-customizations.js +1 -1
  172. package/dist/matter/cart-item-customizations/cart-item-customizations.js.map +1 -1
  173. package/dist/matter/cart-item-discounts/cart-item-discounts.js +1 -1
  174. package/dist/matter/cart-item-discounts/cart-item-discounts.js.map +1 -1
  175. package/dist/matter/cart-item-seller-info/cart-item-seller-info.js +1 -1
  176. package/dist/matter/cart-item-seller-info/cart-item-seller-info.js.map +1 -1
  177. package/dist/matter/cart-item-variations/cart-item-variations.js +1 -1
  178. package/dist/matter/cart-item-variations/cart-item-variations.js.map +1 -1
  179. package/dist/matter/cart-totalizer/cart-totalizer.d.ts +43 -9
  180. package/dist/matter/cart-totalizer/cart-totalizer.js +46 -17
  181. package/dist/matter/cart-totalizer/cart-totalizer.js.map +1 -1
  182. package/dist/matter/challenge-return/challenge-return.d.ts +62 -0
  183. package/dist/matter/challenge-return/challenge-return.js +241 -0
  184. package/dist/matter/challenge-return/challenge-return.js.map +1 -0
  185. package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +86 -17
  186. package/dist/matter/client-preferences/client-preferences-fieldset.js +126 -54
  187. package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -1
  188. package/dist/matter/client-preferences/client-preferences.d.ts +50 -7
  189. package/dist/matter/client-preferences/client-preferences.js +84 -15
  190. package/dist/matter/client-preferences/client-preferences.js.map +1 -1
  191. package/dist/matter/client-profile/client-profile.d.ts +44 -9
  192. package/dist/matter/client-profile/client-profile.js +64 -24
  193. package/dist/matter/client-profile/client-profile.js.map +1 -1
  194. package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -22
  195. package/dist/matter/client-profile-form/client-profile-fieldset.js +161 -73
  196. package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -1
  197. package/dist/matter/delivery-options/delivery-options-accordion.d.ts +49 -4
  198. package/dist/matter/delivery-options/delivery-options-accordion.js +67 -14
  199. package/dist/matter/delivery-options/delivery-options-accordion.js.map +1 -1
  200. package/dist/matter/delivery-options/delivery-options-controller.d.ts +69 -9
  201. package/dist/matter/delivery-options/delivery-options-controller.js +80 -14
  202. package/dist/matter/delivery-options/delivery-options-controller.js.map +1 -1
  203. package/dist/matter/delivery-options/delivery-options.d.ts +37 -2
  204. package/dist/matter/delivery-options/delivery-options.js +75 -12
  205. package/dist/matter/delivery-options/delivery-options.js.map +1 -1
  206. package/dist/matter/delivery-options/use-delivery-option-selection.d.ts +1 -0
  207. package/dist/matter/delivery-options/use-delivery-option-selection.js +1 -1
  208. package/dist/matter/delivery-options/use-delivery-option-selection.js.map +1 -1
  209. package/dist/matter/email-identification/email-identification.d.ts +25 -3
  210. package/dist/matter/email-identification/email-identification.js +49 -12
  211. package/dist/matter/email-identification/email-identification.js.map +1 -1
  212. package/dist/matter/free-shipping-progress/free-shipping-progress.d.ts +79 -7
  213. package/dist/matter/free-shipping-progress/free-shipping-progress.js +77 -20
  214. package/dist/matter/free-shipping-progress/free-shipping-progress.js.map +1 -1
  215. package/dist/matter/identity-edit/identity-edit.d.ts +52 -10
  216. package/dist/matter/identity-edit/identity-edit.js +47 -19
  217. package/dist/matter/identity-edit/identity-edit.js.map +1 -1
  218. package/dist/matter/identity-summary/identity-summary.d.ts +76 -22
  219. package/dist/matter/identity-summary/identity-summary.js +95 -45
  220. package/dist/matter/identity-summary/identity-summary.js.map +1 -1
  221. package/dist/matter/installments/installments-select.d.ts +98 -16
  222. package/dist/matter/installments/installments-select.js +156 -48
  223. package/dist/matter/installments/installments-select.js.map +1 -1
  224. package/dist/matter/installments/installments.d.ts +96 -16
  225. package/dist/matter/installments/installments.js +115 -40
  226. package/dist/matter/installments/installments.js.map +1 -1
  227. package/dist/matter/installments/use-installments-selection.d.ts +1 -0
  228. package/dist/matter/installments/use-installments-selection.js +8 -3
  229. package/dist/matter/installments/use-installments-selection.js.map +1 -1
  230. package/dist/matter/my-account-profile/my-account-profile.d.ts +358 -103
  231. package/dist/matter/my-account-profile/my-account-profile.js +370 -174
  232. package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
  233. package/dist/matter/order-cancel/order-cancel.d.ts +65 -19
  234. package/dist/matter/order-cancel/order-cancel.js +118 -51
  235. package/dist/matter/order-cancel/order-cancel.js.map +1 -1
  236. package/dist/matter/order-delivery-address/order-delivery-address.d.ts +8 -18
  237. package/dist/matter/order-delivery-address/order-delivery-address.js +14 -23
  238. package/dist/matter/order-delivery-address/order-delivery-address.js.map +1 -1
  239. package/dist/matter/order-delivery-group/order-delivery-group.d.ts +40 -24
  240. package/dist/matter/order-delivery-group/order-delivery-group.js +64 -74
  241. package/dist/matter/order-delivery-group/order-delivery-group.js.map +1 -1
  242. package/dist/matter/order-details-header/order-details-header.d.ts +32 -15
  243. package/dist/matter/order-details-header/order-details-header.js +66 -58
  244. package/dist/matter/order-details-header/order-details-header.js.map +1 -1
  245. package/dist/matter/order-invoice-download/order-invoice-download.d.ts +78 -0
  246. package/dist/matter/order-invoice-download/order-invoice-download.js +173 -0
  247. package/dist/matter/order-invoice-download/order-invoice-download.js.map +1 -0
  248. package/dist/matter/order-list/order-list.d.ts +164 -19
  249. package/dist/matter/order-list/order-list.js +229 -87
  250. package/dist/matter/order-list/order-list.js.map +1 -1
  251. package/dist/matter/order-list-row/order-list-row.d.ts +6 -1
  252. package/dist/matter/order-list-row/order-list-row.js +3 -2
  253. package/dist/matter/order-list-row/order-list-row.js.map +1 -1
  254. package/dist/matter/order-list-search/order-list-search.d.ts +18 -26
  255. package/dist/matter/order-list-search/order-list-search.js +30 -69
  256. package/dist/matter/order-list-search/order-list-search.js.map +1 -1
  257. package/dist/matter/order-list-views/order-list-views.d.ts +15 -22
  258. package/dist/matter/order-list-views/order-list-views.js +22 -68
  259. package/dist/matter/order-list-views/order-list-views.js.map +1 -1
  260. package/dist/matter/order-payment/order-payment.d.ts +33 -10
  261. package/dist/matter/order-payment/order-payment.js +44 -39
  262. package/dist/matter/order-payment/order-payment.js.map +1 -1
  263. package/dist/matter/order-placed/order-placed-deliveries.d.ts +144 -12
  264. package/dist/matter/order-placed/order-placed-deliveries.js +115 -40
  265. package/dist/matter/order-placed/order-placed-deliveries.js.map +1 -1
  266. package/dist/matter/order-placed/order-placed.d.ts +198 -31
  267. package/dist/matter/order-placed/order-placed.js +230 -139
  268. package/dist/matter/order-placed/order-placed.js.map +1 -1
  269. package/dist/matter/order-placed-date/order-placed-date.d.ts +5 -8
  270. package/dist/matter/order-placed-date/order-placed-date.js +7 -8
  271. package/dist/matter/order-placed-date/order-placed-date.js.map +1 -1
  272. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +90 -17
  273. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +93 -117
  274. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -1
  275. package/dist/matter/order-progress/order-progress.d.ts +19 -8
  276. package/dist/matter/order-progress/order-progress.js +37 -45
  277. package/dist/matter/order-progress/order-progress.js.map +1 -1
  278. package/dist/matter/order-summary/order-summary.d.ts +19 -9
  279. package/dist/matter/order-summary/order-summary.js +38 -44
  280. package/dist/matter/order-summary/order-summary.js.map +1 -1
  281. package/dist/matter/order-tracking-link/order-tracking-link.d.ts +43 -0
  282. package/dist/matter/order-tracking-link/order-tracking-link.js +107 -0
  283. package/dist/matter/order-tracking-link/order-tracking-link.js.map +1 -0
  284. package/dist/matter/payment/payment-card-capture.js +17 -10
  285. package/dist/matter/payment/payment-card-capture.js.map +1 -1
  286. package/dist/matter/payment/payment-controller.d.ts +4 -2
  287. package/dist/matter/payment/payment-controller.js +30 -9
  288. package/dist/matter/payment/payment-controller.js.map +1 -1
  289. package/dist/matter/payment/payment-method-badges.js +1 -1
  290. package/dist/matter/payment/payment.d.ts +69 -19
  291. package/dist/matter/payment/payment.js +101 -35
  292. package/dist/matter/payment/payment.js.map +1 -1
  293. package/dist/matter/payment/use-payment-method-selection.d.ts +25 -4
  294. package/dist/matter/payment/use-payment-method-selection.js +94 -32
  295. package/dist/matter/payment/use-payment-method-selection.js.map +1 -1
  296. package/dist/matter/payment/use-payment-submit-gate.d.ts +14 -3
  297. package/dist/matter/payment/use-payment-submit-gate.js +47 -8
  298. package/dist/matter/payment/use-payment-submit-gate.js.map +1 -1
  299. package/dist/matter/place-order/place-order.d.ts +171 -57
  300. package/dist/matter/place-order/place-order.js +376 -185
  301. package/dist/matter/place-order/place-order.js.map +1 -1
  302. package/dist/matter/pricing/pricing.d.ts +152 -13
  303. package/dist/matter/pricing/pricing.js +153 -52
  304. package/dist/matter/pricing/pricing.js.map +1 -1
  305. package/dist/matter/product-information/product-information.d.ts +27 -4
  306. package/dist/matter/product-information/product-information.js +32 -15
  307. package/dist/matter/product-information/product-information.js.map +1 -1
  308. package/dist/matter/product-list/product-list.d.ts +24 -6
  309. package/dist/matter/product-list/product-list.js +32 -26
  310. package/dist/matter/product-list/product-list.js.map +1 -1
  311. package/dist/matter/product-shelf/product-shelf.d.ts +1 -3
  312. package/dist/matter/product-shelf/product-shelf.js +2 -15
  313. package/dist/matter/product-shelf/product-shelf.js.map +1 -1
  314. package/dist/matter/region/region.d.ts +33 -16
  315. package/dist/matter/region/region.js +59 -52
  316. package/dist/matter/region/region.js.map +1 -1
  317. package/dist/matter/save-card/save-card.d.ts +43 -8
  318. package/dist/matter/save-card/save-card.js +61 -23
  319. package/dist/matter/save-card/save-card.js.map +1 -1
  320. package/dist/matter/saved-cards/saved-cards.d.ts +113 -21
  321. package/dist/matter/saved-cards/saved-cards.js +349 -77
  322. package/dist/matter/saved-cards/saved-cards.js.map +1 -1
  323. package/dist/matter/shipping-address/address-payload.js +25 -0
  324. package/dist/matter/shipping-address/address-payload.js.map +1 -0
  325. package/dist/matter/shipping-address/country-descriptors.js +71 -71
  326. package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
  327. package/dist/matter/shipping-address/library-country-descriptors.js +136 -0
  328. package/dist/matter/shipping-address/library-country-descriptors.js.map +1 -0
  329. package/dist/matter/shipping-address/shipping-address.d.ts +312 -45
  330. package/dist/matter/shipping-address/shipping-address.js +443 -188
  331. package/dist/matter/shipping-address/shipping-address.js.map +1 -1
  332. package/dist/matter/shipping-simulator/shipping-simulator.d.ts +250 -24
  333. package/dist/matter/shipping-simulator/shipping-simulator.js +257 -51
  334. package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
  335. package/dist/matter/sign-in/sign-in.d.ts +108 -0
  336. package/dist/matter/sign-in/sign-in.js +285 -0
  337. package/dist/matter/sign-in/sign-in.js.map +1 -0
  338. package/dist/matter/sign-in-disclosure/sign-in-disclosure.d.ts +81 -0
  339. package/dist/matter/sign-in-disclosure/sign-in-disclosure.js +232 -0
  340. package/dist/matter/sign-in-disclosure/sign-in-disclosure.js.map +1 -0
  341. package/dist/matter/sign-in-form/sign-in-form.d.ts +93 -62
  342. package/dist/matter/sign-in-form/sign-in-form.js +134 -144
  343. package/dist/matter/sign-in-form/sign-in-form.js.map +1 -1
  344. package/dist/particles/binding-switcher/binding-switcher.d.ts +29 -8
  345. package/dist/particles/binding-switcher/binding-switcher.js +37 -26
  346. package/dist/particles/binding-switcher/binding-switcher.js.map +1 -1
  347. package/dist/particles/card-summary/card-summary.d.ts +38 -8
  348. package/dist/particles/card-summary/card-summary.js +55 -22
  349. package/dist/particles/card-summary/card-summary.js.map +1 -1
  350. package/dist/particles/carousel/carousel.d.ts +99 -11
  351. package/dist/particles/carousel/carousel.js +103 -35
  352. package/dist/particles/carousel/carousel.js.map +1 -1
  353. package/dist/particles/copy-text/copy-text.js +5 -15
  354. package/dist/particles/copy-text/copy-text.js.map +1 -1
  355. package/dist/particles/dialog/dialog.d.ts +14 -6
  356. package/dist/particles/dialog/dialog.js +6 -3
  357. package/dist/particles/dialog/dialog.js.map +1 -1
  358. package/dist/particles/drawer/drawer.js +1 -1
  359. package/dist/particles/drawer/drawer.js.map +1 -1
  360. package/dist/particles/email-field/email-field.d.ts +2 -1
  361. package/dist/particles/email-field/email-field.js +2 -1
  362. package/dist/particles/email-field/email-field.js.map +1 -1
  363. package/dist/particles/footer-link-group/footer-link-group.js +1 -0
  364. package/dist/particles/footer-link-group/footer-link-group.js.map +1 -1
  365. package/dist/particles/form/form.d.ts +85 -13
  366. package/dist/particles/form/form.js +91 -42
  367. package/dist/particles/form/form.js.map +1 -1
  368. package/dist/particles/gallery/gallery.d.ts +129 -12
  369. package/dist/particles/gallery/gallery.js +121 -34
  370. package/dist/particles/gallery/gallery.js.map +1 -1
  371. package/dist/particles/order-details-back/order-details-back.d.ts +5 -3
  372. package/dist/particles/order-details-back/order-details-back.js +8 -10
  373. package/dist/particles/order-details-back/order-details-back.js.map +1 -1
  374. package/dist/particles/order-status-badge/order-status-badge.d.ts +3 -3
  375. package/dist/particles/order-status-badge/order-status-badge.js +4 -20
  376. package/dist/particles/order-status-badge/order-status-badge.js.map +1 -1
  377. package/dist/particles/password-field/password-field.d.ts +1 -1
  378. package/dist/particles/password-field/password-field.js +2 -4
  379. package/dist/particles/password-field/password-field.js.map +1 -1
  380. package/dist/particles/phone-field/phone-field.d.ts +4 -4
  381. package/dist/particles/phone-field/phone-field.js +8 -11
  382. package/dist/particles/phone-field/phone-field.js.map +1 -1
  383. package/dist/particles/spinner/spinner.d.ts +24 -4
  384. package/dist/particles/spinner/spinner.js +25 -9
  385. package/dist/particles/spinner/spinner.js.map +1 -1
  386. package/dist/particles/switch/switch.d.ts +4 -1
  387. package/dist/particles/switch/switch.js +3 -1
  388. package/dist/particles/switch/switch.js.map +1 -1
  389. package/dist/particles/toast/toast.d.ts +29 -3
  390. package/dist/particles/toast/toast.js +34 -10
  391. package/dist/particles/toast/toast.js.map +1 -1
  392. package/dist/schema.graphql +164 -56
  393. package/dist/sdk/address/address-fields.js +4 -4
  394. package/dist/sdk/address/address-fields.js.map +1 -1
  395. package/dist/sdk/address/constants.js +1 -6
  396. package/dist/sdk/address/constants.js.map +1 -1
  397. package/dist/sdk/address/messages.js +44 -44
  398. package/dist/sdk/address/messages.js.map +1 -1
  399. package/dist/sdk/address/postal-code.js +15 -2
  400. package/dist/sdk/address/postal-code.js.map +1 -1
  401. package/dist/sdk/address/types.d.ts +5 -3
  402. package/dist/sdk/address/validate-address.js +3 -3
  403. package/dist/sdk/address/validate-address.js.map +1 -1
  404. package/dist/sdk/analytics/use-analytics-actions.d.ts +1 -1
  405. package/dist/sdk/analytics/use-analytics-actions.js.map +1 -1
  406. package/dist/sdk/cart/use-cart-actions.js.map +1 -1
  407. package/dist/sdk/data/index.js +2 -2
  408. package/dist/sdk/data/reactive-list-item-data-provider.js +2 -2
  409. package/dist/sdk/generated/gql.js +1 -1
  410. package/dist/sdk/generated/gql.js.map +1 -1
  411. package/dist/sdk/generated/graphql.d.ts +20 -5
  412. package/dist/sdk/generated/graphql.js +6 -0
  413. package/dist/sdk/generated/graphql.js.map +1 -1
  414. package/dist/sdk/i18n/format-error-code.js +23 -0
  415. package/dist/sdk/i18n/format-error-code.js.map +1 -0
  416. package/dist/sdk/i18n/message-formatter.d.ts +22 -0
  417. package/dist/sdk/i18n/message-formatter.js +140 -0
  418. package/dist/sdk/i18n/message-formatter.js.map +1 -0
  419. package/dist/sdk/i18n/use-message-formatter.d.ts +8 -0
  420. package/dist/sdk/i18n/use-message-formatter.js +18 -0
  421. package/dist/sdk/i18n/use-message-formatter.js.map +1 -0
  422. package/dist/sdk/identity/use-identity-actions.js.map +1 -1
  423. package/dist/sdk/navigation/redirect.d.ts +2 -0
  424. package/dist/sdk/navigation/redirect.js +19 -1
  425. package/dist/sdk/navigation/redirect.js.map +1 -1
  426. package/dist/sdk/navigation/types.d.ts +13 -0
  427. package/dist/sdk/payment/bin-format.js +21 -0
  428. package/dist/sdk/payment/bin-format.js.map +1 -0
  429. package/dist/sdk/payment/challenge-return.js +37 -0
  430. package/dist/sdk/payment/challenge-return.js.map +1 -0
  431. package/dist/sdk/payment/confirmation-last-four.js +39 -0
  432. package/dist/sdk/payment/confirmation-last-four.js.map +1 -0
  433. package/dist/sdk/payment/gateway.d.ts +1 -1
  434. package/dist/sdk/payment/gateway.js +8 -2
  435. package/dist/sdk/payment/gateway.js.map +1 -1
  436. package/dist/sdk/payment/operations.d.ts +1 -0
  437. package/dist/sdk/payment/operations.js +28 -6
  438. package/dist/sdk/payment/operations.js.map +1 -1
  439. package/dist/sdk/payment/resumed-purchase.d.ts +1 -0
  440. package/dist/sdk/payment/resumed-purchase.js +119 -0
  441. package/dist/sdk/payment/resumed-purchase.js.map +1 -0
  442. package/dist/sdk/payment/security-code-mask.js +16 -23
  443. package/dist/sdk/payment/security-code-mask.js.map +1 -1
  444. package/dist/sdk/payment/submission-registry.js +6 -2
  445. package/dist/sdk/payment/submission-registry.js.map +1 -1
  446. package/dist/sdk/payment/use-payment-actions.d.ts +18 -3
  447. package/dist/sdk/payment/use-payment-actions.js +23 -4
  448. package/dist/sdk/payment/use-payment-actions.js.map +1 -1
  449. package/dist/sdk/payment/use-place-order.d.ts +10 -2
  450. package/dist/sdk/payment/use-place-order.js +138 -76
  451. package/dist/sdk/payment/use-place-order.js.map +1 -1
  452. package/dist/sdk/provider/storefront-provider.d.ts +1 -7
  453. package/dist/sdk/provider/storefront-provider.js +4 -11
  454. package/dist/sdk/provider/storefront-provider.js.map +1 -1
  455. package/dist/sdk/seo/entity-landing.d.ts +24 -0
  456. package/dist/sdk/seo/entity-landing.js +30 -0
  457. package/dist/sdk/seo/entity-landing.js.map +1 -0
  458. package/dist/sdk/seo/page-alternates.d.ts +3 -2
  459. package/dist/sdk/seo/page-alternates.js +4 -2
  460. package/dist/sdk/seo/page-alternates.js.map +1 -1
  461. package/dist/sdk/seo/search-landing.d.ts +42 -0
  462. package/dist/sdk/seo/search-landing.js +85 -0
  463. package/dist/sdk/seo/search-landing.js.map +1 -0
  464. package/dist/sdk/session/mock-session.d.ts +9 -0
  465. package/dist/sdk/session/mock-session.js +31 -0
  466. package/dist/sdk/session/mock-session.js.map +1 -0
  467. package/dist/sdk/session/read-server-session.d.ts +3 -0
  468. package/dist/sdk/session/read-server-session.js +5 -0
  469. package/dist/sdk/session/read-server-session.js.map +1 -1
  470. package/dist/sdk/session/return-path-segment.d.ts +10 -0
  471. package/dist/sdk/session/return-path-segment.js +15 -0
  472. package/dist/sdk/session/return-path-segment.js.map +1 -0
  473. package/dist/sdk/session/use-session-actions.js.map +1 -1
  474. package/dist/sdk/shipping/use-cart-shipping-simulation.js.map +1 -1
  475. package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
  476. package/dist/sdk/shipping/use-shipping-simulation.js.map +1 -1
  477. package/dist/utils/safe-href.d.ts +2 -0
  478. package/package.json +4 -3
  479. package/skills/storefront-asset/SKILL.md +2 -2
  480. package/skills/storefront-asset/references/api-side.md +27 -14
  481. package/skills/storefront-asset/references/findings.md +5 -2
  482. package/src/styles/tailwind.css +8 -0
  483. package/dist/matter/shipping-address/country-descriptors.d.ts +0 -1
  484. package/dist/message-locales.json +0 -7
  485. package/dist/sdk/i18n/client.d.ts +0 -4
  486. package/dist/sdk/i18n/client.js +0 -4
  487. package/dist/sdk/i18n/locales.d.ts +0 -8
  488. package/dist/sdk/i18n/locales.js +0 -57
  489. package/dist/sdk/i18n/locales.js.map +0 -1
  490. package/dist/sdk/i18n/messages/en-US.js +0 -183
  491. package/dist/sdk/i18n/messages/en-US.js.map +0 -1
  492. package/dist/sdk/i18n/messages/en-US.json +0 -178
  493. package/dist/sdk/i18n/messages/en-US.json.d.ts +0 -182
  494. package/dist/sdk/i18n/messages/es-ES.js +0 -183
  495. package/dist/sdk/i18n/messages/es-ES.js.map +0 -1
  496. package/dist/sdk/i18n/messages/es-ES.json +0 -178
  497. package/dist/sdk/i18n/messages/pt-BR.js +0 -183
  498. package/dist/sdk/i18n/messages/pt-BR.js.map +0 -1
  499. package/dist/sdk/i18n/messages/pt-BR.json +0 -178
  500. package/dist/sdk/i18n/provider.d.ts +0 -1
  501. package/dist/sdk/i18n/provider.js +0 -30
  502. package/dist/sdk/i18n/provider.js.map +0 -1
  503. package/dist/sdk/i18n/server.d.ts +0 -28
  504. package/dist/sdk/i18n/server.js +0 -59
  505. package/dist/sdk/i18n/server.js.map +0 -1
  506. package/dist/utils/resolve-authored-copy.js +0 -18
  507. package/dist/utils/resolve-authored-copy.js.map +0 -1
  508. package/dist/utils/substitute-template.js +0 -33
  509. 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 { resolveAuthoredCopy } from "../../utils/resolve-authored-copy.js";
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 four authorable copy props —
81
- * the two surfaces cannot word a tier differently.
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, installmentTemplate, interestFreeLabel, interestTemplate }) {
172
+ function installmentLabel({ tier, locale, messages }) {
84
173
  const formattedValue = formatInstallmentValue(tier.value.amount, locale, tier.value.currencyCode);
85
- const base = substituteTemplate(installmentTemplate, {
86
- count: String(tier.count),
174
+ const base = messages.installment({
175
+ count: tier.count,
87
176
  value: formattedValue
88
177
  });
89
- const interestSuffix = tier.interestRate === 0 ? interestFreeLabel : substituteTemplate(interestTemplate, { rate: formatInterestRate(tier.interestRate, locale) });
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
- installmentTemplate: {
106
- type: "string",
107
- default: "{count}x of {value}",
108
- label: "Installment template"
109
- },
110
- interestFreeLabel: {
111
- type: "string",
112
- default: "interest-free",
113
- label: "Interest-free label"
114
- },
115
- interestTemplate: {
116
- type: "string",
117
- default: "{rate}% interest",
118
- label: "Interest template"
119
- },
120
- legend: {
121
- type: "string",
122
- default: "Installments",
123
- label: "Legend"
124
- }
125
- },
126
- guideline: "Installment selector for checkout: shows count, per-installment value, and interest for the card currently being entered (matched by BIN) or the saved card currently selected (matched by the persisted BIN, falling back to method), rendering exactly the tiers the matched option returns (the API guarantees a 1x tier is present in every option, so one is always shown and selectable — see specs/installments.md). Must be inside a PaymentSectionRoot or PaymentSectionController — place it as a sibling of CardEntry and SavedCards, never nested inside either, so one instance serves both entry paths. Picking a row dispatches $actions.payment.setInstallments and the order total updates elsewhere from the normalized cache with no wiring here. Renders its own rows from real data — no children."
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({ installmentTemplate = "{count}x of {value}", interestFreeLabel = "interest-free", interestTemplate = "{rate}% interest", legend, $data, $actions: $actionsProp, className, ...props }) {
199
+ function Installments({ $content, $locale, $data, $actions: $actionsProp, className, ...props }) {
129
200
  const $actions = useActionsOverride($actionsProp);
130
201
  const { locale } = useStorefrontContext();
131
- const { tiers, selectedCount, state, isPending, selectionError, selectInstallment, placing, cardPaymentCollapsed } = useInstallmentsSelection({
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: resolveAuthoredCopy(legend, "Installments") }), /* @__PURE__ */ jsx(RadioGroup, {
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
- installmentTemplate,
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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"}
@@ -1,3 +1,4 @@
1
+ import "../../sdk/payment/operations.js";
1
2
  import "../../sdk/actions/types.js";
2
3
  import "./installments-select.js";
3
4
  import { InstallmentOption, InstallmentTier } from "./installments.js";
@@ -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 tiers = resolveInstallmentTiers(findInstallmentOption($data?.installmentOptions ?? EMPTY_OPTIONS, {
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.message);
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"}