@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,458 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ //#region src/matter/payment/use-payment-method-selection.ts
4
+ const EMPTY_METHODS = [];
5
+ const EMPTY_BILLING_ADDRESS_UNSAVED_STATE = {
6
+ everPublished: false,
7
+ unsavedKeys: /* @__PURE__ */ new Set()
8
+ };
9
+ /**
10
+ * Resolves the BIN confirmation signal from the persisted BIN, the client draft BIN,
11
+ * and whether a `payment.setMethod` is in flight. Exported for unit testing without
12
+ * rendering, mirroring how `findInstallmentOption` / `resolveInstallmentTiers` are
13
+ * exported from `installments.tsx`.
14
+ */
15
+ function resolveBinConfirmation({ selectedBin, draftBin, selectMethodPending }) {
16
+ if (draftBin === null) return "confirmed";
17
+ if (selectedBin === void 0) return "unknown";
18
+ if (selectMethodPending) return "pending";
19
+ if (selectedBin !== draftBin) return "pending";
20
+ return "confirmed";
21
+ }
22
+ /**
23
+ * The persisted selection is one this section does not serve (T74, FR-7, Decision 7) — `true` only
24
+ * when a method id is persisted and it does not resolve inside the scoped `methods` list. `false` in
25
+ * every other state, including "nothing persisted" (INV-5) and "still loading" (both leave
26
+ * `persistedMethodId` `null`, which alone already keeps this `false`). One of the two selection facts
27
+ * behind `cardPaymentCollapsed` (T191, FR-5). Exported for direct unit testing, mirroring
28
+ * `resolveBinConfirmation`.
29
+ *
30
+ * `methodsReadable` (fix pass, finding R1) tells "the list is empty because it was actually read and
31
+ * holds nothing in scope" apart from "the list is empty because it could not be read at all" — a
32
+ * resolved-but-partial `$data` slice can omit `methods` entirely (a hand-written slice, explicitly
33
+ * supported per `PaymentSectionData.selectedBin`'s own doc comment, or a revalidation frame), and that
34
+ * must not read as "the method is not served": before this guard, that state made a perfectly valid,
35
+ * in-scope selection collapse `CardEntry` and wipe the shopper's typed CVV for a section that simply
36
+ * had not been told its own method list yet.
37
+ */
38
+ function isSelectionOutOfScope({ persistedMethodId, selectedMethod, methodsReadable }) {
39
+ return methodsReadable && persistedMethodId !== null && selectedMethod === null;
40
+ }
41
+ /**
42
+ * The credit-card payment group, named separately from `payment.tsx`'s own `DEFAULT_METHOD_GROUP`
43
+ * (T140, Decision 3): once that constant widens to the whole default *set*, it can no longer double as
44
+ * "the credit group's identifier," and reading "the first member of the set" would make preselection
45
+ * depend on prop authoring order — reintroducing the position-dependence the T79 ruling closed.
46
+ * Declared here, not in `payment.tsx`: the import runs `payment.tsx` -> `use-payment-method-selection.js`
47
+ * (`payment.tsx:8-12`) and never the other way, so the reverse would be a cycle.
48
+ */
49
+ const CREDIT_CARD_PAYMENT_GROUP = "creditCardPaymentGroup";
50
+ /**
51
+ * The one method identity a length-1 list offers, or `null` for any other length. Building block for
52
+ * `resolvePreselectionCandidateId` below, which applies it twice — once to the whole scoped list, once
53
+ * to its credit subset. No longer called directly by `resolveMethodPreselection` or its caller (T140,
54
+ * Decision 3): routing both through that one shared function is what keeps their notion of "the
55
+ * candidate" from drifting apart (fix pass, finding R6).
56
+ */
57
+ function soleMethodId(methods) {
58
+ if (methods.length !== 1) return null;
59
+ const [onlyMethod] = methods;
60
+ return onlyMethod?.id ?? null;
61
+ }
62
+ /**
63
+ * The whole preselection candidate rule (T140, FR-6a, Decision 3): the sole in-scope method when the
64
+ * scoped list holds exactly one, or — when it does not — the sole method in the credit group, so a
65
+ * store offering one credit and one debit method keeps the preselection it had before debit was
66
+ * admitted. `null` when neither tier yields a single identity. Keyed on identity, never position, the
67
+ * same property `soleMethodId` already had (the T79 ruling).
68
+ *
69
+ * Shared by `resolveMethodPreselection`'s own candidate lookup and its caller's "was this id ever
70
+ * attempted this mount" lookup (fix pass, finding R6) — one definition, so a tiebreak added to only one
71
+ * of the two could never let them drift the way it would if each recomputed its own.
72
+ */
73
+ function resolvePreselectionCandidateId(methods) {
74
+ const soleId = soleMethodId(methods);
75
+ if (soleId !== null) return soleId;
76
+ return soleMethodId(methods.filter((method) => method.groupName === CREDIT_CARD_PAYMENT_GROUP));
77
+ }
78
+ /**
79
+ * The payment groups `PaymentMethodList` treats as card-shaped and collapses into one control (T171,
80
+ * FR-1, Decision 6).
81
+ *
82
+ * Deliberately a **second** constant beside `payment.tsx`'s `DEFAULT_METHOD_GROUP`, whose members
83
+ * happen to be the same two strings today: that one is the default *authored scope*, which a store may
84
+ * override with its own group names, while this one is what the section treats as a card group.
85
+ * Collapsing on the authored scope instead would collapse a store's Pix method the moment it authored
86
+ * `group: ['pixPaymentGroup']`.
87
+ *
88
+ * The list's one card control is built from these groups alone. The section's card expression is
89
+ * `isCardMethod` below, these groups or a card-number pattern, which the engineer ruled on 2026-09-29
90
+ * and ADR-0023 records (T191, OQ-3). A store that renamed its card groups gets no card control at all,
91
+ * only one row per method, which is the pre-T171 rendering: a safe degrade, never a wrong dispatch.
92
+ */
93
+ const CARD_METHOD_GROUPS = [CREDIT_CARD_PAYMENT_GROUP, "debitCardPaymentGroup"];
94
+ /**
95
+ * Whether `method` is a card (T191, FR-1, Decision 1): its group is one of `CARD_METHOD_GROUPS`, or its
96
+ * validator carries a card-number pattern. It decides the card-payment collapse, whether a list row can
97
+ * bring the card form back, `CardEntry`'s refusal to post under a method that is not a card, and whether
98
+ * a persisted method's own installment options apply. The list's fold keeps its group-only rule (OQ-3).
99
+ *
100
+ * Where one signal is missing it errs toward "card", since a wrong collapse would hide a form the shopper
101
+ * can pay with. The collapse and `CardEntry`'s non-card refusal read this same classifier, so neither
102
+ * catches the other's miss: a method wrongly counted as a card is neither collapsed nor refused as one
103
+ * that is not. `CardEntry`'s network check still refuses a typed card under it, unless the typed number
104
+ * is detected as that method itself.
105
+ */
106
+ function isCardMethod(method) {
107
+ return CARD_METHOD_GROUPS.includes(method.groupName) || Boolean(method.validator.numberRegex);
108
+ }
109
+ /**
110
+ * Splits a scoped method list the way `PaymentMethodList` renders it (T171, FR-1/FR-2/FR-7,
111
+ * Decision 6). Order-preserving within each list, so the store's own `methods` order still decides the
112
+ * rows. Exported for direct unit testing, mirroring `resolveBinConfirmation` / `isSelectionOutOfScope`.
113
+ *
114
+ * **Scoping-off (`group: ''`) is read off the two lists, not off `group` itself.** `PaymentMethodList`
115
+ * is an independently-placeable part with no `group` prop of its own, and `group` cannot join the
116
+ * `payment` context (INV-6) — so the fact has to arrive through what the context already carries.
117
+ * `usePaymentMethodSelection` returns `allMethods` *by reference* for `group: ''` and a fresh
118
+ * `.filter()` result for every other value, so `methods === allMethods` holds exactly when the section
119
+ * narrowed nothing. That identity is pinned directly in `use-payment-method-selection.test.tsx`.
120
+ *
121
+ * With scoping off there is no collapsing at all: one row per method, mirroring
122
+ * `resolveMethodPreselection`'s own `if (group === '') return null;`. The reason first given for both,
123
+ * that the section must not classify card-ness with scoping off, no longer holds: the card-payment
124
+ * collapse classifies under `group: ''` too, and a card row brings the card form back (T191,
125
+ * Decision 9). The list's opt-out stands, as the engineer ruled on 2026-09-29 (T191, OQ-2).
126
+ */
127
+ function splitCardMethods({ methods, allMethods }) {
128
+ if (methods === allMethods) return {
129
+ collapsedCardMethods: EMPTY_METHODS,
130
+ standaloneMethods: methods
131
+ };
132
+ return {
133
+ collapsedCardMethods: methods.filter((method) => CARD_METHOD_GROUPS.includes(method.groupName)),
134
+ standaloneMethods: methods.filter((method) => !CARD_METHOD_GROUPS.includes(method.groupName))
135
+ };
136
+ }
137
+ /**
138
+ * Whether a rendered `PaymentMethodList` can bring the card form back — the fact it registers as
139
+ * `methodListMounted` (T191, FR-4, Decision 5; before it, T171's "can dispatch an in-scope id").
140
+ *
141
+ * `decisions.md` ruled the guarantee this stands for: only hide the card form when something on the
142
+ * page is capable of un-hiding it. A rendered card option always is: it dispatches its candidate when
143
+ * `resolvePreselectionCandidateId` names one, and records card mode through `selectNewCard()` when it
144
+ * names none. Without a card option — `group: ''`, or a scope with no card-group member — only a row
145
+ * bound to a card method (`isCardMethod`) can bring the form back. A list of non-card rows alone offers
146
+ * no card to pay with, so it registers nothing, and the form it could not reveal never hides.
147
+ */
148
+ function resolveCanRevealCardPayment({ collapsedCardMethods, standaloneMethods }) {
149
+ return collapsedCardMethods.length > 0 || standaloneMethods.some(isCardMethod);
150
+ }
151
+ /**
152
+ * The card-payment collapse every card leaf reads (T191, FR-5, Decision 2): the persisted selection is
153
+ * one this section does not serve or one that is not a card, card mode is not holding the form open,
154
+ * and a mounted list can bring the form back (INV-2). Exported for direct unit testing, mirroring
155
+ * `isSelectionOutOfScope`.
156
+ */
157
+ function resolveCardPaymentCollapsed({ selectionOutOfScope, selectionNonCard, cardModeActive, methodListMounted }) {
158
+ return (selectionOutOfScope || selectionNonCard) && !cardModeActive && methodListMounted;
159
+ }
160
+ /**
161
+ * The whole preselection rule as one pure function (T74, FR-1, Decision 1; the credit-first tiebreak is
162
+ * T140, FR-6a, Decision 3). Returns the method id to dispatch, or `null`. Keys on method identity:
163
+ * whichever tier of `resolvePreselectionCandidateId` answers, the result is invariant under any
164
+ * reordering of `methods` — a future ordering prop cannot silently change what gets preselected (the
165
+ * T79 ruling).
166
+ *
167
+ * Returns `null` while `loading`, when any method id is persisted — `persistedMethodId` is the raw,
168
+ * unscoped fact, so this covers a selection in scope *and* one out of it alike — when a saved-card
169
+ * account is persisted, while `placing`, when `resolvePreselectionCandidateId` finds no candidate
170
+ * (neither the whole scoped list nor its credit subset is singular), when that candidate's id has
171
+ * already been attempted this mounted root (FR-4), and when `group` is `''` (fix pass, finding R4).
172
+ *
173
+ * The `group` check keeps preselection off with scoping off: `methods` is then `allMethods`
174
+ * unfiltered, so a singular result can no longer mean "the one card method" — it can just as easily be
175
+ * a store's single non-card method (e.g. one Pix method, no credit card configured at all).
176
+ * Preselection takes no card expression, although this file has one since T191 (`isCardMethod`,
177
+ * Decision 1): the credit-first tiebreak names one specific, already-authored group rather than
178
+ * classifying card-ness, and preselection stays off under `group=''` by the engineer's 2026-09-29
179
+ * ruling, which ADR-0023 records (T191, OQ-2). The short-circuit below still returns before
180
+ * `resolvePreselectionCandidateId` is ever called, so scoping off adds no card-ness notion to the
181
+ * opt-out path. The collapse this check was first made symmetric with classifies under `group=''`
182
+ * since T191 (Decision 9).
183
+ */
184
+ function resolveMethodPreselection({ loading, methods, persistedMethodId, selectedAccountId, placing, attemptedMethodId, group }) {
185
+ if (loading) return null;
186
+ if (group === "") return null;
187
+ if (persistedMethodId !== null) return null;
188
+ if (selectedAccountId !== null) return null;
189
+ if (placing) return null;
190
+ const candidateId = resolvePreselectionCandidateId(methods);
191
+ if (candidateId === null || candidateId === attemptedMethodId) return null;
192
+ return candidateId;
193
+ }
194
+ const EMPTY_PLACING_STATE = { placingKeys: /* @__PURE__ */ new Set() };
195
+ const EMPTY_METHOD_LIST_PRESENCE_STATE = { mountedKeys: /* @__PURE__ */ new Set() };
196
+ /** `$data` is the payment slice the root has already unwrapped from its keyed `{ payment, cart }`. */
197
+ function usePaymentMethodSelection({ group, $data, $actions }) {
198
+ const loading = $data == null;
199
+ const methodsReadable = $data?.methods !== void 0;
200
+ const allMethods = $data?.methods ?? EMPTY_METHODS;
201
+ const persistedMethodId = $data?.selectedMethodId ?? null;
202
+ const selectedAccountId = $data?.selectedAccountId ?? null;
203
+ const methods = useMemo(() => {
204
+ if (group === "") return allMethods;
205
+ const groups = typeof group === "string" ? [group] : group;
206
+ return allMethods.filter((method) => groups.includes(method.groupName));
207
+ }, [allMethods, group]);
208
+ const selectedMethod = useMemo(() => methods.find((method) => method.id === persistedMethodId) ?? null, [methods, persistedMethodId]);
209
+ const selectedMethodId = selectedMethod?.id ?? null;
210
+ const selectionOutOfScope = isSelectionOutOfScope({
211
+ persistedMethodId,
212
+ selectedMethod,
213
+ methodsReadable
214
+ });
215
+ const selectionNonCard = selectedMethod !== null && !isCardMethod(selectedMethod);
216
+ const selectedBin = !methodsReadable || selectedMethod ? $data?.selectedBin ?? null : null;
217
+ const [draftPath, setDraftPathState] = useState(null);
218
+ const [draftBin, setDraftBinState] = useState(null);
219
+ const draftPathRef = useRef(null);
220
+ const draftBinRef = useRef(null);
221
+ const writeDraftPath = useCallback((next) => {
222
+ draftPathRef.current = next;
223
+ setDraftPathState(next);
224
+ }, []);
225
+ const writeDraftBin = useCallback((next) => {
226
+ draftBinRef.current = next;
227
+ setDraftBinState(next);
228
+ }, []);
229
+ const [draftSaveCard, setDraftSaveCard] = useState(null);
230
+ const [draftCardLabel, setDraftCardLabel] = useState(null);
231
+ const [draftDocument, setDraftDocument] = useState(null);
232
+ const [draftBillingAddressDifferent, setDraftBillingAddressDifferent] = useState(null);
233
+ const [billingAddressUnsavedState, setBillingAddressUnsavedState] = useState(EMPTY_BILLING_ADDRESS_UNSAVED_STATE);
234
+ const [placingState, setPlacingState] = useState(EMPTY_PLACING_STATE);
235
+ const [methodListPresenceState, setMethodListPresenceState] = useState(EMPTY_METHOD_LIST_PRESENCE_STATE);
236
+ const methodListMounted = methodListPresenceState.mountedKeys.size > 0;
237
+ const [cardMode, setCardMode] = useState(false);
238
+ const cardModeActive = cardMode && (selectionOutOfScope || selectionNonCard);
239
+ const cardPaymentCollapsed = resolveCardPaymentCollapsed({
240
+ selectionOutOfScope,
241
+ selectionNonCard,
242
+ cardModeActive,
243
+ methodListMounted
244
+ });
245
+ const [attempt, setAttempt] = useState(null);
246
+ const [selectionError, setSelectionError] = useState(null);
247
+ const [selectMethodPending, setSelectMethodPending] = useState(false);
248
+ const selectionToken = useRef(0);
249
+ const setMethod = $actions?.payment?.setMethod;
250
+ const dispatchSelection = useCallback((input, surfaceError) => {
251
+ if (surfaceError) setSelectionError(null);
252
+ const plainPick = !input.accountId && input.bin === void 0;
253
+ const pickedMethod = plainPick ? allMethods.find((method) => method.id === input.methodId) : void 0;
254
+ const endsCardMode = pickedMethod !== void 0 && !isCardMethod(pickedMethod);
255
+ let revertDraft = null;
256
+ if (!plainPick) {
257
+ const nextPath = input.accountId ? "saved-card" : "new-card";
258
+ const previousPath = draftPathRef.current;
259
+ writeDraftPath(nextPath);
260
+ writeDraftBin(input.bin ?? null);
261
+ revertDraft = () => {
262
+ writeDraftPath(previousPath);
263
+ writeDraftBin(null);
264
+ };
265
+ }
266
+ if (!setMethod) return false;
267
+ const token = ++selectionToken.current;
268
+ setSelectMethodPending(true);
269
+ setMethod(input).then((userErrors) => {
270
+ if (selectionToken.current !== token) return;
271
+ setSelectMethodPending(false);
272
+ const [firstError] = userErrors;
273
+ if (firstError) {
274
+ if (surfaceError) setSelectionError(firstError.message);
275
+ else console.error("[usePaymentMethodSelection] preselection rejected", firstError.code);
276
+ revertDraft?.();
277
+ } else if (plainPick) {
278
+ writeDraftBin(null);
279
+ if (endsCardMode) setCardMode(false);
280
+ }
281
+ }).catch(() => {
282
+ if (selectionToken.current !== token) return;
283
+ setSelectMethodPending(false);
284
+ if (surfaceError) setSelectionError("Unable to select the payment method. Try again.");
285
+ else console.error("[usePaymentMethodSelection] preselection request failed");
286
+ revertDraft?.();
287
+ });
288
+ return true;
289
+ }, [
290
+ setMethod,
291
+ writeDraftPath,
292
+ writeDraftBin,
293
+ allMethods
294
+ ]);
295
+ const selectMethod = useCallback((input) => dispatchSelection(input, true), [dispatchSelection]);
296
+ const selectNewCard = useCallback(() => {
297
+ writeDraftPath("new-card");
298
+ writeDraftBin(null);
299
+ setSelectionError(null);
300
+ setCardMode(true);
301
+ }, [writeDraftPath, writeDraftBin]);
302
+ const setDraftBillingAddressUnsaved = useCallback((key, unsaved) => {
303
+ setBillingAddressUnsavedState((prev) => {
304
+ const isMember = prev.unsavedKeys.has(key);
305
+ if (prev.everPublished && unsaved === isMember) return prev;
306
+ const nextKeys = new Set(prev.unsavedKeys);
307
+ if (unsaved) nextKeys.add(key);
308
+ else nextKeys.delete(key);
309
+ return {
310
+ everPublished: true,
311
+ unsavedKeys: nextKeys
312
+ };
313
+ });
314
+ }, []);
315
+ /**
316
+ * Publishes or retracts one `PlaceOrder` instance's in-flight state (T28, FR-1/FR-7). Ref-counted by
317
+ * `key`, mirroring `setDraftBillingAddressUnsaved` above: bails out to the same state by reference
318
+ * once `key`'s membership already matches `placing`, so an effect re-running on an unrelated
319
+ * dependency change does not force every leaf under this root to re-render (NFR-2). Unlike
320
+ * `setDraftBillingAddressUnsaved`, there is no `everPublished` tri-state to track here — `placing`
321
+ * has only two states, so "no publisher has ever called this" and "every publisher has retracted"
322
+ * collapse to the same `false` (I-5).
323
+ */
324
+ const setPlacing = useCallback((key, nextPlacing) => {
325
+ setPlacingState((prev) => {
326
+ if (nextPlacing === prev.placingKeys.has(key)) return prev;
327
+ const nextKeys = new Set(prev.placingKeys);
328
+ if (nextPlacing) nextKeys.add(key);
329
+ else nextKeys.delete(key);
330
+ return { placingKeys: nextKeys };
331
+ });
332
+ }, []);
333
+ /**
334
+ * Registers or retracts one `PaymentMethodList` instance's ability to bring the card form back (fix
335
+ * pass, finding R2; T171, Decision 4; T191, FR-4). Ref-counted by `key`, mirroring `setPlacing` above,
336
+ * including the same by-reference bail-out.
337
+ */
338
+ const setMethodListMounted = useCallback((key, mounted) => {
339
+ setMethodListPresenceState((prev) => {
340
+ if (mounted === prev.mountedKeys.has(key)) return prev;
341
+ const nextKeys = new Set(prev.mountedKeys);
342
+ if (mounted) nextKeys.add(key);
343
+ else nextKeys.delete(key);
344
+ return { mountedKeys: nextKeys };
345
+ });
346
+ }, []);
347
+ const attemptResolved = attempt !== null && (attempt.phase === "refused" || attempt.phase === "settled");
348
+ const placing = placingState.placingKeys.size > 0 && !attemptResolved;
349
+ const attemptedPreselectionIdsRef = useRef(/* @__PURE__ */ new Set());
350
+ useEffect(() => {
351
+ const candidateId = resolvePreselectionCandidateId(methods);
352
+ const alreadyAttempted = candidateId !== null && attemptedPreselectionIdsRef.current.has(candidateId);
353
+ const target = resolveMethodPreselection({
354
+ loading,
355
+ methods,
356
+ persistedMethodId,
357
+ selectedAccountId,
358
+ placing,
359
+ attemptedMethodId: alreadyAttempted ? candidateId : null,
360
+ group
361
+ });
362
+ if (target === null) return;
363
+ if (dispatchSelection({ methodId: target }, false)) attemptedPreselectionIdsRef.current.add(target);
364
+ }, [
365
+ loading,
366
+ methods,
367
+ persistedMethodId,
368
+ selectedAccountId,
369
+ placing,
370
+ group,
371
+ dispatchSelection
372
+ ]);
373
+ const draft = useMemo(() => ({
374
+ path: draftPath ?? (selectedAccountId ? "saved-card" : null),
375
+ bin: draftBin,
376
+ saveCard: draftSaveCard,
377
+ cardLabel: draftCardLabel,
378
+ document: draftDocument,
379
+ billingAddressDifferent: draftBillingAddressDifferent,
380
+ billingAddressUnsaved: billingAddressUnsavedState.everPublished ? billingAddressUnsavedState.unsavedKeys.size > 0 : null
381
+ }), [
382
+ draftPath,
383
+ selectedAccountId,
384
+ draftBin,
385
+ draftSaveCard,
386
+ draftCardLabel,
387
+ draftDocument,
388
+ draftBillingAddressDifferent,
389
+ billingAddressUnsavedState
390
+ ]);
391
+ const binConfirmation = useMemo(() => resolveBinConfirmation({
392
+ selectedBin: $data?.selectedBin,
393
+ draftBin,
394
+ selectMethodPending
395
+ }), [
396
+ $data,
397
+ draftBin,
398
+ selectMethodPending
399
+ ]);
400
+ return useMemo(() => ({
401
+ methods,
402
+ allMethods,
403
+ persistedMethodId,
404
+ selectedMethodId,
405
+ selectionOutOfScope,
406
+ methodListMounted,
407
+ setMethodListMounted,
408
+ cardModeActive,
409
+ cardPaymentCollapsed,
410
+ selectedAccountId,
411
+ selectedBin,
412
+ binConfirmation,
413
+ selectedMethod,
414
+ draft,
415
+ loading,
416
+ selectionError,
417
+ selectMethod,
418
+ selectNewCard,
419
+ setDraftBin: writeDraftBin,
420
+ setDraftSaveCard,
421
+ setDraftCardLabel,
422
+ setDraftDocument,
423
+ setDraftBillingAddressDifferent,
424
+ setDraftBillingAddressUnsaved,
425
+ placing,
426
+ setPlacing,
427
+ attempt,
428
+ setAttempt
429
+ }), [
430
+ methods,
431
+ allMethods,
432
+ persistedMethodId,
433
+ selectedMethodId,
434
+ selectionOutOfScope,
435
+ methodListMounted,
436
+ setMethodListMounted,
437
+ cardModeActive,
438
+ cardPaymentCollapsed,
439
+ selectedAccountId,
440
+ selectedBin,
441
+ binConfirmation,
442
+ selectedMethod,
443
+ draft,
444
+ loading,
445
+ selectionError,
446
+ selectMethod,
447
+ selectNewCard,
448
+ writeDraftBin,
449
+ setDraftBillingAddressUnsaved,
450
+ placing,
451
+ setPlacing,
452
+ attempt
453
+ ]);
454
+ }
455
+ //#endregion
456
+ export { CARD_METHOD_GROUPS, isCardMethod, isSelectionOutOfScope, resolveBinConfirmation, resolveCanRevealCardPayment, resolveCardPaymentCollapsed, resolveMethodPreselection, resolvePreselectionCandidateId, splitCardMethods, usePaymentMethodSelection };
457
+
458
+ //# sourceMappingURL=use-payment-method-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-payment-method-selection.js","names":[],"sources":["../../../src/matter/payment/use-payment-method-selection.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport type { PaymentMethodSelectInput } from '../../sdk/payment/operations.js';\nimport type { PaymentSubmissionPath } from '../../sdk/payment/submission-registry.js';\nimport type { PlaceOrderAttempt, SetPlaceOrderAttempt } from '../../sdk/payment/use-place-order.js';\n\n/**\n * The payment section's selection engine (specs/payment-section-root.md, T5 of the Credit Card epic).\n * Single source of selection behavior behind both providers of the `payment` context —\n * `PaymentSectionRoot` (always-visible) and `PaymentSectionController` (collapsed overlay) — so the two\n * surfaces cannot drift (NFR-4), mirroring `useDeliveryOptionSelection`.\n *\n * It owns four things: group scoping of the method list (Decision 4), the client-only draft facts\n * (Decision 3; widened by T7 with three absent-tolerant facts, specs/card-entry.md Decision 4), the\n * request-sequenced `payment.setMethod` dispatch (ADR-0010), and the inline error state. It\n * deliberately owns *no* mirror of `selectedMethodId` / `selectedAccountId` — both are read from\n * `$data` on every render, which is `delivery-options`' \"no local selection state\" rule kept\n * verbatim (FR-6).\n */\n\n// ---------------------------------------------------------------------------\n// Raw data types (shape of the reactive fragment result). `PaymentSectionData` is the `Payment` slice\n// of the roots' keyed `$data` (ADR-0015, arity 2: `{ payment, cart }`); the roots unwrap it before it\n// reaches the engine. Fields are optional because the slice arrives partial while the entity resolves;\n// `null` is the unresolved state.\n// ---------------------------------------------------------------------------\n\nexport type PaymentSectionValidator = Readonly<{\n numberMask: string | null;\n numberRegex: string | null;\n cvvMask: string | null;\n cvvRegex: string | null;\n cvvRequired: boolean;\n expirationRequired: boolean;\n holderNameRequired: boolean;\n billingAddressRequired: boolean;\n documentDisplayable: boolean;\n documentRequired: boolean;\n}>;\n\nexport type PaymentSectionMethod = Readonly<{\n id: string;\n name: string;\n groupName: string;\n validator: PaymentSectionValidator;\n}>;\n\nexport type PaymentSectionData = {\n methods?: readonly PaymentSectionMethod[];\n selectedMethodId?: string | null;\n selectedAccountId?: string | null;\n /** Persisted BIN off `payments[0]`. `undefined` while the entity resolves or when a hand-written slice omits it. */\n selectedBin?: string | null;\n} | null;\n\n/**\n * Which entry path the shopper is on. Aliases the submission channel's key type (T13), which the\n * place-order sequence resolves a continuation by — one definition, so the draft fact and the\n * registry key cannot drift apart.\n */\nexport type PaymentPath = PaymentSubmissionPath;\n\n/**\n * The client-only half of the pending selection — the facts `Query.payment` cannot carry\n * (Decision 3). Never the card number, CVV, expiration, or holder name: those live only in the card\n * entry's local state (RFC D13).\n *\n * `saveCard`, `cardLabel`, and `document` are T7's widening (specs/card-entry.md, Decision 4): three\n * non-sensitive facts T10 (save-card + label) and T8 (cardholder document) will eventually collect,\n * which T7's submission continuation folds into the gateway card object without collecting them\n * itself. Nullable rather than optional (`?:`), matching `path`/`bin`'s existing \"always present,\n * `null` means absent\" idiom instead of introducing a second absence convention in the same type.\n * T8/T10 have not landed, so all three are `null` in practice today — every reader must tolerate that.\n */\nexport type PaymentDraft = Readonly<{\n /**\n * `'new-card'` once the shopper leaves the saved-card path — a choice that precedes any server\n * state, since there is no method to select until card entry detects one from the BIN. Falls back\n * to `'saved-card'` while a saved card is the persisted selection, else `null`.\n */\n path: PaymentPath | null;\n /** Detected leading digits, for installment lookup — never the full number. A successful plain pick clears it. */\n bin: string | null;\n /** Save-this-card flag (T10). `null` until T10 lands and calls `setDraftSaveCard`. */\n saveCard: boolean | null;\n /** Shopper-assigned saved-card label (T10). `null` until T10 lands and calls `setDraftCardLabel`. */\n cardLabel: string | null;\n /** Cardholder document, by country (T8). `null` until T8 lands and calls `setDraftDocument`. */\n document: string | null;\n /**\n * Whether the billing address differs from delivery (T11). Mirrors the gateway transaction\n * descriptor's own `billingAddressDifferent` field name (`sdk/payment/gateway.ts:81`) so the fact\n * cannot drift between this publisher and its only consumer, T13's `usePlaceOrder`. `null` until\n * T11's `BillingAddress` lands and calls `setDraftBillingAddressDifferent`.\n */\n billingAddressDifferent: boolean | null;\n /**\n * True while `BillingAddress` is on the custom-address path with nothing persisted — the exact\n * window its own `role=\"status\"` notice renders in (T24). Distinct from `billingAddressDifferent`,\n * which says a different address *is* persisted (PR #544 N1) and is what the gateway descriptor\n * reads. `null` and `false` are equivalent to the submit gate; `null` is the never-touched state, so\n * an absent `BillingAddress` never blocks placement. Nullable, not optional — every `PaymentDraft`\n * literal must carry it.\n */\n billingAddressUnsaved: boolean | null;\n}>;\n\nconst EMPTY_METHODS: readonly PaymentSectionMethod[] = [];\n\n/**\n * Ref-counted state backing `billingAddressUnsaved` (fix pass, findings R2/S2 — see\n * `setDraftBillingAddressUnsaved`'s own doc comment for why a bare boolean cannot represent \"one of\n * several mounted `BillingAddress` instances still needs this\" once another one unmounts).\n */\ntype BillingAddressUnsavedState = Readonly<{\n /** `false` until any `BillingAddress` instance has ever called the setter, in either direction. */\n everPublished: boolean;\n /** Instance keys (`billing-address.tsx`'s own `useId()`) currently asserting \"nothing persisted yet\". */\n unsavedKeys: ReadonlySet<string>;\n}>;\n\nconst EMPTY_BILLING_ADDRESS_UNSAVED_STATE: BillingAddressUnsavedState = {\n everPublished: false,\n unsavedKeys: new Set(),\n};\n\n/**\n * Whether the BIN the server has persisted agrees with the client's current `draft.bin`.\n *\n * - `'confirmed'` — safe to dispatch a BIN-matched write. Either the two agree with no\n * selection in flight, or `draft.bin` is null so the client asserts no BIN of its own.\n * - `'pending'` — a `payment.setMethod` is in flight, or the two disagree. A consumer\n * must hold any dispatch whose pricing depends on the BIN.\n * - `'unknown'` — the persisted BIN is not readable (the entity has not resolved, or the\n * slice omits the field). Consumers must NOT hold: absence is not evidence of a mismatch.\n */\nexport type PaymentBinConfirmation = 'unknown' | 'pending' | 'confirmed';\n\n/**\n * Resolves the BIN confirmation signal from the persisted BIN, the client draft BIN,\n * and whether a `payment.setMethod` is in flight. Exported for unit testing without\n * rendering, mirroring how `findInstallmentOption` / `resolveInstallmentTiers` are\n * exported from `installments.tsx`.\n */\nexport function resolveBinConfirmation({\n selectedBin,\n draftBin,\n selectMethodPending,\n}: {\n selectedBin: string | null | undefined;\n draftBin: string | null;\n selectMethodPending: boolean;\n}): PaymentBinConfirmation {\n if (draftBin === null) return 'confirmed';\n if (selectedBin === undefined) return 'unknown';\n if (selectMethodPending) return 'pending';\n if (selectedBin !== draftBin) return 'pending';\n return 'confirmed';\n}\n\n/**\n * The persisted selection is one this section does not serve (T74, FR-7, Decision 7) — `true` only\n * when a method id is persisted and it does not resolve inside the scoped `methods` list. `false` in\n * every other state, including \"nothing persisted\" (INV-5) and \"still loading\" (both leave\n * `persistedMethodId` `null`, which alone already keeps this `false`). One of the two selection facts\n * behind `cardPaymentCollapsed` (T191, FR-5). Exported for direct unit testing, mirroring\n * `resolveBinConfirmation`.\n *\n * `methodsReadable` (fix pass, finding R1) tells \"the list is empty because it was actually read and\n * holds nothing in scope\" apart from \"the list is empty because it could not be read at all\" — a\n * resolved-but-partial `$data` slice can omit `methods` entirely (a hand-written slice, explicitly\n * supported per `PaymentSectionData.selectedBin`'s own doc comment, or a revalidation frame), and that\n * must not read as \"the method is not served\": before this guard, that state made a perfectly valid,\n * in-scope selection collapse `CardEntry` and wipe the shopper's typed CVV for a section that simply\n * had not been told its own method list yet.\n */\nexport function isSelectionOutOfScope({\n persistedMethodId,\n selectedMethod,\n methodsReadable,\n}: {\n persistedMethodId: string | null;\n selectedMethod: PaymentSectionMethod | null;\n methodsReadable: boolean;\n}): boolean {\n return methodsReadable && persistedMethodId !== null && selectedMethod === null;\n}\n\n/**\n * The credit-card payment group, named separately from `payment.tsx`'s own `DEFAULT_METHOD_GROUP`\n * (T140, Decision 3): once that constant widens to the whole default *set*, it can no longer double as\n * \"the credit group's identifier,\" and reading \"the first member of the set\" would make preselection\n * depend on prop authoring order — reintroducing the position-dependence the T79 ruling closed.\n * Declared here, not in `payment.tsx`: the import runs `payment.tsx` -> `use-payment-method-selection.js`\n * (`payment.tsx:8-12`) and never the other way, so the reverse would be a cycle.\n */\nconst CREDIT_CARD_PAYMENT_GROUP = 'creditCardPaymentGroup';\n\n/**\n * The one method identity a length-1 list offers, or `null` for any other length. Building block for\n * `resolvePreselectionCandidateId` below, which applies it twice — once to the whole scoped list, once\n * to its credit subset. No longer called directly by `resolveMethodPreselection` or its caller (T140,\n * Decision 3): routing both through that one shared function is what keeps their notion of \"the\n * candidate\" from drifting apart (fix pass, finding R6).\n */\nfunction soleMethodId(methods: readonly PaymentSectionMethod[]): string | null {\n if (methods.length !== 1) return null;\n const [onlyMethod] = methods;\n return onlyMethod?.id ?? null;\n}\n\n/**\n * The whole preselection candidate rule (T140, FR-6a, Decision 3): the sole in-scope method when the\n * scoped list holds exactly one, or — when it does not — the sole method in the credit group, so a\n * store offering one credit and one debit method keeps the preselection it had before debit was\n * admitted. `null` when neither tier yields a single identity. Keyed on identity, never position, the\n * same property `soleMethodId` already had (the T79 ruling).\n *\n * Shared by `resolveMethodPreselection`'s own candidate lookup and its caller's \"was this id ever\n * attempted this mount\" lookup (fix pass, finding R6) — one definition, so a tiebreak added to only one\n * of the two could never let them drift the way it would if each recomputed its own.\n */\nexport function resolvePreselectionCandidateId(methods: readonly PaymentSectionMethod[]): string | null {\n const soleId = soleMethodId(methods);\n if (soleId !== null) return soleId;\n const creditMethods = methods.filter((method) => method.groupName === CREDIT_CARD_PAYMENT_GROUP);\n return soleMethodId(creditMethods);\n}\n\n/** The debit payment group, named beside `CREDIT_CARD_PAYMENT_GROUP` for the same reason (T140,\n * Decision 3): reading a member off `DEFAULT_METHOD_GROUP` would make the classification depend on\n * prop-authoring order. */\nconst DEBIT_CARD_PAYMENT_GROUP = 'debitCardPaymentGroup';\n\n/**\n * The payment groups `PaymentMethodList` treats as card-shaped and collapses into one control (T171,\n * FR-1, Decision 6).\n *\n * Deliberately a **second** constant beside `payment.tsx`'s `DEFAULT_METHOD_GROUP`, whose members\n * happen to be the same two strings today: that one is the default *authored scope*, which a store may\n * override with its own group names, while this one is what the section treats as a card group.\n * Collapsing on the authored scope instead would collapse a store's Pix method the moment it authored\n * `group: ['pixPaymentGroup']`.\n *\n * The list's one card control is built from these groups alone. The section's card expression is\n * `isCardMethod` below, these groups or a card-number pattern, which the engineer ruled on 2026-09-29\n * and ADR-0023 records (T191, OQ-3). A store that renamed its card groups gets no card control at all,\n * only one row per method, which is the pre-T171 rendering: a safe degrade, never a wrong dispatch.\n */\nexport const CARD_METHOD_GROUPS: readonly string[] = [CREDIT_CARD_PAYMENT_GROUP, DEBIT_CARD_PAYMENT_GROUP];\n\n/**\n * Whether `method` is a card (T191, FR-1, Decision 1): its group is one of `CARD_METHOD_GROUPS`, or its\n * validator carries a card-number pattern. It decides the card-payment collapse, whether a list row can\n * bring the card form back, `CardEntry`'s refusal to post under a method that is not a card, and whether\n * a persisted method's own installment options apply. The list's fold keeps its group-only rule (OQ-3).\n *\n * Where one signal is missing it errs toward \"card\", since a wrong collapse would hide a form the shopper\n * can pay with. The collapse and `CardEntry`'s non-card refusal read this same classifier, so neither\n * catches the other's miss: a method wrongly counted as a card is neither collapsed nor refused as one\n * that is not. `CardEntry`'s network check still refuses a typed card under it, unless the typed number\n * is detected as that method itself.\n */\nexport function isCardMethod(method: PaymentSectionMethod): boolean {\n return CARD_METHOD_GROUPS.includes(method.groupName) || Boolean(method.validator.numberRegex);\n}\n\n/** How `PaymentMethodList` renders a scoped method list: one control for the card-group members, one\n * row each for the rest (T171, FR-1/FR-2). Every in-scope method lands in exactly one of the two. */\nexport type PaymentMethodListSplit = Readonly<{\n /** The card-group members the single collapsed control stands for. Empty when there is no card control. */\n collapsedCardMethods: readonly PaymentSectionMethod[];\n /** Every other in-scope method, each keeping its own row bound to its own `method.id` (FR-2). */\n standaloneMethods: readonly PaymentSectionMethod[];\n}>;\n\n/**\n * Splits a scoped method list the way `PaymentMethodList` renders it (T171, FR-1/FR-2/FR-7,\n * Decision 6). Order-preserving within each list, so the store's own `methods` order still decides the\n * rows. Exported for direct unit testing, mirroring `resolveBinConfirmation` / `isSelectionOutOfScope`.\n *\n * **Scoping-off (`group: ''`) is read off the two lists, not off `group` itself.** `PaymentMethodList`\n * is an independently-placeable part with no `group` prop of its own, and `group` cannot join the\n * `payment` context (INV-6) — so the fact has to arrive through what the context already carries.\n * `usePaymentMethodSelection` returns `allMethods` *by reference* for `group: ''` and a fresh\n * `.filter()` result for every other value, so `methods === allMethods` holds exactly when the section\n * narrowed nothing. That identity is pinned directly in `use-payment-method-selection.test.tsx`.\n *\n * With scoping off there is no collapsing at all: one row per method, mirroring\n * `resolveMethodPreselection`'s own `if (group === '') return null;`. The reason first given for both,\n * that the section must not classify card-ness with scoping off, no longer holds: the card-payment\n * collapse classifies under `group: ''` too, and a card row brings the card form back (T191,\n * Decision 9). The list's opt-out stands, as the engineer ruled on 2026-09-29 (T191, OQ-2).\n */\nexport function splitCardMethods({\n methods,\n allMethods,\n}: {\n methods: readonly PaymentSectionMethod[];\n allMethods: readonly PaymentSectionMethod[];\n}): PaymentMethodListSplit {\n if (methods === allMethods) return { collapsedCardMethods: EMPTY_METHODS, standaloneMethods: methods };\n return {\n collapsedCardMethods: methods.filter((method) => CARD_METHOD_GROUPS.includes(method.groupName)),\n standaloneMethods: methods.filter((method) => !CARD_METHOD_GROUPS.includes(method.groupName)),\n };\n}\n\n/**\n * Whether a rendered `PaymentMethodList` can bring the card form back — the fact it registers as\n * `methodListMounted` (T191, FR-4, Decision 5; before it, T171's \"can dispatch an in-scope id\").\n *\n * `decisions.md` ruled the guarantee this stands for: only hide the card form when something on the\n * page is capable of un-hiding it. A rendered card option always is: it dispatches its candidate when\n * `resolvePreselectionCandidateId` names one, and records card mode through `selectNewCard()` when it\n * names none. Without a card option — `group: ''`, or a scope with no card-group member — only a row\n * bound to a card method (`isCardMethod`) can bring the form back. A list of non-card rows alone offers\n * no card to pay with, so it registers nothing, and the form it could not reveal never hides.\n */\nexport function resolveCanRevealCardPayment({\n collapsedCardMethods,\n standaloneMethods,\n}: PaymentMethodListSplit): boolean {\n return collapsedCardMethods.length > 0 || standaloneMethods.some(isCardMethod);\n}\n\n/**\n * The card-payment collapse every card leaf reads (T191, FR-5, Decision 2): the persisted selection is\n * one this section does not serve or one that is not a card, card mode is not holding the form open,\n * and a mounted list can bring the form back (INV-2). Exported for direct unit testing, mirroring\n * `isSelectionOutOfScope`.\n */\nexport function resolveCardPaymentCollapsed({\n selectionOutOfScope,\n selectionNonCard,\n cardModeActive,\n methodListMounted,\n}: {\n selectionOutOfScope: boolean;\n selectionNonCard: boolean;\n cardModeActive: boolean;\n methodListMounted: boolean;\n}): boolean {\n return (selectionOutOfScope || selectionNonCard) && !cardModeActive && methodListMounted;\n}\n\n/**\n * The whole preselection rule as one pure function (T74, FR-1, Decision 1; the credit-first tiebreak is\n * T140, FR-6a, Decision 3). Returns the method id to dispatch, or `null`. Keys on method identity:\n * whichever tier of `resolvePreselectionCandidateId` answers, the result is invariant under any\n * reordering of `methods` — a future ordering prop cannot silently change what gets preselected (the\n * T79 ruling).\n *\n * Returns `null` while `loading`, when any method id is persisted — `persistedMethodId` is the raw,\n * unscoped fact, so this covers a selection in scope *and* one out of it alike — when a saved-card\n * account is persisted, while `placing`, when `resolvePreselectionCandidateId` finds no candidate\n * (neither the whole scoped list nor its credit subset is singular), when that candidate's id has\n * already been attempted this mounted root (FR-4), and when `group` is `''` (fix pass, finding R4).\n *\n * The `group` check keeps preselection off with scoping off: `methods` is then `allMethods`\n * unfiltered, so a singular result can no longer mean \"the one card method\" — it can just as easily be\n * a store's single non-card method (e.g. one Pix method, no credit card configured at all).\n * Preselection takes no card expression, although this file has one since T191 (`isCardMethod`,\n * Decision 1): the credit-first tiebreak names one specific, already-authored group rather than\n * classifying card-ness, and preselection stays off under `group=''` by the engineer's 2026-09-29\n * ruling, which ADR-0023 records (T191, OQ-2). The short-circuit below still returns before\n * `resolvePreselectionCandidateId` is ever called, so scoping off adds no card-ness notion to the\n * opt-out path. The collapse this check was first made symmetric with classifies under `group=''`\n * since T191 (Decision 9).\n */\nexport function resolveMethodPreselection({\n loading,\n methods,\n persistedMethodId,\n selectedAccountId,\n placing,\n attemptedMethodId,\n group,\n}: {\n loading: boolean;\n methods: readonly PaymentSectionMethod[];\n persistedMethodId: string | null;\n selectedAccountId: string | null;\n placing: boolean;\n attemptedMethodId: string | null;\n group: string | readonly string[];\n}): string | null {\n if (loading) return null;\n if (group === '') return null;\n if (persistedMethodId !== null) return null;\n if (selectedAccountId !== null) return null;\n if (placing) return null;\n const candidateId = resolvePreselectionCandidateId(methods);\n if (candidateId === null || candidateId === attemptedMethodId) return null;\n return candidateId;\n}\n\n/**\n * Ref-counted state backing `placing` (T28). A bare boolean cannot tell \"no publisher is placing\n * anymore\" apart from \"a different, still-mounted publisher still is\" once one of several unmounts —\n * the same reason `BillingAddressUnsavedState` exists, and the failure `place-order.md:96`'s\n * unmount-on-close case would otherwise produce: a section frozen with no CTA left to unfreeze it.\n */\ntype PlacingState = Readonly<{\n /** Publisher instance keys (`place-order.tsx`'s own `useId()`) with a sequence in flight. */\n placingKeys: ReadonlySet<string>;\n}>;\n\nconst EMPTY_PLACING_STATE: PlacingState = { placingKeys: new Set() };\n\n/**\n * Ref-counted state backing `methodListMounted` (fix pass, finding R2), mirroring `PlacingState`: at\n * most one `PaymentMethodList` is ever really composed per root, but ref-counting by instance key\n * — rather than a bare boolean two effects could race to overwrite — is the same defensive shape this\n * file already uses for `placingState`/`billingAddressUnsavedState`.\n */\ntype MethodListPresenceState = Readonly<{\n /** `PaymentMethodList` instance keys (its own `useId()`) currently able to bring the card form back. */\n mountedKeys: ReadonlySet<string>;\n}>;\n\nconst EMPTY_METHOD_LIST_PRESENCE_STATE: MethodListPresenceState = { mountedKeys: new Set() };\n\nexport type PaymentMethodSelection = {\n /** Methods from the read model, scoped by `group`. Empty until the entity resolves. */\n methods: readonly PaymentSectionMethod[];\n /**\n * Every method from the read model, unfiltered by `group` (fix pass, finding S1). Exposed so\n * `usePaymentSubmitGate()` (`matter/payment/use-payment-submit-gate.ts`) can resolve\n * `persistedMethodId` the same *unscoped* way `usePlaceOrder`'s own pre-flight does\n * (`sdk/payment/use-place-order.ts`, off its own independent fragment slice — neither hook may read\n * the other's module, Decision 2) — a method whose `groupName` sits outside this root's configured\n * `group` must still gate a required document if `usePlaceOrder` is about to treat it as selected.\n * `methods` remains the one every picker and rendering leaf reads from — with one documented\n * exception: `PaymentMethodBadges` (`payment-method-badges.tsx`, T75) reads this unfiltered list\n * instead, because its accepted-method strip is a statement about the store rather than the section,\n * and a wallet method's `groupName` would otherwise never reach it. It detects nothing and renders\n * from no single shared array the way `CardEntry` does, so it carries none of the detection-set\n * risk `methods` exists to guard against (T71, Decision 1).\n */\n allMethods: readonly PaymentSectionMethod[];\n /**\n * The raw persisted method id, unfiltered by `group` — the exact value `$data.selectedMethodId`\n * carries. Unscoped for the same reason `allMethods` is: `usePaymentSubmitGate` must resolve the\n * method `usePlaceOrder`'s own independent by-id lookup would. Rendering leaves want\n * `selectedMethodId` (T74, Decision 4/7).\n */\n persistedMethodId: string | null;\n /**\n * The persisted selection **as this section serves it** — `selectedMethod?.id ?? null`. `null` both\n * when nothing is persisted and when what is persisted sits outside `group`; `persistedMethodId`\n * tells those two apart (T74, Decision 4/7). Read straight off `$data` — never mirrored locally.\n */\n selectedMethodId: string | null;\n /**\n * The persisted selection is one this section does not serve. One of the two selection facts behind\n * `cardPaymentCollapsed` (T74, FR-7; T191, FR-5). `false` whenever `persistedMethodId` is `null`\n * (INV-5), and `false` whenever `methods` could not be read at all rather than genuinely resolving\n * without the persisted id in it (fix pass, finding R1).\n */\n selectionOutOfScope: boolean;\n /**\n * Whether a mounted `PaymentMethodList` can bring the card form back (fix pass, finding R2; re-derived\n * by T171, FR-6, and by T191, FR-4/Decision 5). `cardPaymentCollapsed` requires it: hiding the card\n * form is only safe when something on the page can un-hide it. With no picker composed, the only way\n * back is typing a new card number, which needs `CardEntry` to stay visible and interactive.\n *\n * The name is historical: it stood for \"a picker is mounted\" while every in-scope method had its own\n * row, and for \"a control can dispatch an in-scope id\" from T171 on. `PaymentMethodList` registers the\n * property itself rather than its own presence (`resolveCanRevealCardPayment`): a rendered card option\n * always can, with or without a method it can name, and without one only a card row can.\n *\n * Ref-counted per instance key mirroring `placing`/`billingAddressUnsaved`, though in practice at\n * most one `PaymentMethodList` is ever composed per root.\n */\n methodListMounted: boolean;\n /**\n * Registers (or retracts) one `PaymentMethodList` instance's ability to bring the card form back.\n * Called by `PaymentMethodList` itself. Same signature as before T171; the boolean's meaning is what\n * changed — see `methodListMounted` above.\n */\n setMethodListMounted: (key: string, mounted: boolean) => void;\n /**\n * Card mode is holding the card form open over a persisted selection that is not a card or that this\n * section does not serve (T191, FR-3, Decision 4). `selectNewCard()` turns card mode on, and a plain\n * pick of a method that is not a card turns it off once it succeeds. It counts only over such a\n * selection, so it is `false` whenever nothing or an in-scope card method is persisted (INV-5). Local\n * to this engine: nothing is sent to record it, and a reload forgets it.\n */\n cardModeActive: boolean;\n /**\n * The card payment is collapsed (T191, FR-5, Decision 2): the persisted selection is one this section\n * does not serve or one that is not a card, `cardModeActive` is `false`, and `methodListMounted` is\n * `true`. The one collapse fact every card leaf reads — `CardEntry` hides itself and stays mounted,\n * the others render nothing — so no two leaves can disagree.\n */\n cardPaymentCollapsed: boolean;\n /** The persisted saved-card account reference, read straight off `$data`. */\n selectedAccountId: string | null;\n /**\n * The persisted BIN **as this section serves it** — `null` unless the persisted selection resolves\n * inside the scoped `methods` list, mirroring `selectedMethodId` (fix pass, finding R3). Unscoped\n * before this fix, `useInstallmentsSelection`'s own `draft.bin ?? selectedBin` fallback (its only\n * consumer) could still resolve an out-of-scope method's installment tiers by BIN alone even once\n * FR-9's methodId-arm fix landed — the methodId half of that lookup went `null` for an out-of-scope\n * selection, but the BIN half never did. Never mirrored, like its sibling `selectedMethodId`.\n *\n * Carries the same `methodsReadable` guard as `isSelectionOutOfScope` (fix pass round 2): an absent\n * `methods` forces `selectedMethod` to `null` the same way a genuine out-of-scope selection does, so\n * without the guard this read `null` for the hand-written partial `$data` slice `methodsReadable`'s\n * own doc comment describes as explicitly supported — even while `selectionOutOfScope` correctly\n * stayed `false` for that same slice.\n */\n selectedBin: string | null;\n /** Whether `draft.bin` has actually been persisted. See `PaymentBinConfirmation`. */\n binConfirmation: PaymentBinConfirmation;\n /** `selectedMethodId` resolved against the scoped list; carries the validator config leaves gate on. */\n selectedMethod: PaymentSectionMethod | null;\n /** The client-only facts the read model cannot carry. */\n draft: PaymentDraft;\n /** True until the `Payment` entity resolves. */\n loading: boolean;\n /** Message of the first `userError` from the latest selection, or `null`. */\n selectionError: string | null;\n /**\n * Dispatches `payment.setMethod` with T4's exact input, request-sequenced so only the latest call\n * owns the error state. Records **only the draft facts the input carries**: an `accountId` is the\n * saved-card path, a `bin` is the new-card path, and an input with neither — a plain method pick —\n * leaves both until it succeeds, then releases `draft.bin` and, for a method that is not a card, ends\n * card mode (T191, FR-3). A no-op (no error, no throw) without the action.\n */\n selectMethod: (input: PaymentMethodSelectInput) => void;\n /**\n * Switches to the new-card path, clears the detected BIN, clears any selection error — the message\n * described a picker the shopper has left — and turns card mode on (T191, FR-3). Dispatches nothing:\n * no method is known until card entry detects one, so there is nothing to select yet (US-4).\n */\n selectNewCard: () => void;\n /** Publishes or clears the detected BIN with no write, for per-keystroke detection. */\n setDraftBin: (bin: string | null) => void;\n /**\n * Publishes or clears the save-card flag (T10). A bare `useState` passthrough — no validation, no\n * dispatch. Nothing in this round calls it; it exists for T10 to consume.\n */\n setDraftSaveCard: (saveCard: boolean | null) => void;\n /** Publishes or clears the saved-card label (T10). Bare passthrough, mirroring `setDraftSaveCard`. */\n setDraftCardLabel: (cardLabel: string | null) => void;\n /** Publishes or clears the cardholder document (T8). Bare passthrough, mirroring `setDraftSaveCard`. */\n setDraftDocument: (document: string | null) => void;\n /**\n * Publishes or clears the billing-address-differs flag (T11). Bare passthrough — no validation, no\n * dispatch — mirroring `setDraftSaveCard`/`setDraftCardLabel`/`setDraftDocument`.\n */\n setDraftBillingAddressDifferent: (billingAddressDifferent: boolean | null) => void;\n /**\n * Publishes or clears whether `BillingAddress` is on the custom-address path with nothing persisted\n * (T24) — **not** a bare passthrough like the setters above (fix pass, findings R2/S2). Takes a\n * caller-owned, per-instance `key` (`billing-address.tsx`'s own `useId()`) because\n * `billingAddressUnsaved` is a single fact shared by every `BillingAddress` mounted under this root —\n * `billing-address.md` documents multiple instances as allowed — and an unconditional\n * `setDraftBillingAddressUnsaved(false)` on one instance's unmount would silently clear a *second*,\n * still-open, still-unsaved instance's own block while its own notice keeps rendering (measured:\n * exactly this reproduction blocks placement in the UI and bills the declined address anyway). Each\n * instance's own key is added to an internal set while `unsaved` is `true` and removed while it is\n * `false` — on every call, not only cleanup — so an unmount only ever retracts *that* instance's own\n * contribution. The published `PaymentDraft.billingAddressUnsaved` is `true` while the set is\n * non-empty, `false` once every instance that has ever called this resolves to nothing outstanding,\n * and `null` only before any instance has ever called it at all.\n */\n setDraftBillingAddressUnsaved: (key: string, unsaved: boolean) => void;\n\n /**\n * An order-placement sequence is in flight (T28, specs/place-order-blocking-state.md; REQ-08 s2).\n * A top-level field beside `loading`, deliberately **not** on `draft`: a transition must not change\n * `draft`'s identity, or every keystroke-memoized `draft` consumer would recompute on it (NFR-1).\n * True from the moment the sequence opens a transaction until it settles — never for an attempt\n * refused synchronously, because no sequence runs (FR-3).\n */\n placing: boolean;\n\n /**\n * Publishes one `PlaceOrder` instance's in-flight state. Ref-counted per `key`; a retraction only\n * ever removes its own key. Bails out to the same state by reference when membership already\n * matches, so an effect re-running on an unrelated dependency change does not re-render the section\n * (NFR-2).\n */\n setPlacing: (key: string, placing: boolean) => void;\n\n /**\n * The current order-placement attempt (T29, specs/make-an-order-attempt-survive-a.md), or `null`\n * when none has started under this root. Sits beside `placing` on this same contract for the\n * identical reason `placing` does (T28): one fact, several leaves, must outlive an overlay's own\n * mount — `PlaceOrder` publishes it, `usePlaceOrder` (sdk/payment) reads and writes it, and a remount\n * reads the survivor rather than starting over.\n */\n attempt: PlaceOrderAttempt | null;\n\n /**\n * Records a phase transition. Unlike `setPlacing`, not ref-counted: only one attempt exists at a\n * time under a root (the shared `placing` gate keeps a second sequence from ever opening — T28), so a\n * single last-write-wins `useState` setter is enough. Identity is stable for the root's lifetime, so\n * an in-flight `run()`/`resume()` closure (sdk/payment/use-place-order.ts) keeps writing after the\n * CTA that started it unmounts (NFR-3) — the mechanism that makes the attempt outlive the mount at all.\n *\n * Widened past a plain value (fix pass, High finding in sdk/payment/use-place-order.ts): the raw\n * `useState` setter this field is assigned below has always accepted an updater function too, so\n * accepting `SetPlaceOrderAttempt` here merely lets a caller use a capability the setter already had\n * — `usePlaceOrder`'s own staleness guard needs it to keep a losing racer's late write from\n * clobbering a newer outcome.\n */\n setAttempt: SetPlaceOrderAttempt;\n};\n\n/** `$data` is the payment slice the root has already unwrapped from its keyed `{ payment, cart }`. */\nexport function usePaymentMethodSelection({\n group,\n $data,\n $actions,\n}: {\n group: string | readonly string[];\n $data?: PaymentSectionData;\n $actions?: ComponentActions;\n}): PaymentMethodSelection {\n const loading = $data == null;\n // R1 (fix pass): distinct from `loading` ($data == null) — a resolved-but-partial `$data` slice can\n // omit `methods` entirely (a hand-written slice, explicitly supported per `selectedBin`'s own doc\n // comment below, or a revalidation frame). `allMethods` collapses that case to the same `[]` an\n // honestly-empty list produces, so `methodsReadable` is the only fact that tells them apart for\n // `isSelectionOutOfScope` below.\n const methodsReadable = $data?.methods !== undefined;\n const allMethods = $data?.methods ?? EMPTY_METHODS;\n // The raw, unscoped fact (T74, Decision 4/7) — `allMethods` : `methods` :: `persistedMethodId` :\n // `selectedMethodId`. Rendering leaves want the scoped `selectedMethodId` below;\n // `usePaymentSubmitGate` is the one reader that deliberately wants this one instead.\n const persistedMethodId = $data?.selectedMethodId ?? null;\n const selectedAccountId = $data?.selectedAccountId ?? null;\n\n // An empty string scopes nothing — T1's read model is method-agnostic and assigns the narrowing to\n // the consumer, so opting out has to stay expressible (Decision 4). T140, Decision 1: a single string\n // normalizes to its own one-member set here, so there is exactly one predicate downstream — set\n // membership — rather than a branch for the string form and a second one for the array form.\n const methods = useMemo(() => {\n if (group === '') return allMethods;\n const groups = typeof group === 'string' ? [group] : group;\n return allMethods.filter((method) => groups.includes(method.groupName));\n }, [allMethods, group]);\n\n // Resolved against the *scoped* list, so a selection outside `group` reads as `null` rather than\n // leaking an out-of-group method's validator to a leaf.\n const selectedMethod = useMemo(\n () => methods.find((method) => method.id === persistedMethodId) ?? null,\n [methods, persistedMethodId],\n );\n // The scoped resolution (T74, Decision 4/7) — every rendering consumer's existing read of\n // `selectedMethodId` silently becomes correct once this is what it means.\n const selectedMethodId = selectedMethod?.id ?? null;\n const selectionOutOfScope = isSelectionOutOfScope({ persistedMethodId, selectedMethod, methodsReadable });\n // T191 (FR-2, Decision 9): classified off the scoped `selectedMethod`, for every `group` value —\n // `''` included, where the list still renders one row per method and a card row brings the form back.\n const selectionNonCard = selectedMethod !== null && !isCardMethod(selectedMethod);\n // R3 (fix pass): scoped the same way `selectedMethodId` is — `null` unless the persisted selection\n // resolves inside `methods`. Unscoped, `useInstallmentsSelection`'s own `draft.bin ?? selectedBin`\n // fallback (its only consumer) could still resolve an out-of-scope method's installment tiers by BIN\n // alone, even once the methodId arm of that same lookup went `null` for an out-of-scope selection.\n //\n // Fix pass round 2: `selectedMethod` alone cannot tell \"genuinely out of scope\" apart from \"`methods`\n // was never readable in the first place\" — the same distinction `isSelectionOutOfScope` needs\n // `methodsReadable` for. Before this guard, the hand-written partial `$data` slice that omits\n // `methods` (explicitly supported — see `methodsReadable`'s own doc comment) forced `selectedMethod`\n // to `null` regardless of what was actually persisted, which made this expression hide a perfectly\n // readable `$data.selectedBin` exactly when `selectionOutOfScope` was correctly staying `false` for\n // that same slice — the two facts disagreeing about the same input.\n const selectedBin = !methodsReadable || selectedMethod ? ($data?.selectedBin ?? null) : null;\n\n const [draftPath, setDraftPathState] = useState<PaymentPath | null>(null);\n const [draftBin, setDraftBinState] = useState<string | null>(null);\n // Synchronous mirrors of draftPath/draftBin (react-doctor/no-impure-state-updater fix, T161\n // Decision 1): written at every write site by the two writers below, so `dispatchSelection` can\n // read the pre-write value deterministically instead of through a `useState` updater's own timing,\n // which React does not promise — an updater may run eagerly at call time, again during a render\n // replay, or twice under StrictMode. Touched only inside a `useCallback` body, never a render body\n // (I-3): `no-ref-current-in-render` is itself an `error`-severity rule (NFR-1).\n const draftPathRef = useRef<PaymentPath | null>(null);\n const draftBinRef = useRef<string | null>(null);\n\n // The only writers of draftPath/draftBin (I-2) — every write site, including the exported\n // `setDraftBin`, goes through one of these two, so the ref and the state can never diverge.\n const writeDraftPath = useCallback((next: PaymentPath | null) => {\n draftPathRef.current = next;\n setDraftPathState(next);\n }, []);\n\n const writeDraftBin = useCallback<PaymentMethodSelection['setDraftBin']>((next) => {\n draftBinRef.current = next;\n setDraftBinState(next);\n }, []);\n\n // T7's widening (specs/card-entry.md, Decision 4): three absent-tolerant facts T8/T10 will\n // populate later. Bare state, no writer in this round.\n const [draftSaveCard, setDraftSaveCard] = useState<boolean | null>(null);\n const [draftCardLabel, setDraftCardLabel] = useState<string | null>(null);\n const [draftDocument, setDraftDocument] = useState<string | null>(null);\n // T11's widening: the billing-address-differs flag, absent-tolerant like the four above.\n const [draftBillingAddressDifferent, setDraftBillingAddressDifferent] = useState<boolean | null>(null);\n // T24's widening, ref-counted per instance (fix pass, findings R2/S2): `billingAddressUnsaved` is one\n // fact shared by every `BillingAddress` mounted under this root (`billing-address.md` documents\n // multiple instances as allowed), so a plain `useState<boolean | null>` cannot tell \"no instance needs\n // this anymore\" apart from \"a *different*, still-open instance still does\" once one of several\n // unmounts. `unsavedKeys` tracks which instance keys currently do; an unmount's cleanup only ever\n // retracts its own key, never a sibling's.\n const [billingAddressUnsavedState, setBillingAddressUnsavedState] = useState<BillingAddressUnsavedState>(\n EMPTY_BILLING_ADDRESS_UNSAVED_STATE,\n );\n // T28's widening: ref-counted per publisher key, mirroring `billingAddressUnsavedState` above —\n // see `PlacingState`'s own doc comment for why a bare boolean cannot represent this.\n const [placingState, setPlacingState] = useState<PlacingState>(EMPTY_PLACING_STATE);\n // R2 fix pass: ref-counted per `PaymentMethodList` instance key, mirroring `placingState` above.\n const [methodListPresenceState, setMethodListPresenceState] = useState<MethodListPresenceState>(\n EMPTY_METHOD_LIST_PRESENCE_STATE,\n );\n const methodListMounted = methodListPresenceState.mountedKeys.size > 0;\n // T191 (FR-3, Decision 4): engine-local, not a `draft` fact. Written only by `selectNewCard()` (on) and\n // by a successful plain pick of a method that is not a card (off).\n const [cardMode, setCardMode] = useState(false);\n const cardModeActive = cardMode && (selectionOutOfScope || selectionNonCard);\n const cardPaymentCollapsed = resolveCardPaymentCollapsed({\n selectionOutOfScope,\n selectionNonCard,\n cardModeActive,\n methodListMounted,\n });\n // T29's widening: unlike `placingState`, not ref-counted — only one attempt exists under a root at a\n // time (see `attempt`'s own doc comment on `PaymentMethodSelection`).\n const [attempt, setAttempt] = useState<PlaceOrderAttempt | null>(null);\n const [selectionError, setSelectionError] = useState<string | null>(null);\n const [selectMethodPending, setSelectMethodPending] = useState(false);\n const selectionToken = useRef(0);\n\n const setMethod = $actions?.payment?.setMethod;\n\n // Decision 3 (T74): the dispatch body `selectMethod` always ran, now parameterized by `surfaceError`\n // so the preselection effect below can reuse the exact same machinery — the `selectionToken`\n // sequencing, the pending flag, and the optimistic-write-and-revert branch — without a second\n // dispatch path. `selectMethod` calls this with `true` (unchanged behavior, unchanged public\n // signature); the preselection effect calls it with `false` and logs instead of surfacing, because\n // the shopper never asked for that dispatch and performed no action to be told about a failure\n // (FR-3).\n // Returns whether a dispatch was actually issued (fix pass, finding R5) — `false` when `setMethod` is\n // not yet injected, which the preselection effect below needs so it never arms its \"already\n // attempted\" guard for an id that was never actually sent.\n const dispatchSelection = useCallback(\n (input: PaymentMethodSelectInput, surfaceError: boolean): boolean => {\n if (surfaceError) {\n setSelectionError(null);\n }\n // Write a draft fact only from evidence the input carries (spec Decision 7): `accountId` is the\n // saved-card path, `bin` the new-card one. An input with neither — a plain method pick, which is\n // every preselection dispatch (FR-5) — leaves both alone when it is dispatched: which network the\n // shopper picked says nothing about which card they intend to pay with, and `selectNewCard()` is\n // the explicit way off the saved-card path. The one unasked write is dropping an unrestated BIN\n // when moving *to* a saved card: it belonged to the new card the shopper just abandoned, and T9\n // would look installments up against a dead key.\n //\n // T174 (FR-1, Decision 1): once a plain pick has *succeeded*, it releases `draft.bin` as well. The\n // server persisted it with `bin: null` (`paymentMethodSelect` writes the whole payment), so a kept\n // BIN is one no write carries, and `resolveBinConfirmation` would compare it against `null` with\n // nothing in flight forever — the wedge a network switch after typing a card produced (M-1). It\n // releases only on success and only while it is still the latest dispatch: a refused pick\n // persisted nothing, and a superseded one no longer owns the draft. `draft.path` stays, and\n // `CardEntry` re-dispatches its BIN on the next keystroke (FR-3).\n //\n // F1/F7 (fix pass): this write is optimistic — issued before `setMethod` resolves — so it must\n // be undone if the mutation turns out not to have persisted anything: a resolved call carrying\n // `userErrors`, or a rejected promise. Left standing, an unconfirmed `draft.bin` never equals a\n // `selectedBin` that never moved, and `resolveBinConfirmation` holds 'pending' forever (NFR-1).\n //\n // `draft.path`'s previous value is read from `draftPathRef` — written synchronously by\n // `writeDraftPath` at every call, never inside a state updater (T161 Decision 1) — rather than\n // closed over via `draftPath` directly, so this callback's own dependency list stays free of it.\n // The cost of adding it is not `card-entry.tsx`'s own effect (`:843`) — it already lists `digits`\n // (`:773`) and so already re-runs every keystroke regardless. The real cost lands two hops up:\n // `selectMethod` depends on this callback (`:672`), and this hook's own returned `useMemo`\n // (`:851`) depends on `selectMethod` in turn (`:895`) — so adding it would churn that outer\n // `useMemo` every keystroke, and with it the `payment` context value both section roots build\n // from this hook's whole return (`payment.tsx`, `payment-controller.tsx`), re-rendering every\n // `usePayment()` leaf, not only the ones that read `draft`.\n //\n // `draft.bin` does NOT restore a captured previous value (T162): it clears to `null`\n // unconditionally on rollback instead. `card-entry.tsx`'s `handleNumberChange` calls\n // `setDraftBin` directly, then dispatches, in one synchronous handler (`:917`/`:921`) — so a\n // `previousBin` snapshotted from `draftBinRef.current` here would already be the value *that\n // same handler* just queued, not the value genuinely committed before it. Restoring it is then a\n // no-op against the one fact `resolveBinConfirmation` checks, wedging `binConfirmation` at\n // 'pending' forever. `null` is safe regardless of what else wrote `draftBinRef` in between: it is\n // the codebase's own convention for \"nothing reliable is currently known\" (`card-entry.tsx`'s F5\n // branch), and `use-installments-selection.ts`'s `draft.bin ?? selectedBin` fallback (T20)\n // recovers the real persisted value whenever one exists.\n //\n // T191 (FR-3): a plain pick of a method that is not a card also ends card mode, on the same terms\n // as the BIN release — once it succeeds, and only while it is still the latest dispatch. A refused\n // pick persisted nothing, so the shopper is still on the card. `allMethods` joins this callback's\n // dependency list for the lookup; it changes only when the method list does, never per keystroke.\n const plainPick = !input.accountId && input.bin === undefined;\n const pickedMethod = plainPick ? allMethods.find((method) => method.id === input.methodId) : undefined;\n const endsCardMode = pickedMethod !== undefined && !isCardMethod(pickedMethod);\n let revertDraft: (() => void) | null = null;\n if (!plainPick) {\n const nextPath: PaymentPath = input.accountId ? 'saved-card' : 'new-card';\n const previousPath = draftPathRef.current;\n writeDraftPath(nextPath);\n writeDraftBin(input.bin ?? null);\n revertDraft = () => {\n writeDraftPath(previousPath);\n writeDraftBin(null);\n };\n }\n if (!setMethod) return false;\n const token = ++selectionToken.current;\n setSelectMethodPending(true);\n setMethod(input)\n .then((userErrors) => {\n if (selectionToken.current !== token) return;\n setSelectMethodPending(false);\n const [firstError] = userErrors;\n if (firstError) {\n if (surfaceError) {\n setSelectionError(firstError.message);\n } else {\n console.error('[usePaymentMethodSelection] preselection rejected', firstError.code);\n }\n revertDraft?.();\n } else if (plainPick) {\n writeDraftBin(null);\n if (endsCardMode) setCardMode(false);\n }\n })\n .catch(() => {\n if (selectionToken.current !== token) return;\n setSelectMethodPending(false);\n if (surfaceError) {\n setSelectionError('Unable to select the payment method. Try again.');\n } else {\n console.error('[usePaymentMethodSelection] preselection request failed');\n }\n revertDraft?.();\n });\n return true;\n },\n [setMethod, writeDraftPath, writeDraftBin, allMethods],\n );\n\n const selectMethod = useCallback<PaymentMethodSelection['selectMethod']>(\n (input) => dispatchSelection(input, true),\n [dispatchSelection],\n );\n\n const selectNewCard = useCallback(() => {\n writeDraftPath('new-card');\n writeDraftBin(null);\n // A rejection that described the picker the shopper just left must not outlive the transition.\n setSelectionError(null);\n // T191 (FR-3, Decision 4): the one recorder of card mode, for the card option with no method to name\n // and for `CardEntry`'s first keystroke off the new-card path. Sends nothing.\n setCardMode(true);\n }, [writeDraftPath, writeDraftBin]);\n\n // Fix pass, findings R2/S2: each `BillingAddress` instance retracts only its own `key` on unmount or\n // once it resolves to \"nothing outstanding\" — never a sibling's. See the type's own doc comment above\n // and `setDraftBillingAddressUnsaved`'s in `PaymentMethodSelection` for the full reasoning.\n const setDraftBillingAddressUnsaved = useCallback<PaymentMethodSelection['setDraftBillingAddressUnsaved']>(\n (key, unsaved) => {\n setBillingAddressUnsavedState((prev) => {\n const isMember = prev.unsavedKeys.has(key);\n // Bails out to the *same* state (by reference) once at least one call has landed and this key's\n // membership already matches — the effect that calls this runs its body on every dependency\n // change, not only on a real transition, and this must not force every `draft` consumer to\n // recompute on a no-op republish (NFR-3's own reasoning, applied to this fact).\n if (prev.everPublished && unsaved === isMember) return prev;\n const nextKeys = new Set(prev.unsavedKeys);\n if (unsaved) {\n nextKeys.add(key);\n } else {\n nextKeys.delete(key);\n }\n return { everPublished: true, unsavedKeys: nextKeys };\n });\n },\n [],\n );\n\n /**\n * Publishes or retracts one `PlaceOrder` instance's in-flight state (T28, FR-1/FR-7). Ref-counted by\n * `key`, mirroring `setDraftBillingAddressUnsaved` above: bails out to the same state by reference\n * once `key`'s membership already matches `placing`, so an effect re-running on an unrelated\n * dependency change does not force every leaf under this root to re-render (NFR-2). Unlike\n * `setDraftBillingAddressUnsaved`, there is no `everPublished` tri-state to track here — `placing`\n * has only two states, so \"no publisher has ever called this\" and \"every publisher has retracted\"\n * collapse to the same `false` (I-5).\n */\n const setPlacing = useCallback<PaymentMethodSelection['setPlacing']>((key, nextPlacing) => {\n setPlacingState((prev) => {\n const isMember = prev.placingKeys.has(key);\n if (nextPlacing === isMember) return prev;\n const nextKeys = new Set(prev.placingKeys);\n if (nextPlacing) {\n nextKeys.add(key);\n } else {\n nextKeys.delete(key);\n }\n return { placingKeys: nextKeys };\n });\n }, []);\n\n /**\n * Registers or retracts one `PaymentMethodList` instance's ability to bring the card form back (fix\n * pass, finding R2; T171, Decision 4; T191, FR-4). Ref-counted by `key`, mirroring `setPlacing` above,\n * including the same by-reference bail-out.\n */\n const setMethodListMounted = useCallback<PaymentMethodSelection['setMethodListMounted']>((key, mounted) => {\n setMethodListPresenceState((prev) => {\n const isMember = prev.mountedKeys.has(key);\n if (mounted === isMember) return prev;\n const nextKeys = new Set(prev.mountedKeys);\n if (mounted) {\n nextKeys.add(key);\n } else {\n nextKeys.delete(key);\n }\n return { mountedKeys: nextKeys };\n });\n }, []);\n\n // Fix pass (Critical finding, T29): `placingKeys` is a ref count keyed by CTA instance, and a key\n // published by a CTA that then unmounts mid-sequence is never retracted by anything — FR-5's own\n // unmount cleanup (place-order.tsx) deliberately leaves it, precisely because the abandoned\n // `run()`/`resume()` closure might still be genuinely in flight (INV-5). That leaves `placingKeys`\n // permanently non-empty from that point on, regardless of what the closure eventually resolves the\n // shared attempt to — nothing else was ever going to flip the key back. Folding the attempt's own\n // phase in here is what un-sticks every reader of `placing` (this root's own `blockedByOtherInstance`\n // override in place-order.tsx, and every sibling leaf's `inert={placing}`, e.g. card-entry.tsx,\n // saved-cards.tsx) once that abandoned attempt reaches a phase that means nothing is actively\n // happening any more — `refused` (nothing was paid) or `settled` (already placed).\n //\n // Deliberately phrased as an override on top of `placingKeys.size > 0`, not as a second precondition\n // alongside it (i.e. `attempt !== null && …`): `placing`/`setPlacing` predate T29 and are a\n // general-purpose fact T24's submit gate and this file's own tests publish directly with no attempt\n // ever created (`payment.test.tsx`'s \"placing (T28)\" suite, `billing-address.test.tsx`'s inert\n // check) — requiring a non-null attempt here made `placing` permanently `false` for every one of\n // those callers, which is a regression this fix pass caught by running that suite, not something\n // asked for by the finding it was fixing. `sent`/`unresolved`/`finalizing` still block (the card's\n // fate is unknown or a finalization is in flight, and a shopper must not be able to re-edit payment\n // method or card fields underneath that), and so does the ordinary \"no attempt at all\" case — this\n // only ever *releases* a stuck key, never adds a new reason to hold one.\n const attemptResolved = attempt !== null && (attempt.phase === 'refused' || attempt.phase === 'settled');\n // Not part of `draft` (NFR-1, Decision 2): a placing transition must change this hook's own returned\n // identity, so every leaf re-renders, without touching `draft`'s — every keystroke-memoized `draft`\n // consumer (e.g. `usePaymentSubmitGate`'s CPF check, `Installments`' BIN lookup) would otherwise\n // recompute on every placing transition too.\n const placing = placingState.placingKeys.size > 0 && !attemptResolved;\n\n // FR-1/FR-2/FR-4 (Decision 1, Decision 2): preselects the single scoped method when nothing is\n // persisted yet — keyed on method identity (`methods[0]` in that branch is not a position, it is the\n // only identity in scope) — through the same request-sequenced `payment.setMethod` machinery\n // `selectMethod` uses, silently (FR-3) and at most once per target id (FR-4, INV-4). Lives in the\n // shared engine rather than in `PaymentMethodList` (Decision 2) so both roots behave identically\n // (NFR-1) and the fact is established even when no picker is composed, or the overlay it lives inside\n // has not opened.\n //\n // Fix pass, finding R6: a `Set`, not a single slot — the single-slot version this replaced only ever\n // remembered the *last* target, so a scoped list cycling `[visa] -> [master] -> [visa]` forgot `visa`\n // had already been attempted the moment `master` overwrote the slot, and dispatched it a second time.\n // A `Set` remembers every id ever attempted for the life of this mounted root, matching FR-4/INV-4's\n // own wording (\"at most once per mounted root\") rather than \"not twice in a row.\"\n const attemptedPreselectionIdsRef = useRef<Set<string>>(new Set());\n\n useEffect(() => {\n // Fix pass, finding R6 (widened by T140, Decision 3): re-derives the same candidate\n // `resolveMethodPreselection` computes internally (via the shared `resolvePreselectionCandidateId`\n // helper) so this effect can check the *Set* for \"was this id ever attempted\" before calling in —\n // the pure resolver's own `attemptedMethodId` parameter stays a single id, matching its tested,\n // documented signature; only what this caller derives for it changes.\n const candidateId = resolvePreselectionCandidateId(methods);\n const alreadyAttempted = candidateId !== null && attemptedPreselectionIdsRef.current.has(candidateId);\n const target = resolveMethodPreselection({\n loading,\n methods,\n persistedMethodId,\n selectedAccountId,\n placing,\n attemptedMethodId: alreadyAttempted ? candidateId : null,\n group,\n });\n if (target === null) return;\n // Fix pass, finding R5: the \"attempted\" guard is armed only once `dispatchSelection` confirms it\n // actually issued the dispatch (i.e. `setMethod` was injected) — not before calling it. Arming it\n // unconditionally let a root that first renders with `$actions` undefined mark its sole candidate\n // \"attempted\" having dispatched nothing, so the preselection could never fire for that mount even\n // once `$actions` arrived on a later render. `dispatchSelection` still starts the request (and the\n // guard is still armed well before the promise settles) — only the *no-op* branch (no `setMethod`\n // injected, which for a plain `{ methodId }` preselection input happens before any draft write) no\n // longer arms it.\n const dispatched = dispatchSelection({ methodId: target }, false);\n if (dispatched) {\n attemptedPreselectionIdsRef.current.add(target);\n }\n }, [loading, methods, persistedMethodId, selectedAccountId, placing, group, dispatchSelection]);\n\n const draft = useMemo<PaymentDraft>(\n () => ({\n path: draftPath ?? (selectedAccountId ? 'saved-card' : null),\n bin: draftBin,\n saveCard: draftSaveCard,\n cardLabel: draftCardLabel,\n document: draftDocument,\n billingAddressDifferent: draftBillingAddressDifferent,\n billingAddressUnsaved: billingAddressUnsavedState.everPublished\n ? billingAddressUnsavedState.unsavedKeys.size > 0\n : null,\n }),\n [\n draftPath,\n selectedAccountId,\n draftBin,\n draftSaveCard,\n draftCardLabel,\n draftDocument,\n draftBillingAddressDifferent,\n billingAddressUnsavedState,\n ],\n );\n\n const binConfirmation = useMemo(\n () => resolveBinConfirmation({ selectedBin: $data?.selectedBin, draftBin, selectMethodPending }),\n [$data, draftBin, selectMethodPending],\n );\n\n return useMemo(\n () => ({\n methods,\n allMethods,\n persistedMethodId,\n selectedMethodId,\n selectionOutOfScope,\n methodListMounted,\n setMethodListMounted,\n cardModeActive,\n cardPaymentCollapsed,\n selectedAccountId,\n selectedBin,\n binConfirmation,\n selectedMethod,\n draft,\n loading,\n selectionError,\n selectMethod,\n selectNewCard,\n setDraftBin: writeDraftBin,\n setDraftSaveCard,\n setDraftCardLabel,\n setDraftDocument,\n setDraftBillingAddressDifferent,\n setDraftBillingAddressUnsaved,\n placing,\n setPlacing,\n attempt,\n setAttempt,\n }),\n [\n methods,\n allMethods,\n persistedMethodId,\n selectedMethodId,\n selectionOutOfScope,\n methodListMounted,\n setMethodListMounted,\n cardModeActive,\n cardPaymentCollapsed,\n selectedAccountId,\n selectedBin,\n binConfirmation,\n selectedMethod,\n draft,\n loading,\n selectionError,\n selectMethod,\n selectNewCard,\n writeDraftBin,\n setDraftBillingAddressUnsaved,\n placing,\n setPlacing,\n attempt,\n ],\n );\n}\n"],"mappings":"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"}