@vtex/faststore 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/actions.json +181 -0
  3. package/dist/catalog.json +1595 -481
  4. package/dist/client.d.ts +2 -1
  5. package/dist/client.js +2 -1
  6. package/dist/components.d.ts +31 -7
  7. package/dist/components.js +35 -13
  8. package/dist/content-types.json +73 -0
  9. package/dist/docs/access-code-sign-in.md +1 -1
  10. package/dist/docs/account-access-gate.md +153 -0
  11. package/dist/docs/account-entry-point.md +1 -2
  12. package/dist/docs/account-menu.md +164 -0
  13. package/dist/docs/address-summary.md +24 -3
  14. package/dist/docs/alert-dialog.md +204 -0
  15. package/dist/docs/billing-address.md +284 -0
  16. package/dist/docs/buy-button.md +1 -1
  17. package/dist/docs/card-entry.md +250 -0
  18. package/dist/docs/cardholder-document.md +142 -0
  19. package/dist/docs/cart.md +1 -1
  20. package/dist/docs/client-preferences-fieldset.md +148 -0
  21. package/dist/docs/client-preferences.md +2 -1
  22. package/dist/docs/client-profile-fieldset.md +184 -0
  23. package/dist/docs/client-profile.md +1 -1
  24. package/dist/docs/document-field.md +58 -4
  25. package/dist/docs/field.md +317 -0
  26. package/dist/docs/form.md +307 -0
  27. package/dist/docs/identity-edit.md +6 -6
  28. package/dist/docs/identity-summary.md +5 -5
  29. package/dist/docs/installments.md +248 -0
  30. package/dist/docs/my-account-profile.md +1 -0
  31. package/dist/docs/order-placed-payment-summary.md +138 -0
  32. package/dist/docs/payment-method-badges.md +232 -0
  33. package/dist/docs/payment-methods.md +1 -0
  34. package/dist/docs/payment.md +218 -0
  35. package/dist/docs/phone-field.md +58 -4
  36. package/dist/docs/place-order.md +157 -0
  37. package/dist/docs/save-card.md +186 -0
  38. package/dist/docs/saved-cards.md +218 -0
  39. package/dist/docs/select.md +465 -0
  40. package/dist/docs/session.md +1 -1
  41. package/dist/docs/shipping-address.md +110 -258
  42. package/dist/docs/sign-in-redirect.md +1 -1
  43. package/dist/index.d.ts +4 -1
  44. package/dist/internal/client.d.ts +3 -1
  45. package/dist/internal/client.js +3 -1
  46. package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
  47. package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
  48. package/dist/matter/account-access-gate/account-access-gate.d.ts +86 -0
  49. package/dist/matter/account-access-gate/account-access-gate.js +283 -0
  50. package/dist/matter/account-access-gate/account-access-gate.js.map +1 -0
  51. package/dist/matter/account-access-gate/sign-out-intent.d.ts +17 -0
  52. package/dist/matter/account-access-gate/sign-out-intent.js +62 -0
  53. package/dist/matter/account-access-gate/sign-out-intent.js.map +1 -0
  54. package/dist/matter/account-entry-point/account-entry-point.js +2 -2
  55. package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
  56. package/dist/matter/account-menu/account-menu.d.ts +88 -0
  57. package/dist/matter/account-menu/account-menu.js +246 -0
  58. package/dist/matter/account-menu/account-menu.js.map +1 -0
  59. package/dist/matter/billing-address/billing-address.d.ts +165 -0
  60. package/dist/matter/billing-address/billing-address.js +408 -0
  61. package/dist/matter/billing-address/billing-address.js.map +1 -0
  62. package/dist/matter/card-entry/card-capture.d.ts +25 -0
  63. package/dist/matter/card-entry/card-capture.js +25 -0
  64. package/dist/matter/card-entry/card-capture.js.map +1 -0
  65. package/dist/matter/card-entry/card-entry.d.ts +120 -0
  66. package/dist/matter/card-entry/card-entry.js +832 -0
  67. package/dist/matter/card-entry/card-entry.js.map +1 -0
  68. package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
  69. package/dist/matter/cardholder-document/cardholder-document.js +70 -0
  70. package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
  71. package/dist/matter/cart/cart.d.ts +1 -1
  72. package/dist/matter/cart/cart.js +2 -2
  73. package/dist/matter/cart/cart.js.map +1 -1
  74. package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
  75. package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
  76. package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
  77. package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
  78. package/dist/matter/client-preferences/client-preferences.js +6 -6
  79. package/dist/matter/client-preferences/client-preferences.js.map +1 -1
  80. package/dist/matter/client-profile/client-profile.d.ts +4 -6
  81. package/dist/matter/client-profile/client-profile.js +9 -6
  82. package/dist/matter/client-profile/client-profile.js.map +1 -1
  83. package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
  84. package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
  85. package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
  86. package/dist/matter/delivery-options/delivery-options.js.map +1 -1
  87. package/dist/matter/identity-edit/identity-edit.js +2 -2
  88. package/dist/matter/identity-edit/identity-edit.js.map +1 -1
  89. package/dist/matter/identity-summary/identity-summary.js +2 -2
  90. package/dist/matter/identity-summary/identity-summary.js.map +1 -1
  91. package/dist/matter/installments/installments-select.d.ts +42 -0
  92. package/dist/matter/installments/installments-select.js +167 -0
  93. package/dist/matter/installments/installments-select.js.map +1 -0
  94. package/dist/matter/installments/installments.d.ts +58 -0
  95. package/dist/matter/installments/installments.js +176 -0
  96. package/dist/matter/installments/installments.js.map +1 -0
  97. package/dist/matter/installments/use-installments-selection.d.ts +29 -0
  98. package/dist/matter/installments/use-installments-selection.js +95 -0
  99. package/dist/matter/installments/use-installments-selection.js.map +1 -0
  100. package/dist/matter/my-account-profile/my-account-profile.js +5 -5
  101. package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
  102. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
  103. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
  104. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
  105. package/dist/matter/payment/payment-card-capture.d.ts +19 -0
  106. package/dist/matter/payment/payment-card-capture.js +129 -0
  107. package/dist/matter/payment/payment-card-capture.js.map +1 -0
  108. package/dist/matter/payment/payment-controller.d.ts +24 -0
  109. package/dist/matter/payment/payment-controller.js +107 -0
  110. package/dist/matter/payment/payment-controller.js.map +1 -0
  111. package/dist/matter/payment/payment-method-badges.d.ts +57 -0
  112. package/dist/matter/payment/payment-method-badges.js +192 -0
  113. package/dist/matter/payment/payment-method-badges.js.map +1 -0
  114. package/dist/matter/payment/payment.d.ts +89 -0
  115. package/dist/matter/payment/payment.js +263 -0
  116. package/dist/matter/payment/payment.js.map +1 -0
  117. package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
  118. package/dist/matter/payment/use-payment-method-selection.js +458 -0
  119. package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
  120. package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
  121. package/dist/matter/payment/use-payment-submit-gate.js +144 -0
  122. package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
  123. package/dist/matter/place-order/place-order.d.ts +74 -0
  124. package/dist/matter/place-order/place-order.js +379 -0
  125. package/dist/matter/place-order/place-order.js.map +1 -0
  126. package/dist/matter/product-list/product-list.d.ts +1 -0
  127. package/dist/matter/product-list/product-list.js.map +1 -1
  128. package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
  129. package/dist/matter/product-shelf/product-shelf.js +2 -0
  130. package/dist/matter/product-shelf/product-shelf.js.map +1 -1
  131. package/dist/matter/region/region.js +1 -1
  132. package/dist/matter/region/region.js.map +1 -1
  133. package/dist/matter/save-card/save-card.d.ts +49 -0
  134. package/dist/matter/save-card/save-card.js +188 -0
  135. package/dist/matter/save-card/save-card.js.map +1 -0
  136. package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
  137. package/dist/matter/saved-cards/saved-cards.js +387 -0
  138. package/dist/matter/saved-cards/saved-cards.js.map +1 -0
  139. package/dist/matter/shipping-address/country-descriptors.js +20 -10
  140. package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
  141. package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
  142. package/dist/matter/shipping-address/shipping-address.js +481 -792
  143. package/dist/matter/shipping-address/shipping-address.js.map +1 -1
  144. package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
  145. package/dist/matter/variant-selector/variant-selector.js +1 -1
  146. package/dist/particles/address-summary/address-summary.d.ts +24 -0
  147. package/dist/particles/address-summary/address-summary.js +83 -2
  148. package/dist/particles/address-summary/address-summary.js.map +1 -1
  149. package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
  150. package/dist/particles/alert-dialog/alert-dialog.js +187 -0
  151. package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
  152. package/dist/particles/checkbox/checkbox.d.ts +7 -3
  153. package/dist/particles/checkbox/checkbox.js +4 -1
  154. package/dist/particles/checkbox/checkbox.js.map +1 -1
  155. package/dist/particles/document-field/document-field.d.ts +48 -5
  156. package/dist/particles/document-field/document-field.js +189 -32
  157. package/dist/particles/document-field/document-field.js.map +1 -1
  158. package/dist/particles/field/bound-field-registry.js +52 -0
  159. package/dist/particles/field/bound-field-registry.js.map +1 -0
  160. package/dist/particles/field/field.d.ts +116 -0
  161. package/dist/particles/field/field.js +497 -6
  162. package/dist/particles/field/field.js.map +1 -1
  163. package/dist/particles/fieldset/fieldset.js +2 -2
  164. package/dist/particles/fieldset/fieldset.js.map +1 -1
  165. package/dist/particles/footer-social/footer-social.js +1 -1
  166. package/dist/particles/form/form.d.ts +104 -0
  167. package/dist/particles/form/form.js +485 -0
  168. package/dist/particles/form/form.js.map +1 -0
  169. package/dist/particles/incentives/incentives.js +1 -1
  170. package/dist/particles/phone-field/phone-field.d.ts +38 -10
  171. package/dist/particles/phone-field/phone-field.js +183 -39
  172. package/dist/particles/phone-field/phone-field.js.map +1 -1
  173. package/dist/particles/postal-code-field/postal-code-field.js +2 -2
  174. package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
  175. package/dist/particles/select/field-select.d.ts +43 -0
  176. package/dist/particles/select/field-select.js +150 -0
  177. package/dist/particles/select/field-select.js.map +1 -0
  178. package/dist/particles/select/select.d.ts +2 -0
  179. package/dist/particles/select/select.js.map +1 -1
  180. package/dist/particles/shared/compose-refs.js +21 -0
  181. package/dist/particles/shared/compose-refs.js.map +1 -0
  182. package/dist/schema.graphql +701 -2
  183. package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
  184. package/dist/sdk/actions/analytics-action-provider.js +19 -0
  185. package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
  186. package/dist/sdk/actions/cart-action-provider.d.ts +1 -1
  187. package/dist/sdk/actions/cart-action-provider.js +1 -1
  188. package/dist/sdk/actions/cart-action-provider.js.map +1 -1
  189. package/dist/sdk/actions/index.d.ts +3 -1
  190. package/dist/sdk/actions/index.js +3 -1
  191. package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
  192. package/dist/sdk/actions/payment-action-provider.js +26 -0
  193. package/dist/sdk/actions/payment-action-provider.js.map +1 -0
  194. package/dist/sdk/actions/session-action-provider.d.ts +1 -1
  195. package/dist/sdk/actions/session-action-provider.js +1 -1
  196. package/dist/sdk/actions/session-action-provider.js.map +1 -1
  197. package/dist/sdk/actions/shipping-action-provider.d.ts +1 -1
  198. package/dist/sdk/actions/shipping-action-provider.js +1 -1
  199. package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
  200. package/dist/sdk/actions/types.d.ts +6 -2
  201. package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
  202. package/dist/sdk/analytics/analytics-destination.js +90 -0
  203. package/dist/sdk/analytics/analytics-destination.js.map +1 -0
  204. package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
  205. package/dist/sdk/analytics/use-analytics-actions.js +108 -0
  206. package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
  207. package/dist/sdk/cache/storefront-cache.js +1 -1
  208. package/dist/sdk/cache/storefront-cache.js.map +1 -1
  209. package/dist/sdk/cart/operations.d.ts +4 -4
  210. package/dist/sdk/cart/operations.js +1 -1
  211. package/dist/sdk/cart/operations.js.map +1 -1
  212. package/dist/sdk/cart/optimistic.js +1 -1
  213. package/dist/sdk/cart/optimistic.js.map +1 -1
  214. package/dist/sdk/cart/use-cart-actions.d.ts +1 -1
  215. package/dist/sdk/cart/use-cart-actions.js +4 -4
  216. package/dist/sdk/cart/use-cart-actions.js.map +1 -1
  217. package/dist/sdk/cart/use-cart-validate-on-open.js +1 -1
  218. package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
  219. package/dist/sdk/cart/write-gate.js +1 -1
  220. package/dist/sdk/cart/write-gate.js.map +1 -1
  221. package/dist/sdk/data/entity-resolution-context.js +31 -0
  222. package/dist/sdk/data/entity-resolution-context.js.map +1 -0
  223. package/dist/sdk/data/entity-subscriber.js +18 -8
  224. package/dist/sdk/data/entity-subscriber.js.map +1 -1
  225. package/dist/sdk/data/index.js +1 -0
  226. package/dist/sdk/data/page-reactive-data-root.d.ts +38 -2
  227. package/dist/sdk/data/page-reactive-data-root.js +4 -2
  228. package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
  229. package/dist/sdk/data/reactive-data-provider.d.ts +1 -1
  230. package/dist/sdk/data/reactive-data-provider.js +8 -4
  231. package/dist/sdk/data/reactive-data-provider.js.map +1 -1
  232. package/dist/sdk/data/reactive-list-item-data-provider.d.ts +1 -1
  233. package/dist/sdk/data/reactive-list-item-data-provider.js +13 -5
  234. package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
  235. package/dist/sdk/data/use-reactive-data-settled.js +39 -0
  236. package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
  237. package/dist/sdk/data/use-reactive-entity.js +6 -2
  238. package/dist/sdk/data/use-reactive-entity.js.map +1 -1
  239. package/dist/sdk/generated/fragment-types.js +2 -0
  240. package/dist/sdk/generated/fragment-types.js.map +1 -1
  241. package/dist/sdk/generated/gql.js +2 -1
  242. package/dist/sdk/generated/gql.js.map +1 -1
  243. package/dist/sdk/generated/graphql.js +85 -1
  244. package/dist/sdk/generated/graphql.js.map +1 -1
  245. package/dist/sdk/i18n/messages/en-US.js +3 -0
  246. package/dist/sdk/i18n/messages/en-US.json +3 -0
  247. package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
  248. package/dist/sdk/i18n/messages/es-ES.js +3 -0
  249. package/dist/sdk/i18n/messages/es-ES.json +3 -0
  250. package/dist/sdk/i18n/messages/pt-BR.js +3 -0
  251. package/dist/sdk/i18n/messages/pt-BR.json +3 -0
  252. package/dist/sdk/identity/operations.js.map +1 -1
  253. package/dist/sdk/identity/use-identity-actions.d.ts +1 -1
  254. package/dist/sdk/identity/use-identity-actions.js +3 -2
  255. package/dist/sdk/identity/use-identity-actions.js.map +1 -1
  256. package/dist/sdk/payment/gateway-origin.js +35 -0
  257. package/dist/sdk/payment/gateway-origin.js.map +1 -0
  258. package/dist/sdk/payment/gateway.d.ts +85 -0
  259. package/dist/sdk/payment/gateway.js +183 -0
  260. package/dist/sdk/payment/gateway.js.map +1 -0
  261. package/dist/sdk/payment/operations.d.ts +52 -0
  262. package/dist/sdk/payment/operations.js +223 -0
  263. package/dist/sdk/payment/operations.js.map +1 -0
  264. package/dist/sdk/payment/security-code-mask.js +71 -0
  265. package/dist/sdk/payment/security-code-mask.js.map +1 -0
  266. package/dist/sdk/payment/submission-registry.d.ts +33 -0
  267. package/dist/sdk/payment/submission-registry.js +58 -0
  268. package/dist/sdk/payment/submission-registry.js.map +1 -0
  269. package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
  270. package/dist/sdk/payment/use-payment-actions.js +276 -0
  271. package/dist/sdk/payment/use-payment-actions.js.map +1 -0
  272. package/dist/sdk/payment/use-place-order.d.ts +65 -0
  273. package/dist/sdk/payment/use-place-order.js +640 -0
  274. package/dist/sdk/payment/use-place-order.js.map +1 -0
  275. package/dist/sdk/session/auth-operations.d.ts +1 -1
  276. package/dist/sdk/session/auth-operations.js.map +1 -1
  277. package/dist/sdk/session/use-session-actions.d.ts +1 -1
  278. package/dist/sdk/session/use-session-actions.js.map +1 -1
  279. package/dist/sdk/session/use-session-revalidation.js +65 -0
  280. package/dist/sdk/session/use-session-revalidation.js.map +1 -0
  281. package/dist/sdk/shipping/option-select-operations.d.ts +1 -1
  282. package/dist/sdk/shipping/option-select-operations.js.map +1 -1
  283. package/dist/sdk/shipping/request-timeout.js +1 -1
  284. package/dist/sdk/shipping/request-timeout.js.map +1 -1
  285. package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
  286. package/dist/sdk/shipping/shipping-operations.js.map +1 -1
  287. package/dist/sdk/shipping/use-shipping-actions.d.ts +2 -2
  288. package/dist/sdk/shipping/use-shipping-actions.js +1 -1
  289. package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
  290. package/dist/sdk/state/cart.d.ts +3 -4
  291. package/dist/sdk/state/cart.js +6 -7
  292. package/dist/sdk/state/cart.js.map +1 -1
  293. package/dist/utils/loggable-error-name.js +49 -0
  294. package/dist/utils/loggable-error-name.js.map +1 -0
  295. package/dist/utils/resolve-authored-copy.js +18 -0
  296. package/dist/utils/resolve-authored-copy.js.map +1 -0
  297. package/dist/utils/safe-href.js +3 -3
  298. package/dist/utils/safe-href.js.map +1 -1
  299. package/dist/utils/substitute-template.js +33 -0
  300. package/dist/utils/substitute-template.js.map +1 -0
  301. package/package.json +2 -1
  302. package/src/styles/tailwind.css +34 -0
  303. package/dist/docs/client-profile-form.md +0 -192
  304. package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
  305. package/dist/matter/client-profile-form/client-profile-form.js +0 -662
  306. package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
@@ -0,0 +1,232 @@
1
+ # PaymentMethodBadges
2
+
3
+ **Category:** commerce
4
+ **Purpose:** A read-only strip of one badge per payment method the store currently accepts. Derived from the live order form on every render — never authored, never CMS content — so it reflects a method being enabled or disabled with no content edit and no deployment. Ships as **two** components sharing one render contract: `PaymentMethodBadges`, for the checkout payment step, requires a payment-section ancestor; `AcceptedPaymentMethodBadges`, for any other surface — the cart order summary above all — requires none, reading its own `Query.payment` fragment instead. See [Choose between the two](#choose-between-the-two).
5
+
6
+ ---
7
+
8
+ ## Choose between the two
9
+
10
+ | Surface | Component | Ancestor required |
11
+ |---|---|---|
12
+ | Checkout payment step, alongside the method picker and the card form | `PaymentMethodBadges` | `PaymentSectionRoot` or `PaymentSectionController` |
13
+ | Cart order summary, or any other surface outside a payment section | `AcceptedPaymentMethodBadges` | None — owns its own `Query.payment` fragment |
14
+
15
+ Both render the identical DOM (the same `data-fs-payment-method-badge*` hooks) from the identical, unfiltered method list, produced by one shared internal render path — so the two surfaces cannot drift apart, and a store styles either one from the same documented hooks (see [Styling](#styling)). Composing both on the same page is legal — they normalize onto the same `Payment` entity — but redundant; pick one per surface.
16
+
17
+ Neither is one of these three look-alikes:
18
+
19
+ - **`PaymentMethods`** (`particles/payment-methods/`) — the CMS-authored **footer** strip. Its set is authored content and never reacts to a method being enabled or disabled in the payment configuration; both badge components here read live order-form data and do.
20
+ - **`PaymentMethodList`** — the **selectable** radio picker. Both badge strips are read-only reassurance and dispatch no action.
21
+ - **`CardEntry`'s own detected-network mark** — a single mark (0 or 1), shown only once a network is recognized from typed digits, positioned inside the card-number field's own box and scoped to the card group only. It cannot be placed independently and cannot carry a wallet method.
22
+
23
+ ---
24
+
25
+ ## Visual cues (how to identify in an image)
26
+
27
+ The image is the source of truth for layout. A `PaymentMethodBadges` strip is present when you see:
28
+
29
+ - A **horizontal row of small payment-brand flags** (Visa, Mastercard, PayPal, Pix, …) **on the checkout payment step itself** — not in a footer band.
30
+ - Often a **short heading above or beside it**: *Aceitamos*, *We accept*, *Formas de pagamento*.
31
+ - The row sits alongside other payment controls — a card form, a method picker, an installment control — as reassurance, not as one of them: no radio dot, no checkbox, no focus ring on any flag.
32
+ - A flag whose brand has no glyph renders as **plain text naming the method** in the same row — never a blank box for that reason. A *mapped* glyph the store never declared under `icons.include` is a separate, unclosable failure mode — see [Store setup: icon declaration](#store-setup-icon-declaration).
33
+
34
+ If the same-looking row sits in a **footer band** instead, that is `PaymentMethods`, not this component — see [When NOT to use](#when-not-to-use).
35
+
36
+ The identical-looking row can also sit in a **cart order summary** — near the subtotal/total block, before checkout, not inside a payment step at all. That is `AcceptedPaymentMethodBadges`, not `PaymentMethodBadges` — see [Choose between the two](#choose-between-the-two).
37
+
38
+ ---
39
+
40
+ ## When to use
41
+
42
+ - The checkout payment step needs a non-interactive statement of every method the store currently accepts, wallets included, alongside the picker and the card form.
43
+ - One instance per payment step.
44
+ - **A cart order summary** needs the same statement, before the shopper ever reaches checkout → use `AcceptedPaymentMethodBadges` instead (same look, no payment-section ancestor required) — see [Choose between the two](#choose-between-the-two).
45
+
46
+ ## When NOT to use
47
+
48
+ - A **footer band** of accepted-brand flags, editable in the CMS → use `PaymentMethods`. Its set is authored content and does not react to a method being enabled or disabled in the payment configuration; this component reads real order-form data and does.
49
+ - The **card-number field's own detected-network mark** — a single icon (or name) for the network just recognized from typed digits, scoped to the card group → that is `CardEntry`'s own mark, built into the number field's own box. It cannot be placed independently and cannot carry a wallet method.
50
+ - A **selectable list** of payment methods, with radio controls → use `PaymentMethodList`. This strip dispatches no action and selects nothing.
51
+ - The shopper's **saved cards** → that is `SavedCards`, a different list with different data.
52
+ - **A cart order summary, or any surface with no payment section on the page** → use `AcceptedPaymentMethodBadges`. This component throws when rendered outside a `PaymentSectionRoot` / `PaymentSectionController`; it is not meant to be placed on a cart page.
53
+
54
+ ---
55
+
56
+ ## Composition
57
+
58
+ `PaymentMethodBadges` is a leaf: it takes no children and renders its own row from `usePayment()`. It requires a payment-section ancestor and declares no fragment of its own — `PaymentSectionRoot` / `PaymentSectionController` already selected the data it reads.
59
+
60
+ ```
61
+ PaymentSectionRoot
62
+ ├─ PaymentMethodBadges ← this strip, reads the section's unfiltered method list
63
+ ├─ PaymentMethodList ← the selectable picker, a sibling — reads a group-scoped subset
64
+ ├─ CardEntry
65
+ └─ …
66
+ ```
67
+
68
+ Also composes, unchanged, inside `PaymentSectionController`'s popup — same context, same contract.
69
+
70
+ `AcceptedPaymentMethodBadges` is also a leaf, but requires **no** ancestor at all: it owns its own `Query.payment` fragment and reads `$data` directly, so it drops independently into either shipped store's own, very different cart-summary composition:
71
+
72
+ ```
73
+ CartRoot Container (bare — no totalizer at all)
74
+ ├─ CartTotalizerRoot ├─ Text
75
+ │ ├─ CartTotalizerLine ├─ CartTotal
76
+ │ └─ CartTotal ├─ AcceptedPaymentMethodBadges ← this strip, no ancestor needed
77
+ ├─ AcceptedPaymentMethodBadges └─ CartGoCheckout
78
+ └─ CartGoCheckout
79
+ ```
80
+
81
+ No `PaymentSectionRoot`, `PaymentSectionController`, `CartTotalizerRoot`, or any other specific ancestor is required — the compiler resolves and injects its `$data` from a page-level query regardless of what else the page composes.
82
+
83
+ ---
84
+
85
+ ## Store setup: icon declaration
86
+
87
+ This obligation applies identically to **both** components — they resolve icons through the same frozen
88
+ `PAYMENT_METHOD_ICON_BY_KEY` map and the same shared render path, so there is nothing to declare twice or
89
+ differently for `AcceptedPaymentMethodBadges` versus `PaymentMethodBadges`.
90
+
91
+ `PaymentMethodBadges` renders each badge's glyph from a name computed in component code — the method's
92
+ store-configured `name`, matched against `PAYMENT_METHOD_ICON_BY_KEY` — never from a schema prop, so the
93
+ compiler's prop-type-driven icon collection never sees it. Same mechanism, same risk, as `CardEntry`'s own
94
+ detected-network icon (see its `.md`, "Store setup: network icon declaration").
95
+
96
+ **Any store composing `PaymentMethodBadges` or `AcceptedPaymentMethodBadges` must declare every name a
97
+ configured method can resolve to in `global.icons.include`.** Either strip can resolve up to nine
98
+ base-dialect names — `CardEntry`'s five networks plus four wallet glyphs this component adds:
99
+
100
+ ```json
101
+ {
102
+ "icons": {
103
+ "include": ["amex", "diners", "elo-card", "mastercard", "visa", "paypal", "google-pay", "pix", "stripe"]
104
+ }
105
+ }
106
+ ```
107
+
108
+ Declare only the names the store actually configures as payment methods — an unresolved name renders no
109
+ glyph at all, so an unused `include` entry costs nothing, but a store with PayPal enabled and no `paypal`
110
+ entry ships a blank 24×24 box with no build error and no console error. The matching is **token-based, not
111
+ exact-string** (see the component's own `paymentMethodIconName` doc comment), so a name the store never typed
112
+ verbatim can still resolve to a declared key — `"WH Google Pay"` resolves to `google-pay` — which means the
113
+ set of names a strip can produce is wider than the store's literal method labels and has to be read off this
114
+ list, not off the payment configuration's screen.
115
+
116
+ **If the store's `global.json` already has an `icons` object, extend it — do not replace it**, for the same
117
+ reason `card-entry.md` gives: replacing it silently drops `collection` (or any existing `include` entries),
118
+ and every other icon in that store re-resolves through the fallback default with no error to flag it.
119
+
120
+ ---
121
+
122
+ ## Components
123
+
124
+ ### PaymentMethodBadges
125
+
126
+ | Prop | Type | Default | Notes |
127
+ |------|------|---------|-------|
128
+ | `heading` | string | *(none)* | Names the strip and its list for assistive tech. Absent or blank renders no heading element and leaves the list unnamed — never an invented English default |
129
+
130
+ **Data:** reads `usePayment()`'s method list; declares no fragment of its own. Reads the section's **unfiltered** list — every method the store returns, regardless of the section's `group` — not the group-scoped list `PaymentMethodList` and `CardEntry` read. A wallet method outside the default credit-card group still shows here, which is why this strip and the picker can show different sets on the same page.
131
+
132
+ **Writes:** none. No action, no selection, no draft.
133
+
134
+ ### AcceptedPaymentMethodBadges
135
+
136
+ | Prop | Type | Default | Notes |
137
+ |------|------|---------|-------|
138
+ | `heading` | string | *(none)* | Names the strip and its list for assistive tech. Absent or blank renders no heading element and leaves the list unnamed — never an invented English default |
139
+
140
+ **Data:** owns its own `Query.payment` fragment (`payment { methods { id name } }`) and reads it via `$data` — no context, no `usePayment()`. Reads the **unfiltered** `Payment.methods` list, the identical field `PaymentMethodBadges` reads through `allMethods` — same set, different plumbing. Renders the container in every case (`loading` while `$data` is unresolved, `empty` once resolved with zero methods — including a partial slice with no `methods` key at all — `ready` otherwise); never throws for lack of a payment-section ancestor, which is the one thing `PaymentMethodBadges` cannot do.
141
+
142
+ **Writes:** none. No action, no selection, no draft.
143
+
144
+ ---
145
+
146
+ ## Canonical schema example
147
+
148
+ ```json
149
+ {
150
+ "payment-section": {
151
+ "type": "PaymentSectionRoot",
152
+ "children": ["payment-method-badges", "payment-method-list", "payment-card-entry", "payment-place-order"]
153
+ },
154
+ "payment-method-badges": { "type": "PaymentMethodBadges" }
155
+ }
156
+ ```
157
+
158
+ `AcceptedPaymentMethodBadges` in a cart order summary, composed as a sibling under `CartRoot` — no payment section anywhere on the page:
159
+
160
+ ```json
161
+ {
162
+ "cart-root": {
163
+ "type": "CartRoot",
164
+ "children": ["cart-summary", "cart-accepted-methods", "cart-go-checkout"]
165
+ },
166
+ "cart-summary": {
167
+ "type": "CartTotalizerRoot",
168
+ "children": ["cart-total-line", "cart-total"]
169
+ },
170
+ "cart-total-line": { "type": "CartTotalizerLine" },
171
+ "cart-total": { "type": "CartTotal" },
172
+ "cart-accepted-methods": { "type": "AcceptedPaymentMethodBadges", "props": { "heading": "Aceitamos" } },
173
+ "cart-go-checkout": { "type": "CartGoCheckout" }
174
+ }
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Common mistakes
180
+
181
+ - **`PaymentBadges`, `PaymentFlags`, `CartPaymentMethodBadges`.** The correct names are `PaymentMethodBadges` (payment step) and `AcceptedPaymentMethodBadges` (everywhere else).
182
+ - **Confusing either one with `PaymentMethods`.** That one is the CMS-authored footer strip. Both badge components here are data-driven and react live to the payment configuration.
183
+ - **Confusing either one with `PaymentMethodList`.** That one is the selectable radio picker. Both badge strips are read-only reassurance; neither dispatches anything.
184
+ - **Authoring a `methods` prop, or expecting a default `heading`, on either component.** There is no `methods` prop on either one — the list is always real order-form data — and `heading` ships with no default on either; an unnamed strip is a valid, intentional state.
185
+ - **Placing `PaymentMethodBadges` outside a payment root.** Like every payment leaf, it requires a `PaymentSectionRoot` or `PaymentSectionController` ancestor and fails the build otherwise. Placing it on a cart page is exactly this mistake — use `AcceptedPaymentMethodBadges` there instead.
186
+ - **Adding a `PaymentSectionRoot` to a cart page just to host `PaymentMethodBadges` there.** Don't — that drags in a submission provider and a write action namespace that do not belong on a cart page. `AcceptedPaymentMethodBadges` needs no such root at all.
187
+ - **Expecting `group`/`groupName` scoping on `AcceptedPaymentMethodBadges`.** There is no such prop on either component — both read the section's/read-model's unfiltered list, on purpose (a scoped strip could never show that a wallet method was enabled or disabled).
188
+ - **Expecting brand-accurate multicolor marks.** The glyphs are single-colour sprite symbols that inherit `currentColor`; literal brand colours are not reachable through either component (see Styling).
189
+
190
+ ---
191
+
192
+ ## Styling
193
+
194
+ `PaymentMethodBadges` and `AcceptedPaymentMethodBadges` render one root you style directly, plus per-part hooks — the identical set for both, since both are produced by the same internal render path. Each carries its own catalog name, so each gets its own independent swatch entry in `global.json`'s `styles` key even though the two emit the same hooks; style each surface separately if it needs to look different.
195
+
196
+ | Component | What you style | Plausible classes |
197
+ |---|---|---|
198
+ | `PaymentMethodBadges` / `AcceptedPaymentMethodBadges` (root `div`) | Stacks the heading over the strip | `flex flex-col gap-[12px]` |
199
+ | `data-fs-payment-method-badges-heading` | The strip heading | `text-[14px] font-bold uppercase text-[#1a1a1a]` |
200
+ | `data-fs-payment-method-badges-list` | The badge row, wrapping on narrow screens | `flex flex-wrap items-center gap-[8px] list-none p-0 m-0` |
201
+ | `data-fs-payment-method-badge` | The box one badge occupies | `flex items-center` |
202
+ | `data-fs-payment-method-badge-icon` | The glyph, sized consistently across the row | `w-[32px] h-[20px] text-[#1a1a1a]` |
203
+
204
+ State hooks worth styling:
205
+
206
+ - `data-fs-payment-method-badges-state="loading"` / `="empty"` / `="ready"` — always present. Give `loading` and `empty` a `min-h-*` so the strip does not collapse to zero height while resolving or when the store has no method configured.
207
+
208
+ The list carries an explicit `role="list"`. Keep it: WebKit drops the implicit list role from a `<ul>` whose computed `list-style` is `none`, and `list-none` is exactly what the strip needs.
209
+
210
+ A method whose name maps to no glyph renders its name as plain text in place of the icon — style that case through `data-fs-payment-method-badge` itself (font, colour); there is no separate icon hook on a text-only badge.
211
+
212
+ Styled example:
213
+
214
+ ```json
215
+ {
216
+ "payment-method-badges": {
217
+ "type": "PaymentMethodBadges",
218
+ "style": ["flex", "flex-col", "gap-[12px]"],
219
+ "props": { "heading": "Aceitamos" }
220
+ }
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Related
227
+
228
+ - [[payment]] — `PaymentSectionRoot` / `PaymentMethodList`: the required ancestor and the sibling method picker. `PaymentMethodBadges` cannot render outside a `PaymentSectionRoot` (or `PaymentSectionController`); `AcceptedPaymentMethodBadges` needs none of it.
229
+ - [[payment-methods]] — the CMS-authored footer strip with the same look; neither badge component is ever interchangeable with it — see [When NOT to use](#when-not-to-use).
230
+ - [[cart-totalizer]] — `CartTotalizerRoot` / `CartTotalizerLine` / `CartTotal`: one of the two shipped shapes a cart order summary can take. This doc's canonical example composes `AcceptedPaymentMethodBadges` as `CartTotalizerRoot`'s sibling, under `CartRoot` (see [Canonical schema example](#canonical-schema-example)). Nothing in the component enforces that position — `CartTotalizerRoot` renders its own `children` through unchanged, so nesting the strip inside them also renders correctly; the sibling placement above is the documented convention, not a requirement.
231
+ - `CardEntry` — owns its own single detected-network mark (0 or 1, never a standing row of every accepted network), scoped to the card group and built into the number field. A different task's deliverable, always a sibling, never a parent of either badge component.
232
+ - `Icon` — the particle each mapped glyph renders. It carries no `.md` of its own; read its schema from `catalog.json`.
@@ -23,6 +23,7 @@ The image is the source of truth for layout. A PaymentMethods strip is present w
23
23
 
24
24
  ## When NOT to use
25
25
 
26
+ - The **checkout payment step's** own accepted-method strip, which must update the moment a method is enabled or disabled → use `PaymentMethodBadges` (`matter/payment/`). This component's set is CMS content edited by a merchant and does not react to the payment configuration; that one reads the live order form and never needs a content edit.
26
27
  - A product's **installment plan or "in 12× of R$ 9,90"** copy → use `PricingPaymentMethodsList`, which reads commerce data for one product rather than store-level configuration.
27
28
  - The shopper's **saved cards or payment selection** in checkout → that is a form, not this display strip.
28
29
  - A row of **social network icons** → use [[footer-social]]; different content model and different glyph set, and every glyph is a link.
@@ -0,0 +1,218 @@
1
+ # PaymentSection
2
+
3
+ **Category:** commerce
4
+ **Purpose:** The checkout payment section: owns the payment-method list and the current selection from the order form, and provides the `payment` context every payment leaf (card entry, cardholder document, installments, save-card, billing address, saved cards, place-order) reads. Ships as two interchangeable providers — an always-visible section (`PaymentSectionRoot`) and a state-only controller for a collapsed drawer/modal (`PaymentSectionController`) — plus the method picker (`PaymentMethodList`).
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ Use these signals to detect that a payment section is present. Position on the page is irrelevant for detection.
11
+
12
+ A payment section block contains any combination of:
13
+
14
+ - A **group of payment-option rows with radio controls** — one card option (e.g. "Pay with Credit / Debit Card"), plus a row for any non-card option the store offers (e.g. "Pix") — one selectable at a time → `PaymentMethodList`. A picker showing one row per card *network* ("Visa", "Mastercard", "Amex") is still this component: it renders one card option instead, because the network comes from the number the shopper types.
15
+ - A **card form** (number, holder name, expiration, CVV) → its own component, placed inside the section.
16
+ - An **installment select or radio list** ("3x de R$ 33,33 sem juros") → its own component, inside the section.
17
+ - A **saved-card list** with masked digits and a "use a new card" entry → its own component, inside the section.
18
+ - A **place-order button** at the bottom of the block.
19
+
20
+ If the image shows any two or more of those together, they belong inside **one** payment section — wrap them in `PaymentSectionRoot`, never side by side as unrelated blocks.
21
+
22
+ If the image shows only a **short read-only recap** of the chosen card ("Visa •••• 4242, 3x de R$ 33,33") with no control to change it, that is not this section, and no component renders it in checkout.
23
+
24
+ If the payment choice is shown as a **summary row that opens a drawer or modal**, use `PaymentSectionController` with the overlay particles, and put a static label (`Text`) inside the trigger — no component echoes the current choice there.
25
+
26
+ ---
27
+
28
+ ## When to use
29
+
30
+ - The checkout payment step, as the container for every payment control.
31
+ - A compact checkout where payment lives in a drawer or modal (`PaymentSectionController`).
32
+
33
+ ## When NOT to use
34
+
35
+ - A read-only recap of the chosen card and installment — no component renders one in checkout.
36
+ - A PDP's *advertised* installment plans (display only, no selection) → use `Pricing` / `PricingInstallments`.
37
+ - A post-purchase order-confirmation page. The section reads the *active* order form, which no longer resolves once the order is placed.
38
+ - As a wrapper around non-payment checkout content. The section provides one context and one container; delivery, identity, and cart have their own roots.
39
+
40
+ ---
41
+
42
+ ## Composition
43
+
44
+ One of the two providers wraps everything payment-related:
45
+
46
+ ```
47
+ PaymentSectionRoot ← always-visible section
48
+ ├── PaymentMethodList
49
+ ├── SavedCards
50
+ ├── CardEntry
51
+ ├── CardholderDocument
52
+ ├── Installments
53
+ ├── SaveCard
54
+ ├── BillingAddress
55
+ └── PlaceOrder
56
+ ```
57
+
58
+ ```
59
+ PaymentSectionController ← collapsed section; renders no markup of its own
60
+ └── DrawerRoot | DialogRoot ← receives its open state from the controller
61
+ ├── DrawerTrigger
62
+ │ └── Text ← the trigger's static label; nothing echoes the current choice
63
+ └── DrawerPortal
64
+ ├── DrawerBackdrop
65
+ └── DrawerPopup
66
+ ├── PaymentMethodList
67
+ └── … the same leaves
68
+ ```
69
+
70
+ Both providers declare `providesContext: ['payment', 'card-capture']`. Every leaf declares `requiresContext: 'payment'` except `CardEntry`, which requires only `card-capture`, so the two providers are interchangeable with no change to the children. A leaf placed with **no** payment-section ancestor fails the build with a `ContextConstraintError` — the compiler checks this before emitting any file.
71
+
72
+ `PaymentSectionController` must wrap **exactly one** overlay `Root` (a `DialogRoot` or `DrawerRoot`) as its child. Anything else is returned untouched and provides no context.
73
+
74
+ ---
75
+
76
+ ## Components
77
+
78
+ ### PaymentSectionRoot
79
+
80
+ | Prop | Type | Default | Notes |
81
+ |------|------|---------|-------|
82
+ | `group` | string, or array of strings | `["creditCardPaymentGroup", "debitCardPaymentGroup"]` | Scopes the method list to a set of payment groups — a method renders when its `groupName` is a member. A single string still scopes to exactly that one group. Set to `""` (the empty **string**) to show every method the store offers. An empty **array** (`[]`) is not a second spelling of that opt-out — it matches no group, so every method is scoped out and the list renders empty. It only *filters* — it never supplies methods. |
83
+
84
+ **Data:** declares `fragment PaymentSectionData on Query`, which selects two root fields: `Query.payment` (the method list with its validator config, plus `selectedMethodId` / `selectedAccountId` / `selectedBin`) and `Query.cart.total`, for the `add_payment_info` event. The compiler merges it into the page query — no `dataSources` entry needed — and, because it has two root fields, delivers `$data` keyed as `{ payment, cart }` (ADR-0015). `fetchStrategy: 'reactive'` — picking a method re-renders the whole section from the cache, no refetch.
85
+
86
+ **Writes:** `actions: { payment: ['setMethod', 'sendPayments'], analytics: ['track'] }`. Selecting a method dispatches `setMethod`. The root also provides `card-capture` to `CardEntry`: when the place-order sequence runs the `new-card` submission, `CardEntry` builds the card and the root folds in the draft's document, label, and save flag and sends it through `sendPayments`. Once a `CardEntry` is mounted and the cart total has resolved, the root fires `add_payment_info` once.
87
+
88
+ Renders one container `div` and its children. No heading, no copy, no layout — those belong to the composing schema.
89
+
90
+ ### PaymentSectionController
91
+
92
+ Same props, same fragment shape, same action. Renders **no element at all**: it holds the overlay's open state and hands it to its single overlay child. Unlike the delivery-options controller, a successful method selection does **not** close the surface — the shopper still has a card to type — so close it from a `DrawerClose` / `DialogClose` particle or from the leaf that finishes the flow.
93
+
94
+ ### PaymentMethodList
95
+
96
+ | Prop | Type | Default | Notes |
97
+ |------|------|---------|-------|
98
+ | `legend` | string | `"Payment method"` | The radio group's accessible name, rendered as a visible fieldset legend above the rows (T38). An authored empty or whitespace-only string falls back to this default rather than rendering a nameless group. |
99
+ | `cardLabel` | string | `"Pay with Credit / Debit Card"` | The card option's own name (T171). One control stands for every in-scope credit and debit method at once, so it cannot be labelled from one method's name — this is the copy instead. An authored empty or whitespace-only string falls back to this default rather than rendering a nameless control. |
100
+
101
+ Renders the inline selection error (`role="alert"`) and the payment options, inside a fieldset named by `legend`. Requires a payment-section ancestor.
102
+
103
+ **Every in-scope credit and debit method is one option**, named by `cardLabel` — not one row per network. Which card network the shopper is paying with comes from the number they type into the card form, which is the only mechanism that can be right: a shopper who picked "Visa" and then typed a Mastercard number had their pick silently overwritten. Every other in-scope method (e.g. Pix) keeps its own row, labelled with the method's name from the order form and bound to that method's own id, exactly as before. Only the two card groups fold into the card option — a card-like custom method in another group keeps a row of its own. Set `group` to `""` on the root to switch the folding off entirely and get one row per method; the card form still hides for a non-card row and comes back for a card row (see below).
104
+
105
+ The card option reads as checked while the persisted selection is one of the methods it stands for, **and while nothing is persisted at all** — the card option is the default — and while the section is in card mode (below). It is not bound to any single method id, so activating it dispatches the same candidate the preselection rule would choose (the sole in-scope method, else the sole method in the credit group). When neither is singular — a store with Visa and Mastercard — it dispatches nothing and puts the section in **card mode** instead: the card form comes back at once, the option reads checked, and the method arrives from the typed number. No made-up `"card"` method id is ever sent: the only ids that reach `payment.setMethod` are ids the order form offered.
106
+
107
+ The container always carries `data-fs-payment-method-list-state` — `"loading"`, `"empty"`, or `"ready"` — so a row-less list is still something a swatch can see. Style at least the `empty` case: with no rows the container has no content, and under a `flex flex-col gap-2` swatch that is a zero-height element the shopper cannot tell apart from a missing component. `empty` means the order form resolved and no method's `groupName` matched the scope; `loading` means it has not resolved yet.
108
+
109
+ **Keep it composed.** It is the only element that surfaces a rejected method selection, and the only control that can recover a selection this section does not serve: omit it and a shopper whose persisted method sits outside `group` has no reachable way back at all. It also now answers for an action taken elsewhere — a rejection triggered by typing a card number appears in *this* component's `role="alert"`, once per detected network, so place it where a shopper typing into the card form can see it.
110
+
111
+ ### Preselection and the card-payment collapse
112
+
113
+ When the order form carries no persisted selection and the scoped method list resolves to exactly one method, the section preselects it automatically — one silent `payment.setMethod` dispatch, keyed on that method's id, never on row position. When the scoped list holds more than one method, the section falls back to the sole method in the credit group, if there is exactly one — so a store offering one credit and one debit method keeps preselecting the credit one, the same method it preselected before debit was admitted. **"The credit group" here is always the fixed name `creditCardPaymentGroup`, not whichever member of `group` the store authored** — a store that renamed its credit payment group and listed the new name in `group` gets that method rendered and selectable, but not preselected, and its `add_payment_info` `payment_type` stays unset once picked, because both derive from this same hardcoded name rather than from the store's authored scope. Every other shape preselects nothing. Nothing is preselected while loading, while an order-placement sequence is in flight, when a method or saved-card account is already persisted (in scope or out of it), once a candidate id has already been attempted once, or when `group` is `""` (fix pass, finding R4). Preselection takes no card expression: under `group=""` it stays off even for a single card method, and naming the credit group for the tiebreak never runs there at all. A failed preselection is logged, never shown as `role="alert"` — the shopper never asked for that dispatch.
114
+
115
+ The rule lives in the shared selection engine, not in `PaymentMethodList`: it fires even when no picker is composed, and before a `PaymentSectionController`'s overlay is ever opened.
116
+
117
+ The same engine derives the one collapse fact every card leaf reads (T191). **The card payment collapses** while the persisted selection is a method this section does not serve — an id that does not resolve inside the scoped list — or a method that is **not a card**: one in neither card group (`creditCardPaymentGroup`, `debitCardPaymentGroup`) whose validator carries no card-number pattern. A store's card-like custom method, in another group but with a number pattern, is a card and hides nothing. While collapsed, `CardEntry` hides itself and stays mounted, and `SavedCards`, `CardholderDocument`, `Installments`, `InstallmentsSelect`, `SaveCard` and `BillingAddress` render nothing; `PlaceOrder`, `PaymentMethodList` and `PaymentMethodBadges` stay. See each leaf's own doc for what it keeps for the way back.
118
+
119
+ **The card option brings them back**, with or without a method it can name. With one, it dispatches it, and the leaves return once the order form confirms the selection. With none, it puts the section in card mode, locally — nothing is sent — and the leaves return at once; a card number already typed with at least eight digits is then sent again, once, for its own network. Card mode ends when another method that is not a card is picked and the order form accepts it; a refused pick leaves the shopper on the card, with the refusal in this component's `role="alert"`. Card mode is not persisted: a reload starts over from the order form's selection. Under `group=""` the collapse classifies the same way: a non-card row hides the leaves and a card row brings them back.
120
+
121
+ Hiding is also conditioned on a composed `PaymentMethodList` being able to bring the card form back (fix pass, finding R2): a leaf must not hide the only remaining way a shopper has to pay by card. A rendered card option always can. Without one, only a row bound to a card method can — a list of non-card rows alone offers no card to pay with. A composition with no picker at all — e.g. `SavedCards` + `CardEntry` — never collapses, whatever the persisted selection is, and typing a new card number stays the way back. Even visible, `CardEntry` never posts a card while the persisted selection is a method it cannot pay (see its own doc).
122
+
123
+ ---
124
+
125
+ ## Canonical schema example
126
+
127
+ ```json
128
+ {
129
+ "payment-section": {
130
+ "type": "PaymentSectionRoot",
131
+ "children": ["payment-method-list", "payment-card-entry", "payment-installments", "payment-place-order"]
132
+ },
133
+ "payment-method-list": { "type": "PaymentMethodList" }
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Common mistakes
140
+
141
+ - **Authoring the payment methods as props.** There is no `methods`, `options`, or `labels` prop. Which methods exist is real order-form data. `group` only narrows it, whether given as one group name or a set of them.
142
+ - **Inventing `PaymentSection`, `PaymentRoot`, or `PaymentMethodSelector`.** The components are `PaymentSectionRoot`, `PaymentSectionController`, and `PaymentMethodList`.
143
+ - **Placing a payment leaf outside the section.** Every payment leaf requires the `payment` context; a leaf with no `PaymentSectionRoot` / `PaymentSectionController` ancestor fails the build.
144
+ - **Using both providers on one page for the same section.** Pick one: the always-visible root, or the collapsed controller.
145
+ - **Giving the controller more than one child, or a plain string.** It controls exactly one overlay `Root`.
146
+ - **Adding a `collapsed` or `variant` prop to switch between the two.** There is none — compose the provider that matches the look.
147
+ - **Expecting a card number or CVV anywhere.** No prop, no fragment field, and no context field carries either; they exist only inside the card-entry component's own state.
148
+ - **Styling the section with a `padding`/`gap` prop.** There is none; use swatches on the element.
149
+
150
+ ---
151
+
152
+ ## Styling
153
+
154
+ `PaymentSectionRoot` and `PaymentMethodList` each render one element and forward `className`. `PaymentSectionController` renders nothing to style — style the overlay particles as their own schema elements instead. This asymmetry reaches T28's `data-fs-payment-section-placing` too: a store built on `PaymentSectionController` has every entry leaf's own `-placing` hook (each leaf renders its own element regardless of which root provides the context) but no section-level one to style, since the controller has no container of its own to carry it.
155
+
156
+ | Component | What you style | Plausible classes |
157
+ |---|---|---|
158
+ | `PaymentSectionRoot` (root `div`) | Stacks the section's parts | `flex flex-col gap-6` |
159
+ | `PaymentMethodList` (root `div`) | Stacks the payment options and the error | `flex flex-col gap-2` |
160
+ | Fieldset (`data-fs-payment-method-list-fieldset`, native `<fieldset>`) | Wraps the legend and the radio rows (T38) — gives the group its visible, accessible name | usually left unstyled; a merchant border/padding choice |
161
+ | Legend (`data-fs-fieldset-legend`, native `<legend>`) | The group's visible name, from `legend` | `text-[14px] font-medium text-[#1A1A1A]` |
162
+ | Empty / loading picker (`data-fs-payment-method-list-state`) | Keeps the row-less container visible | `data-[fs-payment-method-list-state=empty]:min-h-12` |
163
+ | Option row (`data-fs-radio-root`) | Each selectable option — the card option and any standalone method row alike | `flex items-center gap-3 border border-[#E0E0E0] p-3` |
164
+ | Indicator (`data-fs-payment-method-indicator`) | The selected dot. Present on the card option too | `size-2 rounded-full bg-[#1A1A1A]` |
165
+ | Method name (`data-fs-payment-method-name`) | The option's label — `cardLabel` on the card option, `method.name` on a standalone row | `text-[14px] text-[#1A1A1A]` |
166
+ | Error (`data-fs-payment-method-list-error`) | Rejected-selection message | `text-[12px] text-[#B00020]` |
167
+ | Placing, section (`data-fs-payment-section-placing`) | Present on `PaymentSectionRoot`'s own container while an order-placement sequence is in flight (T28) — presence only; this container is never made `inert` (see the warning below) | `opacity-90` |
168
+ | Placing, method list (`data-fs-payment-method-list-placing`) | Present on `PaymentMethodList`'s own root while placing; the picker is also `inert` and `aria-disabled` at that point | `opacity-60 cursor-not-allowed` |
169
+
170
+ State hooks worth styling:
171
+
172
+ - `data-fs-payment-section-path="new-card"` / `="saved-card"` on the section container — present only once a path resolves, so a swatch can emphasize the active half of the section.
173
+ - `data-fs-payment-method-list-state="loading"` / `="empty"` / `="ready"` on the picker container — always present. Give `empty` (and usually `loading`) a `min-h-*`, a border, or a placeholder: without one, a list with no rows collapses to zero height and the picker looks absent rather than empty. A store whose credit or debit group is named something other than the two defaults lands exactly here.
174
+ - `data-checked` on each row, set by the radio particle.
175
+ - `data-fs-payment-section-placing` — **never** style this hook with `pointer-events-none` (or any rule that blocks interaction with a descendant). `PaymentSectionRoot`'s container deliberately stays interactive while placing (T28, Decision 5) because `PlaceOrder` composes as its child: the CTA's own `role="alert"`/`role="status"` outcome regions must stay reachable so the shopper is told what is happening. Blocking pointer events at the section level would freeze the CTA too, reintroducing the exact defect the per-leaf `inert` hooks (`data-fs-payment-method-list-placing` above, and each entry leaf's own `-placing` hook) exist to avoid. Style this hook for a dimming/overlay *look* only — the actual freeze is each leaf's own `inert`.
176
+
177
+ Styled example:
178
+
179
+ ```json
180
+ {
181
+ "payment-section": {
182
+ "type": "PaymentSectionRoot",
183
+ "style": ["flex", "flex-col", "gap-6"],
184
+ "children": ["payment-method-list"]
185
+ },
186
+ "payment-method-list": {
187
+ "type": "PaymentMethodList",
188
+ "style": ["flex", "flex-col", "gap-2"]
189
+ }
190
+ }
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Accessibility Contract
196
+
197
+ **What the platform promises:** `PaymentMethodList`'s radio group carries a visible, programmatic
198
+ accessible name (`legend`, default `"Payment method"`) — a screen-reader user hears what the rows are
199
+ a choice *between*, not just "radio button, 1 of 3." An authored empty or whitespace-only `legend`
200
+ falls back to the default rather than shipping a nameless group. Each row keeps its own accessible name
201
+ from the method's own display name. The group is a native `<fieldset>`/`<legend>` pair (not `aria-label`
202
+ on a `<div>`), so it is also visible to sighted and low-vision shoppers and reachable by page-level
203
+ translation tooling. Keyboard: the group is one Tab stop — Tab lands on the checked row, and the arrow
204
+ keys move the checked row and change the selection. A default `:focus-visible` outline is painted from
205
+ the platform's own stylesheet on the checked row itself — the selector is a descendant combinator, so
206
+ it cannot match the list root or the fieldset it wraps, since neither is a focusable element.
207
+
208
+ **What the platform does not promise:** colour contrast. `PaymentMethodList` (and every other payment
209
+ component) exposes `data-fs-*` hooks so a merchant can author contrast-compliant swatches, but no
210
+ component in this package measures or guarantees any contrast ratio — that is the merchant's theme, not
211
+ this platform.
212
+
213
+ ---
214
+
215
+ ## Related
216
+
217
+ - [[delivery-options]] — the pattern this section mirrors: one shared selection hook, a presentation variant, and a state-only controller that injects open/close into an overlay child.
218
+ - [[session]] — the closest precedent for a reactive root that broadcasts state through a guarded context hook.
@@ -30,16 +30,22 @@ The image is the source of truth for layout. If the input is the profile phone o
30
30
 
31
31
  ## Composition
32
32
 
33
- `PhoneField` is a single leaf particle. Internally it renders:
33
+ `PhoneField` provides `phone-field` context and, childless, renders:
34
34
 
35
35
  ```
36
36
  PhoneField (data-fs-phone-field-root + data-fs-phone-field-root-country)
37
37
  ├── Field.Label (data-fs-phone-field-label; the `label` prop)
38
- ├── Input (data-fs-phone-field-input; masked <input type="tel" inputMode="numeric">)
38
+ ├── PhoneFieldInput (data-fs-phone-field-input; masked <input type="tel" inputMode="numeric">)
39
39
  └── Field.Error (data-fs-phone-field-error; shown when invalid)
40
40
  ```
41
41
 
42
- With `requirement: 'hidden'` the particle renders `null` — no DOM node is emitted.
42
+ **Recomposable (RFC:503):** given children, `PhoneField` renders them in place of the three parts above,
43
+ in any order. `PhoneFieldInput` is the one catalogued part that requires `phone-field` context — it reads
44
+ the masked value, the composed ref and the particle's own `onChange`/`onBlur` from `PhoneField` itself, so
45
+ it must sit inside it. A composed childless `Field.Label` / `Field.Error` (the generic parts from the
46
+ `field` particle) works exactly as it does under any other bound Control; a composed generic
47
+ `Field.Control` instead re-registers the field with react-hook-form and loses the mask — unsupported, the
48
+ RFC's named double-binder hole.
43
49
 
44
50
  ---
45
51
 
@@ -58,8 +64,10 @@ With `requirement: 'hidden'` the particle renders `null` — no DOM node is emit
58
64
  | `error` | string | — | Host-supplied error text; overrides the internal error string when set. |
59
65
  | `errorInvalid` | string | `'Invalid phone number'` | Inline error for an invalid format. |
60
66
  | `errorRequired` | string | `'Phone number is required'` | Inline error when a mandatory field is empty. |
61
- | `requirement` | `'mandatory' \| 'optional' \| 'hidden'` | `'mandatory'` | `mandatory`: empty blocks; `optional`: empty allowed, format still checked when present; `hidden`: renders `null`. |
67
+ | `requirement` | `'mandatory' \| 'optional'` | — | `mandatory`: empty blocks; `optional`: empty allowed, format still checked when present. Unset is no opinion, so a phone with no fieldset baseline is optional. It only ever raises — bound to a fieldset whose rule already says required, `'optional'` does not lower it. There is no value that suppresses the field: a store that does not want it removes the particle from the page instead (`rfc-user-identity.md` F9). |
62
68
  | `onBlur` | `() => void` | — | Fires on blur, after validation runs. |
69
+ | `ref` | `Ref<HTMLInputElement>` | — | Lands on the masked `<input type="tel">`. Composed with react-hook-form's own ref on the bound path — a caller ref and react-hook-form's ref are both honoured. |
70
+ | `children` | `ReactNode` | — | Given, replaces the default `Field.Label` + `PhoneFieldInput` + `Field.Error` anatomy, in any order (RFC:503). |
63
71
 
64
72
  **Validation:** the number is checked on blur/submit only — never per keystroke. A prior error clears while the shopper edits and is re-evaluated on the next blur. Changing `country` resets the local validation state.
65
73
 
@@ -70,6 +78,46 @@ With `requirement: 'hidden'` the particle renders `null` — no DOM node is emit
70
78
 
71
79
  ---
72
80
 
81
+ ## Bound behaviour
82
+
83
+ `PhoneField` reads the field contract through `useField()` and binds through `useController` when both a
84
+ field contract (`FieldBinding`) and a react-hook-form `FormProvider` are present above it; otherwise it
85
+ runs the plain, unbound behaviour documented above, unaltered.
86
+
87
+ - **What the contract supplies**: `label` and `placeholder`, when the particle's own prop is omitted
88
+ (an explicit prop always wins); `defaultValue`, seeded into `useController`; and `rule`, merged with the
89
+ particle's own format validator (below).
90
+ - **What stays the particle's**: the US/BR mask, `country`, `requirement`, and the format/required error
91
+ copy (`errorInvalid`/`errorRequired`).
92
+ - **Value shape stored in react-hook-form**: the same **unmasked** string the unbound particle emits
93
+ through `onValueChange` — not the masked display value. A fieldset's `toPayload` reads exactly what a
94
+ non-form caller reads today; E.164 normalisation for persistence is a separate step (`toE164`), not
95
+ applied here.
96
+ - **Validation**: the composite composes its own format check (using `errorInvalid`) alongside whatever
97
+ the contract's `rule` validates, never instead of it. Requiredness is not decided here: the particle
98
+ passes `requirement === 'mandatory'` as a raise, and `useBoundField` resolves it against the contract's
99
+ own `rule.required`, carrying `errorRequired` as the copy when the raise is what makes the field
100
+ required. A contract that omits `required` still behaves as the unbound particle does.
101
+ - **Focus**: the `ref` lands on the masked `<input type="tel">`, so a failed submit moves focus to it.
102
+ - **Explicit `invalid`/`error` props still win** over react-hook-form's own error state — a host-forced
103
+ error renders even when react-hook-form holds none for the name. This is unconditional on the default
104
+ (childless) anatomy, which renders the raw Base UI `Field.Label`/`Field.Error` primitives directly
105
+ rather than this package's own bound-aware `Field.*` wrapper, so it never gates on react-hook-form's
106
+ own error state the way a composed childless `Field.Error` intentionally does (below).
107
+ - `PhoneFieldDefinition` gains no `requiresContext` — the particle stays placeable outside any form. It
108
+ does declare `providesContext: 'phone-field'` — that is what makes `PhoneFieldInput` a legal child.
109
+ - **Recomposition (RFC:503):** when bound, `PhoneField` re-provides the field contract with the
110
+ *resolved* `name`/`label`/`placeholder` (the values above, after the particle's own prop/contract
111
+ fallback chain) so a composed childless `Field.Label` renders real text and a composed childless
112
+ `Field.Error` renders react-hook-form's real message for the field, instead of the empty contract an
113
+ earlier design (`FieldContractBoundary`) used to push. `useController` above remains the sole binder for
114
+ the name either way — the re-provided contract is read-only-safe, not a second registration.
115
+ - **Focus stays composed-safe:** the composed ref threads through to `PhoneFieldInput` regardless of
116
+ where the author places it, so focus-on-failed-submit lands on the masked input in a recomposed anatomy
117
+ too, not only the default one.
118
+
119
+ ---
120
+
73
121
  ## Accessibility
74
122
 
75
123
  The label is associated with the input and the error is linked via `aria-describedby` automatically by the Base UI `Field` primitive (WCAG 2.1 AA). The input uses `type="tel"` and `inputMode="numeric"` for a numeric mobile keypad. Validation never blocks typing.
@@ -88,6 +136,12 @@ The label is associated with the input and the error is linked via `aria-describ
88
136
  | `data-fs-phone-field-input` | The input box; use `data-invalid:` for the error border. |
89
137
  | `data-fs-phone-field-error` | The inline error text (small, red). |
90
138
 
139
+ `data-fs-phone-field-input` stays on `PhoneFieldInput` in both the default and a recomposed anatomy — it
140
+ is the one catalogued part, so there is exactly one place that hook lives. A recomposed anatomy's
141
+ author-supplied `Field.Label` / `Field.Error` (the generic parts, not the default branch's own) carry
142
+ only `data-fs-field-label` / `data-fs-field-error`, not the phone-specific hooks above — a documented
143
+ trade of Decision 4; widen store CSS keyed on the specific hooks if it must also match a recomposed page.
144
+
91
145
  ---
92
146
 
93
147
  ## Common mistakes