@salesforce/retail-react-app 1.0.0-preview.0

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 (425) hide show
  1. package/.eslintignore +7 -0
  2. package/.eslintrc.js +25 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc.yaml +7 -0
  5. package/CHANGELOG.md +173 -0
  6. package/LICENSE +14 -0
  7. package/README.md +48 -0
  8. package/app/assets/svg/account.svg +3 -0
  9. package/app/assets/svg/alert.svg +3 -0
  10. package/app/assets/svg/basket.svg +3 -0
  11. package/app/assets/svg/brand-logo.svg +10 -0
  12. package/app/assets/svg/cc-amex.svg +7 -0
  13. package/app/assets/svg/cc-cvv.svg +8 -0
  14. package/app/assets/svg/cc-discover.svg +14 -0
  15. package/app/assets/svg/cc-mastercard.svg +8 -0
  16. package/app/assets/svg/cc-visa.svg +11 -0
  17. package/app/assets/svg/check-circle.svg +3 -0
  18. package/app/assets/svg/check.svg +3 -0
  19. package/app/assets/svg/chevron-down.svg +3 -0
  20. package/app/assets/svg/chevron-left.svg +3 -0
  21. package/app/assets/svg/chevron-right.svg +3 -0
  22. package/app/assets/svg/chevron-up.svg +3 -0
  23. package/app/assets/svg/close.svg +3 -0
  24. package/app/assets/svg/dashboard.svg +4 -0
  25. package/app/assets/svg/figma-logo.svg +14 -0
  26. package/app/assets/svg/file.svg +3 -0
  27. package/app/assets/svg/filter.svg +3 -0
  28. package/app/assets/svg/flag-ca.svg +5 -0
  29. package/app/assets/svg/flag-cn.svg +19 -0
  30. package/app/assets/svg/flag-fr.svg +19 -0
  31. package/app/assets/svg/flag-gb.svg +16 -0
  32. package/app/assets/svg/flag-it.svg +29 -0
  33. package/app/assets/svg/flag-jp.svg +10 -0
  34. package/app/assets/svg/flag-us.svg +7 -0
  35. package/app/assets/svg/github-logo.svg +40 -0
  36. package/app/assets/svg/hamburger.svg +8 -0
  37. package/app/assets/svg/heart-solid.svg +7 -0
  38. package/app/assets/svg/heart.svg +3 -0
  39. package/app/assets/svg/info.svg +3 -0
  40. package/app/assets/svg/like.svg +4 -0
  41. package/app/assets/svg/location.svg +3 -0
  42. package/app/assets/svg/lock.svg +3 -0
  43. package/app/assets/svg/paypal.svg +19 -0
  44. package/app/assets/svg/plug.svg +3 -0
  45. package/app/assets/svg/plus.svg +3 -0
  46. package/app/assets/svg/receipt.svg +3 -0
  47. package/app/assets/svg/search.svg +8 -0
  48. package/app/assets/svg/signout.svg +3 -0
  49. package/app/assets/svg/social-facebook.svg +3 -0
  50. package/app/assets/svg/social-instagram.svg +3 -0
  51. package/app/assets/svg/social-pinterest.svg +4 -0
  52. package/app/assets/svg/social-twitter.svg +3 -0
  53. package/app/assets/svg/social-youtube.svg +3 -0
  54. package/app/assets/svg/user.svg +3 -0
  55. package/app/assets/svg/visibility-off.svg +5 -0
  56. package/app/assets/svg/visibility.svg +3 -0
  57. package/app/components/_app/index.jsx +401 -0
  58. package/app/components/_app/index.test.js +85 -0
  59. package/app/components/_app/partials/above-header.jsx +10 -0
  60. package/app/components/_app-config/index.jsx +125 -0
  61. package/app/components/_app-config/index.test.js +77 -0
  62. package/app/components/_error/index.jsx +142 -0
  63. package/app/components/_error/index.test.js +25 -0
  64. package/app/components/action-card/index.jsx +75 -0
  65. package/app/components/address-display/index.jsx +30 -0
  66. package/app/components/basic-tile/index.jsx +65 -0
  67. package/app/components/basic-tile/index.test.js +23 -0
  68. package/app/components/breadcrumb/index.jsx +67 -0
  69. package/app/components/breadcrumb/index.test.js +30 -0
  70. package/app/components/confirmation-modal/index.jsx +111 -0
  71. package/app/components/confirmation-modal/index.test.js +98 -0
  72. package/app/components/drawer-menu/index.jsx +405 -0
  73. package/app/components/drawer-menu/index.test.js +33 -0
  74. package/app/components/dynamic-image/index.jsx +56 -0
  75. package/app/components/field/index.jsx +161 -0
  76. package/app/components/footer/index.jsx +269 -0
  77. package/app/components/footer/index.test.js +22 -0
  78. package/app/components/forms/address-fields.jsx +49 -0
  79. package/app/components/forms/credit-card-fields.jsx +149 -0
  80. package/app/components/forms/form-action-buttons.jsx +55 -0
  81. package/app/components/forms/login-fields.jsx +31 -0
  82. package/app/components/forms/password-requirements.jsx +99 -0
  83. package/app/components/forms/post-checkout-registration-fields.jsx +43 -0
  84. package/app/components/forms/profile-fields.jsx +36 -0
  85. package/app/components/forms/promo-code-fields.jsx +43 -0
  86. package/app/components/forms/registration-fields.jsx +42 -0
  87. package/app/components/forms/reset-password-fields.jsx +31 -0
  88. package/app/components/forms/state-province-options.jsx +75 -0
  89. package/app/components/forms/update-password-fields.jsx +49 -0
  90. package/app/components/forms/useAddressFields.jsx +196 -0
  91. package/app/components/forms/useCreditCardFields.jsx +146 -0
  92. package/app/components/forms/useLoginFields.jsx +52 -0
  93. package/app/components/forms/useProfileFields.jsx +95 -0
  94. package/app/components/forms/usePromoCodeFields.jsx +39 -0
  95. package/app/components/forms/useRegistrationFields.jsx +136 -0
  96. package/app/components/forms/useResetPasswordFields.jsx +40 -0
  97. package/app/components/forms/useUpdatePasswordFields.jsx +89 -0
  98. package/app/components/header/index.jsx +290 -0
  99. package/app/components/header/index.test.js +217 -0
  100. package/app/components/hero/index.jsx +84 -0
  101. package/app/components/hero/index.test.js +40 -0
  102. package/app/components/icons/index.jsx +158 -0
  103. package/app/components/icons/index.test.js +20 -0
  104. package/app/components/image-gallery/index.jsx +176 -0
  105. package/app/components/image-gallery/index.test.js +485 -0
  106. package/app/components/item-variant/index.jsx +33 -0
  107. package/app/components/item-variant/item-attributes.jsx +107 -0
  108. package/app/components/item-variant/item-image.jsx +73 -0
  109. package/app/components/item-variant/item-name.jsx +28 -0
  110. package/app/components/item-variant/item-price.jsx +117 -0
  111. package/app/components/link/index.jsx +32 -0
  112. package/app/components/link/index.test.js +72 -0
  113. package/app/components/links-list/index.jsx +89 -0
  114. package/app/components/links-list/index.test.js +62 -0
  115. package/app/components/list-menu/index.jsx +280 -0
  116. package/app/components/list-menu/index.test.js +44 -0
  117. package/app/components/loading-spinner/index.jsx +46 -0
  118. package/app/components/locale-selector/index.jsx +124 -0
  119. package/app/components/locale-selector/index.test.js +37 -0
  120. package/app/components/locale-text/index.jsx +97 -0
  121. package/app/components/locale-text/index.test.js +36 -0
  122. package/app/components/login/index.jsx +96 -0
  123. package/app/components/nested-accordion/index.jsx +185 -0
  124. package/app/components/nested-accordion/index.test.js +98 -0
  125. package/app/components/offline-banner/index.jsx +40 -0
  126. package/app/components/offline-banner/index.test.js +15 -0
  127. package/app/components/offline-boundary/index.jsx +104 -0
  128. package/app/components/offline-boundary/index.test.js +123 -0
  129. package/app/components/order-summary/index.jsx +331 -0
  130. package/app/components/page-action-placeholder/index.jsx +50 -0
  131. package/app/components/pagination/index.jsx +134 -0
  132. package/app/components/pagination/index.test.js +25 -0
  133. package/app/components/product-item/index.jsx +146 -0
  134. package/app/components/product-item/index.test.js +38 -0
  135. package/app/components/product-scroller/index.jsx +172 -0
  136. package/app/components/product-scroller/index.test.js +98 -0
  137. package/app/components/product-tile/index.jsx +195 -0
  138. package/app/components/product-tile/index.test.js +96 -0
  139. package/app/components/product-view/index.jsx +538 -0
  140. package/app/components/product-view/index.test.js +224 -0
  141. package/app/components/product-view-modal/index.jsx +48 -0
  142. package/app/components/product-view-modal/index.test.js +72 -0
  143. package/app/components/promo-code/index.jsx +162 -0
  144. package/app/components/promo-popover/index.jsx +83 -0
  145. package/app/components/quantity-picker/index.jsx +58 -0
  146. package/app/components/radio-card/index.jsx +75 -0
  147. package/app/components/recommended-products/index.jsx +227 -0
  148. package/app/components/register/index.jsx +114 -0
  149. package/app/components/reset-password/index.jsx +87 -0
  150. package/app/components/responsive/index.jsx +29 -0
  151. package/app/components/scroll-to-top/index.jsx +24 -0
  152. package/app/components/scroll-to-top/index.test.js +46 -0
  153. package/app/components/search/index.jsx +279 -0
  154. package/app/components/search/index.test.js +127 -0
  155. package/app/components/search/partials/recent-searches.jsx +76 -0
  156. package/app/components/search/partials/search-suggestions.jsx +45 -0
  157. package/app/components/search/partials/suggestions.jsx +43 -0
  158. package/app/components/section/index.jsx +68 -0
  159. package/app/components/seo/index.jsx +33 -0
  160. package/app/components/social-icons/index.jsx +101 -0
  161. package/app/components/social-icons/index.test.js +30 -0
  162. package/app/components/swatch-group/index.jsx +77 -0
  163. package/app/components/swatch-group/index.test.js +136 -0
  164. package/app/components/swatch-group/swatch.jsx +94 -0
  165. package/app/components/toggle-card/index.jsx +97 -0
  166. package/app/components/with-registration/index.jsx +58 -0
  167. package/app/components/with-registration/index.test.js +85 -0
  168. package/app/constants.js +109 -0
  169. package/app/contexts/index.js +92 -0
  170. package/app/hooks/einstein-mock-data.js +916 -0
  171. package/app/hooks/index.js +17 -0
  172. package/app/hooks/use-add-to-cart-modal.js +344 -0
  173. package/app/hooks/use-add-to-cart-modal.test.js +625 -0
  174. package/app/hooks/use-auth-modal.js +337 -0
  175. package/app/hooks/use-auth-modal.test.js +365 -0
  176. package/app/hooks/use-currency.js +20 -0
  177. package/app/hooks/use-currency.test.js +41 -0
  178. package/app/hooks/use-current-basket.js +39 -0
  179. package/app/hooks/use-current-customer.js +29 -0
  180. package/app/hooks/use-derived-product.js +77 -0
  181. package/app/hooks/use-derived-product.test.js +69 -0
  182. package/app/hooks/use-einstein.js +512 -0
  183. package/app/hooks/use-einstein.test.js +224 -0
  184. package/app/hooks/use-intersection-observer.js +64 -0
  185. package/app/hooks/use-limit-urls.js +31 -0
  186. package/app/hooks/use-limit-urls.test.js +40 -0
  187. package/app/hooks/use-multi-site.js +36 -0
  188. package/app/hooks/use-multi-site.test.js +53 -0
  189. package/app/hooks/use-navigation.js +37 -0
  190. package/app/hooks/use-navigation.test.js +109 -0
  191. package/app/hooks/use-page-urls.js +35 -0
  192. package/app/hooks/use-page-urls.test.js +39 -0
  193. package/app/hooks/use-pdp-search-params.js +16 -0
  194. package/app/hooks/use-pdp-search-params.test.js +52 -0
  195. package/app/hooks/use-previous.js +17 -0
  196. package/app/hooks/use-product-view-modal.js +93 -0
  197. package/app/hooks/use-product-view-modal.test.js +172 -0
  198. package/app/hooks/use-search-params.js +96 -0
  199. package/app/hooks/use-search-params.test.js +91 -0
  200. package/app/hooks/use-sort-urls.js +33 -0
  201. package/app/hooks/use-sort-urls.test.js +42 -0
  202. package/app/hooks/use-toast.js +68 -0
  203. package/app/hooks/use-toast.test.js +58 -0
  204. package/app/hooks/use-variant.js +32 -0
  205. package/app/hooks/use-variant.test.js +81 -0
  206. package/app/hooks/use-variation-attributes.js +138 -0
  207. package/app/hooks/use-variation-attributes.test.js +119 -0
  208. package/app/hooks/use-variation-params.js +31 -0
  209. package/app/hooks/use-variation-params.test.js +73 -0
  210. package/app/hooks/use-wish-list.js +42 -0
  211. package/app/main.jsx +14 -0
  212. package/app/mocks/basket-with-suit.js +146 -0
  213. package/app/mocks/empty-basket.js +39 -0
  214. package/app/mocks/mock-data.js +5632 -0
  215. package/app/mocks/product-set-winter-lookM.js +1224 -0
  216. package/app/mocks/searchResults.js +144 -0
  217. package/app/mocks/variant-750518699578M.js +434 -0
  218. package/app/page-designer/README.md +102 -0
  219. package/app/page-designer/assets/image-tile/index.jsx +51 -0
  220. package/app/page-designer/assets/image-tile/index.test.js +30 -0
  221. package/app/page-designer/assets/image-with-text/index.jsx +140 -0
  222. package/app/page-designer/assets/image-with-text/index.test.js +38 -0
  223. package/app/page-designer/assets/index.js +9 -0
  224. package/app/page-designer/index.js +10 -0
  225. package/app/page-designer/layouts/carousel/index.jsx +222 -0
  226. package/app/page-designer/layouts/carousel/index.test.js +43 -0
  227. package/app/page-designer/layouts/index.js +14 -0
  228. package/app/page-designer/layouts/mobileGrid1r1c/index.jsx +36 -0
  229. package/app/page-designer/layouts/mobileGrid1r1c/index.test.js +35 -0
  230. package/app/page-designer/layouts/mobileGrid2r1c/index.jsx +37 -0
  231. package/app/page-designer/layouts/mobileGrid2r1c/index.test.js +47 -0
  232. package/app/page-designer/layouts/mobileGrid2r2c/index.jsx +37 -0
  233. package/app/page-designer/layouts/mobileGrid2r2c/index.test.js +71 -0
  234. package/app/page-designer/layouts/mobileGrid2r3c/index.jsx +37 -0
  235. package/app/page-designer/layouts/mobileGrid2r3c/index.test.js +95 -0
  236. package/app/page-designer/layouts/mobileGrid3r1c/index.jsx +37 -0
  237. package/app/page-designer/layouts/mobileGrid3r1c/index.test.js +59 -0
  238. package/app/page-designer/layouts/mobileGrid3r2c/index.jsx +37 -0
  239. package/app/page-designer/layouts/mobileGrid3r2c/index.test.js +95 -0
  240. package/app/page-designer/utils.js +14 -0
  241. package/app/pages/account/addresses.jsx +382 -0
  242. package/app/pages/account/addresses.test.js +120 -0
  243. package/app/pages/account/constant.js +57 -0
  244. package/app/pages/account/index.jsx +237 -0
  245. package/app/pages/account/index.test.js +188 -0
  246. package/app/pages/account/order-detail.jsx +397 -0
  247. package/app/pages/account/order-history.jsx +264 -0
  248. package/app/pages/account/orders.jsx +30 -0
  249. package/app/pages/account/orders.test.js +95 -0
  250. package/app/pages/account/profile.jsx +357 -0
  251. package/app/pages/account/wishlist/index.jsx +195 -0
  252. package/app/pages/account/wishlist/index.mock.js +1481 -0
  253. package/app/pages/account/wishlist/index.test.js +56 -0
  254. package/app/pages/account/wishlist/partials/wishlist-primary-action.jsx +170 -0
  255. package/app/pages/account/wishlist/partials/wishlist-primary-action.mock.js +1623 -0
  256. package/app/pages/account/wishlist/partials/wishlist-primary-action.test.js +99 -0
  257. package/app/pages/account/wishlist/partials/wishlist-secondary-button-group.jsx +120 -0
  258. package/app/pages/account/wishlist/partials/wishlist-secondary-button-group.test.js +391 -0
  259. package/app/pages/cart/index.jsx +476 -0
  260. package/app/pages/cart/index.test.js +481 -0
  261. package/app/pages/cart/partials/cart-cta.jsx +46 -0
  262. package/app/pages/cart/partials/cart-secondary-button-group.jsx +135 -0
  263. package/app/pages/cart/partials/cart-secondary-button-group.test.js +103 -0
  264. package/app/pages/cart/partials/cart-skeleton.jsx +93 -0
  265. package/app/pages/cart/partials/cart-title.jsx +27 -0
  266. package/app/pages/cart/partials/empty-cart.jsx +86 -0
  267. package/app/pages/checkout/confirmation.jsx +541 -0
  268. package/app/pages/checkout/confirmation.mock.js +450 -0
  269. package/app/pages/checkout/confirmation.test.js +114 -0
  270. package/app/pages/checkout/index.jsx +169 -0
  271. package/app/pages/checkout/index.test.js +582 -0
  272. package/app/pages/checkout/partials/cc-radio-group.jsx +122 -0
  273. package/app/pages/checkout/partials/checkout-footer.jsx +140 -0
  274. package/app/pages/checkout/partials/checkout-footer.test.js +16 -0
  275. package/app/pages/checkout/partials/checkout-header.jsx +54 -0
  276. package/app/pages/checkout/partials/checkout-header.test.js +16 -0
  277. package/app/pages/checkout/partials/checkout-skeleton.jsx +52 -0
  278. package/app/pages/checkout/partials/contact-info.jsx +251 -0
  279. package/app/pages/checkout/partials/contact-info.test.js +43 -0
  280. package/app/pages/checkout/partials/payment-form.jsx +97 -0
  281. package/app/pages/checkout/partials/payment.jsx +276 -0
  282. package/app/pages/checkout/partials/shipping-address-selection.jsx +377 -0
  283. package/app/pages/checkout/partials/shipping-address.jsx +132 -0
  284. package/app/pages/checkout/partials/shipping-options.jsx +232 -0
  285. package/app/pages/checkout/util/checkout-context.js +94 -0
  286. package/app/pages/home/data.js +134 -0
  287. package/app/pages/home/index.jsx +301 -0
  288. package/app/pages/home/index.test.js +23 -0
  289. package/app/pages/login/index.jsx +123 -0
  290. package/app/pages/login/index.test.js +229 -0
  291. package/app/pages/login-redirect/index.jsx +23 -0
  292. package/app/pages/login-redirect/index.test.js +16 -0
  293. package/app/pages/page-not-found/index.jsx +90 -0
  294. package/app/pages/page-not-found/index.test.js +31 -0
  295. package/app/pages/product-detail/index.jsx +394 -0
  296. package/app/pages/product-detail/index.mock.js +197 -0
  297. package/app/pages/product-detail/index.test.js +162 -0
  298. package/app/pages/product-detail/partials/information-accordion.jsx +121 -0
  299. package/app/pages/product-list/index.jsx +735 -0
  300. package/app/pages/product-list/index.test.js +180 -0
  301. package/app/pages/product-list/partials/above-page-header.jsx +10 -0
  302. package/app/pages/product-list/partials/checkbox-refinements.jsx +41 -0
  303. package/app/pages/product-list/partials/checkbox-refinements.test.js +53 -0
  304. package/app/pages/product-list/partials/color-refinements.jsx +88 -0
  305. package/app/pages/product-list/partials/empty-results.jsx +118 -0
  306. package/app/pages/product-list/partials/link-refinements.jsx +38 -0
  307. package/app/pages/product-list/partials/page-header.jsx +42 -0
  308. package/app/pages/product-list/partials/radio-refinements.jsx +60 -0
  309. package/app/pages/product-list/partials/refinements.jsx +144 -0
  310. package/app/pages/product-list/partials/selected-refinements.jsx +100 -0
  311. package/app/pages/product-list/partials/size-refinements.jsx +55 -0
  312. package/app/pages/registration/index.jsx +87 -0
  313. package/app/pages/registration/index.test.jsx +132 -0
  314. package/app/pages/reset-password/index.jsx +112 -0
  315. package/app/pages/reset-password/index.test.jsx +141 -0
  316. package/app/request-processor.js +118 -0
  317. package/app/request-processor.test.js +23 -0
  318. package/app/routes.jsx +111 -0
  319. package/app/routes.test.js +13 -0
  320. package/app/ssr.js +70 -0
  321. package/app/static/ico/favicon.ico +0 -0
  322. package/app/static/img/global/app-icon-192.png +0 -0
  323. package/app/static/img/global/app-icon-512.png +0 -0
  324. package/app/static/img/global/apple-touch-icon.png +0 -0
  325. package/app/static/img/hero.png +0 -0
  326. package/app/static/manifest.json +19 -0
  327. package/app/static/robots.txt +2 -0
  328. package/app/theme/components/base/accordion.js +21 -0
  329. package/app/theme/components/base/alert.js +17 -0
  330. package/app/theme/components/base/badge.js +25 -0
  331. package/app/theme/components/base/button.js +77 -0
  332. package/app/theme/components/base/checkbox.js +30 -0
  333. package/app/theme/components/base/container.js +17 -0
  334. package/app/theme/components/base/drawer.js +26 -0
  335. package/app/theme/components/base/formLabel.js +13 -0
  336. package/app/theme/components/base/icon.js +13 -0
  337. package/app/theme/components/base/input.js +44 -0
  338. package/app/theme/components/base/modal.js +11 -0
  339. package/app/theme/components/base/popover.js +61 -0
  340. package/app/theme/components/base/radio.js +33 -0
  341. package/app/theme/components/base/select.js +15 -0
  342. package/app/theme/components/base/skeleton.js +12 -0
  343. package/app/theme/components/base/tooltip.js +19 -0
  344. package/app/theme/components/project/_app.js +25 -0
  345. package/app/theme/components/project/breadcrumb.js +25 -0
  346. package/app/theme/components/project/checkout-footer.js +35 -0
  347. package/app/theme/components/project/drawer-menu.js +66 -0
  348. package/app/theme/components/project/footer.js +84 -0
  349. package/app/theme/components/project/header.js +84 -0
  350. package/app/theme/components/project/image-gallery.js +59 -0
  351. package/app/theme/components/project/links-list.js +43 -0
  352. package/app/theme/components/project/list-menu.js +91 -0
  353. package/app/theme/components/project/locale-selector.js +42 -0
  354. package/app/theme/components/project/nested-accordion.js +26 -0
  355. package/app/theme/components/project/offline-banner.js +25 -0
  356. package/app/theme/components/project/pagination.js +22 -0
  357. package/app/theme/components/project/product-tile.js +32 -0
  358. package/app/theme/components/project/social-icons.js +52 -0
  359. package/app/theme/components/project/swatch-group.js +115 -0
  360. package/app/theme/foundations/colors.js +170 -0
  361. package/app/theme/foundations/gradients.js +9 -0
  362. package/app/theme/foundations/layerStyles.js +41 -0
  363. package/app/theme/foundations/shadows.js +9 -0
  364. package/app/theme/foundations/sizes.js +18 -0
  365. package/app/theme/foundations/space.js +9 -0
  366. package/app/theme/foundations/styles.js +21 -0
  367. package/app/theme/index.js +104 -0
  368. package/app/utils/cc-utils.js +112 -0
  369. package/app/utils/cc-utils.test.js +41 -0
  370. package/app/utils/image-groups-utils.js +62 -0
  371. package/app/utils/image-groups-utils.test.js +65 -0
  372. package/app/utils/locale.js +78 -0
  373. package/app/utils/locale.test.js +112 -0
  374. package/app/utils/password-utils.js +21 -0
  375. package/app/utils/phone-utils.js +22 -0
  376. package/app/utils/phone-utils.test.js +15 -0
  377. package/app/utils/product-utils.js +35 -0
  378. package/app/utils/product-utils.test.js +51 -0
  379. package/app/utils/responsive-image.js +198 -0
  380. package/app/utils/responsive-image.test.js +170 -0
  381. package/app/utils/routes-utils.js +111 -0
  382. package/app/utils/routes-utils.test.js +291 -0
  383. package/app/utils/site-utils.js +222 -0
  384. package/app/utils/site-utils.test.js +376 -0
  385. package/app/utils/test-utils.js +257 -0
  386. package/app/utils/url.js +291 -0
  387. package/app/utils/url.test.js +421 -0
  388. package/app/utils/utils.js +201 -0
  389. package/app/utils/utils.test.js +182 -0
  390. package/babel.config.js +7 -0
  391. package/cache-hash-config.json +8 -0
  392. package/config/default.js +64 -0
  393. package/config/mocks/default.js +131 -0
  394. package/config/sites.js +78 -0
  395. package/jest-setup.js +191 -0
  396. package/jest.config.js +50 -0
  397. package/jsconfig.json +13 -0
  398. package/package.json +105 -0
  399. package/scripts/extract-default-messages.js +92 -0
  400. package/tests/lighthouserc.js +37 -0
  401. package/translations/README.md +127 -0
  402. package/translations/compiled/de-DE.json +3212 -0
  403. package/translations/compiled/en-GB.json +3212 -0
  404. package/translations/compiled/en-US.json +3212 -0
  405. package/translations/compiled/en-XA.json +6948 -0
  406. package/translations/compiled/es-MX.json +3216 -0
  407. package/translations/compiled/fr-FR.json +3216 -0
  408. package/translations/compiled/it-IT.json +3188 -0
  409. package/translations/compiled/ja-JP.json +3200 -0
  410. package/translations/compiled/ko-KR.json +3180 -0
  411. package/translations/compiled/pt-BR.json +3220 -0
  412. package/translations/compiled/zh-CN.json +3212 -0
  413. package/translations/compiled/zh-TW.json +3208 -0
  414. package/translations/de-DE.json +1417 -0
  415. package/translations/en-GB.json +1417 -0
  416. package/translations/en-US.json +1417 -0
  417. package/translations/es-MX.json +1417 -0
  418. package/translations/fr-FR.json +1417 -0
  419. package/translations/it-IT.json +1417 -0
  420. package/translations/ja-JP.json +1417 -0
  421. package/translations/ko-KR.json +1417 -0
  422. package/translations/pt-BR.json +1417 -0
  423. package/translations/zh-CN.json +1417 -0
  424. package/translations/zh-TW.json +1417 -0
  425. package/worker/main.js +36 -0
@@ -0,0 +1,365 @@
1
+ /*
2
+ * Copyright (c) 2022, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import PropTypes from 'prop-types'
9
+ import {screen, within, waitFor} from '@testing-library/react'
10
+ import userEvent from '@testing-library/user-event'
11
+ import {
12
+ renderWithProviders,
13
+ createPathWithDefaults,
14
+ guestToken
15
+ } from '@salesforce/retail-react-app/app/utils/test-utils'
16
+ import {AuthModal, useAuthModal} from '@salesforce/retail-react-app/app/hooks/use-auth-modal'
17
+ import {BrowserRouter as Router, Route} from 'react-router-dom'
18
+ import Account from '@salesforce/retail-react-app/app/pages/account'
19
+ import {rest} from 'msw'
20
+ import {mockedRegisteredCustomer} from '@salesforce/retail-react-app/app/mocks/mock-data'
21
+
22
+ jest.setTimeout(60000)
23
+
24
+ const mockMergedBasket = {
25
+ basketId: 'a10ff320829cb0eef93ca5310a',
26
+ currency: 'USD',
27
+ customerInfo: {
28
+ customerId: 'registeredCustomerId',
29
+ email: 'customer@test.com'
30
+ }
31
+ }
32
+ const mockPasswordToken = {
33
+ email: 'foo@test.com',
34
+ expiresInMinutes: 10,
35
+ login: 'foo@test.com',
36
+ resetToken: 'testresettoken'
37
+ }
38
+
39
+ const mockRegisteredCustomer = {
40
+ authType: 'registered',
41
+ customerId: 'registeredCustomerId',
42
+ customerNo: 'testno',
43
+ email: 'customer@test.com',
44
+ firstName: 'Tester',
45
+ lastName: 'Testing',
46
+ login: 'customer@test.com'
47
+ }
48
+
49
+ let authModal = undefined
50
+ const MockedComponent = (props) => {
51
+ const {initialView} = props
52
+ authModal = useAuthModal(initialView || undefined)
53
+ const match = {
54
+ params: {pageName: 'profile'}
55
+ }
56
+ return (
57
+ <Router>
58
+ <button onClick={authModal.onOpen}>Open Modal</button>
59
+ <AuthModal {...authModal} />
60
+ <Route path={createPathWithDefaults('/account')}>
61
+ <Account match={match} />
62
+ </Route>
63
+ </Router>
64
+ )
65
+ }
66
+ MockedComponent.propTypes = {
67
+ initialView: PropTypes.string
68
+ }
69
+
70
+ // Set up and clean up
71
+ beforeEach(() => {
72
+ authModal = undefined
73
+ global.server.use(
74
+ rest.post('*/customers', (req, res, ctx) => {
75
+ return res(ctx.delay(0), ctx.status(200), ctx.json(mockRegisteredCustomer))
76
+ }),
77
+ rest.get('*/customers/:customerId', (req, res, ctx) => {
78
+ return res(ctx.delay(0), ctx.status(200), ctx.json(mockRegisteredCustomer))
79
+ }),
80
+ rest.post('*/customers/password/actions/create-reset-token', (req, res, ctx) => {
81
+ return res(ctx.delay(0), ctx.status(200), ctx.json(mockPasswordToken))
82
+ }),
83
+ rest.post('*/oauth2/token', (req, res, ctx) =>
84
+ res(
85
+ ctx.delay(0),
86
+ ctx.json({
87
+ customer_id: 'customerid',
88
+ access_token: guestToken,
89
+ refresh_token: 'testrefeshtoken',
90
+ usid: 'testusid',
91
+ enc_user_id: 'testEncUserId',
92
+ id_token: 'testIdToken'
93
+ })
94
+ )
95
+ ),
96
+ rest.post('*/baskets/actions/merge', (req, res, ctx) => {
97
+ return res(ctx.delay(0), ctx.json(mockMergedBasket))
98
+ })
99
+ )
100
+ })
101
+ afterEach(() => {
102
+ localStorage.clear()
103
+ jest.resetModules()
104
+ })
105
+
106
+ test('Renders login modal by default', async () => {
107
+ const user = userEvent.setup()
108
+
109
+ renderWithProviders(<MockedComponent />)
110
+
111
+ // open the modal
112
+ const trigger = screen.getByText(/open modal/i)
113
+ await user.click(trigger)
114
+
115
+ await waitFor(() => {
116
+ expect(screen.getByText(/welcome back/i)).toBeInTheDocument()
117
+ expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
118
+ expect(screen.getByLabelText(/Password/)).toBeInTheDocument()
119
+ expect(screen.getByText(/forgot password/i)).toBeInTheDocument()
120
+ expect(screen.getByText(/sign in/i)).toBeInTheDocument()
121
+ })
122
+ })
123
+
124
+ // TODO: Fix flaky/broken test
125
+ // eslint-disable-next-line jest/no-disabled-tests
126
+ test.skip('Renders error when given incorrect log in credentials', async () => {
127
+ const user = userEvent.setup()
128
+
129
+ // render our test component
130
+ renderWithProviders(<MockedComponent />, {
131
+ wrapperProps: {
132
+ bypassAuth: false
133
+ }
134
+ })
135
+
136
+ // open the modal
137
+ const trigger = screen.getByText(/open modal/i)
138
+ await user.click(trigger)
139
+
140
+ // enter credentials and submit
141
+ await user.type(screen.getByLabelText('Email'), 'bad@test.com')
142
+ await user.type(screen.getByLabelText('Password'), 'SomeFakePassword1!')
143
+
144
+ // mock failed auth request
145
+ global.server.use(
146
+ rest.post('*/oauth2/login', (req, res, ctx) =>
147
+ res(ctx.delay(0), ctx.status(401), ctx.json({message: 'Unauthorized Credentials.'}))
148
+ ),
149
+ rest.post('*/customers', (req, res, ctx) => {
150
+ return res(ctx.delay(0), ctx.status(404), ctx.json({message: 'Not Found.'}))
151
+ })
152
+ )
153
+
154
+ await user.click(screen.getByText(/sign in/i))
155
+ // give it some time to show the error in the form
156
+ await waitFor(
157
+ () => {
158
+ // wait for login error alert to appear
159
+ expect(
160
+ screen.getByText(/something's not right with your email or password\. try again\./i)
161
+ ).toBeInTheDocument()
162
+ },
163
+ {
164
+ timeout: 10000
165
+ }
166
+ )
167
+ })
168
+
169
+ test('Allows customer to create an account', async () => {
170
+ const user = userEvent.setup()
171
+
172
+ // render our test component
173
+ renderWithProviders(<MockedComponent />, {
174
+ wrapperProps: {
175
+ bypassAuth: true
176
+ }
177
+ })
178
+
179
+ // open the modal
180
+ const trigger = screen.getByText('Open Modal')
181
+
182
+ await user.click(trigger)
183
+ let form
184
+ await waitFor(() => {
185
+ form = screen.queryByTestId('sf-auth-modal-form')
186
+ expect(form).toBeInTheDocument()
187
+ })
188
+ const createAccount = screen.getByText(/create account/i)
189
+ await user.click(createAccount)
190
+ let registerForm
191
+ await waitFor(() => {
192
+ registerForm = screen.getByTestId('sf-auth-modal-form-register')
193
+ expect(registerForm).toBeInTheDocument()
194
+ })
195
+
196
+ const withinForm = within(registerForm)
197
+ // fill out form and submit
198
+ await waitFor(() => {
199
+ const firstName = withinForm.getByLabelText(/First Name/i)
200
+ expect(firstName).toBeInTheDocument()
201
+ })
202
+
203
+ await user.type(withinForm.getByLabelText('First Name'), 'Tester')
204
+ await user.type(withinForm.getByLabelText('Last Name'), 'Tester')
205
+ await user.type(withinForm.getByPlaceholderText(/you@email.com/i), 'customer@test.com')
206
+ await user.type(withinForm.getAllByLabelText(/password/i)[0], 'Password!1')
207
+
208
+ // login with credentials
209
+ global.server.use(
210
+ rest.post('*/oauth2/token', (req, res, ctx) => {
211
+ return res(
212
+ ctx.delay(0),
213
+ ctx.json({
214
+ customer_id: 'customerid_1',
215
+ access_token:
216
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQuYWRkcmVzc2VzIHNmY2Muc2hvcHBlci1wcm9kdWN0cyBzZmNjLnNob3BwZXItZGlzY292ZXJ5LXNlYXJjaCBzZmNjLnNob3BwZXItbXlhY2NvdW50LnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1jdXN0b21lcnMubG9naW4gc2ZjYy5zaG9wcGVyLWV4cGVyaWVuY2Ugc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5vcmRlcnMgc2ZjYy5zaG9wcGVyLWN1c3RvbWVycy5yZWdpc3RlciBzZmNjLnNob3BwZXItYmFza2V0cy1vcmRlcnMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5wcm9kdWN0bGlzdHMucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3RsaXN0cyBzZmNjLnNob3BwZXItcHJvbW90aW9ucyBzZmNjLnNob3BwZXItYmFza2V0cy1vcmRlcnMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5wYXltZW50aW5zdHJ1bWVudHMucncgc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1wcm9kdWN0LXNlYXJjaCBzZmNjLnNob3BwZXItbXlhY2NvdW50LnByb2R1Y3RsaXN0cyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItbXlhY2NvdW50Iiwic3ViIjoiY2Mtc2xhczo6enpyZl8wMDE6OnNjaWQ6YzljNDViZmQtMGVkMy00YWEyLTk5NzEtNDBmODg5NjJiODM2Ojp1c2lkOjhlODgzOTczLTY4ZWItNDFmZS1hM2M1LTc1NjIzMjY1MmZmNSIsImN0eCI6InNsYXMiLCJpc3MiOiJzbGFzL3Byb2QvenpyZl8wMDEiLCJpc3QiOjEsImF1ZCI6ImNvbW1lcmNlY2xvdWQvcHJvZC96enJmXzAwMSIsIm5iZiI6MTY3ODgzNDI3MSwic3R5IjoiVXNlciIsImlzYiI6InVpZG86ZWNvbTo6dXBuOmtldjVAdGVzdC5jb206OnVpZG46a2V2aW4gaGU6OmdjaWQ6YWJtZXMybWJrM2xYa1JsSEZKd0dZWWt1eEo6OnJjaWQ6YWJVTXNhdnBEOVk2alcwMGRpMlNqeEdDTVU6OmNoaWQ6UmVmQXJjaEdsb2JhbCIsImV4cCI6MjY3ODgzNjEwMSwiaWF0IjoxNjc4ODM0MzAxLCJqdGkiOiJDMkM0ODU2MjAxODYwLTE4OTA2Nzg5MDM0ODA1ODMyNTcwNjY2NTQyIn0._tUrxeXdFYPj6ZoY-GILFRd3-aD1RGPkZX6TqHeS494',
217
+ refresh_token: 'testrefeshtoken_1',
218
+ usid: 'testusid_1',
219
+ enc_user_id: 'testEncUserId_1',
220
+ id_token: 'testIdToken_1'
221
+ })
222
+ )
223
+ }),
224
+ rest.post('*/oauth2/login', (req, res, ctx) => {
225
+ return res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
226
+ }),
227
+ rest.get('*/customers/:customerId', (req, res, ctx) => {
228
+ return res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
229
+ })
230
+ )
231
+ const submitButton = withinForm.getByText(/create account/i)
232
+ await user.click(submitButton)
233
+
234
+ await waitFor(() => {
235
+ expect(form).not.toBeInTheDocument()
236
+ })
237
+ // wait for success state to appear
238
+ await waitFor(
239
+ () => {
240
+ expect(window.location.pathname).toBe('/uk/en-GB/account')
241
+ const myAccount = screen.getAllByText(/My Account/)
242
+ expect(myAccount).toHaveLength(2)
243
+ },
244
+ {
245
+ timeout: 5000
246
+ }
247
+ )
248
+ })
249
+
250
+ // TODO: investingate why this test is failing when running with other tests
251
+ // eslint-disable-next-line jest/no-disabled-tests
252
+ test.skip('Allows customer to sign in to their account', async () => {
253
+ const user = userEvent.setup()
254
+
255
+ // render our test component
256
+ renderWithProviders(<MockedComponent />, {
257
+ wrapperProps: {
258
+ bypassAuth: false
259
+ }
260
+ })
261
+
262
+ // open the modal
263
+ const trigger = screen.getByText(/open modal/i)
264
+ await user.click(trigger)
265
+
266
+ // enter credentials and submit
267
+ await user.type(screen.getByLabelText('Email'), 'customer@test.com')
268
+ await user.type(screen.getByLabelText('Password'), 'Password!1')
269
+
270
+ // login with credentials
271
+ global.server.use(
272
+ rest.post('*/oauth2/token', (req, res, ctx) =>
273
+ res(
274
+ ctx.delay(0),
275
+ ctx.json({
276
+ customer_id: 'customerid_1',
277
+ access_token:
278
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQuYWRkcmVzc2VzIHNmY2Muc2hvcHBlci1wcm9kdWN0cyBzZmNjLnNob3BwZXItZGlzY292ZXJ5LXNlYXJjaCBzZmNjLnNob3BwZXItbXlhY2NvdW50LnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1jdXN0b21lcnMubG9naW4gc2ZjYy5zaG9wcGVyLWV4cGVyaWVuY2Ugc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5vcmRlcnMgc2ZjYy5zaG9wcGVyLWN1c3RvbWVycy5yZWdpc3RlciBzZmNjLnNob3BwZXItYmFza2V0cy1vcmRlcnMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5wcm9kdWN0bGlzdHMucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3RsaXN0cyBzZmNjLnNob3BwZXItcHJvbW90aW9ucyBzZmNjLnNob3BwZXItYmFza2V0cy1vcmRlcnMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5wYXltZW50aW5zdHJ1bWVudHMucncgc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1wcm9kdWN0LXNlYXJjaCBzZmNjLnNob3BwZXItbXlhY2NvdW50LnByb2R1Y3RsaXN0cyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItbXlhY2NvdW50Iiwic3ViIjoiY2Mtc2xhczo6enpyZl8wMDE6OnNjaWQ6YzljNDViZmQtMGVkMy00YWEyLTk5NzEtNDBmODg5NjJiODM2Ojp1c2lkOjhlODgzOTczLTY4ZWItNDFmZS1hM2M1LTc1NjIzMjY1MmZmNSIsImN0eCI6InNsYXMiLCJpc3MiOiJzbGFzL3Byb2QvenpyZl8wMDEiLCJpc3QiOjEsImF1ZCI6ImNvbW1lcmNlY2xvdWQvcHJvZC96enJmXzAwMSIsIm5iZiI6MTY3ODgzNDI3MSwic3R5IjoiVXNlciIsImlzYiI6InVpZG86ZWNvbTo6dXBuOmtldjVAdGVzdC5jb206OnVpZG46a2V2aW4gaGU6OmdjaWQ6YWJtZXMybWJrM2xYa1JsSEZKd0dZWWt1eEo6OnJjaWQ6YWJVTXNhdnBEOVk2alcwMGRpMlNqeEdDTVU6OmNoaWQ6UmVmQXJjaEdsb2JhbCIsImV4cCI6MjY3ODgzNjEwMSwiaWF0IjoxNjc4ODM0MzAxLCJqdGkiOiJDMkM0ODU2MjAxODYwLTE4OTA2Nzg5MDM0ODA1ODMyNTcwNjY2NTQyIn0._tUrxeXdFYPj6ZoY-GILFRd3-aD1RGPkZX6TqHeS494',
279
+ refresh_token: 'testrefeshtoken_1',
280
+ usid: 'testusid_1',
281
+ enc_user_id: 'testEncUserId_1',
282
+ id_token: 'testIdToken_1'
283
+ })
284
+ )
285
+ )
286
+ )
287
+ await user.click(screen.getByText(/sign in/i))
288
+
289
+ // allow time to transition to account page
290
+ await waitFor(
291
+ () => {
292
+ expect(window.location.pathname).toBe('/uk/en-GB/account')
293
+ expect(screen.getByText(/My Profile/i)).toBeInTheDocument()
294
+ },
295
+ {timeout: 5000}
296
+ )
297
+ })
298
+
299
+ describe('Reset password', function () {
300
+ beforeEach(() => {
301
+ global.server.use(
302
+ rest.post('*/customers/password/actions/create-reset-token', (req, res, ctx) =>
303
+ res(ctx.delay(0), ctx.status(200), ctx.json(mockPasswordToken))
304
+ )
305
+ )
306
+ })
307
+
308
+ // TODO: Fix flaky/broken test
309
+ // eslint-disable-next-line jest/no-disabled-tests
310
+ test.skip('Allows customer to generate password token', async () => {
311
+ const user = userEvent.setup()
312
+
313
+ // render our test component
314
+ renderWithProviders(<MockedComponent initialView="password" />, {
315
+ wrapperProps: {
316
+ bypassAuth: false
317
+ }
318
+ })
319
+
320
+ // open the modal
321
+ const trigger = screen.getByText(/open modal/i)
322
+ await user.click(trigger)
323
+ expect(authModal.isOpen).toBe(true)
324
+
325
+ // enter credentials and submit
326
+ // const withinForm = within(screen.getByTestId('sf-auth-modal-form'))
327
+
328
+ let resetPwForm = await screen.findByTestId('sf-auth-modal-form-reset-pw')
329
+ expect(resetPwForm).toBeInTheDocument()
330
+ const withinForm = within(resetPwForm)
331
+ await user.type(withinForm.getByLabelText('Email'), 'foo@test.com')
332
+ await user.click(withinForm.getByText(/reset password/i))
333
+
334
+ // wait for success state
335
+ await waitFor(() => {
336
+ expect(screen.getByText(/password reset/i)).toBeInTheDocument()
337
+ expect(screen.getByText(/foo@test.com/i)).toBeInTheDocument()
338
+ })
339
+ })
340
+
341
+ // TODO: Fix flaky/broken test
342
+ // eslint-disable-next-line jest/no-disabled-tests
343
+ test.skip('Allows customer to open generate password token modal from everywhere', async () => {
344
+ const user = userEvent.setup()
345
+
346
+ // render our test component
347
+ renderWithProviders(<MockedComponent initialView="password" />)
348
+
349
+ // open the modal
350
+ const trigger = screen.getByText(/open modal/i)
351
+ await user.click(trigger)
352
+ expect(authModal.isOpen).toBe(true)
353
+
354
+ const withinForm = within(screen.getByTestId('sf-auth-modal-form'))
355
+
356
+ expect(withinForm.getByText(/Reset Password/i)).toBeInTheDocument()
357
+
358
+ // close the modal
359
+ const switchToSignIn = screen.getByText(/Sign in/i)
360
+ await user.click(switchToSignIn)
361
+
362
+ // check that the modal is closed
363
+ expect(authModal.isOpen).toBe(false)
364
+ })
365
+ })
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import {useContext} from 'react'
8
+ import {CurrencyContext} from '@salesforce/retail-react-app/app/contexts'
9
+
10
+ /**
11
+ * Custom React hook to get the currency for the active locale and to change it to a different currency
12
+ * @returns {{currency: string, setCurrency: function}[]}
13
+ */
14
+ export const useCurrency = () => {
15
+ const context = useContext(CurrencyContext)
16
+ if (context === undefined) {
17
+ throw new Error('useCurrency must be used within CurrencyProvider')
18
+ }
19
+ return context
20
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import React from 'react'
9
+ import {renderHook} from '@testing-library/react'
10
+ import {useCurrency} from '@salesforce/retail-react-app/app/hooks/use-currency'
11
+ import {CurrencyProvider} from '@salesforce/retail-react-app/app/contexts'
12
+ import {DEFAULT_CURRENCY} from '@salesforce/retail-react-app/app/constants'
13
+
14
+ const wrapper = ({children}) => <CurrencyProvider>{children}</CurrencyProvider>
15
+
16
+ let resultCurrency = {}
17
+
18
+ const mockSetCurrency = jest.fn().mockImplementation((currency) => {
19
+ resultCurrency = {currency}
20
+
21
+ return resultCurrency
22
+ })
23
+
24
+ const mockUseContext = jest.fn().mockImplementation(() => ({
25
+ currency: {},
26
+ setCurrency: mockSetCurrency
27
+ }))
28
+
29
+ React.useContext = mockUseContext
30
+ describe('useCurrency', () => {
31
+ it('should set initial currency', () => {
32
+ const {result} = renderHook(() => useCurrency(), {wrapper})
33
+
34
+ expect(resultCurrency).toMatchObject({})
35
+
36
+ result.current.setCurrency(DEFAULT_CURRENCY)
37
+
38
+ expect(mockUseContext).toHaveBeenCalled()
39
+ expect(resultCurrency).toMatchObject({currency: DEFAULT_CURRENCY})
40
+ })
41
+ })
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Copyright (c) 2023, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import {useCustomerBaskets} from '@salesforce/commerce-sdk-react'
8
+ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
9
+ import {isServer} from '@salesforce/retail-react-app/app/utils/utils'
10
+
11
+ /**
12
+ * This hook combine some commerce-react-sdk hooks to provide more derived data for Retail App baskets
13
+ * @param id - basket id to get the current used basket among baskets returned, use first basket in the array if not defined
14
+ * @param shouldFetchProductDetail - boolean to indicate if the baskets should fetch product details based on basket items
15
+ */
16
+ export const useCurrentBasket = ({id = ''} = {}) => {
17
+ const {data: customer} = useCurrentCustomer()
18
+ const {customerId} = customer
19
+ const {data: basketsData, ...restOfQuery} = useCustomerBaskets(
20
+ {parameters: {customerId}},
21
+ {
22
+ enabled: !!customerId && !isServer
23
+ }
24
+ )
25
+
26
+ // if id is not defined, by default use the first basket in the list
27
+ const currentBasket =
28
+ basketsData?.baskets?.find((basket) => basket.basketId === id) || basketsData?.baskets?.[0]
29
+
30
+ return {
31
+ ...restOfQuery,
32
+ data: currentBasket,
33
+ derivedData: {
34
+ hasBasket: basketsData?.total > 0,
35
+ totalItems:
36
+ currentBasket?.productItems?.reduce((acc, item) => acc + item.quantity, 0) || 0
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright (c) 2022, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import {useCustomer, useCustomerId, useCustomerType} from '@salesforce/commerce-sdk-react'
9
+
10
+ /**
11
+ * A hook that returns the current customer.
12
+ *
13
+ */
14
+ export const useCurrentCustomer = () => {
15
+ const customerId = useCustomerId()
16
+ const {isRegistered, isGuest, customerType} = useCustomerType()
17
+ const query = useCustomer({parameters: {customerId}}, {enabled: !!customerId && isRegistered})
18
+ const value = {
19
+ ...query,
20
+ data: {
21
+ ...query.data,
22
+ customerType,
23
+ customerId,
24
+ isRegistered,
25
+ isGuest
26
+ }
27
+ }
28
+ return value
29
+ }
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import {useEffect, useState} from 'react'
9
+ import {useVariant} from '@salesforce/retail-react-app/app/hooks/use-variant'
10
+ import {useIntl} from 'react-intl'
11
+ import {useVariationParams} from '@salesforce/retail-react-app/app/hooks/use-variation-params'
12
+ import {useVariationAttributes} from '@salesforce/retail-react-app/app/hooks/use-variation-attributes'
13
+
14
+ const OUT_OF_STOCK = 'OUT_OF_STOCK'
15
+ const UNFULFILLABLE = 'UNFULFILLABLE'
16
+
17
+ // TODO: This needs to be refactored.
18
+ export const useDerivedProduct = (product, isProductPartOfSet = false) => {
19
+ const showLoading = !product
20
+ const stockLevel = product?.inventory?.stockLevel || 0
21
+ const stepQuantity = product?.stepQuantity || 1
22
+ const minOrderQuantity = stockLevel > 0 ? product?.minOrderQuantity || 1 : 0
23
+ const initialQuantity = product?.quantity || product?.minOrderQuantity || 1
24
+
25
+ const intl = useIntl()
26
+ const variant = useVariant(product, isProductPartOfSet)
27
+ const variationParams = useVariationParams(product, isProductPartOfSet)
28
+ const variationAttributes = useVariationAttributes(product, isProductPartOfSet)
29
+ const [quantity, setQuantity] = useState(initialQuantity)
30
+
31
+ // A product is considered out of stock if the stock level is 0 or if we have all our
32
+ // variation attributes selected, but don't have a variant. We do this because the API
33
+ // will sometimes return all the variants even if they are out of stock, but for other
34
+ // products it won't.
35
+ const isOutOfStock =
36
+ !stockLevel ||
37
+ (!variant && Object.keys(variationParams).length === variationAttributes.length)
38
+ const unfulfillable = stockLevel < quantity
39
+ const inventoryMessages = {
40
+ [OUT_OF_STOCK]: intl.formatMessage({
41
+ defaultMessage: 'Out of stock',
42
+ id: 'use_product.message.out_of_stock'
43
+ }),
44
+ [UNFULFILLABLE]: intl.formatMessage(
45
+ {
46
+ defaultMessage: 'Only {stockLevel} left!',
47
+ id: 'use_product.message.inventory_remaining'
48
+ },
49
+ {stockLevel}
50
+ )
51
+ }
52
+ const showInventoryMessage = variant && (isOutOfStock || unfulfillable)
53
+ const inventoryMessage =
54
+ (isOutOfStock && inventoryMessages[OUT_OF_STOCK]) ||
55
+ (unfulfillable && inventoryMessages[UNFULFILLABLE])
56
+
57
+ // If the `initialQuantity` changes, update the state. This typically happens
58
+ // when either the master product changes, or the inventory of the product changes
59
+ // from out-of-stock to in-stock or vice versa.
60
+ useEffect(() => {
61
+ setQuantity(initialQuantity)
62
+ }, [initialQuantity])
63
+
64
+ return {
65
+ showLoading,
66
+ showInventoryMessage,
67
+ inventoryMessage,
68
+ variationAttributes,
69
+ quantity,
70
+ minOrderQuantity,
71
+ stepQuantity,
72
+ variationParams,
73
+ setQuantity,
74
+ variant,
75
+ stockLevel
76
+ }
77
+ }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import React from 'react'
9
+ import PropTypes from 'prop-types'
10
+
11
+ import {screen} from '@testing-library/react'
12
+ import {createMemoryHistory} from 'history'
13
+ import {useDerivedProduct} from '@salesforce/retail-react-app/app/hooks/use-derived-product'
14
+ import mockProductDetail from '@salesforce/retail-react-app/app/mocks/variant-750518699578M'
15
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
16
+
17
+ const MockComponent = ({product}) => {
18
+ const {inventoryMessage, quantity, variationParams, variant} = useDerivedProduct(product)
19
+
20
+ return (
21
+ <div>
22
+ <div>{`Quantity: ${quantity}`}</div>
23
+ <div>{inventoryMessage}</div>
24
+ <div>{JSON.stringify(variant)}</div>
25
+ <div>{JSON.stringify(variationParams)}</div>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ MockComponent.propTypes = {
31
+ product: PropTypes.object
32
+ }
33
+
34
+ describe('useDerivedProduct hook', () => {
35
+ test('runs properly', () => {
36
+ const history = createMemoryHistory()
37
+ history.push('/test/path?test')
38
+
39
+ renderWithProviders(<MockComponent product={mockProductDetail} />)
40
+
41
+ expect(screen.getByText(/Quantity: 1/)).toBeInTheDocument()
42
+ expect(
43
+ screen.getByText(
44
+ /{"orderable":true,"price":299.99,"productId":"750518699578M","variationValues":{"color":"BLACKFB","size":"038","width":"V"}}/
45
+ )
46
+ ).toBeInTheDocument()
47
+ })
48
+
49
+ test('has out of stock message', () => {
50
+ const history = createMemoryHistory()
51
+ history.push('/test/path')
52
+
53
+ const mockData = {
54
+ ...mockProductDetail,
55
+ inventory: {
56
+ ats: 0,
57
+ backorderable: false,
58
+ id: 'inventory_m',
59
+ orderable: false,
60
+ preorderable: false,
61
+ stockLevel: 0
62
+ }
63
+ }
64
+
65
+ renderWithProviders(<MockComponent product={mockData} />)
66
+
67
+ expect(screen.getByText(/Out of stock/)).toBeInTheDocument()
68
+ })
69
+ })