@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
@@ -4,8 +4,7 @@ const orderFormIdVar = makeVar(null);
4
4
  /**
5
5
  * Latest `userErrors` from any cart write, surfaced context-free. The cart action hook publishes
6
6
  * here on every call (cleared synchronously at the start of the next call) and `CartError` reads it
7
- * via `useReactiveVar`, decoupling error display from `CartRoot`'s data context (specs/actions.md
8
- * FR-9). Stays in client state — never a query variable or request header.
7
+ * via `useReactiveVar`, decoupling error display from `CartRoot`'s data context (docs/storefront-core/reference-free-particle-model.md § The cart write contract). Stays in client state — never a query variable or request header.
9
8
  */
10
9
  const cartUserErrorsVar = makeVar([]);
11
10
  /**
@@ -13,7 +12,7 @@ const cartUserErrorsVar = makeVar([]);
13
12
  * error and network-catch branches leave it untouched. `CartOpenOnAdd` and `CartRedirectOnAdd` key
14
13
  * a `useEffect` on it via `useReactiveVar`, so every successful add re-fires their behavior even when
15
14
  * the drawer is already open or was just closed — a boolean would not re-fire on an unchanged value
16
- * (specs/fspg-822-minicart-open-on-add.md, Decision 2). Client state only — never a query variable
15
+ * (docs/storefront-core/reference-free-particle-model.md § Read side — state). Client state only — never a query variable
17
16
  * or request header.
18
17
  */
19
18
  const cartLastAddVar = makeVar(0);
@@ -28,13 +27,13 @@ const miniCartOpenVar = makeVar(false);
28
27
  /**
29
28
  * Latest `userErrors` from a coupon write (`applyCoupon` / `removeCoupon`), surfaced context-free.
30
29
  * A dedicated channel mirroring `cartUserErrorsVar` so coupon feedback sits next to the coupon form
31
- * and never collides with item-write errors (specs/fspg-830-coupon-entry.md AD-2). The coupon
30
+ * and never collides with item-write errors (docs/storefront-core/reference-free-particle-model.md § The cart write contract). The coupon
32
31
  * actions publish here on every call (cleared at the start of the next call) and `CartCouponError`
33
32
  * reads it via `useReactiveVar`. Stays in client state — never a query variable or request header.
34
33
  */
35
34
  const cartCouponErrorsVar = makeVar([]);
36
35
  /**
37
- * Count of cart writes currently in flight (specs/mini-cart-validation-wiring.md). Every mutating
36
+ * Count of cart writes currently in flight (docs/storefront-core/reference-free-particle-model.md § The cart write contract). Every mutating
38
37
  * handler in the cart action namespace registers for the lifetime of its call, so `> 0` means a
39
38
  * write is outstanding. The stock-validation open call reads this to skip while a write is running —
40
39
  * that write's own response already carries current availability, so re-validating would be
@@ -45,7 +44,7 @@ const cartWritesInFlightVar = makeVar(0);
45
44
  /**
46
45
  * Epoch-ms timestamp of when the client last received current availability (`0` = never). Stamped on
47
46
  * a successful `validate` and on every successful cart write, because a write's response already
48
- * carries recomputed availability (specs/mini-cart-validation-wiring.md, Decision 2). The
47
+ * carries recomputed availability (docs/storefront-core/reference-cart.md § Validation). The
49
48
  * stock-validation open call compares `now - cartAvailabilityFreshAtVar()` against
50
49
  * `CART_VALIDATE_STALENESS_MS` and skips while inside the window, so reopening the overlay — or
51
50
  * opening it right after an add — does not re-validate. Client state only.
@@ -54,7 +53,7 @@ const cartAvailabilityFreshAtVar = makeVar(0);
54
53
  /**
55
54
  * The staleness window for the stock-validation open call, in milliseconds. The single tuning knob:
56
55
  * the platform's rate limits under real traffic are not known yet, so this value is a starting point,
57
- * not a contract (specs/mini-cart-validation-wiring.md). The pre-checkout call ignores it and always
56
+ * not a contract (docs/storefront-core/reference-cart.md § Validation). The pre-checkout call ignores it and always
58
57
  * runs.
59
58
  */
60
59
  const CART_VALIDATE_STALENESS_MS = 3e4;
@@ -1 +1 @@
1
- {"version":3,"file":"cart.js","names":[],"sources":["../../../src/sdk/state/cart.ts"],"sourcesContent":["import { makeVar } from '@apollo/client';\nimport type { CartUserError } from '../cart/operations.js';\n\n// Session-scoped cart identifier (`Cart.id` from the cart query response). Client state only —\n// never sent as a query variable or request header; the x-order-form-id cookie carries cart\n// context on the transport. Starts null; hydrated when the cart entity arrives, which opens the\n// cart action ready gate. See specs/cart-context-client-migrate.md (Decisions 2, 6).\nexport const orderFormIdVar = makeVar<string | null>(null);\n\n/**\n * Latest `userErrors` from any cart write, surfaced context-free. The cart action hook publishes\n * here on every call (cleared synchronously at the start of the next call) and `CartError` reads it\n * via `useReactiveVar`, decoupling error display from `CartRoot`'s data context (specs/actions.md\n * FR-9). Stays in client state — never a query variable or request header.\n */\nexport const cartUserErrorsVar = makeVar<readonly CartUserError[]>([]);\n\n/**\n * Monotonic add-success pulse. `addItem` increments it on each successful add (no userErrors); the\n * error and network-catch branches leave it untouched. `CartOpenOnAdd` and `CartRedirectOnAdd` key\n * a `useEffect` on it via `useReactiveVar`, so every successful add re-fires their behavior even when\n * the drawer is already open or was just closed — a boolean would not re-fire on an unchanged value\n * (specs/fspg-822-minicart-open-on-add.md, Decision 2). Client state only — never a query variable\n * or request header.\n */\nexport const cartLastAddVar = makeVar<number>(0);\n\n/**\n * The mini-cart's open state, surfaced context-free. `CartOpenOnAdd` reads it via `useReactiveVar`\n * to control the `DrawerRoot` and writes it back through `onOpenChange` so the header trigger and\n * manual close (Escape / backdrop / close button) stay in sync. The single source of truth for the\n * controlled drawer; `addItem` never writes it. Client state only — never a query variable or\n * request header.\n */\nexport const miniCartOpenVar = makeVar<boolean>(false);\n\n/**\n * Latest `userErrors` from a coupon write (`applyCoupon` / `removeCoupon`), surfaced context-free.\n * A dedicated channel mirroring `cartUserErrorsVar` so coupon feedback sits next to the coupon form\n * and never collides with item-write errors (specs/fspg-830-coupon-entry.md AD-2). The coupon\n * actions publish here on every call (cleared at the start of the next call) and `CartCouponError`\n * reads it via `useReactiveVar`. Stays in client state — never a query variable or request header.\n */\nexport const cartCouponErrorsVar = makeVar<readonly CartUserError[]>([]);\n\n/**\n * Count of cart writes currently in flight (specs/mini-cart-validation-wiring.md). Every mutating\n * handler in the cart action namespace registers for the lifetime of its call, so `> 0` means a\n * write is outstanding. The stock-validation open call reads this to skip while a write is running —\n * that write's own response already carries current availability, so re-validating would be\n * redundant. `validate` itself is read-only upstream and never registers here. Client state only —\n * never a query variable or request header.\n */\nexport const cartWritesInFlightVar = makeVar<number>(0);\n\n/**\n * Epoch-ms timestamp of when the client last received current availability (`0` = never). Stamped on\n * a successful `validate` and on every successful cart write, because a write's response already\n * carries recomputed availability (specs/mini-cart-validation-wiring.md, Decision 2). The\n * stock-validation open call compares `now - cartAvailabilityFreshAtVar()` against\n * `CART_VALIDATE_STALENESS_MS` and skips while inside the window, so reopening the overlay — or\n * opening it right after an add — does not re-validate. Client state only.\n */\nexport const cartAvailabilityFreshAtVar = makeVar<number>(0);\n\n/**\n * The staleness window for the stock-validation open call, in milliseconds. The single tuning knob:\n * the platform's rate limits under real traffic are not known yet, so this value is a starting point,\n * not a contract (specs/mini-cart-validation-wiring.md). The pre-checkout call ignores it and always\n * runs.\n */\nexport const CART_VALIDATE_STALENESS_MS = 30_000;\n"],"mappings":";;AAOA,MAAa,iBAAiB,QAAuB,IAAI;;;;;;;AAQzD,MAAa,oBAAoB,QAAkC,CAAC,CAAC;;;;;;;;;AAUrE,MAAa,iBAAiB,QAAgB,CAAC;;;;;;;;AAS/C,MAAa,kBAAkB,QAAiB,KAAK;;;;;;;;AASrD,MAAa,sBAAsB,QAAkC,CAAC,CAAC;;;;;;;;;AAUvE,MAAa,wBAAwB,QAAgB,CAAC;;;;;;;;;AAUtD,MAAa,6BAA6B,QAAgB,CAAC;;;;;;;AAQ3D,MAAa,6BAA6B"}
1
+ {"version":3,"file":"cart.js","names":[],"sources":["../../../src/sdk/state/cart.ts"],"sourcesContent":["import { makeVar } from '@apollo/client';\nimport type { CartUserError } from '../cart/operations.js';\n\n// Session-scoped cart identifier (`Cart.id` from the cart query response). Client state only —\n// never sent as a query variable or request header; the x-order-form-id cookie carries cart\n// context on the transport. Starts null; hydrated when the cart entity arrives, which opens the\n// cart action ready gate. See docs/storefront-core/reference-cart.md § Cart context; docs/storefront-core/reference-free-particle-model.md § The cart write contract.\nexport const orderFormIdVar = makeVar<string | null>(null);\n\n/**\n * Latest `userErrors` from any cart write, surfaced context-free. The cart action hook publishes\n * here on every call (cleared synchronously at the start of the next call) and `CartError` reads it\n * via `useReactiveVar`, decoupling error display from `CartRoot`'s data context (docs/storefront-core/reference-free-particle-model.md § The cart write contract). Stays in client state — never a query variable or request header.\n */\nexport const cartUserErrorsVar = makeVar<readonly CartUserError[]>([]);\n\n/**\n * Monotonic add-success pulse. `addItem` increments it on each successful add (no userErrors); the\n * error and network-catch branches leave it untouched. `CartOpenOnAdd` and `CartRedirectOnAdd` key\n * a `useEffect` on it via `useReactiveVar`, so every successful add re-fires their behavior even when\n * the drawer is already open or was just closed — a boolean would not re-fire on an unchanged value\n * (docs/storefront-core/reference-free-particle-model.md § Read side — state). Client state only — never a query variable\n * or request header.\n */\nexport const cartLastAddVar = makeVar<number>(0);\n\n/**\n * The mini-cart's open state, surfaced context-free. `CartOpenOnAdd` reads it via `useReactiveVar`\n * to control the `DrawerRoot` and writes it back through `onOpenChange` so the header trigger and\n * manual close (Escape / backdrop / close button) stay in sync. The single source of truth for the\n * controlled drawer; `addItem` never writes it. Client state only — never a query variable or\n * request header.\n */\nexport const miniCartOpenVar = makeVar<boolean>(false);\n\n/**\n * Latest `userErrors` from a coupon write (`applyCoupon` / `removeCoupon`), surfaced context-free.\n * A dedicated channel mirroring `cartUserErrorsVar` so coupon feedback sits next to the coupon form\n * and never collides with item-write errors (docs/storefront-core/reference-free-particle-model.md § The cart write contract). The coupon\n * actions publish here on every call (cleared at the start of the next call) and `CartCouponError`\n * reads it via `useReactiveVar`. Stays in client state — never a query variable or request header.\n */\nexport const cartCouponErrorsVar = makeVar<readonly CartUserError[]>([]);\n\n/**\n * Count of cart writes currently in flight (docs/storefront-core/reference-free-particle-model.md § The cart write contract). Every mutating\n * handler in the cart action namespace registers for the lifetime of its call, so `> 0` means a\n * write is outstanding. The stock-validation open call reads this to skip while a write is running —\n * that write's own response already carries current availability, so re-validating would be\n * redundant. `validate` itself is read-only upstream and never registers here. Client state only —\n * never a query variable or request header.\n */\nexport const cartWritesInFlightVar = makeVar<number>(0);\n\n/**\n * Epoch-ms timestamp of when the client last received current availability (`0` = never). Stamped on\n * a successful `validate` and on every successful cart write, because a write's response already\n * carries recomputed availability (docs/storefront-core/reference-cart.md § Validation). The\n * stock-validation open call compares `now - cartAvailabilityFreshAtVar()` against\n * `CART_VALIDATE_STALENESS_MS` and skips while inside the window, so reopening the overlay — or\n * opening it right after an add — does not re-validate. Client state only.\n */\nexport const cartAvailabilityFreshAtVar = makeVar<number>(0);\n\n/**\n * The staleness window for the stock-validation open call, in milliseconds. The single tuning knob:\n * the platform's rate limits under real traffic are not known yet, so this value is a starting point,\n * not a contract (docs/storefront-core/reference-cart.md § Validation). The pre-checkout call ignores it and always\n * runs.\n */\nexport const CART_VALIDATE_STALENESS_MS = 30_000;\n"],"mappings":";;AAOA,MAAa,iBAAiB,QAAuB,IAAI;;;;;;AAOzD,MAAa,oBAAoB,QAAkC,CAAC,CAAC;;;;;;;;;AAUrE,MAAa,iBAAiB,QAAgB,CAAC;;;;;;;;AAS/C,MAAa,kBAAkB,QAAiB,KAAK;;;;;;;;AASrD,MAAa,sBAAsB,QAAkC,CAAC,CAAC;;;;;;;;;AAUvE,MAAa,wBAAwB,QAAgB,CAAC;;;;;;;;;AAUtD,MAAa,6BAA6B,QAAgB,CAAC;;;;;;;AAQ3D,MAAa,6BAA6B"}
@@ -0,0 +1,49 @@
1
+ //#region src/utils/loggable-error-name.ts
2
+ /**
3
+ * `Error.prototype.name` is a writable own property (security pass, T156 review; fixed here, T160):
4
+ * any collaborator a caller wraps in a `try`/`catch` — a holder's continuation, a pre-flight check,
5
+ * an action provider, a GraphQL mutation rejection — can throw an object whose `.name` is arbitrary
6
+ * text, including text built from card data or PII the caller exists to keep out of a log. An
7
+ * allow-list, not a pattern or a deny-list, mirrors this package's own rule for exactly this shape of
8
+ * problem (`matter/sensitive-field-protection.test.ts`'s own "an allow-list fails loudly ... a
9
+ * deny-list fails silently"). Lives in `utils/`, not in any one domain's `sdk/` subdirectory, because
10
+ * its callers already span two peer domains — `payment` and `identity` — neither of which should
11
+ * import a logging helper from the other (T160, Decision 6).
12
+ */
13
+ const LOGGABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
14
+ "Error",
15
+ "EvalError",
16
+ "RangeError",
17
+ "ReferenceError",
18
+ "SyntaxError",
19
+ "TypeError",
20
+ "URIError",
21
+ "AggregateError"
22
+ ]);
23
+ /**
24
+ * The one place every caller below decides what to log for a caught error. Never the raw `.name`.
25
+ *
26
+ * `error` is an untrusted value: a holder's continuation, a pre-flight check, or any other
27
+ * pluggable collaborator can throw whatever object it wants, engineered so that merely inspecting
28
+ * it — not only reading `.name` — throws (fix rounds 1-3, all Medium findings). A `Proxy` can trap
29
+ * `getPrototypeOf` (triggered by the `instanceof` check below), `get` (triggered by the `.name`
30
+ * read), or make either one answer differently across repeated calls. Rather than guard each
31
+ * mechanism one at a time, every operation this function performs on `error` — the `instanceof`
32
+ * check, the `.name` read, and the allow-list membership check — runs inside one `try`/`catch`:
33
+ * whatever throws, and for whatever reason, collapses to `'UnknownError'` exactly like any other
34
+ * unrecognised name. `error.name` is still read exactly once, into `name` below, so a getter that
35
+ * answers differently across separate calls has no second read left to answer differently on.
36
+ */
37
+ function loggableErrorName(error) {
38
+ try {
39
+ if (!(error instanceof Error)) return "UnknownError";
40
+ const name = error.name;
41
+ return LOGGABLE_ERROR_NAMES.has(name) ? name : "UnknownError";
42
+ } catch {
43
+ return "UnknownError";
44
+ }
45
+ }
46
+ //#endregion
47
+ export { loggableErrorName };
48
+
49
+ //# sourceMappingURL=loggable-error-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loggable-error-name.js","names":[],"sources":["../../src/utils/loggable-error-name.ts"],"sourcesContent":["/**\n * `Error.prototype.name` is a writable own property (security pass, T156 review; fixed here, T160):\n * any collaborator a caller wraps in a `try`/`catch` — a holder's continuation, a pre-flight check,\n * an action provider, a GraphQL mutation rejection — can throw an object whose `.name` is arbitrary\n * text, including text built from card data or PII the caller exists to keep out of a log. An\n * allow-list, not a pattern or a deny-list, mirrors this package's own rule for exactly this shape of\n * problem (`matter/sensitive-field-protection.test.ts`'s own \"an allow-list fails loudly ... a\n * deny-list fails silently\"). Lives in `utils/`, not in any one domain's `sdk/` subdirectory, because\n * its callers already span two peer domains — `payment` and `identity` — neither of which should\n * import a logging helper from the other (T160, Decision 6).\n */\nconst LOGGABLE_ERROR_NAMES: ReadonlySet<string> = new Set([\n 'Error',\n 'EvalError',\n 'RangeError',\n 'ReferenceError',\n 'SyntaxError',\n 'TypeError',\n 'URIError',\n 'AggregateError',\n]);\n\n/**\n * The one place every caller below decides what to log for a caught error. Never the raw `.name`.\n *\n * `error` is an untrusted value: a holder's continuation, a pre-flight check, or any other\n * pluggable collaborator can throw whatever object it wants, engineered so that merely inspecting\n * it — not only reading `.name` — throws (fix rounds 1-3, all Medium findings). A `Proxy` can trap\n * `getPrototypeOf` (triggered by the `instanceof` check below), `get` (triggered by the `.name`\n * read), or make either one answer differently across repeated calls. Rather than guard each\n * mechanism one at a time, every operation this function performs on `error` — the `instanceof`\n * check, the `.name` read, and the allow-list membership check — runs inside one `try`/`catch`:\n * whatever throws, and for whatever reason, collapses to `'UnknownError'` exactly like any other\n * unrecognised name. `error.name` is still read exactly once, into `name` below, so a getter that\n * answers differently across separate calls has no second read left to answer differently on.\n */\nexport function loggableErrorName(error: unknown): string {\n try {\n if (!(error instanceof Error)) {\n return 'UnknownError';\n }\n\n const name = error.name;\n return LOGGABLE_ERROR_NAMES.has(name) ? name : 'UnknownError';\n } catch {\n return 'UnknownError';\n }\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,uCAA4C,IAAI,IAAI;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;;AAgBD,SAAgB,kBAAkB,OAAwB;CACxD,IAAI;EACF,IAAI,EAAE,iBAAiB,QACrB,OAAO;EAGT,MAAM,OAAO,MAAM;EACnB,OAAO,qBAAqB,IAAI,IAAI,IAAI,OAAO;CACjD,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -0,0 +1,18 @@
1
+ //#region src/utils/resolve-authored-copy.ts
2
+ /**
3
+ * Resolve an authored copy value against its default. A store author can write "" or " " into any
4
+ * `type: 'string'` schema prop — both are valid JSON strings — and neither `??` nor a default
5
+ * parameter falls through on them, so the surface renders a blank label, legend or alert. Logical OR
6
+ * is not a fix either: " " is truthy.
7
+ *
8
+ * The authored value is returned VERBATIM when it has content — trimmed only to decide, never to
9
+ * render, matching `cart.tsx`'s `configuredLabel` and `region.tsx`'s `$content` idiom.
10
+ */
11
+ function resolveAuthoredCopy(authored, fallback) {
12
+ if (authored == null) return fallback;
13
+ return authored.trim().length > 0 ? authored : fallback;
14
+ }
15
+ //#endregion
16
+ export { resolveAuthoredCopy };
17
+
18
+ //# sourceMappingURL=resolve-authored-copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-authored-copy.js","names":[],"sources":["../../src/utils/resolve-authored-copy.ts"],"sourcesContent":["/**\n * Resolve an authored copy value against its default. A store author can write \"\" or \" \" into any\n * `type: 'string'` schema prop — both are valid JSON strings — and neither `??` nor a default\n * parameter falls through on them, so the surface renders a blank label, legend or alert. Logical OR\n * is not a fix either: \" \" is truthy.\n *\n * The authored value is returned VERBATIM when it has content — trimmed only to decide, never to\n * render, matching `cart.tsx`'s `configuredLabel` and `region.tsx`'s `$content` idiom.\n */\nexport function resolveAuthoredCopy(authored: string | undefined | null, fallback: string): string {\n if (authored == null) return fallback;\n return authored.trim().length > 0 ? authored : fallback;\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,oBAAoB,UAAqC,UAA0B;CACjG,IAAI,YAAY,MAAM,OAAO;CAC7B,OAAO,SAAS,KAAK,CAAC,CAAC,SAAS,IAAI,WAAW;AACjD"}
@@ -7,9 +7,9 @@ const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set([
7
7
  ]);
8
8
  /**
9
9
  * The only gate on a CMS-authored destination: `http:`, `https:`, `mailto:`, `tel:`, and paths
10
- * starting with a single `/`. Neither the runtime slice nor the sukima preflight enforces a model's
11
- * `pattern`, so an authored `javascript:` value reaches the component intact and this is what stands
12
- * between it and an `href`.
10
+ * starting with a single `/`. The runtime slice does not enforce a model's `pattern`, and the sukima
11
+ * preflight that does never sees content published after a deploy, so an authored `javascript:` value
12
+ * can reach the component intact and this is what stands between it and an `href`.
13
13
  *
14
14
  * Rejects rather than sanitizes: a destination that comes back safe is character-for-character the
15
15
  * trimmed authored value, so no value is ever silently rewritten into a different working one.
@@ -1 +1 @@
1
- {"version":3,"file":"safe-href.js","names":[],"sources":["../../src/utils/safe-href.ts"],"sourcesContent":["export type SafeHrefRejection =\n | 'blank'\n | 'malformed'\n | 'protocol-relative'\n | 'forbidden-scheme'\n | 'absolute'\n | 'sign-in-route';\n\nexport type SafeHrefResult = { safe: true; href: string } | { safe: false; reason: SafeHrefRejection };\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * The only gate on a CMS-authored destination: `http:`, `https:`, `mailto:`, `tel:`, and paths\n * starting with a single `/`. Neither the runtime slice nor the sukima preflight enforces a model's\n * `pattern`, so an authored `javascript:` value reaches the component intact and this is what stands\n * between it and an `href`.\n *\n * Rejects rather than sanitizes: a destination that comes back safe is character-for-character the\n * trimmed authored value, so no value is ever silently rewritten into a different working one.\n */\nexport function resolveSafeHref(raw: string): SafeHrefResult {\n const href = raw.trim();\n if (href.length === 0) {\n return { safe: false, reason: 'blank' };\n }\n // Browsers strip ASCII tabs and newlines from a URL before parsing it, so `java<TAB>script:alert(1)`\n // executes. Refusing the whole value is the only defense that does not depend on stripping the same\n // characters in the same order the parser does.\n if (hasStrippableCharacter(href)) {\n return { safe: false, reason: 'malformed' };\n }\n // A URL parser reads `\\` as `/` under a special scheme, so `/\\evil.example` resolves against the\n // store's origin as an authority rather than a path. Any pair of leading slashes is an off-site\n // jump however it is spelled, and only `//` looks like one.\n if (/^[/\\\\]{2}/.test(href)) {\n return { safe: false, reason: 'protocol-relative' };\n }\n if (href.startsWith('/')) {\n return { safe: true, href };\n }\n\n let protocol: string;\n try {\n protocol = new URL(href).protocol;\n } catch {\n return { safe: false, reason: 'malformed' };\n }\n // `new URL` lowercases the protocol, so `JavaScript:` and `javascript:` are the same rejection.\n return ALLOWED_PROTOCOLS.has(protocol) ? { safe: true, href } : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/** The storefront's own sign-in route, which forwards to the platform's login. A fixed convention,\n * never an authored value. */\nconst SIGN_IN_ROUTE = '/login';\n\n/** The parameter carrying the shopper's way back, read by the sign-in route and the platform's login. */\nexport const RETURN_URL_PARAM = 'returnUrl';\n\n/**\n * `resolveSafeHref` narrowed to the site-relative subset: the destinations a shopper may be sent back to\n * across the auth hop. The allowlist admits absolute http(s) values, which are legitimate for an authored\n * link but are exactly what would turn the hop into an open redirect. Every other rejection travels out\n * unchanged, so a caller logging the reason still sees which spelling arrived.\n *\n * Both ends of the flow narrow through here rather than only the sender, because the sender cannot vouch\n * for a URL the shopper typed: the account button builds the destination through `resolveSignInHref`, and\n * the sign-in route revalidates what arrives through `resolveReturnHref`.\n */\nexport function resolveSiteRelativeHref(raw: string): SafeHrefResult {\n const destination = resolveSafeHref(raw);\n if (!destination.safe) {\n return destination;\n }\n return destination.href.startsWith('/') ? destination : { safe: false, reason: 'absolute' };\n}\n\n/** The subset of the allowlist that resolves to a page a document can be handed over to. */\nconst NAVIGABLE_PROTOCOLS = new Set(['http:', 'https:']);\n\n/**\n * `resolveSafeHref` narrowed to the destinations that resolve to a page: site-relative, or absolute\n * `http(s)`. `mailto:` and `tel:` are legitimate for an authored link and refused here, since handing a\n * document over to one leaves the shopper in a mail client instead of a login.\n */\nexport function resolveNavigableHref(raw: string): SafeHrefResult {\n const destination = resolveSafeHref(raw);\n if (!destination.safe || destination.href.startsWith('/')) {\n return destination;\n }\n // The allowlist already parsed this value, so the protocol is readable without a second guard.\n const { protocol } = new URL(destination.href);\n return NAVIGABLE_PROTOCOLS.has(protocol) ? destination : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/**\n * `resolveSiteRelativeHref` narrowed further, for a return arriving at the sign-in route: the route\n * itself is refused. Admitting it sends a shopper who just authenticated back to the trampoline, which\n * hands them to the platform a second time — a sign-in that never lands where the parameter promised.\n */\nexport function resolveReturnHref(raw: string): SafeHrefResult {\n const destination = resolveSiteRelativeHref(raw);\n if (!destination.safe) {\n return destination;\n }\n return isSignInRoute(destination.href) ? { safe: false, reason: 'sign-in-route' } : destination;\n}\n\n/** Whether a site-relative value names the storefront's own sign-in route, in any of its spellings. */\nexport function isSignInRoute(href: string): boolean {\n return pathOf(href) === SIGN_IN_ROUTE;\n}\n\n/**\n * `resolveNavigableHref` narrowed for the address a handover targets: the storefront's own sign-in\n * route is refused. Sending `/login` to `/login` reloads the same document, which mounts the same\n * handover again — a loop the shopper never gets past, and the mirror of the refusal\n * `resolveReturnHref` already makes on the way back.\n *\n * Only the site-relative spelling is decidable here, being same-origin by construction. An absolute\n * address needs the document's own origin to compare against, which this module does not have and a\n * prerender does not either, so `isSignInRoute` is exported for the caller that does. A cross-origin\n * login page whose path happens to be `/login` is a different page and is left alone.\n */\nexport function resolveLoginAddressHref(raw: string): SafeHrefResult {\n const address = resolveNavigableHref(raw);\n if (!address.safe || !address.href.startsWith('/')) {\n return address;\n }\n return isSignInRoute(address.href) ? { safe: false, reason: 'sign-in-route' } : address;\n}\n\n/**\n * Builds the sign-in destination, carrying the current path so the shopper can be returned where they\n * started. The sender's half only: nothing stops a hand-crafted link, so the sign-in route is to narrow\n * the parameter again on arrival. A path the narrowing refuses drops the parameter instead of\n * travelling encoded, and so does a router reporting no path — `usePathname()`'s type is why this takes\n * a nullable one.\n */\nexport function resolveSignInHref(pathname: string | null): string {\n if (!pathname) {\n return SIGN_IN_ROUTE;\n }\n const destination = resolveSiteRelativeHref(pathname);\n if (!destination.safe) {\n return SIGN_IN_ROUTE;\n }\n return `${SIGN_IN_ROUTE}?${RETURN_URL_PARAM}=${encodeURIComponent(destination.href)}`;\n}\n\n/** Opaque base for resolving a site-relative value to a path. Reserved by RFC 6761, so it never names a\n * real host; nothing is navigated to it and only `pathname` is read back. */\nconst PATH_COMPARISON_BASE = 'https://store.invalid';\n\n/**\n * The path of a site-relative value, without its query or fragment and without a trailing slash, so a\n * route comparison reads the route rather than one of its spellings. `/loginhelp` is a different path\n * and stays one.\n *\n * Parsed rather than sliced, because `/account/../login`, `/account\\..\\login` and `/account/%2e%2e/login`\n * are all spellings a URL parser resolves to `/login`. A slice compares them literally and admits them,\n * and the value then reaches a receiver that does resolve them — which puts the shopper back on the\n * trampoline the refusal exists to prevent. Only the comparison is normalized: the caller returns the\n * value as it arrived, so the promise that nothing is silently rewritten still holds.\n */\nfunction pathOf(href: string): string {\n const { pathname } = new URL(href, PATH_COMPARISON_BASE);\n return pathname.replace(/\\/+$/, '') || '/';\n}\n\n/** Any C0 control or space anywhere in the value — the set a URL parser removes or trims. */\nfunction hasStrippableCharacter(value: string): boolean {\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0;\n if (code <= 0x20 || code === 0x7f) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";AAUA,MAAM,oCAAoB,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;;;;;AAWxE,SAAgB,gBAAgB,KAA6B;CAC3D,MAAM,OAAO,IAAI,KAAK;CACtB,IAAI,KAAK,WAAW,GAClB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAQ;CAKxC,IAAI,uBAAuB,IAAI,GAC7B,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAY;CAK5C,IAAI,YAAY,KAAK,IAAI,GACvB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAoB;CAEpD,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO;EAAE,MAAM;EAAM;CAAK;CAG5B,IAAI;CACJ,IAAI;EACF,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC;CAC3B,QAAQ;EACN,OAAO;GAAE,MAAM;GAAO,QAAQ;EAAY;CAC5C;CAEA,OAAO,kBAAkB,IAAI,QAAQ,IAAI;EAAE,MAAM;EAAM;CAAK,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAmB;AAC5G;;;AAIA,MAAM,gBAAgB;;AAGtB,MAAa,mBAAmB;;;;;;;;;;;AAYhC,SAAgB,wBAAwB,KAA6B;CACnE,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,YAAY,KAAK,WAAW,GAAG,IAAI,cAAc;EAAE,MAAM;EAAO,QAAQ;CAAW;AAC5F;;AAGA,MAAM,sCAAsB,IAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;;;;;;AAOvD,SAAgB,qBAAqB,KAA6B;CAChE,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,QAAQ,YAAY,KAAK,WAAW,GAAG,GACtD,OAAO;CAGT,MAAM,EAAE,aAAa,IAAI,IAAI,YAAY,IAAI;CAC7C,OAAO,oBAAoB,IAAI,QAAQ,IAAI,cAAc;EAAE,MAAM;EAAO,QAAQ;CAAmB;AACrG;;;;;;AAOA,SAAgB,kBAAkB,KAA6B;CAC7D,MAAM,cAAc,wBAAwB,GAAG;CAC/C,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,cAAc,YAAY,IAAI,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAgB,IAAI;AACtF;;AAGA,SAAgB,cAAc,MAAuB;CACnD,OAAO,OAAO,IAAI,MAAM;AAC1B;;;;;;;;;;;;AAaA,SAAgB,wBAAwB,KAA6B;CACnE,MAAM,UAAU,qBAAqB,GAAG;CACxC,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,KAAK,WAAW,GAAG,GAC/C,OAAO;CAET,OAAO,cAAc,QAAQ,IAAI,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAgB,IAAI;AAClF;;;;;;;;AASA,SAAgB,kBAAkB,UAAiC;CACjE,IAAI,CAAC,UACH,OAAO;CAET,MAAM,cAAc,wBAAwB,QAAQ;CACpD,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,GAAG,cAAc,GAAG,iBAAiB,GAAG,mBAAmB,YAAY,IAAI;AACpF;;;AAIA,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,SAAS,OAAO,MAAsB;CACpC,MAAM,EAAE,aAAa,IAAI,IAAI,MAAM,oBAAoB;CACvD,OAAO,SAAS,QAAQ,QAAQ,EAAE,KAAK;AACzC;;AAGA,SAAS,uBAAuB,OAAwB;CACtD,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,OAAO,UAAU,YAAY,CAAC,KAAK;EACzC,IAAI,QAAQ,MAAQ,SAAS,KAC3B,OAAO;CAEX;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"safe-href.js","names":[],"sources":["../../src/utils/safe-href.ts"],"sourcesContent":["export type SafeHrefRejection =\n | 'blank'\n | 'malformed'\n | 'protocol-relative'\n | 'forbidden-scheme'\n | 'absolute'\n | 'sign-in-route';\n\nexport type SafeHrefResult = { safe: true; href: string } | { safe: false; reason: SafeHrefRejection };\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * The only gate on a CMS-authored destination: `http:`, `https:`, `mailto:`, `tel:`, and paths\n * starting with a single `/`. The runtime slice does not enforce a model's `pattern`, and the sukima\n * preflight that does never sees content published after a deploy, so an authored `javascript:` value\n * can reach the component intact and this is what stands between it and an `href`.\n *\n * Rejects rather than sanitizes: a destination that comes back safe is character-for-character the\n * trimmed authored value, so no value is ever silently rewritten into a different working one.\n */\nexport function resolveSafeHref(raw: string): SafeHrefResult {\n const href = raw.trim();\n if (href.length === 0) {\n return { safe: false, reason: 'blank' };\n }\n // Browsers strip ASCII tabs and newlines from a URL before parsing it, so `java<TAB>script:alert(1)`\n // executes. Refusing the whole value is the only defense that does not depend on stripping the same\n // characters in the same order the parser does.\n if (hasStrippableCharacter(href)) {\n return { safe: false, reason: 'malformed' };\n }\n // A URL parser reads `\\` as `/` under a special scheme, so `/\\evil.example` resolves against the\n // store's origin as an authority rather than a path. Any pair of leading slashes is an off-site\n // jump however it is spelled, and only `//` looks like one.\n if (/^[/\\\\]{2}/.test(href)) {\n return { safe: false, reason: 'protocol-relative' };\n }\n if (href.startsWith('/')) {\n return { safe: true, href };\n }\n\n let protocol: string;\n try {\n protocol = new URL(href).protocol;\n } catch {\n return { safe: false, reason: 'malformed' };\n }\n // `new URL` lowercases the protocol, so `JavaScript:` and `javascript:` are the same rejection.\n return ALLOWED_PROTOCOLS.has(protocol) ? { safe: true, href } : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/** The storefront's own sign-in route, which forwards to the platform's login. A fixed convention,\n * never an authored value. */\nconst SIGN_IN_ROUTE = '/login';\n\n/** The parameter carrying the shopper's way back, read by the sign-in route and the platform's login. */\nexport const RETURN_URL_PARAM = 'returnUrl';\n\n/**\n * `resolveSafeHref` narrowed to the site-relative subset: the destinations a shopper may be sent back to\n * across the auth hop. The allowlist admits absolute http(s) values, which are legitimate for an authored\n * link but are exactly what would turn the hop into an open redirect. Every other rejection travels out\n * unchanged, so a caller logging the reason still sees which spelling arrived.\n *\n * Both ends of the flow narrow through here rather than only the sender, because the sender cannot vouch\n * for a URL the shopper typed: the account button builds the destination through `resolveSignInHref`, and\n * the sign-in route revalidates what arrives through `resolveReturnHref`.\n */\nexport function resolveSiteRelativeHref(raw: string): SafeHrefResult {\n const destination = resolveSafeHref(raw);\n if (!destination.safe) {\n return destination;\n }\n return destination.href.startsWith('/') ? destination : { safe: false, reason: 'absolute' };\n}\n\n/** The subset of the allowlist that resolves to a page a document can be handed over to. */\nconst NAVIGABLE_PROTOCOLS = new Set(['http:', 'https:']);\n\n/**\n * `resolveSafeHref` narrowed to the destinations that resolve to a page: site-relative, or absolute\n * `http(s)`. `mailto:` and `tel:` are legitimate for an authored link and refused here, since handing a\n * document over to one leaves the shopper in a mail client instead of a login.\n */\nexport function resolveNavigableHref(raw: string): SafeHrefResult {\n const destination = resolveSafeHref(raw);\n if (!destination.safe || destination.href.startsWith('/')) {\n return destination;\n }\n // The allowlist already parsed this value, so the protocol is readable without a second guard.\n const { protocol } = new URL(destination.href);\n return NAVIGABLE_PROTOCOLS.has(protocol) ? destination : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/**\n * `resolveSiteRelativeHref` narrowed further, for a return arriving at the sign-in route: the route\n * itself is refused. Admitting it sends a shopper who just authenticated back to the trampoline, which\n * hands them to the platform a second time — a sign-in that never lands where the parameter promised.\n */\nexport function resolveReturnHref(raw: string): SafeHrefResult {\n const destination = resolveSiteRelativeHref(raw);\n if (!destination.safe) {\n return destination;\n }\n return isSignInRoute(destination.href) ? { safe: false, reason: 'sign-in-route' } : destination;\n}\n\n/** Whether a site-relative value names the storefront's own sign-in route, in any of its spellings. */\nexport function isSignInRoute(href: string): boolean {\n return pathOf(href) === SIGN_IN_ROUTE;\n}\n\n/**\n * `resolveNavigableHref` narrowed for the address a handover targets: the storefront's own sign-in\n * route is refused. Sending `/login` to `/login` reloads the same document, which mounts the same\n * handover again — a loop the shopper never gets past, and the mirror of the refusal\n * `resolveReturnHref` already makes on the way back.\n *\n * Only the site-relative spelling is decidable here, being same-origin by construction. An absolute\n * address needs the document's own origin to compare against, which this module does not have and a\n * prerender does not either, so `isSignInRoute` is exported for the caller that does. A cross-origin\n * login page whose path happens to be `/login` is a different page and is left alone.\n */\nexport function resolveLoginAddressHref(raw: string): SafeHrefResult {\n const address = resolveNavigableHref(raw);\n if (!address.safe || !address.href.startsWith('/')) {\n return address;\n }\n return isSignInRoute(address.href) ? { safe: false, reason: 'sign-in-route' } : address;\n}\n\n/**\n * Builds the sign-in destination, carrying the current path so the shopper can be returned where they\n * started. The sender's half only: nothing stops a hand-crafted link, so the sign-in route is to narrow\n * the parameter again on arrival. A path the narrowing refuses drops the parameter instead of\n * travelling encoded, and so does a router reporting no path — `usePathname()`'s type is why this takes\n * a nullable one.\n */\nexport function resolveSignInHref(pathname: string | null): string {\n if (!pathname) {\n return SIGN_IN_ROUTE;\n }\n const destination = resolveSiteRelativeHref(pathname);\n if (!destination.safe) {\n return SIGN_IN_ROUTE;\n }\n return `${SIGN_IN_ROUTE}?${RETURN_URL_PARAM}=${encodeURIComponent(destination.href)}`;\n}\n\n/** Opaque base for resolving a site-relative value to a path. Reserved by RFC 6761, so it never names a\n * real host; nothing is navigated to it and only `pathname` is read back. */\nconst PATH_COMPARISON_BASE = 'https://store.invalid';\n\n/**\n * The path of a site-relative value, without its query or fragment and without a trailing slash, so a\n * route comparison reads the route rather than one of its spellings. `/loginhelp` is a different path\n * and stays one.\n *\n * Parsed rather than sliced, because `/account/../login`, `/account\\..\\login` and `/account/%2e%2e/login`\n * are all spellings a URL parser resolves to `/login`. A slice compares them literally and admits them,\n * and the value then reaches a receiver that does resolve them — which puts the shopper back on the\n * trampoline the refusal exists to prevent. Only the comparison is normalized: the caller returns the\n * value as it arrived, so the promise that nothing is silently rewritten still holds.\n */\nfunction pathOf(href: string): string {\n const { pathname } = new URL(href, PATH_COMPARISON_BASE);\n return pathname.replace(/\\/+$/, '') || '/';\n}\n\n/** Any C0 control or space anywhere in the value — the set a URL parser removes or trims. */\nfunction hasStrippableCharacter(value: string): boolean {\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0;\n if (code <= 0x20 || code === 0x7f) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";AAUA,MAAM,oCAAoB,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;;;;;AAWxE,SAAgB,gBAAgB,KAA6B;CAC3D,MAAM,OAAO,IAAI,KAAK;CACtB,IAAI,KAAK,WAAW,GAClB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAQ;CAKxC,IAAI,uBAAuB,IAAI,GAC7B,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAY;CAK5C,IAAI,YAAY,KAAK,IAAI,GACvB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAoB;CAEpD,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO;EAAE,MAAM;EAAM;CAAK;CAG5B,IAAI;CACJ,IAAI;EACF,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC;CAC3B,QAAQ;EACN,OAAO;GAAE,MAAM;GAAO,QAAQ;EAAY;CAC5C;CAEA,OAAO,kBAAkB,IAAI,QAAQ,IAAI;EAAE,MAAM;EAAM;CAAK,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAmB;AAC5G;;;AAIA,MAAM,gBAAgB;;AAGtB,MAAa,mBAAmB;;;;;;;;;;;AAYhC,SAAgB,wBAAwB,KAA6B;CACnE,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,YAAY,KAAK,WAAW,GAAG,IAAI,cAAc;EAAE,MAAM;EAAO,QAAQ;CAAW;AAC5F;;AAGA,MAAM,sCAAsB,IAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;;;;;;AAOvD,SAAgB,qBAAqB,KAA6B;CAChE,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,QAAQ,YAAY,KAAK,WAAW,GAAG,GACtD,OAAO;CAGT,MAAM,EAAE,aAAa,IAAI,IAAI,YAAY,IAAI;CAC7C,OAAO,oBAAoB,IAAI,QAAQ,IAAI,cAAc;EAAE,MAAM;EAAO,QAAQ;CAAmB;AACrG;;;;;;AAOA,SAAgB,kBAAkB,KAA6B;CAC7D,MAAM,cAAc,wBAAwB,GAAG;CAC/C,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,cAAc,YAAY,IAAI,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAgB,IAAI;AACtF;;AAGA,SAAgB,cAAc,MAAuB;CACnD,OAAO,OAAO,IAAI,MAAM;AAC1B;;;;;;;;;;;;AAaA,SAAgB,wBAAwB,KAA6B;CACnE,MAAM,UAAU,qBAAqB,GAAG;CACxC,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,KAAK,WAAW,GAAG,GAC/C,OAAO;CAET,OAAO,cAAc,QAAQ,IAAI,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAgB,IAAI;AAClF;;;;;;;;AASA,SAAgB,kBAAkB,UAAiC;CACjE,IAAI,CAAC,UACH,OAAO;CAET,MAAM,cAAc,wBAAwB,QAAQ;CACpD,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,GAAG,cAAc,GAAG,iBAAiB,GAAG,mBAAmB,YAAY,IAAI;AACpF;;;AAIA,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,SAAS,OAAO,MAAsB;CACpC,MAAM,EAAE,aAAa,IAAI,IAAI,MAAM,oBAAoB;CACvD,OAAO,SAAS,QAAQ,QAAQ,EAAE,KAAK;AACzC;;AAGA,SAAS,uBAAuB,OAAwB;CACtD,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,OAAO,UAAU,YAAY,CAAC,KAAK;EACzC,IAAI,QAAQ,MAAQ,SAAS,KAC3B,OAAO;CAEX;CACA,OAAO;AACT"}
@@ -0,0 +1,33 @@
1
+ //#region src/utils/substitute-template.ts
2
+ const TOKEN_PATTERN = /\{(\w+)\}/g;
3
+ /**
4
+ * Shared copy-template substitution (T97, T31 — specs/make-the-payment-components-authored-copy.md
5
+ * Decision 7). Replaces every seven-plus `String.prototype.replace('{token}', value)` call sites
6
+ * across `payment-summary.tsx`, `order-placed-payment-summary.tsx`, and `installments.tsx`, each of
7
+ * which substituted only the *first* occurrence of a token and, at two sites, passed the value as a
8
+ * replacement *string* — letting a server-sourced value containing `$&`/`` $` ``/`$'`/`$$` rewrite the
9
+ * surrounding template text.
10
+ *
11
+ * One `/\{(\w+)\}/g` pass with a replacer *function*: every occurrence of every token is substituted
12
+ * in a single scan, every value is inserted literally (a function's return value is never
13
+ * re-interpreted as a replacement pattern), and a token with no matching entry in `tokens` is left
14
+ * verbatim — so a template can be reworded freely without this helper silently dropping an
15
+ * unrecognized placeholder. Because it is one pass, a substituted value can never itself be re-scanned
16
+ * by a later token's own substitution.
17
+ *
18
+ * `Object.hasOwn`, not `tokenName in tokens` or a bare index lookup (fix pass, finding 6): every plain
19
+ * object inherits from `Object.prototype`, so `tokens['toString']` resolves to
20
+ * `Object.prototype.toString` — a function, therefore never `null`/`undefined` — rather than falling
21
+ * through to `match` on a `??` lookup. `{toString}`, `{constructor}`, and `{__proto__}` would each
22
+ * silently render an inherited value instead of being left verbatim as documented above.
23
+ */
24
+ function substituteTemplate(template, tokens) {
25
+ return template.replace(TOKEN_PATTERN, (match, tokenName) => {
26
+ if (!Object.hasOwn(tokens, tokenName)) return match;
27
+ return tokens[tokenName] ?? match;
28
+ });
29
+ }
30
+ //#endregion
31
+ export { substituteTemplate };
32
+
33
+ //# sourceMappingURL=substitute-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"substitute-template.js","names":[],"sources":["../../src/utils/substitute-template.ts"],"sourcesContent":["const TOKEN_PATTERN = /\\{(\\w+)\\}/g;\n\n/**\n * Shared copy-template substitution (T97, T31 — specs/make-the-payment-components-authored-copy.md\n * Decision 7). Replaces every seven-plus `String.prototype.replace('{token}', value)` call sites\n * across `payment-summary.tsx`, `order-placed-payment-summary.tsx`, and `installments.tsx`, each of\n * which substituted only the *first* occurrence of a token and, at two sites, passed the value as a\n * replacement *string* — letting a server-sourced value containing `$&`/`` $` ``/`$'`/`$$` rewrite the\n * surrounding template text.\n *\n * One `/\\{(\\w+)\\}/g` pass with a replacer *function*: every occurrence of every token is substituted\n * in a single scan, every value is inserted literally (a function's return value is never\n * re-interpreted as a replacement pattern), and a token with no matching entry in `tokens` is left\n * verbatim — so a template can be reworded freely without this helper silently dropping an\n * unrecognized placeholder. Because it is one pass, a substituted value can never itself be re-scanned\n * by a later token's own substitution.\n *\n * `Object.hasOwn`, not `tokenName in tokens` or a bare index lookup (fix pass, finding 6): every plain\n * object inherits from `Object.prototype`, so `tokens['toString']` resolves to\n * `Object.prototype.toString` — a function, therefore never `null`/`undefined` — rather than falling\n * through to `match` on a `??` lookup. `{toString}`, `{constructor}`, and `{__proto__}` would each\n * silently render an inherited value instead of being left verbatim as documented above.\n */\nexport function substituteTemplate(template: string, tokens: Readonly<Record<string, string>>): string {\n return template.replace(TOKEN_PATTERN, (match, tokenName: string) => {\n if (!Object.hasOwn(tokens, tokenName)) return match;\n // `noUncheckedIndexedAccess` still types this lookup as `string | undefined`; the `Object.hasOwn`\n // check above is what actually guarantees a string is here, so this `??` can never fall through\n // for a real token — it only satisfies the index-signature type, never re-opens finding 6's bug.\n return tokens[tokenName] ?? match;\n });\n}\n"],"mappings":";AAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;AAuBtB,SAAgB,mBAAmB,UAAkB,QAAkD;CACrG,OAAO,SAAS,QAAQ,gBAAgB,OAAO,cAAsB;EACnE,IAAI,CAAC,OAAO,OAAO,QAAQ,SAAS,GAAG,OAAO;EAI9C,OAAO,OAAO,cAAc;CAC9B,CAAC;AACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "FastStore AI Native component library built on Base UI with AI-editable styles",
5
5
  "license": "MIT",
6
6
  "homepage": "https://developers.vtex.com/docs/guides/faststore",
@@ -52,6 +52,7 @@
52
52
  "@base-ui/react": "^1.3.0",
53
53
  "@graphql-typed-document-node/core": "^3.2.0",
54
54
  "graphql": "^16.13.1",
55
+ "react-hook-form": "^7.85.0",
55
56
  "react-intl": "^10.1.22",
56
57
  "rxjs": "^7.8.2",
57
58
  "tailwindcss": "^4.2.2",
@@ -30,3 +30,37 @@
30
30
  --max-width-md: var(--container-md);
31
31
  --max-width-lg: var(--container-lg);
32
32
  }
33
+
34
+ @layer base {
35
+ /* Default visible focus indicator for the payment surface (T38, REQ-11's WCAG scenario).
36
+ `@layer base` so any merchant utility class outranks it under Tailwind 4's layer order.
37
+ `currentColor` because the merchant's palette is unknown here — see Decision 6 on contrast.
38
+ Scoped by an explicit hook list, not by an ancestor: `PaymentSectionController` renders no DOM
39
+ element, so no ancestor selector reaches a controller-composed page. */
40
+ [data-fs-payment-method-list] :focus-visible,
41
+ [data-fs-installments] :focus-visible,
42
+ [data-fs-saved-cards] :focus-visible,
43
+ [data-fs-billing-address] :focus-visible,
44
+ [data-fs-card-entry] :focus-visible,
45
+ [data-fs-cardholder-document] :focus-visible,
46
+ [data-fs-save-card] :focus-visible,
47
+ [data-fs-place-order] :focus-visible,
48
+ [data-fs-installments-option]:focus-visible {
49
+ outline: 2px solid currentColor;
50
+ outline-offset: 2px;
51
+ }
52
+
53
+ /* `<fieldset>` carries `min-inline-size: min-content`, which Tailwind's preflight does not reset,
54
+ so it refuses to shrink inside a flex row. Live already via CardEntry's fieldset; mitigated here
55
+ for the three groups T38 adds. Scoped by the same explicit hook-list approach as the focus rule
56
+ above, not by `[data-fs-fieldset-root]`: the Fieldset particle emits that attribute
57
+ unconditionally, so it also reaches `ClientProfileForm`'s optional grouping fieldset — a
58
+ non-payment component whose rendering must not change (measured: 95px vs. 113px in a 200px flex
59
+ row with a competing sibling). */
60
+ [data-fs-payment-method-list-fieldset],
61
+ [data-fs-installments-fieldset],
62
+ [data-fs-saved-cards-fieldset],
63
+ [data-fs-card-entry-fieldset] {
64
+ min-inline-size: 0;
65
+ }
66
+ }
@@ -1,192 +0,0 @@
1
- # ClientProfileForm
2
-
3
- **Category:** identity
4
- **Purpose:** The batched customer-information form for the checkout identity step — the `InformationFormUSA` replacement. Pre-fills first/last name, phone, document, and both consent opt-ins from the shopper's saved profile and preferences; displays the email read-only. A single Save / Continue action validates the draft and dispatches at most one `clientProfileUpdate` and one `clientPreferencesUpdate`, each carrying only the fields the shopper edited, with pending/confirmation/error feedback.
5
-
6
- ---
7
-
8
- ## Visual cues (how to identify in an image)
9
-
10
- - A single form block on a checkout identity step titled "Your information" (or similar), grouping: a read-only email display, first name, last name, a country-masked phone field, an optional tax-document field (e.g. CPF for Brazil), and two checkboxes ("email me with news and offers", "save my information for next time").
11
- - One visible primary button at the bottom labelled "Save and continue" (or similar) — not a hidden submit.
12
- - Pre-filled values for a returning/identified shopper; blank, still-editable fields for a guest or an incomplete profile.
13
- - An inline error banner on failure, or a confirmation line on success, appearing near the button.
14
-
15
- Use `ClientProfileForm` (not a hand-assembled combination of `Field`/`PhoneField`/`DocumentField`) whenever the image shows this whole batched-save customer-information block.
16
-
17
- ---
18
-
19
- ## When to use
20
-
21
- - The customer-information section of the checkout identity step, for both returning/identified shoppers (pre-fill + edit) and guests (fill from empty).
22
- - Anywhere name, phone, document, and consent opt-ins must be collected together and persisted in one explicit action.
23
-
24
- ## When NOT to use
25
-
26
- - Editing the email — that is `EmailIdentificationControl`'s job; this form only displays it.
27
- - A single standalone phone or opt-in field with its own save-on-blur behavior — use `ClientProfilePhoneControl` / `NewsletterOptInControl` / `SavePersonalDataOptInControl` instead. **Never place a standalone per-field identity Control and this form for the same field on the same view** — both would write the field, racing each other.
28
- - Address fields — those belong to Delivery, not this form.
29
-
30
- ---
31
-
32
- ## Composition
33
-
34
- `ClientProfileForm` is a **compound**: `Root` owns the data + mechanics; the field/fieldset/feedback
35
- parts are composed freely (any order, any subset). The schema author decides which fields render — a
36
- merchant whose platform config drops a field (or whose account can't persist one, e.g. an unsupported
37
- `savePersonalData`) simply does not compose that part.
38
-
39
- ```
40
- SessionRoot (required ancestor; session tri-state + canEditData)
41
- └── ClientProfileForm.Root (data-fs-client-profile-form; own fragment/data root; owns draft + Save)
42
- ├── ClientProfileForm.Fieldset section="profile" [legend]
43
- │ ├── .EmailField (read-only)
44
- │ ├── .FirstNameField / .LastNameField
45
- │ ├── .PhoneField (uses country from Root)
46
- │ └── .DocumentField (self-gates on documentType)
47
- ├── ClientProfileForm.Fieldset section="preferences" [legend]
48
- │ ├── .NewsletterCheckbox
49
- │ └── .SavePersonalDataCheckbox (omit to drop the field entirely — no CSS hack)
50
- ├── .Error / .Confirmation
51
- └── .SubmitButton
52
- ```
53
-
54
- `Root` is self-contained: it declares the colocated fragment over `clientProfile` + `clientPreferences`
55
- and must not be nested inside `ClientProfileRoot` / `ClientPreferencesRoot` (those self-persist per
56
- field on blur, double-writing against this form's batched Save). Every part `requiresContext:
57
- 'client-profile-form'`; a custom/derived field declares its own additional `requiresContext`.
58
-
59
- ---
60
-
61
- ## Behavior
62
-
63
- - **Sections**: compose `ClientProfileForm.Fieldset` blocks with a `section` name → `data-fs-client-profile-form-section="<name>"`, and an optional `legend`. Not hard-coded — the author groups fields however the design needs.
64
- - **Pre-fill**: every field seeds from `$data` on mount; missing profile fields render empty and stay editable.
65
- - **Draft & dirty tracking**: edits are tracked locally per field; only touched fields are dispatched. Background `$data` refreshes re-seed untouched fields only — an in-progress edit is never clobbered.
66
- - **Save**: validates **field format only** — phone mask and document check-digit, each blocking Save when the field is non-empty and malformed. It does not enforce requiredness: the parts are composed freely by the schema, so a required check here could fire for a field the page never mounted. Which fields a shopper must fill is a business rule, waiting on the composable entity forms RFC (#572) to decide where it belongs. The format checks carry the same composition hazard in a narrower form, and it is still open: they key off the Root's `country` and `documentType` rather than off which leaves were composed, so a store that sets `documentType` without composing `DocumentField` can block Save with no field to render the error in and nowhere to move focus. Closing it properly means the leaves registering themselves with the Root, which is that same RFC's territory. The phone case is wider than the document one and needs no merchant configuration at all — see Known gaps below. On Save it dispatches at most one `clientProfileUpdate` (dirty profile fields) and one `clientPreferencesUpdate` (dirty consent flags) in parallel; a Save with nothing dirty dispatches nothing and shows the confirmation, so the press still answers.
67
- - **Document erase**: clearing a previously-saved document and pressing Save sends `document: ''` — an explicit erase, not an omission.
68
- - **Feedback**: `data-fs-client-profile-form-pending` while a write is in flight (fieldset disabled, Save not re-triggerable); an inline `role="alert"` region on `userErrors`; an `<output>` confirmation on success, cleared on the next edit.
69
- - **Read-only**: `session.canEditData === false` renders `data-fs-client-profile-form-readonly`, disables every field, and omits the Save button entirely.
70
- - **The form announces nothing**: a completed Save shows the confirmation and stops there. It publishes no signal, so nothing elsewhere collapses, advances or reacts. Whether finishing this form should move a section or a wizard is a sequencing question, and the mechanism for it is being designed in the flow-of-steps RFC (#611) rather than assumed here.
71
- - **The form never learns who listens.** It reports nothing at all, so `IdentityEditController` cannot react to it: that section moves only on its own buttons. Compose the swap around this form: an `IdentityEditController` holding an `IdentitySummaryView` with the summary, an `IdentityEditForm` with `ClientProfileForm.Root`, an `IdentityEditButton` in the heading, and an `IdentityEditCancelButton` as cancel next to the form. The same form therefore works unchanged inside a collapsing section or on a page of its own.
72
-
73
- ---
74
-
75
- ## Props
76
-
77
- **`ClientProfileForm.Root`** — `country` (`'US' | 'BR'`, default `'US'`; provided to the phone leaf via
78
- context), `documentType` (string, e.g. `'CPF'`; the document leaf renders only when supported).
79
- Declares `actions: { identity: ['clientProfileUpdate', 'clientPreferencesUpdate'] }`,
80
- `requiresContext: 'session'`, `providesContext: 'client-profile-form'`.
81
-
82
- **`ClientProfileForm.Fieldset`** — `legend` (optional visible title), `section` (name → `data-fs-*-section`).
83
-
84
- **Field parts** (`.EmailField`, `.FirstNameField`, `.LastNameField`, `.PhoneField`, `.DocumentField`,
85
- `.NewsletterCheckbox`, `.SavePersonalDataCheckbox`) — each takes an optional `label` (sensible default).
86
- **`.Confirmation`** — `message` (default `'Your information has been saved.'`). **`.SubmitButton`** —
87
- `label` (default `'Save and continue'`) or composed `children`.
88
-
89
- ---
90
-
91
- ## Canonical schema example
92
-
93
- ```json
94
- {
95
- "session": {
96
- "type": "SessionRoot",
97
- "children": ["email-identification", "profile-form"]
98
- },
99
- "email-identification": {
100
- "type": "EmailIdentificationControl",
101
- "children": []
102
- },
103
- "profile-form": {
104
- "type": "ClientProfileFormRoot",
105
- "props": { "country": "BR", "documentType": "CPF" },
106
- "children": ["profile-fieldset", "preferences-fieldset", "form-error", "form-confirmation", "form-submit"]
107
- },
108
- "profile-fieldset": {
109
- "type": "ClientProfileFormFieldset",
110
- "props": { "section": "profile", "legend": "Your information" },
111
- "children": ["email", "first-name", "last-name", "phone", "document"]
112
- },
113
- "email": { "type": "ClientProfileFormEmailField", "children": [] },
114
- "first-name": { "type": "ClientProfileFormFirstNameField", "children": [] },
115
- "last-name": { "type": "ClientProfileFormLastNameField", "children": [] },
116
- "phone": { "type": "ClientProfileFormPhoneField", "children": [] },
117
- "document": { "type": "ClientProfileFormDocumentField", "children": [] },
118
- "preferences-fieldset": {
119
- "type": "ClientProfileFormFieldset",
120
- "props": { "section": "preferences" },
121
- "children": ["newsletter"]
122
- },
123
- "newsletter": { "type": "ClientProfileFormNewsletterCheckbox", "children": [] },
124
- "form-error": { "type": "ClientProfileFormError", "children": [] },
125
- "form-confirmation": { "type": "ClientProfileFormConfirmation", "children": [] },
126
- "form-submit": { "type": "ClientProfileFormSubmitButton", "children": [] }
127
- }
128
- ```
129
-
130
- Both fieldsets are children of the Root, not siblings of it: every part declares
131
- `requiresContext: 'client-profile-form'`, so a fieldset placed outside is rejected by the
132
- context-constraint walk at build time.
133
-
134
- (Omitting `ClientProfileFormSavePersonalDataCheckbox` above drops that field entirely — the intended
135
- way to exclude a field the account can't persist, with no CSS hack and no shared-component change.)
136
-
137
- ---
138
-
139
- ## Common mistakes
140
-
141
- - **Rendering `ClientProfileForm` outside `SessionRoot`.** `useSession()` throws; every identity page must host a `SessionRoot`.
142
- - **Nesting it inside `ClientProfileRoot` / `ClientPreferencesRoot`, or placing a standalone Control for the same field alongside it.** The form is its own data root and its own write surface — double-nesting or double-placing creates a double-write race.
143
- - **Expecting the email to be editable.** It is always read-only here; email changes go through `EmailIdentificationControl`.
144
- - **Expecting per-keystroke or per-blur saves.** All fields are form-bound; only pressing Save dispatches a write.
145
- - **Expecting the form to enforce required fields.** It validates format only. A blank field saves, because the form cannot know which of its freely-composed parts the page actually mounted.
146
- - **Rendering a field part outside `ClientProfileForm.Root`.** Every part `requiresContext: 'client-profile-form'` and throws otherwise.
147
-
148
- ---
149
-
150
- ## Styling
151
-
152
- | Component | What you style | Plausible classes |
153
- |---|---|---|
154
- | `ClientProfileForm` | The `data-fs-client-profile-form` root; carries `-pending`/`-readonly`/`-saved` presence attributes | `flex flex-col gap-10` |
155
- | Section | `data-fs-client-profile-form-section="profile\|preferences"` — the two entity-anchored fieldsets | `flex flex-col gap-4` |
156
- | Field wrapper | `data-fs-client-profile-form-field="email\|first-name\|last-name\|phone\|document\|newsletter\|save-personal-data"` | `flex flex-col gap-4` |
157
- | Error / confirmation | `[data-fs-client-profile-form-error]` / `[data-fs-client-profile-form-confirmation]` | `text-sm text-red-600` / `text-sm text-green-600` |
158
-
159
- Field-level visuals (borders, focus rings, error color) are owned by the composed particles' own swatches (`field`, `phone-field`, `document-field`, `checkbox`, `button`) and are not redefined here.
160
-
161
- ---
162
-
163
- ## Known gaps
164
-
165
- Found by review and deliberately left for the dedicated revision of these components, rather than patched
166
- here. The first is the widest.
167
-
168
- - **Format validation runs against fields the page may never have composed, and the phone case needs no
169
- merchant configuration at all.** `country` defaults to `'US'`, so the phone check always runs. A store
170
- composing `Root` + name fields + `SubmitButton`, serving a shopper whose stored phone is a legacy
171
- malformed value, gets a Save that sets `errors.phone`, finds no phone input to focus, renders no message
172
- because `PhoneField` is not mounted, and returns — dead on every press, button still enabled. The
173
- `documentType` variant of the same hole at least requires the merchant to have set a Root prop. Closing
174
- both properly means the leaves registering themselves with the Root; see the composable entity forms RFC
175
- (#572).
176
- - **Two other early returns leave Save silent.** A missing `$actions.identity`, or a dirty field with no
177
- matching write function, returns without touching `pending`, `serverError` or `saved` — the same dead
178
- press the pristine branch above was changed to avoid.
179
- - **`VALIDATION_ORDER` is a hard-coded `['phone', 'document']`,** but the leaves are now composed in any
180
- order. With both fields malformed, focus jumps to the phone input even when the document error is the one
181
- the shopper can see first. The order should come from DOM position now that the fixed template is gone.
182
- - **The re-seed of untouched fields mutates a ref during render** and queues its `setDraft` in the same
183
- pass. A discarded render keeps the ref but loses the update, so pristine fields can hold stale values
184
- after `$data` arrives.
185
-
186
- ## Related
187
-
188
- - [[email-identification]] — the sibling identification control this form's email display defers to.
189
- - [[client-profile]] — the standalone, per-field-blur phone binding; do not compose alongside this form for the same field.
190
- - [[client-preferences]] — the standalone, per-field-blur consent Controls; do not compose alongside this form for the same field.
191
- - [[session]] — provides the identification tri-state and the required `SessionRoot` ancestor.
192
- - [[phone-field]], [[document-field]], [[field]], [[fieldset]], [[checkbox]] — the particles this matter composes directly.