@alphasquad/saleor-template-advance 0.1.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 (441) hide show
  1. package/.env.example +57 -0
  2. package/APPLE_PAY_QUICK_START.md +165 -0
  3. package/APPLE_PAY_SETUP.md +331 -0
  4. package/README.md +46 -0
  5. package/SEO_AUDIT_CHECKLIST_STATUS.md +244 -0
  6. package/SEO_AUDIT_REPORT.md +66 -0
  7. package/eslint.config.mjs +16 -0
  8. package/next-env.d.ts +5 -0
  9. package/next.config.ts +109 -0
  10. package/package.json +47 -0
  11. package/postcss.config.mjs +5 -0
  12. package/public/.well-known/apple-developer-merchantid-domain-association +1 -0
  13. package/public/Logo.png +0 -0
  14. package/public/brand-video.mp4 +0 -0
  15. package/public/favicon.ico +0 -0
  16. package/public/file.svg +1 -0
  17. package/public/footer/facebook.tsx +34 -0
  18. package/public/footer/instagram.tsx +27 -0
  19. package/public/footer/mail.tsx +5 -0
  20. package/public/footer/x.tsx +35 -0
  21. package/public/globe.svg +1 -0
  22. package/public/icons/Authorize.net.webp +0 -0
  23. package/public/icons/amex.gif +0 -0
  24. package/public/icons/appIcon.png +0 -0
  25. package/public/icons/discover.gif +0 -0
  26. package/public/icons/master.gif +0 -0
  27. package/public/icons/paypal.png +0 -0
  28. package/public/icons/stripe.png +0 -0
  29. package/public/icons/visa.gif +0 -0
  30. package/public/images/BackgroundNoise.png +0 -0
  31. package/public/images/footer-background.png +0 -0
  32. package/public/next.svg +1 -0
  33. package/public/no-image-avail-large.png +0 -0
  34. package/public/random-car-1.jpeg +0 -0
  35. package/public/random-car-2.png +0 -0
  36. package/public/random-car-3.jpg +0 -0
  37. package/public/random-car-4.jpg +0 -0
  38. package/public/random-car-5.jpg +0 -0
  39. package/public/star.svg +3 -0
  40. package/public/vercel.svg +1 -0
  41. package/public/window.svg +1 -0
  42. package/scripts/seo-audit/generate-checklist.mjs +156 -0
  43. package/src/app/(auth)/account/forgot-password/layout.tsx +16 -0
  44. package/src/app/(auth)/account/forgot-password/page.tsx +135 -0
  45. package/src/app/(auth)/account/login/layout.tsx +16 -0
  46. package/src/app/(auth)/account/login/page.tsx +288 -0
  47. package/src/app/(auth)/account/otp/layout.tsx +16 -0
  48. package/src/app/(auth)/account/otp/page.tsx +108 -0
  49. package/src/app/(auth)/account/register/layout.tsx +16 -0
  50. package/src/app/(auth)/account/register/page.tsx +431 -0
  51. package/src/app/(auth)/account/reset-password/layout.tsx +16 -0
  52. package/src/app/(auth)/account/reset-password/page.tsx +222 -0
  53. package/src/app/[slug]/page.tsx +43 -0
  54. package/src/app/about/loading.tsx +17 -0
  55. package/src/app/about/page.tsx +61 -0
  56. package/src/app/account/address/layout.tsx +15 -0
  57. package/src/app/account/address/page.tsx +166 -0
  58. package/src/app/account/head.tsx +4 -0
  59. package/src/app/account/layout.tsx +62 -0
  60. package/src/app/account/orders/[id]/layout.tsx +17 -0
  61. package/src/app/account/orders/[id]/page.tsx +115 -0
  62. package/src/app/account/orders/components/orderDetailsModal.tsx +410 -0
  63. package/src/app/account/orders/layout.tsx +15 -0
  64. package/src/app/account/orders/page.tsx +146 -0
  65. package/src/app/account/page.tsx +39 -0
  66. package/src/app/account/settings/components/editProfileSuccessModal.tsx +28 -0
  67. package/src/app/account/settings/layout.tsx +15 -0
  68. package/src/app/account/settings/page.tsx +260 -0
  69. package/src/app/api/affirm/check-status/route.ts +94 -0
  70. package/src/app/api/affirm/create-checkout/route.ts +109 -0
  71. package/src/app/api/affirm/get-config/route.ts +108 -0
  72. package/src/app/api/affirm/process-payment/route.ts +244 -0
  73. package/src/app/api/affirm/test-connection/route.ts +45 -0
  74. package/src/app/api/auth/clear/route.ts +16 -0
  75. package/src/app/api/auth/clear-cookies/route.ts +42 -0
  76. package/src/app/api/auth/set/route.ts +47 -0
  77. package/src/app/api/configuration/route.ts +18 -0
  78. package/src/app/api/dynamic-page/[slug]/route.ts +24 -0
  79. package/src/app/api/form-submission/route.ts +237 -0
  80. package/src/app/api/paypal/capture-order/route.ts +303 -0
  81. package/src/app/api/paypal/create-order/route.ts +211 -0
  82. package/src/app/api/paypal/get-config/route.ts +240 -0
  83. package/src/app/api/search-proxy/route.ts +52 -0
  84. package/src/app/authorize-net-success/layout.tsx +19 -0
  85. package/src/app/authorize-net-success/page.tsx +12 -0
  86. package/src/app/authorize-net-success/summary.tsx +486 -0
  87. package/src/app/blog/[slug]/blogContentRenderer.tsx +369 -0
  88. package/src/app/blog/[slug]/layout.tsx +17 -0
  89. package/src/app/blog/[slug]/page.tsx +151 -0
  90. package/src/app/blog/constant.tsx +147 -0
  91. package/src/app/blog/layout.tsx +31 -0
  92. package/src/app/blog/page.tsx +81 -0
  93. package/src/app/brand/[id]/BrandPageClient.tsx +188 -0
  94. package/src/app/brand/[id]/layout.tsx +17 -0
  95. package/src/app/brand/[id]/page.tsx +176 -0
  96. package/src/app/brands/components/brandsListingClient.tsx +97 -0
  97. package/src/app/brands/layout.tsx +31 -0
  98. package/src/app/brands/page.tsx +40 -0
  99. package/src/app/cancellation-policy/page.tsx +53 -0
  100. package/src/app/cart/layout.tsx +19 -0
  101. package/src/app/cart/page.tsx +752 -0
  102. package/src/app/category/[slug]/CategoryPageClient.tsx +377 -0
  103. package/src/app/category/[slug]/layout.tsx +17 -0
  104. package/src/app/category/[slug]/page.tsx +224 -0
  105. package/src/app/category/page.tsx +114 -0
  106. package/src/app/checkout/components/addNewAddressModal.tsx +474 -0
  107. package/src/app/checkout/layout.tsx +19 -0
  108. package/src/app/checkout/page.tsx +3312 -0
  109. package/src/app/components/account/AccountTabs.tsx +40 -0
  110. package/src/app/components/ads/GoogleAdSense.tsx +74 -0
  111. package/src/app/components/analytics/AnalyticsScripts.tsx +78 -0
  112. package/src/app/components/analytics/ConditionalGTMNoscript.tsx +24 -0
  113. package/src/app/components/analytics/ConditionalGoogleAnalytics.tsx +16 -0
  114. package/src/app/components/ancillary/AncillaryContent.tsx +7 -0
  115. package/src/app/components/auth/TokenExpirationHandler.tsx +8 -0
  116. package/src/app/components/blog/BlogList.tsx +112 -0
  117. package/src/app/components/checkout/AddressInformationSection.tsx +34 -0
  118. package/src/app/components/checkout/AddressManagement.tsx +571 -0
  119. package/src/app/components/checkout/CheckoutHeader.tsx +51 -0
  120. package/src/app/components/checkout/CheckoutQuestions.tsx +454 -0
  121. package/src/app/components/checkout/CheckoutTermsModal.tsx +81 -0
  122. package/src/app/components/checkout/ContactDetailsSection.tsx +52 -0
  123. package/src/app/components/checkout/DealerShippingSection.tsx +359 -0
  124. package/src/app/components/checkout/DeliveryMethodSection.tsx +249 -0
  125. package/src/app/components/checkout/OrderSummary.tsx +386 -0
  126. package/src/app/components/checkout/TermsContentRenderer.tsx +147 -0
  127. package/src/app/components/checkout/WillCallSection.tsx +133 -0
  128. package/src/app/components/checkout/affirmPayment.tsx +383 -0
  129. package/src/app/components/checkout/checkoutProcessingModal.tsx +96 -0
  130. package/src/app/components/checkout/googlePayButton.tsx +334 -0
  131. package/src/app/components/checkout/paymentStep.tsx +180 -0
  132. package/src/app/components/checkout/paypalPayment.tsx +1083 -0
  133. package/src/app/components/checkout/saleorNativePayment.tsx +1758 -0
  134. package/src/app/components/dynamicPage/DynamicPageRenderer.tsx +13 -0
  135. package/src/app/components/dynamicPage/HtmlWidgetRenderer.tsx +144 -0
  136. package/src/app/components/filtersCollapsible/index.tsx +365 -0
  137. package/src/app/components/globalSearch/index.tsx +423 -0
  138. package/src/app/components/layout/cartDropDown.tsx +628 -0
  139. package/src/app/components/layout/components/FooterNewsletter.tsx +21 -0
  140. package/src/app/components/layout/footer.tsx +283 -0
  141. package/src/app/components/layout/header/accountMenuDropdown.tsx +53 -0
  142. package/src/app/components/layout/header/components/CartBadge.tsx +18 -0
  143. package/src/app/components/layout/header/components/LoadingState.tsx +17 -0
  144. package/src/app/components/layout/header/components/MenuItemDropdown.tsx +124 -0
  145. package/src/app/components/layout/header/components/MobileNavbar.tsx +123 -0
  146. package/src/app/components/layout/header/components/NavbarActions.tsx +125 -0
  147. package/src/app/components/layout/header/components/NavbarBrand.tsx +29 -0
  148. package/src/app/components/layout/header/components/NavigationLinks.tsx +131 -0
  149. package/src/app/components/layout/header/hamMenuSlide.tsx +318 -0
  150. package/src/app/components/layout/header/header.tsx +44 -0
  151. package/src/app/components/layout/header/hooks/useDropdown.ts +45 -0
  152. package/src/app/components/layout/header/hooks/useNavbarData.ts +138 -0
  153. package/src/app/components/layout/header/hooks/useNavbarState.ts +66 -0
  154. package/src/app/components/layout/header/megaMenuDropdown.tsx +116 -0
  155. package/src/app/components/layout/header/navBar.tsx +121 -0
  156. package/src/app/components/layout/header/search.tsx +418 -0
  157. package/src/app/components/layout/header/styles/navbarStyles.ts +27 -0
  158. package/src/app/components/layout/header/topBar.tsx +214 -0
  159. package/src/app/components/layout/joinNewsletterForm/index.tsx +72 -0
  160. package/src/app/components/layout/mobileAccordian/index.tsx +92 -0
  161. package/src/app/components/layout/paymentMethods.tsx +75 -0
  162. package/src/app/components/layout/rootLayout.tsx +23 -0
  163. package/src/app/components/layout/siteInfo.tsx +103 -0
  164. package/src/app/components/layout/socialLinks.tsx +65 -0
  165. package/src/app/components/newsletterSection/emailListSection.tsx +224 -0
  166. package/src/app/components/newsletterSection/emailSectionServer.tsx +8 -0
  167. package/src/app/components/providers/ApolloWrapper.tsx +12 -0
  168. package/src/app/components/providers/AppConfigurationProvider.tsx +108 -0
  169. package/src/app/components/providers/GoogleAnalyticsProvider.tsx +149 -0
  170. package/src/app/components/providers/GoogleTagManagerProvider.tsx +31 -0
  171. package/src/app/components/providers/RecaptchaProvider.tsx +18 -0
  172. package/src/app/components/providers/ServerAppConfigurationProvider.tsx +133 -0
  173. package/src/app/components/providers/YMMStatusProvider.tsx +15 -0
  174. package/src/app/components/reuseableUI/AboutUs.tsx +115 -0
  175. package/src/app/components/reuseableUI/AddToCartClient.tsx +125 -0
  176. package/src/app/components/reuseableUI/EditorJsRenderer.tsx +219 -0
  177. package/src/app/components/reuseableUI/HeroSectionsearchByVehicle.tsx +188 -0
  178. package/src/app/components/reuseableUI/ImageWithFallback.tsx +41 -0
  179. package/src/app/components/reuseableUI/Toast.tsx +101 -0
  180. package/src/app/components/reuseableUI/blogCard.tsx +52 -0
  181. package/src/app/components/reuseableUI/brandCard.tsx +68 -0
  182. package/src/app/components/reuseableUI/breadcrumb.tsx +38 -0
  183. package/src/app/components/reuseableUI/categoryCard.tsx +37 -0
  184. package/src/app/components/reuseableUI/categorySkeleton.tsx +31 -0
  185. package/src/app/components/reuseableUI/commonButton.tsx +48 -0
  186. package/src/app/components/reuseableUI/defaultInputField/index.tsx +84 -0
  187. package/src/app/components/reuseableUI/emptyState.tsx +29 -0
  188. package/src/app/components/reuseableUI/errorTag.tsx +15 -0
  189. package/src/app/components/reuseableUI/heading/index.tsx +20 -0
  190. package/src/app/components/reuseableUI/input.tsx +117 -0
  191. package/src/app/components/reuseableUI/listCard.tsx +137 -0
  192. package/src/app/components/reuseableUI/loadingUI.tsx +12 -0
  193. package/src/app/components/reuseableUI/modalLayout.tsx +76 -0
  194. package/src/app/components/reuseableUI/newsletter/newsletterClient.tsx +622 -0
  195. package/src/app/components/reuseableUI/newsletter/newslettersHomeModal.tsx +68 -0
  196. package/src/app/components/reuseableUI/offerCard.tsx +42 -0
  197. package/src/app/components/reuseableUI/passwordRules/passwordRules.tsx +56 -0
  198. package/src/app/components/reuseableUI/primaryButton/index.tsx +34 -0
  199. package/src/app/components/reuseableUI/productCard.tsx +118 -0
  200. package/src/app/components/reuseableUI/productSkeleton.tsx +34 -0
  201. package/src/app/components/reuseableUI/searchByVehicle.tsx +187 -0
  202. package/src/app/components/reuseableUI/secondaryButton/index.tsx +34 -0
  203. package/src/app/components/reuseableUI/section.tsx +20 -0
  204. package/src/app/components/reuseableUI/select/index.tsx +98 -0
  205. package/src/app/components/reuseableUI/skeletonLoader.tsx +117 -0
  206. package/src/app/components/reuseableUI/statusTag.tsx +24 -0
  207. package/src/app/components/reuseableUI/tags/saleTag.tsx +19 -0
  208. package/src/app/components/reuseableUI/testimonialCard.tsx +93 -0
  209. package/src/app/components/richText/EditorRenderer.tsx +318 -0
  210. package/src/app/components/search/HierarchicalCategoryFilter.tsx +155 -0
  211. package/src/app/components/search/SearchFilters.tsx +155 -0
  212. package/src/app/components/search/YMMSearchSidebar.tsx +187 -0
  213. package/src/app/components/seo/ServerProductCard.tsx +91 -0
  214. package/src/app/components/seo/ServerProductGrid.tsx +45 -0
  215. package/src/app/components/shop/CategoryFilter.tsx +184 -0
  216. package/src/app/components/shop/ItemsPerPageSelect.tsx +69 -0
  217. package/src/app/components/shop/ItemsPerPageSelectClient.tsx +58 -0
  218. package/src/app/components/shop/MobileFilters.tsx +103 -0
  219. package/src/app/components/shop/ProductGridSkeleton.tsx +16 -0
  220. package/src/app/components/shop/ProductsGrid.tsx +230 -0
  221. package/src/app/components/shop/SearchFilter.tsx +218 -0
  222. package/src/app/components/shop/SearchFilterClient.tsx +122 -0
  223. package/src/app/components/shop/SearchLoadingOverlay.tsx +32 -0
  224. package/src/app/components/shop/ShopMobileFilters.tsx +205 -0
  225. package/src/app/components/showroom/VehicleSearchDropdowns.tsx +187 -0
  226. package/src/app/components/showroom/brandsSwiper.tsx +49 -0
  227. package/src/app/components/showroom/brandsSwiperClient copy.tsx +93 -0
  228. package/src/app/components/showroom/brandsSwiperClient.tsx +122 -0
  229. package/src/app/components/showroom/brandsSwiperServer.tsx +42 -0
  230. package/src/app/components/showroom/bundleProducts.tsx +120 -0
  231. package/src/app/components/showroom/categoryGrid.tsx +51 -0
  232. package/src/app/components/showroom/categoryGridServer.tsx +45 -0
  233. package/src/app/components/showroom/categorySwiper.tsx +115 -0
  234. package/src/app/components/showroom/featureStrip.tsx +139 -0
  235. package/src/app/components/showroom/offersSwiper.tsx +181 -0
  236. package/src/app/components/showroom/productGrid.tsx +56 -0
  237. package/src/app/components/showroom/productSwiper.tsx +119 -0
  238. package/src/app/components/showroom/promotion-slider.tsx +138 -0
  239. package/src/app/components/showroom/promotion.tsx +207 -0
  240. package/src/app/components/showroom/promotionsSwiper.tsx +174 -0
  241. package/src/app/components/showroom/showroomHeroCarousel.tsx +141 -0
  242. package/src/app/components/showroom/testimonialsGrid.tsx +106 -0
  243. package/src/app/components/skeletons/ContentSkeleton.tsx +14 -0
  244. package/src/app/components/sortDropdown/index.tsx +116 -0
  245. package/src/app/components/tertiaryButton/index.tsx +25 -0
  246. package/src/app/components/theme/theme-provider.tsx +82 -0
  247. package/src/app/contact/layout.tsx +32 -0
  248. package/src/app/contact/page.tsx +591 -0
  249. package/src/app/content/[slug]/layout.tsx +17 -0
  250. package/src/app/content/[slug]/page.tsx +159 -0
  251. package/src/app/content/layout.tsx +31 -0
  252. package/src/app/content/page.tsx +88 -0
  253. package/src/app/core-policies/page.tsx +55 -0
  254. package/src/app/discounts/page.tsx +54 -0
  255. package/src/app/frequently-asked-questions/page.tsx +57 -0
  256. package/src/app/globals.css +440 -0
  257. package/src/app/hooks/useDealerLocations.ts +259 -0
  258. package/src/app/hooks/useGTMEngagement.ts +71 -0
  259. package/src/app/hooks/useGoogleAnalytics.ts +145 -0
  260. package/src/app/layout.tsx +149 -0
  261. package/src/app/not-found.tsx +31 -0
  262. package/src/app/order-confirmation/layout.tsx +19 -0
  263. package/src/app/order-confirmation/page.tsx +12 -0
  264. package/src/app/order-confirmation/summary.tsx +1775 -0
  265. package/src/app/page.tsx +194 -0
  266. package/src/app/privacy-policy/loading.tsx +17 -0
  267. package/src/app/privacy-policy/page.tsx +56 -0
  268. package/src/app/product/[id]/ProductDetailClient.tsx +2448 -0
  269. package/src/app/product/[id]/components/itemInquiryModal.tsx +461 -0
  270. package/src/app/product/[id]/layout.tsx +116 -0
  271. package/src/app/product/[id]/page.tsx +200 -0
  272. package/src/app/product/layout.tsx +15 -0
  273. package/src/app/products/all/AllProductsClient.tsx +743 -0
  274. package/src/app/products/all/page.tsx +176 -0
  275. package/src/app/products/components/shopEmptyState.tsx +29 -0
  276. package/src/app/request-return/layout.tsx +36 -0
  277. package/src/app/request-return/page.tsx +597 -0
  278. package/src/app/robots.txt/route.ts +27 -0
  279. package/src/app/search/layout.tsx +16 -0
  280. package/src/app/search/page.tsx +736 -0
  281. package/src/app/shipping-returns/page.tsx +60 -0
  282. package/src/app/site-map/layout.tsx +33 -0
  283. package/src/app/site-map/page.tsx +113 -0
  284. package/src/app/sitemap-index.xml/route.ts +20 -0
  285. package/src/app/sitemap.ts +10 -0
  286. package/src/app/terms-and-conditions/loading.tsx +17 -0
  287. package/src/app/terms-and-conditions/page.tsx +56 -0
  288. package/src/app/utils/appConfiguration.ts +327 -0
  289. package/src/app/utils/branding.ts +52 -0
  290. package/src/app/utils/configurationService.ts +202 -0
  291. package/src/app/utils/constant.tsx +242 -0
  292. package/src/app/utils/editorJsUtils.tsx +249 -0
  293. package/src/app/utils/functions.ts +146 -0
  294. package/src/app/utils/googleAnalytics.ts +168 -0
  295. package/src/app/utils/googleTagManager.ts +475 -0
  296. package/src/app/utils/ipDetection.ts +270 -0
  297. package/src/app/utils/serverConfigurationService.ts +209 -0
  298. package/src/app/utils/svgs/GridIcon.tsx +45 -0
  299. package/src/app/utils/svgs/account/myAccount/listDotIcon.tsx +3 -0
  300. package/src/app/utils/svgs/account/myAccount/tickIcon.tsx +10 -0
  301. package/src/app/utils/svgs/account/orderHistory/InfoIcon.tsx +49 -0
  302. package/src/app/utils/svgs/arrowDownIcon.tsx +17 -0
  303. package/src/app/utils/svgs/arrowIcon.tsx +25 -0
  304. package/src/app/utils/svgs/arrowUpIcon.tsx +16 -0
  305. package/src/app/utils/svgs/brandsSearchIcon.tsx +25 -0
  306. package/src/app/utils/svgs/cart/cartIcon.tsx +31 -0
  307. package/src/app/utils/svgs/cart/plusIcon.tsx +13 -0
  308. package/src/app/utils/svgs/cart/subtractIcon.tsx +13 -0
  309. package/src/app/utils/svgs/cart/successTickIcon.tsx +14 -0
  310. package/src/app/utils/svgs/chevronDownIcon.tsx +21 -0
  311. package/src/app/utils/svgs/closeEyeIcon.tsx +47 -0
  312. package/src/app/utils/svgs/crossIcon.tsx +25 -0
  313. package/src/app/utils/svgs/eyeIcon.tsx +29 -0
  314. package/src/app/utils/svgs/featureTag.tsx +20 -0
  315. package/src/app/utils/svgs/filterIcon.tsx +3 -0
  316. package/src/app/utils/svgs/globleIcon.tsx +41 -0
  317. package/src/app/utils/svgs/infoIcon.tsx +34 -0
  318. package/src/app/utils/svgs/listIcon.tsx +50 -0
  319. package/src/app/utils/svgs/logOutIcon.tsx +35 -0
  320. package/src/app/utils/svgs/menuIcon.tsx +8 -0
  321. package/src/app/utils/svgs/minusIcon.tsx +18 -0
  322. package/src/app/utils/svgs/newsletterIcon.tsx +19 -0
  323. package/src/app/utils/svgs/noDataFoundIcon-.tsx +26 -0
  324. package/src/app/utils/svgs/noProductFoundIcon.tsx +43 -0
  325. package/src/app/utils/svgs/passwordIcons/errorIcon.tsx +31 -0
  326. package/src/app/utils/svgs/passwordIcons/successIcon.tsx +24 -0
  327. package/src/app/utils/svgs/paymentProcessingIcons/hourglassIcon.tsx +43 -0
  328. package/src/app/utils/svgs/paymentProcessingIcons/modalCrossIcon.tsx +23 -0
  329. package/src/app/utils/svgs/paymentProcessingIcons/paymentFailedIcon.tsx +47 -0
  330. package/src/app/utils/svgs/pencilIcon.tsx +11 -0
  331. package/src/app/utils/svgs/plusIcon.tsx +25 -0
  332. package/src/app/utils/svgs/productInquiryIcon.tsx +40 -0
  333. package/src/app/utils/svgs/searchIcon.tsx +31 -0
  334. package/src/app/utils/svgs/shoppingCart.tsx +32 -0
  335. package/src/app/utils/svgs/spinnerIcon.tsx +22 -0
  336. package/src/app/utils/svgs/spinnerLoadingIcon.tsx +26 -0
  337. package/src/app/utils/svgs/successTickIcon.tsx +40 -0
  338. package/src/app/utils/svgs/swiperArrowIconLeft.tsx +18 -0
  339. package/src/app/utils/svgs/swiperArrowIconRight.tsx +18 -0
  340. package/src/app/utils/svgs/userProfileIcon.tsx +31 -0
  341. package/src/app/utils/svgs/warningCircleIcon.tsx +15 -0
  342. package/src/app/warranty/constant.tsx +63 -0
  343. package/src/app/warranty/loading.tsx +17 -0
  344. package/src/app/warranty/page.tsx +56 -0
  345. package/src/graphql/client.ts +288 -0
  346. package/src/graphql/mutations/accountAddressCreate.ts +56 -0
  347. package/src/graphql/mutations/accountAddressDelete.ts +23 -0
  348. package/src/graphql/mutations/accountAddressUpdate.ts +55 -0
  349. package/src/graphql/mutations/accountSetDefaultAddress.ts +32 -0
  350. package/src/graphql/mutations/accountUpdate.ts +34 -0
  351. package/src/graphql/mutations/changePassword.ts +25 -0
  352. package/src/graphql/mutations/checkout.ts +117 -0
  353. package/src/graphql/mutations/checkoutAddVoucher.ts +63 -0
  354. package/src/graphql/mutations/checkoutComplete.ts +79 -0
  355. package/src/graphql/mutations/checkoutCreate.ts +131 -0
  356. package/src/graphql/mutations/checkoutCustomerAttach.ts +50 -0
  357. package/src/graphql/mutations/checkoutEmailUpdate.ts +15 -0
  358. package/src/graphql/mutations/checkoutLineMetadataUpdate.ts +52 -0
  359. package/src/graphql/mutations/checkoutPaymentCreate.ts +82 -0
  360. package/src/graphql/mutations/paymentGatewayInitialize.ts +58 -0
  361. package/src/graphql/mutations/registerAccount.ts +65 -0
  362. package/src/graphql/mutations/requestPasswordReset.ts +32 -0
  363. package/src/graphql/mutations/setPassword.ts +49 -0
  364. package/src/graphql/mutations/signIn.ts +50 -0
  365. package/src/graphql/mutations/tokenRefresh.ts +19 -0
  366. package/src/graphql/mutations/updateCheckoutMetadata.ts +49 -0
  367. package/src/graphql/mutations/updateProfile.ts +18 -0
  368. package/src/graphql/mutations/willCallDeliveryMethod.ts +81 -0
  369. package/src/graphql/queries/checkout.ts +168 -0
  370. package/src/graphql/queries/findProductByOldSlug.ts +58 -0
  371. package/src/graphql/queries/getAboutPage.ts +24 -0
  372. package/src/graphql/queries/getAboutPageId.ts +9 -0
  373. package/src/graphql/queries/getAboutUs.ts +38 -0
  374. package/src/graphql/queries/getAddressInformation.ts +38 -0
  375. package/src/graphql/queries/getAllCategories.ts +41 -0
  376. package/src/graphql/queries/getAllCategoriesTree.ts +67 -0
  377. package/src/graphql/queries/getAllCategoriesWithProducts.ts +29 -0
  378. package/src/graphql/queries/getAllCollectionsWithProducts.ts +16 -0
  379. package/src/graphql/queries/getBlogs.ts +222 -0
  380. package/src/graphql/queries/getBrands.ts +17 -0
  381. package/src/graphql/queries/getBundles.ts +43 -0
  382. package/src/graphql/queries/getCategories.ts +20 -0
  383. package/src/graphql/queries/getChannels.ts +77 -0
  384. package/src/graphql/queries/getCheckoutQuestions.ts +115 -0
  385. package/src/graphql/queries/getCheckoutTermsAndConditions.ts +37 -0
  386. package/src/graphql/queries/getContactPage.ts +117 -0
  387. package/src/graphql/queries/getContentPage.ts +191 -0
  388. package/src/graphql/queries/getDiscountOffers.ts +18 -0
  389. package/src/graphql/queries/getDynamicPageBySlug.ts +251 -0
  390. package/src/graphql/queries/getFeaturedProducts.ts +48 -0
  391. package/src/graphql/queries/getHeroMetadata.ts +23 -0
  392. package/src/graphql/queries/getMenuBySlug.ts +84 -0
  393. package/src/graphql/queries/getMyProfile.ts +23 -0
  394. package/src/graphql/queries/getNewsletter.ts +122 -0
  395. package/src/graphql/queries/getNewsletterPage.ts +111 -0
  396. package/src/graphql/queries/getPageBySlug.ts +52 -0
  397. package/src/graphql/queries/getPageTypeId.ts +27 -0
  398. package/src/graphql/queries/getPaymentMethods.ts +61 -0
  399. package/src/graphql/queries/getProducts.ts +78 -0
  400. package/src/graphql/queries/getPromotions.ts +24 -0
  401. package/src/graphql/queries/getRequestReturnPage.ts +121 -0
  402. package/src/graphql/queries/getSiteInfo.ts +54 -0
  403. package/src/graphql/queries/getSocialLinks.ts +52 -0
  404. package/src/graphql/queries/getTestimonials.ts +25 -0
  405. package/src/graphql/queries/getUserWithCheckout.ts +27 -0
  406. package/src/graphql/queries/getVehicleMakes.ts +21 -0
  407. package/src/graphql/queries/getVehicleModels.ts +21 -0
  408. package/src/graphql/queries/getVehicleYears.ts +21 -0
  409. package/src/graphql/queries/meAddresses.ts +56 -0
  410. package/src/graphql/queries/myOrders.ts +37 -0
  411. package/src/graphql/queries/orderDetail.ts +231 -0
  412. package/src/graphql/queries/productDetailsById.ts +197 -0
  413. package/src/graphql/queries/productInquiry.ts +115 -0
  414. package/src/graphql/queries/productsByCategoriesAndCollections.ts +39 -0
  415. package/src/graphql/queries/willCallCollectionPoints.ts +55 -0
  416. package/src/graphql/server-client.ts +54 -0
  417. package/src/graphql/types/categories.ts +9 -0
  418. package/src/graphql/types/checkout.ts +168 -0
  419. package/src/graphql/types/offer.ts +12 -0
  420. package/src/graphql/types/product.ts +44 -0
  421. package/src/hooks/scrollPageTop.ts +9 -0
  422. package/src/hooks/serverNavbarData.ts +79 -0
  423. package/src/hooks/useCartSync.ts +24 -0
  424. package/src/hooks/useRecaptcha.ts +33 -0
  425. package/src/hooks/useTokenExpiration.ts +81 -0
  426. package/src/hooks/useVehicleData.ts +346 -0
  427. package/src/lib/api/kount.ts +165 -0
  428. package/src/lib/api/shop.ts +1445 -0
  429. package/src/lib/saleor/getSaleorApiUrl.ts +25 -0
  430. package/src/lib/schema.ts +303 -0
  431. package/src/lib/seo/extractTextFromEditorJs.ts +58 -0
  432. package/src/lib/seo/site.ts +10 -0
  433. package/src/lib/urls/normalizeInternalUrl.ts +53 -0
  434. package/src/middleware.ts +134 -0
  435. package/src/sitemaps/README.md +105 -0
  436. package/src/sitemaps/dynamic-pages-sitemap.ts +247 -0
  437. package/src/sitemaps/sitemap-index.ts +21 -0
  438. package/src/sitemaps/static-pages-sitemap.ts +36 -0
  439. package/src/store/useGlobalStore.tsx +1656 -0
  440. package/src/types/global.d.ts +148 -0
  441. package/tsconfig.json +27 -0
@@ -0,0 +1,1656 @@
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+ import { createJSONStorage, persist } from "zustand/middleware";
5
+ import {
6
+ CHECKOUT_CREATE,
7
+ CHECKOUT_LINES_ADD,
8
+ CHECKOUT_LINES_DELETE,
9
+ CHECKOUT_LINES_UPDATE,
10
+ } from "../graphql/mutations/checkoutCreate";
11
+ import { getSaleorApiUrl } from "@/lib/saleor/getSaleorApiUrl";
12
+
13
+ // ---- Local helper types (avoid global redeclare conflicts) ----
14
+ type NarrowWindowForApollo = {
15
+ __APOLLO_CLIENT__?: {
16
+ clearStore?: () => Promise<void>;
17
+ };
18
+ };
19
+
20
+ type IDBWithDatabases = IDBFactory & {
21
+ databases?: () => Promise<Array<{ name?: string | null }>>;
22
+ };
23
+
24
+ // ----- Types -----
25
+ export interface CartItemOption {
26
+ variantId: string;
27
+ name: string;
28
+ price: number;
29
+ optionSetName: string;
30
+ optionSetLabel: string;
31
+ }
32
+
33
+ export interface CartItem {
34
+ key: string; // Unique identifier combining variant ID + options + custom inputs
35
+ id: string;
36
+ name: string;
37
+ price: number;
38
+ image: string;
39
+ quantity: number;
40
+ sku?: string;
41
+ category?: string;
42
+ // Option sets - selected variant options consolidated into this item
43
+ selectedOptions?: CartItemOption[];
44
+ // Non-SKU options - custom inputs like text, date, datetime
45
+ customInputs?: Record<string, string>;
46
+ // Skip adding base product to checkout (when base_product_required is false)
47
+ skipBaseProduct?: boolean;
48
+ }
49
+
50
+ // Input type for addToCart - key is optional because the store generates it
51
+ export type CartItemInput = Omit<CartItem, 'key'> & { key?: string };
52
+ export interface ShippingInfo {
53
+ firstName: string;
54
+ lastName: string;
55
+ address: string;
56
+ city: string;
57
+ state: string;
58
+ zipCode: string;
59
+ country: string;
60
+ phone: string;
61
+ }
62
+ export interface User {
63
+ name: string;
64
+ email?: string;
65
+ id?: string;
66
+ }
67
+ export type YMMYear = { id: number; name: string };
68
+
69
+ export interface GlobalState {
70
+ isLoggedIn: boolean;
71
+ user: User | null;
72
+ hydrated: boolean;
73
+ guestEmail: string;
74
+ guestShippingInfo: ShippingInfo;
75
+ cartItems: CartItem[];
76
+ totalItems: number;
77
+ totalAmount: number;
78
+ checkoutId?: string | null;
79
+ checkoutToken?: string | null;
80
+ selectedShippingMethodId?: string | null;
81
+ syncingCart: boolean;
82
+ isYMMActive: boolean;
83
+ ymmYears: YMMYear[];
84
+ ymmYearsLoaded: boolean;
85
+ login: (user: User) => void;
86
+ logout: () => Promise<void>;
87
+ initAuthFromToken: () => void;
88
+ setHydrated: (v: boolean) => void;
89
+ setGuestEmail: (email: string) => void;
90
+ setGuestShippingInfo: (info: Partial<ShippingInfo>) => void;
91
+ addToCart: (itemToAdd: CartItemInput) => Promise<void>;
92
+ removeFromCart: (key: string) => void;
93
+ updateQuantity: (key: string, quantity: number) => void;
94
+ clearCart: () => void;
95
+ setCheckoutId: (id: string | null) => void;
96
+ setCheckoutToken: (token: string | null) => void;
97
+ setSelectedShippingMethodId: (id: string | null) => void;
98
+ syncCartWithSaleor: () => Promise<void>;
99
+ loadCartFromSaleor: () => Promise<void>;
100
+ setSyncingCart: (syncing: boolean) => void;
101
+ setIsYMMActive: (active: boolean) => void;
102
+ checkYMMStatus: () => Promise<void>;
103
+ loadYMMYears: () => Promise<void>;
104
+ storeCheckoutInUserMetadata: (
105
+ checkoutId: string,
106
+ checkoutToken?: string
107
+ ) => Promise<void>;
108
+ loadCheckoutFromUserMetadata: () => Promise<string | null>;
109
+ finalizeCheckoutCleanup: () => Promise<void>;
110
+ syncCartQuantityWithSaleor: (
111
+ cartItem: CartItem,
112
+ newQuantity: number,
113
+ oldQuantity: number
114
+ ) => Promise<void>;
115
+ syncCartRemovalWithSaleor: (cartItem: CartItem) => Promise<void>;
116
+ }
117
+
118
+ // ----- Helpers -----
119
+ const calculateTotals = (cartItems: CartItem[]) => {
120
+ const totalItems = cartItems.reduce((t, i) => t + i.quantity, 0);
121
+ // Calculate total using the discounted unit prices + selected options prices
122
+ const totalAmount = cartItems.reduce((t, i) => {
123
+ let itemTotal = i.price;
124
+ // Add prices from selected options if present
125
+ if (i.selectedOptions?.length) {
126
+ itemTotal += i.selectedOptions.reduce((sum, opt) => sum + opt.price, 0);
127
+ }
128
+ return t + itemTotal * i.quantity;
129
+ }, 0);
130
+ return { totalItems, totalAmount };
131
+ };
132
+
133
+ /**
134
+ * Generate a unique key for a cart item that includes the base variant ID,
135
+ * selected option variant IDs, and custom inputs. This allows the same base
136
+ * product with different option selections to exist as separate cart items.
137
+ */
138
+ const generateCartItemKey = (
139
+ variantId: string,
140
+ selectedOptions?: CartItemOption[],
141
+ customInputs?: Record<string, string>
142
+ ): string => {
143
+ const optionsKey = selectedOptions
144
+ ?.map((opt) => opt.variantId)
145
+ .sort()
146
+ .join("|") || "";
147
+ const inputsKey = customInputs
148
+ ? Object.entries(customInputs)
149
+ .sort(([a], [b]) => a.localeCompare(b))
150
+ .map(([k, v]) => `${k}=${v}`)
151
+ .join("|")
152
+ : "";
153
+ return `${variantId}::${optionsKey}::${inputsKey}`;
154
+ };
155
+
156
+ /**
157
+ * Parse option_set metadata from a variant's metadata array.
158
+ * Returns the parsed option set info if present, null otherwise.
159
+ */
160
+ const parseOptionSetMetadata = (
161
+ metadata: Array<{ key: string; value: string }> | null | undefined
162
+ ): { name: string; label: string } | null => {
163
+ if (!metadata) return null;
164
+ const optionMeta = metadata.find((m) => m.key === "option_set");
165
+ if (!optionMeta?.value) return null;
166
+ try {
167
+ return JSON.parse(optionMeta.value);
168
+ } catch {
169
+ return null;
170
+ }
171
+ };
172
+
173
+ async function clearAllAuthStorage() {
174
+ if (typeof window === "undefined") return;
175
+
176
+ try {
177
+ // localStorage keys we care about
178
+ const authKeys = [
179
+ "token",
180
+ "refreshToken",
181
+ "wsm-global-store",
182
+ "user",
183
+ "auth",
184
+ "session",
185
+ "apollo-cache-persist",
186
+ "apollo-cache",
187
+ ];
188
+ for (const k of authKeys) {
189
+ try {
190
+ localStorage.removeItem(k);
191
+ } catch {
192
+ /* ignore */
193
+ }
194
+ }
195
+
196
+ // sessionStorage
197
+ try {
198
+ sessionStorage.clear();
199
+ } catch {
200
+ /* ignore */
201
+ }
202
+
203
+ // IndexedDB (guard for Safari / older browsers)
204
+ try {
205
+ const idb = window.indexedDB as IDBWithDatabases;
206
+ if (typeof idb?.databases === "function") {
207
+ const databases = await idb.databases();
208
+ await Promise.all(
209
+ databases
210
+ .map((db) => db?.name)
211
+ .filter((name): name is string => Boolean(name))
212
+ .map(
213
+ (name) =>
214
+ new Promise<void>((resolve) => {
215
+ const req = idb.deleteDatabase(name);
216
+ req.onsuccess = () => resolve();
217
+ req.onerror = () => resolve(); // swallow
218
+ })
219
+ )
220
+ );
221
+ }
222
+ } catch {
223
+ /* ignore */
224
+ }
225
+
226
+ // Clear cookies via API route
227
+ try {
228
+ await fetch("/api/auth/clear", {
229
+ method: "POST",
230
+ credentials: "same-origin",
231
+ headers: { "Content-Type": "application/json" },
232
+ cache: "no-store",
233
+ redirect: "manual",
234
+ });
235
+ } catch (e) {
236
+ /* ignore */
237
+ // ignore
238
+ }
239
+
240
+ // Apollo cache (use narrow window type to avoid global redeclare conflicts)
241
+ try {
242
+ const w = window as unknown as NarrowWindowForApollo;
243
+ await w.__APOLLO_CLIENT__?.clearStore?.();
244
+ } catch {
245
+ /* ignore */
246
+ }
247
+
248
+ // Service worker caches
249
+ try {
250
+ if ("caches" in window) {
251
+ const names = await caches.keys();
252
+ await Promise.all(names.map((n) => caches.delete(n)));
253
+ }
254
+ } catch {
255
+ /* ignore */
256
+ }
257
+ } catch {
258
+ // swallow – we still want to redirect
259
+ }
260
+ }
261
+
262
+ // ----- Store -----
263
+ export const useGlobalStore = create<GlobalState>()(
264
+ persist(
265
+ (set) => ({
266
+ isLoggedIn: false,
267
+ user: null,
268
+ hydrated: false,
269
+ guestEmail: "",
270
+ guestShippingInfo: {
271
+ firstName: "",
272
+ lastName: "",
273
+ address: "",
274
+ city: "",
275
+ state: "",
276
+ zipCode: "",
277
+ country: "",
278
+ phone: "",
279
+ },
280
+ cartItems: [],
281
+ totalItems: 0,
282
+ totalAmount: 0,
283
+ checkoutId: null,
284
+ checkoutToken: null,
285
+ selectedShippingMethodId: null,
286
+ syncingCart: false,
287
+ isYMMActive: false,
288
+ ymmYears: [],
289
+ ymmYearsLoaded: false,
290
+
291
+ login: (user) => {
292
+ set({ isLoggedIn: true, user });
293
+ // Load cart from Saleor after login
294
+ setTimeout(() => {
295
+ useGlobalStore.getState().loadCartFromSaleor().catch(console.error);
296
+ }, 100);
297
+ },
298
+
299
+ logout: async () => {
300
+ const currentState = useGlobalStore.getState();
301
+
302
+ // Store the checkout info before logout so it can be restored on login
303
+ const checkoutId = currentState.checkoutId;
304
+ const checkoutToken = currentState.checkoutToken;
305
+
306
+ // reset state immediately to avoid UI races
307
+ set({
308
+ isLoggedIn: false,
309
+ user: null,
310
+ cartItems: [],
311
+ totalItems: 0,
312
+ totalAmount: 0,
313
+ // Clear checkout info completely on logout to prevent race conditions
314
+ checkoutId: null,
315
+ checkoutToken: null,
316
+ selectedShippingMethodId: null, // Clear shipping selection on logout
317
+ guestEmail: "",
318
+ guestShippingInfo: {
319
+ firstName: "",
320
+ lastName: "",
321
+ address: "",
322
+ city: "",
323
+ state: "",
324
+ zipCode: "",
325
+ country: "",
326
+ phone: "",
327
+ },
328
+ });
329
+
330
+ await clearAllAuthStorage();
331
+
332
+ // Only restore checkout info if user was in middle of checkout process
333
+ const currentPath = window.location.pathname;
334
+ const isInCheckout =
335
+ currentPath.includes("/checkout") || currentPath.includes("/cart");
336
+
337
+ if (checkoutId && isInCheckout) {
338
+ try {
339
+ localStorage.setItem("checkoutId", checkoutId);
340
+ if (checkoutToken) {
341
+ localStorage.setItem("checkoutToken", checkoutToken);
342
+ }
343
+ } catch {}
344
+ } else {
345
+ // Clear checkout data completely if not in checkout flow
346
+ try {
347
+ localStorage.removeItem("checkoutId");
348
+ localStorage.removeItem("checkoutToken");
349
+ localStorage.removeItem("selectedShippingMethodId");
350
+ } catch {}
351
+ }
352
+
353
+ // single hard redirect – no extra reloads
354
+ if (typeof window !== "undefined") {
355
+ window.location.replace("/");
356
+ }
357
+ },
358
+
359
+ initAuthFromToken: () => {
360
+ if (typeof window === "undefined") return;
361
+ try {
362
+ const token = localStorage.getItem("token");
363
+ const isLoggedIn = !!token;
364
+ set({ isLoggedIn });
365
+
366
+ // Load cart from Saleor if logged in
367
+ if (isLoggedIn) {
368
+ setTimeout(() => {
369
+ useGlobalStore
370
+ .getState()
371
+ .loadCartFromSaleor()
372
+ .catch(console.error);
373
+ }, 500); // Delay to ensure everything is initialized
374
+ }
375
+ } catch {
376
+ set({ isLoggedIn: false, user: null });
377
+ }
378
+ },
379
+
380
+ setHydrated: (v) => set({ hydrated: v }),
381
+ setGuestEmail: (email) => set({ guestEmail: email }),
382
+ setGuestShippingInfo: (info) =>
383
+ set((state) => ({
384
+ guestShippingInfo: { ...state.guestShippingInfo, ...info },
385
+ })),
386
+
387
+ addToCart: async (itemToAdd) => {
388
+ const state = useGlobalStore.getState();
389
+ try {
390
+ const endpoint = getSaleorApiUrl();
391
+ // Use GraphQL add to cart functionality
392
+ const token = localStorage.getItem("token");
393
+
394
+ let checkoutId = state.checkoutId;
395
+ const channel = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
396
+
397
+ // Build lines for all variants (base product + selected options)
398
+ const lines: Array<{ variantId: string; quantity: number }> = [];
399
+
400
+ // Add base product only if skipBaseProduct is not true
401
+ if (!itemToAdd.skipBaseProduct) {
402
+ lines.push({ variantId: itemToAdd.id, quantity: itemToAdd.quantity });
403
+ }
404
+
405
+ // Add option variant lines if present
406
+ if (itemToAdd.selectedOptions?.length) {
407
+ for (const option of itemToAdd.selectedOptions) {
408
+ lines.push({
409
+ variantId: option.variantId,
410
+ quantity: itemToAdd.quantity,
411
+ });
412
+ }
413
+ }
414
+
415
+ // Create checkout if it doesn't exist
416
+ if (!checkoutId) {
417
+ const createResponse = await fetch(
418
+ endpoint,
419
+ {
420
+ method: "POST",
421
+ headers: {
422
+ "Content-Type": "application/json",
423
+ ...(token && { Authorization: `Bearer ${token}` }),
424
+ },
425
+ body: JSON.stringify({
426
+ query: CHECKOUT_CREATE,
427
+ variables: {
428
+ input: {
429
+ channel,
430
+ lines,
431
+ },
432
+ },
433
+ }),
434
+ }
435
+ );
436
+
437
+ if (createResponse.ok) {
438
+ const createData = await createResponse.json();
439
+ const checkout = createData?.data?.checkoutCreate?.checkout;
440
+ const errors = createData?.data?.checkoutCreate?.errors;
441
+
442
+ if (errors?.length > 0) {
443
+ throw new Error(
444
+ `GraphQL checkout creation errors: ${errors
445
+ .map((e: { message: string }) => e.message)
446
+ .join(", ")}`
447
+ );
448
+ }
449
+
450
+ if (checkout) {
451
+ checkoutId = checkout.id;
452
+
453
+ // Update checkout info
454
+ set({
455
+ checkoutId: checkout.id,
456
+ checkoutToken: checkout.token,
457
+ });
458
+
459
+ // Store in localStorage
460
+ try {
461
+ localStorage.setItem("checkoutId", checkout.id);
462
+ if (checkout.token) {
463
+ localStorage.setItem("checkoutToken", checkout.token);
464
+ }
465
+ } catch {}
466
+
467
+ // Store in user metadata for cross-device persistence
468
+ state
469
+ .storeCheckoutInUserMetadata(checkout.id, checkout.token)
470
+ .catch(console.error);
471
+ }
472
+ } else {
473
+ throw new Error("Failed to create checkout via GraphQL");
474
+ }
475
+ } else {
476
+ // Add to existing checkout
477
+ const addResponse = await fetch(
478
+ endpoint,
479
+ {
480
+ method: "POST",
481
+ headers: {
482
+ "Content-Type": "application/json",
483
+ ...(token && { Authorization: `Bearer ${token}` }),
484
+ },
485
+ body: JSON.stringify({
486
+ query: CHECKOUT_LINES_ADD,
487
+ variables: {
488
+ id: checkoutId,
489
+ lines,
490
+ },
491
+ }),
492
+ }
493
+ );
494
+
495
+ if (addResponse.ok) {
496
+ const addData = await addResponse.json();
497
+ const checkout = addData?.data?.checkoutLinesAdd?.checkout;
498
+ const errors = addData?.data?.checkoutLinesAdd?.errors;
499
+
500
+ if (errors?.length > 0) {
501
+ throw new Error(
502
+ `GraphQL add to cart errors: ${errors
503
+ .map((e: { message: string }) => e.message)
504
+ .join(", ")}`
505
+ );
506
+ }
507
+
508
+ if (checkout) {
509
+ // Update checkout info
510
+ set({
511
+ checkoutId: checkout.id,
512
+ checkoutToken: checkout.token,
513
+ });
514
+
515
+ // Store in localStorage
516
+ try {
517
+ localStorage.setItem("checkoutId", checkout.id);
518
+ if (checkout.token) {
519
+ localStorage.setItem("checkoutToken", checkout.token);
520
+ }
521
+ } catch {}
522
+
523
+ // Store in user metadata for cross-device persistence
524
+ state
525
+ .storeCheckoutInUserMetadata(checkout.id, checkout.token)
526
+ .catch(console.error);
527
+ }
528
+ } else {
529
+ throw new Error("Failed to add item to checkout via GraphQL");
530
+ }
531
+ }
532
+
533
+ // Add to local state after successful GraphQL operation
534
+ // Store as a single consolidated item (with options embedded)
535
+ // Use key-based deduplication to allow same base product with different options
536
+ set((state) => {
537
+ const itemKey = generateCartItemKey(
538
+ itemToAdd.id,
539
+ itemToAdd.selectedOptions,
540
+ itemToAdd.customInputs
541
+ );
542
+ const itemWithKey = { ...itemToAdd, key: itemKey };
543
+ const existing = state.cartItems.find((i) => i.key === itemKey);
544
+ const cartItems = existing
545
+ ? state.cartItems.map((i) =>
546
+ i.key === itemKey
547
+ ? { ...i, quantity: i.quantity + itemToAdd.quantity }
548
+ : i
549
+ )
550
+ : [...state.cartItems, itemWithKey];
551
+ const { totalItems, totalAmount } = calculateTotals(cartItems);
552
+ return { cartItems, totalItems, totalAmount };
553
+ });
554
+ } catch (error) {
555
+ console.error(error);
556
+ // Re-throw the error so the UI can handle it
557
+ throw error;
558
+ }
559
+ },
560
+
561
+ removeFromCart: (key) => {
562
+ // Find the item first so we can sync its removal properly
563
+ const currentState = useGlobalStore.getState();
564
+ const itemToRemove = currentState.cartItems.find((i) => i.key === key);
565
+
566
+ // Sync with Saleor FIRST (before clearing local state)
567
+ if (currentState.checkoutId && itemToRemove) {
568
+ currentState.syncCartRemovalWithSaleor(itemToRemove).catch(console.error);
569
+ }
570
+
571
+ // Then update local state using key-based filtering
572
+ set((state) => {
573
+ const cartItems = state.cartItems.filter((i) => i.key !== key);
574
+ const { totalItems, totalAmount } = calculateTotals(cartItems);
575
+
576
+ // If cart becomes empty, clear checkout info
577
+ if (cartItems.length === 0) {
578
+ try {
579
+ localStorage.removeItem("checkoutId");
580
+ localStorage.removeItem("checkoutToken");
581
+ } catch {}
582
+ return {
583
+ cartItems,
584
+ totalItems,
585
+ totalAmount,
586
+ checkoutId: null,
587
+ checkoutToken: null,
588
+ };
589
+ }
590
+
591
+ return { cartItems, totalItems, totalAmount };
592
+ });
593
+ },
594
+
595
+ updateQuantity: (key, quantity) => {
596
+ const prevState = useGlobalStore.getState();
597
+ const oldItem = prevState.cartItems.find((item) => item.key === key);
598
+ const oldQuantity = oldItem?.quantity || 0;
599
+
600
+ set((state) => {
601
+ const cartItems = state.cartItems
602
+ .map((item) =>
603
+ item.key === key
604
+ ? { ...item, quantity: Math.max(0, quantity) }
605
+ : item
606
+ )
607
+ .filter((item) => item.quantity > 0);
608
+ const { totalItems, totalAmount } = calculateTotals(cartItems);
609
+
610
+ // If cart becomes empty, clear checkout info
611
+ if (cartItems.length === 0) {
612
+ // Cart is now empty after quantity update; clear checkout info.
613
+ try {
614
+ localStorage.removeItem("checkoutId");
615
+ localStorage.removeItem("checkoutToken");
616
+ } catch {}
617
+ return {
618
+ cartItems,
619
+ totalItems,
620
+ totalAmount,
621
+ checkoutId: null,
622
+ checkoutToken: null,
623
+ };
624
+ }
625
+
626
+ return { cartItems, totalItems, totalAmount };
627
+ });
628
+
629
+ // Sync with Saleor if there's a checkout and quantity actually changed
630
+ const currentState = useGlobalStore.getState();
631
+ if (currentState.checkoutId && oldItem && oldQuantity !== quantity) {
632
+ if (quantity === 0) {
633
+ // Item was removed
634
+ currentState.syncCartRemovalWithSaleor(oldItem).catch(console.error);
635
+ } else {
636
+ // Quantity was updated
637
+ currentState
638
+ .syncCartQuantityWithSaleor(oldItem, quantity, oldQuantity)
639
+ .catch(console.error);
640
+ }
641
+ }
642
+ },
643
+
644
+ clearCart: () => {
645
+ // Clear cart and checkout info
646
+ try {
647
+ localStorage.removeItem("checkoutId");
648
+ localStorage.removeItem("checkoutToken");
649
+ } catch {}
650
+ set({
651
+ cartItems: [],
652
+ totalItems: 0,
653
+ totalAmount: 0,
654
+ checkoutId: null,
655
+ checkoutToken: null,
656
+ });
657
+ },
658
+ setCheckoutId: (id) => set({ checkoutId: id }),
659
+ setCheckoutToken: (token) => set({ checkoutToken: token }),
660
+ setSelectedShippingMethodId: (id) =>
661
+ set({ selectedShippingMethodId: id }),
662
+ setSyncingCart: (syncing) => set({ syncingCart: syncing }),
663
+ setIsYMMActive: (active) => set({ isYMMActive: active }),
664
+
665
+ // Check YMM API status
666
+ checkYMMStatus: async () => {
667
+ if (typeof window === "undefined") return;
668
+
669
+ try {
670
+ const partsLogicUrl = process.env.NEXT_PUBLIC_PARTSLOGIC_URL;
671
+ if (!partsLogicUrl) {
672
+ set({ isYMMActive: false });
673
+ return;
674
+ }
675
+
676
+ const response = await fetch(`${partsLogicUrl}/api/ping`, {
677
+ method: "GET",
678
+ headers: {
679
+ "Content-Type": "application/json",
680
+ Accept: "application/json",
681
+ },
682
+ cache: "no-store",
683
+ });
684
+
685
+ if (response.ok) {
686
+ const data = await response.json();
687
+ const isActive = data.message === "pong";
688
+ set({ isYMMActive: isActive });
689
+ // YMM API status loaded.
690
+
691
+ // If YMM is active, load years data
692
+ if (isActive) {
693
+ const state = useGlobalStore.getState();
694
+ }
695
+ } else {
696
+ set({ isYMMActive: false });
697
+ // YMM API status fetch failed.
698
+ }
699
+ } catch (error) {
700
+ set({ isYMMActive: false });
701
+ console.error("Failed to check YMM status:", error);
702
+ }
703
+ },
704
+
705
+ // Load YMM years (called only once)
706
+ loadYMMYears: async () => {
707
+ if (typeof window === "undefined") return;
708
+ const state = useGlobalStore.getState();
709
+ // if (state.ymmYearsLoaded) return; // Prevent multiple calls
710
+
711
+ try {
712
+ const partsLogicUrl = process.env.NEXT_PUBLIC_PARTSLOGIC_URL;
713
+ if (!partsLogicUrl) return;
714
+
715
+ const response = await fetch(
716
+ `${partsLogicUrl}/api/fitment-search/root-types`,
717
+ {
718
+ method: "GET",
719
+ headers: {
720
+ "Content-Type": "application/json",
721
+ Accept: "application/json",
722
+ },
723
+ cache: "no-store",
724
+ }
725
+ );
726
+
727
+ if (response.ok) {
728
+ const data = await response.json();
729
+ if (data) {
730
+ set({ ymmYears: data.data, ymmYearsLoaded: true });
731
+ // YMM years loaded.
732
+ }
733
+ }
734
+ } catch (error) {
735
+ console.error("Failed to load YMM years:", error);
736
+ }
737
+ },
738
+
739
+ // Load cart from Saleor checkout when user logs in
740
+ loadCartFromSaleor: async () => {
741
+ if (typeof window === "undefined") return;
742
+
743
+ const state = useGlobalStore.getState();
744
+ if (!state.isLoggedIn) return;
745
+
746
+ try {
747
+ state.setSyncingCart(true);
748
+
749
+ const token = localStorage.getItem("token");
750
+ if (!token) return;
751
+
752
+ // For logged-in users, get checkout ID from user metadata (cross-device)
753
+ const checkoutId = await state.loadCheckoutFromUserMetadata();
754
+
755
+ if (!checkoutId) {
756
+ return;
757
+ }
758
+
759
+ const endpoint = getSaleorApiUrl();
760
+ const response = await fetch(
761
+ endpoint,
762
+ {
763
+ method: "POST",
764
+ headers: {
765
+ "Content-Type": "application/json",
766
+ ...(token && { Authorization: `Bearer ${token}` }),
767
+ },
768
+ body: JSON.stringify({
769
+ query: `
770
+ query GetCheckoutDetails($id: ID!) {
771
+ checkout(id: $id) {
772
+ id
773
+ token
774
+ totalPrice { gross { amount currency } }
775
+ subtotalPrice { gross { amount currency } }
776
+ lines {
777
+ id
778
+ quantity
779
+ totalPrice { gross { amount currency } }
780
+ metadata {
781
+ key
782
+ value
783
+ }
784
+ variant {
785
+ id
786
+ name
787
+ sku
788
+ metadata {
789
+ key
790
+ value
791
+ }
792
+ product {
793
+ id
794
+ name
795
+ thumbnail { url }
796
+ category { name }
797
+ pricing {
798
+ discount { gross { amount currency } }
799
+ }
800
+ }
801
+ pricing {
802
+ price { gross { amount currency } }
803
+ }
804
+ }
805
+ }
806
+ }
807
+ }
808
+ `,
809
+ variables: { id: checkoutId },
810
+ }),
811
+ }
812
+ );
813
+
814
+ if (!response.ok) throw new Error("Failed to fetch checkout");
815
+
816
+ const data = await response.json();
817
+ const checkout = data?.data?.checkout;
818
+
819
+ if (checkout && checkout.lines?.length > 0) {
820
+ // Type for checkout line from GraphQL
821
+ type CheckoutLine = {
822
+ id: string;
823
+ quantity: number;
824
+ totalPrice: { gross: { amount: number; currency: string } };
825
+ metadata?: { key: string; value: string }[] | null;
826
+ variant: {
827
+ id: string;
828
+ name: string;
829
+ sku?: string | null;
830
+ metadata?: { key: string; value: string }[] | null;
831
+ product: {
832
+ id: string;
833
+ name: string;
834
+ thumbnail?: { url: string } | null;
835
+ category?: { name: string } | null;
836
+ pricing?: {
837
+ discount?: {
838
+ gross: { amount: number; currency: string };
839
+ } | null;
840
+ } | null;
841
+ };
842
+ pricing?: {
843
+ price?: {
844
+ gross: { amount: number; currency: string };
845
+ } | null;
846
+ } | null;
847
+ };
848
+ };
849
+
850
+ // Group lines by product ID, separating base products from option variants
851
+ const productMap = new Map<
852
+ string,
853
+ {
854
+ baseLine: CheckoutLine | null;
855
+ optionLines: Array<{
856
+ line: CheckoutLine;
857
+ optionMeta: { name: string; label: string };
858
+ }>;
859
+ }
860
+ >();
861
+
862
+ for (const line of checkout.lines as CheckoutLine[]) {
863
+ const productId = line.variant.product.id;
864
+ const optionMeta = parseOptionSetMetadata(line.variant.metadata);
865
+
866
+ if (!productMap.has(productId)) {
867
+ productMap.set(productId, { baseLine: null, optionLines: [] });
868
+ }
869
+
870
+ const entry = productMap.get(productId)!;
871
+
872
+ if (optionMeta) {
873
+ // This is an option variant (has option_set metadata)
874
+ entry.optionLines.push({ line, optionMeta });
875
+ } else {
876
+ // This is a base product variant
877
+ entry.baseLine = line;
878
+ }
879
+ }
880
+
881
+ // Convert the grouped map into CartItems
882
+ // Handle case where same product is added multiple times with different option selections
883
+ const cartItems: CartItem[] = [];
884
+
885
+ for (const [, entry] of productMap) {
886
+ const { baseLine, optionLines } = entry;
887
+
888
+ // Determine the display line (prefer base, fall back to first option)
889
+ const displayLine = baseLine ?? optionLines[0]?.line;
890
+ if (!displayLine) continue;
891
+
892
+ // If no option lines, create a single cart item for just the base product
893
+ if (optionLines.length === 0) {
894
+ const qty = Math.max(1, displayLine.quantity);
895
+ const baseLineTotal = baseLine?.totalPrice?.gross?.amount ?? 0;
896
+ const basePrice = baseLineTotal / qty;
897
+
898
+ const customInputs = displayLine.metadata?.reduce(
899
+ (acc, item) => {
900
+ if (item.key && item.value) {
901
+ acc[item.key] = item.value;
902
+ }
903
+ return acc;
904
+ },
905
+ {} as Record<string, string>
906
+ );
907
+ const finalCustomInputs =
908
+ customInputs && Object.keys(customInputs).length > 0
909
+ ? customInputs
910
+ : undefined;
911
+
912
+ const itemKey = generateCartItemKey(
913
+ displayLine.variant.id,
914
+ undefined,
915
+ finalCustomInputs
916
+ );
917
+
918
+ cartItems.push({
919
+ key: itemKey,
920
+ id: displayLine.variant.id,
921
+ name: displayLine.variant.product.name,
922
+ price: basePrice,
923
+ image: displayLine.variant.product.thumbnail?.url || "",
924
+ quantity: qty,
925
+ sku: displayLine.variant.sku || undefined,
926
+ category: displayLine.variant.product.category?.name || undefined,
927
+ customInputs: finalCustomInputs,
928
+ });
929
+ continue;
930
+ }
931
+
932
+ // Group option lines by their option set name to detect multiple selections
933
+ const optionsBySetName = new Map<
934
+ string,
935
+ Array<{ line: CheckoutLine; optionMeta: { name: string; label: string } }>
936
+ >();
937
+ for (const opt of optionLines) {
938
+ const setName = opt.optionMeta.name;
939
+ if (!optionsBySetName.has(setName)) {
940
+ optionsBySetName.set(setName, []);
941
+ }
942
+ optionsBySetName.get(setName)!.push(opt);
943
+ }
944
+
945
+ // The number of cart items = max count of options in any single option set
946
+ // (Multiple options from same set = multiple cart items with different selections)
947
+ const numCartItems = Math.max(
948
+ ...Array.from(optionsBySetName.values()).map((arr) => arr.length)
949
+ );
950
+
951
+ // Calculate base unit price
952
+ const baseQty = baseLine?.quantity ?? 0;
953
+ const baseLineTotal = baseLine?.totalPrice?.gross?.amount ?? 0;
954
+ const baseUnitPrice = baseQty > 0 ? baseLineTotal / baseQty : 0;
955
+ const skipBaseProduct = !baseLine;
956
+
957
+ // For each cart item, pair options by quantity (options with same qty likely belong together)
958
+ // Sort each option set's lines by quantity to pair them correctly
959
+ for (const opts of optionsBySetName.values()) {
960
+ opts.sort((a, b) => a.line.quantity - b.line.quantity);
961
+ }
962
+
963
+ for (let i = 0; i < numCartItems; i++) {
964
+ // Collect one option from each set for this cart item (the i-th one if it exists)
965
+ const selectedOptions: CartItemOption[] = [];
966
+ const optionQuantities: number[] = [];
967
+
968
+ for (const [, opts] of optionsBySetName) {
969
+ if (i < opts.length) {
970
+ const { line, optionMeta } = opts[i];
971
+ const optQty = Math.max(1, line.quantity);
972
+ const optLineTotal = line.totalPrice?.gross?.amount ?? 0;
973
+
974
+ optionQuantities.push(optQty);
975
+
976
+ selectedOptions.push({
977
+ variantId: line.variant.id,
978
+ name: line.variant.name,
979
+ price: optLineTotal / optQty,
980
+ optionSetName: optionMeta.name,
981
+ optionSetLabel: optionMeta.label,
982
+ });
983
+ }
984
+ }
985
+
986
+ // Use the first option's quantity (all options in same cart item should have same qty)
987
+ // Only fallback to 1 if no options found (shouldn't happen in this branch)
988
+ const cartItemQty = optionQuantities.length > 0
989
+ ? optionQuantities[0]
990
+ : 1;
991
+
992
+ const customInputs = displayLine.metadata?.reduce(
993
+ (acc, item) => {
994
+ if (item.key && item.value) {
995
+ acc[item.key] = item.value;
996
+ }
997
+ return acc;
998
+ },
999
+ {} as Record<string, string>
1000
+ );
1001
+ const finalCustomInputs =
1002
+ customInputs && Object.keys(customInputs).length > 0
1003
+ ? customInputs
1004
+ : undefined;
1005
+
1006
+ const itemKey = generateCartItemKey(
1007
+ displayLine.variant.id,
1008
+ selectedOptions.length > 0 ? selectedOptions : undefined,
1009
+ finalCustomInputs
1010
+ );
1011
+
1012
+ cartItems.push({
1013
+ key: itemKey,
1014
+ id: displayLine.variant.id,
1015
+ name: displayLine.variant.product.name,
1016
+ price: baseUnitPrice,
1017
+ image: displayLine.variant.product.thumbnail?.url || "",
1018
+ quantity: cartItemQty,
1019
+ sku: displayLine.variant.sku || undefined,
1020
+ category: displayLine.variant.product.category?.name || undefined,
1021
+ selectedOptions:
1022
+ selectedOptions.length > 0 ? selectedOptions : undefined,
1023
+ customInputs: finalCustomInputs,
1024
+ skipBaseProduct,
1025
+ });
1026
+ }
1027
+ }
1028
+
1029
+ const { totalItems, totalAmount } = calculateTotals(cartItems);
1030
+
1031
+ set({
1032
+ cartItems,
1033
+ totalItems,
1034
+ totalAmount,
1035
+ checkoutId: checkout.id,
1036
+ checkoutToken: checkout.token,
1037
+ });
1038
+
1039
+ // Store in localStorage
1040
+ try {
1041
+ localStorage.setItem("checkoutId", checkout.id);
1042
+ if (checkout.token) {
1043
+ localStorage.setItem("checkoutToken", checkout.token);
1044
+ }
1045
+ } catch {}
1046
+ } else {
1047
+ // No items found in checkout or checkout is empty.
1048
+ }
1049
+ } catch (error) {
1050
+ console.error("Failed to load cart from Saleor:", error);
1051
+ // If the checkout is not found or invalid, clear the stored checkout ID
1052
+ if (error instanceof Error && error.message.includes("not found")) {
1053
+ try {
1054
+ localStorage.removeItem("checkoutId");
1055
+ localStorage.removeItem("checkoutToken");
1056
+ set({ checkoutId: null, checkoutToken: null });
1057
+ } catch {}
1058
+ }
1059
+ } finally {
1060
+ state.setSyncingCart(false);
1061
+ }
1062
+ },
1063
+
1064
+ // Sync local cart with backend
1065
+ syncCartWithSaleor: async () => {
1066
+ if (typeof window === "undefined") return;
1067
+
1068
+ const state = useGlobalStore.getState();
1069
+ if (state.cartItems.length === 0) return;
1070
+
1071
+ try {
1072
+ state.setSyncingCart(true);
1073
+
1074
+ const token = localStorage.getItem("token");
1075
+ if (!token) return;
1076
+ const endpoint = getSaleorApiUrl();
1077
+
1078
+ const lastItem = state.cartItems[state.cartItems.length - 1];
1079
+ if (!lastItem) return;
1080
+
1081
+ let checkoutId = state.checkoutId;
1082
+ const channel = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
1083
+
1084
+ // Create checkout if it doesn't exist
1085
+ if (!checkoutId) {
1086
+ const createResponse = await fetch(
1087
+ endpoint,
1088
+ {
1089
+ method: "POST",
1090
+ headers: {
1091
+ "Content-Type": "application/json",
1092
+ ...(token && { Authorization: `Bearer ${token}` }),
1093
+ },
1094
+ body: JSON.stringify({
1095
+ query: CHECKOUT_CREATE,
1096
+ variables: {
1097
+ input: {
1098
+ channel,
1099
+ lines: [
1100
+ {
1101
+ variantId: lastItem.id,
1102
+ quantity: lastItem.quantity,
1103
+ },
1104
+ ],
1105
+ },
1106
+ },
1107
+ }),
1108
+ }
1109
+ );
1110
+
1111
+ if (createResponse.ok) {
1112
+ const createData = await createResponse.json();
1113
+ const checkout = createData?.data?.checkoutCreate?.checkout;
1114
+ const errors = createData?.data?.checkoutCreate?.errors;
1115
+
1116
+ if (errors?.length > 0) {
1117
+ throw new Error(
1118
+ `GraphQL checkout creation errors: ${errors
1119
+ .map((e: { message: string }) => e.message)
1120
+ .join(", ")}`
1121
+ );
1122
+ }
1123
+
1124
+ if (checkout) {
1125
+ checkoutId = checkout.id;
1126
+
1127
+ // Update checkout info
1128
+ set({
1129
+ checkoutId: checkout.id,
1130
+ checkoutToken: checkout.token,
1131
+ });
1132
+
1133
+ // Store in localStorage
1134
+ try {
1135
+ localStorage.setItem("checkoutId", checkout.id);
1136
+ if (checkout.token) {
1137
+ localStorage.setItem("checkoutToken", checkout.token);
1138
+ }
1139
+ } catch {}
1140
+
1141
+ // Store in user metadata for cross-device persistence
1142
+ if (state.isLoggedIn) {
1143
+ state
1144
+ .storeCheckoutInUserMetadata(checkout.id, checkout.token)
1145
+ .catch(console.error);
1146
+ }
1147
+ }
1148
+ } else {
1149
+ throw new Error("Failed to create checkout via GraphQL");
1150
+ }
1151
+ } else {
1152
+ // Add to existing checkout using GraphQL
1153
+ const addResponse = await fetch(
1154
+ endpoint,
1155
+ {
1156
+ method: "POST",
1157
+ headers: {
1158
+ "Content-Type": "application/json",
1159
+ ...(token && { Authorization: `Bearer ${token}` }),
1160
+ },
1161
+ body: JSON.stringify({
1162
+ query: CHECKOUT_LINES_ADD,
1163
+ variables: {
1164
+ id: checkoutId,
1165
+ lines: [
1166
+ {
1167
+ variantId: lastItem.id,
1168
+ quantity: lastItem.quantity,
1169
+ },
1170
+ ],
1171
+ },
1172
+ }),
1173
+ }
1174
+ );
1175
+
1176
+ if (addResponse.ok) {
1177
+ const addData = await addResponse.json();
1178
+ const checkout = addData?.data?.checkoutLinesAdd?.checkout;
1179
+ const errors = addData?.data?.checkoutLinesAdd?.errors;
1180
+
1181
+ if (errors?.length > 0) {
1182
+ throw new Error(
1183
+ `GraphQL add to cart errors: ${errors
1184
+ .map((e: { message: string }) => e.message)
1185
+ .join(", ")}`
1186
+ );
1187
+ }
1188
+
1189
+ if (checkout) {
1190
+ // Update checkout info
1191
+ set({
1192
+ checkoutId: checkout.id,
1193
+ checkoutToken: checkout.token,
1194
+ });
1195
+
1196
+ // Store in localStorage
1197
+ try {
1198
+ localStorage.setItem("checkoutId", checkout.id);
1199
+ if (checkout.token) {
1200
+ localStorage.setItem("checkoutToken", checkout.token);
1201
+ }
1202
+ } catch {}
1203
+
1204
+ // Store in user metadata for cross-device persistence
1205
+ if (state.isLoggedIn) {
1206
+ state
1207
+ .storeCheckoutInUserMetadata(checkout.id, checkout.token)
1208
+ .catch(console.error);
1209
+ }
1210
+ }
1211
+ } else {
1212
+ throw new Error("Failed to add item to checkout via GraphQL");
1213
+ }
1214
+ }
1215
+ } catch (error) {
1216
+ console.error("Failed to sync cart with GraphQL:", error);
1217
+ } finally {
1218
+ state.setSyncingCart(false);
1219
+ }
1220
+ },
1221
+
1222
+ // Store checkout ID in user's metadata for cross-device persistence
1223
+ storeCheckoutInUserMetadata: async (
1224
+ checkoutId: string,
1225
+ checkoutToken?: string
1226
+ ) => {
1227
+ if (typeof window === "undefined") return;
1228
+
1229
+ const state = useGlobalStore.getState();
1230
+ if (!state.isLoggedIn) return;
1231
+
1232
+ try {
1233
+ const token = localStorage.getItem("token");
1234
+ if (!token) return;
1235
+ const endpoint = getSaleorApiUrl();
1236
+
1237
+ const response = await fetch(
1238
+ endpoint,
1239
+ {
1240
+ method: "POST",
1241
+ headers: {
1242
+ "Content-Type": "application/json",
1243
+ ...(token && { Authorization: `Bearer ${token}` }),
1244
+ },
1245
+ body: JSON.stringify({
1246
+ query: `
1247
+ mutation AccountUpdate($input: AccountInput!) {
1248
+ accountUpdate(input: $input) {
1249
+ user {
1250
+ id
1251
+ metadata {
1252
+ key
1253
+ value
1254
+ }
1255
+ }
1256
+ errors {
1257
+ field
1258
+ message
1259
+ }
1260
+ }
1261
+ }
1262
+ `,
1263
+ variables: {
1264
+ input: {
1265
+ metadata: [
1266
+ {
1267
+ key: "activeCheckoutId",
1268
+ value: checkoutId,
1269
+ },
1270
+ ...(checkoutToken
1271
+ ? [
1272
+ {
1273
+ key: "activeCheckoutToken",
1274
+ value: checkoutToken,
1275
+ },
1276
+ ]
1277
+ : []),
1278
+ ],
1279
+ },
1280
+ },
1281
+ }),
1282
+ }
1283
+ );
1284
+
1285
+ if (response.ok) {
1286
+ const data = await response.json();
1287
+ const errors = data?.data?.accountUpdate?.errors;
1288
+
1289
+ if (errors?.length > 0) {
1290
+ console.error(
1291
+ "Failed to store checkout in user metadata:",
1292
+ errors
1293
+ );
1294
+ }
1295
+ }
1296
+ } catch (error) {
1297
+ console.error("Failed to store checkout in user metadata:", error);
1298
+ }
1299
+ },
1300
+
1301
+ // Load checkout ID from user's metadata
1302
+ loadCheckoutFromUserMetadata: async (): Promise<string | null> => {
1303
+ if (typeof window === "undefined") return null;
1304
+
1305
+ const state = useGlobalStore.getState();
1306
+ if (!state.isLoggedIn) return null;
1307
+
1308
+ try {
1309
+ const token = localStorage.getItem("token");
1310
+ if (!token) return null;
1311
+ const endpoint = getSaleorApiUrl();
1312
+
1313
+ const response = await fetch(
1314
+ endpoint,
1315
+ {
1316
+ method: "POST",
1317
+ headers: {
1318
+ "Content-Type": "application/json",
1319
+ ...(token && { Authorization: `Bearer ${token}` }),
1320
+ },
1321
+ body: JSON.stringify({
1322
+ query: `
1323
+ query GetUserWithCheckout {
1324
+ me {
1325
+ id
1326
+ metadata {
1327
+ key
1328
+ value
1329
+ }
1330
+ }
1331
+ }
1332
+ `,
1333
+ }),
1334
+ }
1335
+ );
1336
+
1337
+ if (response.ok) {
1338
+ const data = await response.json();
1339
+ const metadata = data?.data?.me?.metadata || [];
1340
+
1341
+ const checkoutIdMetadata = metadata.find(
1342
+ (item: { key: string; value: string }) =>
1343
+ item.key === "activeCheckoutId"
1344
+ );
1345
+ const checkoutTokenMetadata = metadata.find(
1346
+ (item: { key: string; value: string }) =>
1347
+ item.key === "activeCheckoutToken"
1348
+ );
1349
+
1350
+ if (checkoutIdMetadata?.value) {
1351
+ // Store in localStorage as fallback
1352
+ localStorage.setItem("checkoutId", checkoutIdMetadata.value);
1353
+ if (checkoutTokenMetadata?.value) {
1354
+ localStorage.setItem(
1355
+ "checkoutToken",
1356
+ checkoutTokenMetadata.value
1357
+ );
1358
+ }
1359
+
1360
+ return checkoutIdMetadata.value;
1361
+ }
1362
+ }
1363
+ } catch (error) {
1364
+ console.error("Failed to load checkout from user metadata:", error);
1365
+ }
1366
+
1367
+ return null;
1368
+ },
1369
+
1370
+ // ✅ NEW: blow away all local + server metadata after successful order
1371
+ finalizeCheckoutCleanup: async () => {
1372
+ const state = useGlobalStore.getState();
1373
+
1374
+ // 1) Clear cart in store
1375
+ state.clearCart();
1376
+
1377
+ // 2) Clear checkout ids and shipping method in store
1378
+ set({
1379
+ checkoutId: null,
1380
+ checkoutToken: null,
1381
+ selectedShippingMethodId: null,
1382
+ });
1383
+
1384
+ // 3) Clear guest data only if not logged in (for fresh start on next order)
1385
+ if (!state.isLoggedIn) {
1386
+ set({
1387
+ guestEmail: "",
1388
+ guestShippingInfo: {
1389
+ firstName: "",
1390
+ lastName: "",
1391
+ address: "",
1392
+ city: "",
1393
+ state: "",
1394
+ zipCode: "",
1395
+ country: "",
1396
+ phone: "",
1397
+ },
1398
+ });
1399
+ }
1400
+
1401
+ // 4) Clear localStorage copies
1402
+ try {
1403
+ localStorage.removeItem("checkoutId");
1404
+ localStorage.removeItem("checkoutToken");
1405
+ localStorage.removeItem("selectedShippingMethodId");
1406
+ localStorage.removeItem("pendingCheckoutId");
1407
+ localStorage.removeItem("pendingTransactionId");
1408
+ } catch {}
1409
+
1410
+ // 5) Clear sessionStorage copies
1411
+ try {
1412
+ sessionStorage.removeItem("checkoutId");
1413
+ sessionStorage.removeItem("transactionId");
1414
+ } catch {}
1415
+
1416
+ // 6) Clear server-side metadata so old checkout can't rehydrate
1417
+ try {
1418
+ const token = localStorage.getItem("token");
1419
+ if (token) {
1420
+ await fetch(getSaleorApiUrl(), {
1421
+ method: "POST",
1422
+ headers: {
1423
+ "Content-Type": "application/json",
1424
+ ...(token && { Authorization: `Bearer ${token}` }),
1425
+ },
1426
+ body: JSON.stringify({
1427
+ query: `
1428
+ mutation AccountUpdate($input: AccountInput!) {
1429
+ accountUpdate(input: $input) {
1430
+ user { id }
1431
+ errors { field message }
1432
+ }
1433
+ }
1434
+ `,
1435
+ variables: {
1436
+ input: {
1437
+ metadata: [
1438
+ { key: "activeCheckoutId", value: "" },
1439
+ { key: "activeCheckoutToken", value: "" },
1440
+ ],
1441
+ },
1442
+ },
1443
+ }),
1444
+ });
1445
+ }
1446
+ } catch (e) {
1447
+ console.warn("[checkout cleanup] failed to clear user metadata", e);
1448
+ }
1449
+ },
1450
+
1451
+ // Sync cart quantity changes with Saleor
1452
+ // Now accepts a CartItem to properly sync all variant IDs (base + options)
1453
+ syncCartQuantityWithSaleor: async (
1454
+ cartItem: CartItem,
1455
+ newQuantity: number,
1456
+ oldQuantity: number
1457
+ ) => {
1458
+ if (typeof window === "undefined") return;
1459
+
1460
+ const state = useGlobalStore.getState();
1461
+ if (!state.checkoutId) return;
1462
+
1463
+ try {
1464
+ const token = localStorage.getItem("token");
1465
+ const endpoint = getSaleorApiUrl();
1466
+
1467
+ // If quantity is 0, we need to remove the line entirely
1468
+ if (newQuantity === 0) {
1469
+ await state.syncCartRemovalWithSaleor(cartItem);
1470
+ return;
1471
+ }
1472
+
1473
+ // Build lines for all variants (base product + selected options)
1474
+ const lines: Array<{ variantId: string; quantity: number }> = [];
1475
+
1476
+ // Add base product only if not skipBaseProduct
1477
+ if (!cartItem.skipBaseProduct) {
1478
+ lines.push({ variantId: cartItem.id, quantity: newQuantity });
1479
+ }
1480
+
1481
+ // Add option variant lines if present
1482
+ if (cartItem.selectedOptions?.length) {
1483
+ for (const option of cartItem.selectedOptions) {
1484
+ lines.push({
1485
+ variantId: option.variantId,
1486
+ quantity: newQuantity,
1487
+ });
1488
+ }
1489
+ }
1490
+
1491
+ const response = await fetch(
1492
+ endpoint,
1493
+ {
1494
+ method: "POST",
1495
+ headers: {
1496
+ "Content-Type": "application/json",
1497
+ ...(token && { Authorization: `Bearer ${token}` }),
1498
+ },
1499
+ body: JSON.stringify({
1500
+ query: CHECKOUT_LINES_UPDATE,
1501
+ variables: {
1502
+ id: state.checkoutId,
1503
+ lines,
1504
+ },
1505
+ }),
1506
+ }
1507
+ );
1508
+
1509
+ if (response.ok) {
1510
+ const data = await response.json();
1511
+ const errors = data?.data?.checkoutLinesUpdate?.errors;
1512
+
1513
+ if (errors?.length > 0) {
1514
+ console.error("Checkout quantity update errors:", errors);
1515
+ }
1516
+ } else {
1517
+ console.error("Failed to update quantity in Saleor checkout");
1518
+ }
1519
+ } catch (error) {
1520
+ console.error("Failed to sync quantity with Saleor:", error);
1521
+ }
1522
+ },
1523
+
1524
+ // Sync cart item removal with Saleor
1525
+ // Now accepts a CartItem to properly remove all variant IDs (base + options)
1526
+ syncCartRemovalWithSaleor: async (cartItem: CartItem) => {
1527
+ if (typeof window === "undefined") return;
1528
+
1529
+ const state = useGlobalStore.getState();
1530
+ if (!state.checkoutId) return;
1531
+
1532
+ try {
1533
+ const token = localStorage.getItem("token");
1534
+ const endpoint = getSaleorApiUrl();
1535
+
1536
+ // Collect all variant IDs to remove (base product + selected options)
1537
+ const variantIdsToRemove: string[] = [];
1538
+
1539
+ // Add base product only if not skipBaseProduct
1540
+ if (!cartItem.skipBaseProduct) {
1541
+ variantIdsToRemove.push(cartItem.id);
1542
+ }
1543
+
1544
+ // Add option variant IDs if present
1545
+ if (cartItem.selectedOptions?.length) {
1546
+ for (const option of cartItem.selectedOptions) {
1547
+ variantIdsToRemove.push(option.variantId);
1548
+ }
1549
+ }
1550
+
1551
+ if (variantIdsToRemove.length === 0) {
1552
+ return;
1553
+ }
1554
+
1555
+ // Step 1: Get the checkout lines to find the correct line IDs
1556
+ const getCheckoutResponse = await fetch(
1557
+ endpoint,
1558
+ {
1559
+ method: "POST",
1560
+ headers: {
1561
+ "Content-Type": "application/json",
1562
+ ...(token && { Authorization: `Bearer ${token}` }),
1563
+ },
1564
+ body: JSON.stringify({
1565
+ query: `
1566
+ query GetCheckoutLines($id: ID!) {
1567
+ checkout(id: $id) {
1568
+ id
1569
+ lines {
1570
+ id
1571
+ variant { id }
1572
+ }
1573
+ }
1574
+ }
1575
+ `,
1576
+ variables: {
1577
+ id: state.checkoutId,
1578
+ },
1579
+ }),
1580
+ }
1581
+ );
1582
+
1583
+ if (!getCheckoutResponse.ok) {
1584
+ throw new Error("Failed to fetch checkout lines");
1585
+ }
1586
+
1587
+ const checkoutData = await getCheckoutResponse.json();
1588
+ const checkout = checkoutData?.data?.checkout;
1589
+
1590
+ if (!checkout?.lines) {
1591
+ console.error("No checkout lines found");
1592
+ return;
1593
+ }
1594
+
1595
+ // Step 2: Find line IDs for all variants we want to remove
1596
+ const lineIdsToRemove: string[] = [];
1597
+ for (const variantId of variantIdsToRemove) {
1598
+ const lineToRemove = checkout.lines.find(
1599
+ (line: { id: string; variant: { id: string } }) =>
1600
+ line.variant.id === variantId
1601
+ );
1602
+ if (lineToRemove) {
1603
+ lineIdsToRemove.push(lineToRemove.id);
1604
+ } else {
1605
+ console.warn(`No checkout line found for variant ${variantId}`);
1606
+ }
1607
+ }
1608
+
1609
+ if (lineIdsToRemove.length === 0) {
1610
+ console.error("No checkout lines found to remove");
1611
+ return;
1612
+ }
1613
+
1614
+ // Step 3: Remove all lines using the checkout line IDs
1615
+ const deleteResponse = await fetch(
1616
+ endpoint,
1617
+ {
1618
+ method: "POST",
1619
+ headers: {
1620
+ "Content-Type": "application/json",
1621
+ ...(token && { Authorization: `Bearer ${token}` }),
1622
+ },
1623
+ body: JSON.stringify({
1624
+ query: CHECKOUT_LINES_DELETE,
1625
+ variables: {
1626
+ id: state.checkoutId,
1627
+ linesIds: lineIdsToRemove,
1628
+ },
1629
+ }),
1630
+ }
1631
+ );
1632
+
1633
+ if (deleteResponse.ok) {
1634
+ const data = await deleteResponse.json();
1635
+ const errors = data?.data?.checkoutLinesDelete?.errors;
1636
+
1637
+ if (errors?.length > 0) {
1638
+ console.error("Checkout line removal errors:", errors);
1639
+ }
1640
+ } else {
1641
+ console.error("Failed to remove checkout lines from Saleor");
1642
+ }
1643
+ } catch (error) {
1644
+ console.error("Failed to sync removal with Saleor:", error);
1645
+ }
1646
+ },
1647
+ }),
1648
+ {
1649
+ name: "wsm-global-store",
1650
+ storage: createJSONStorage(() => localStorage),
1651
+ }
1652
+ )
1653
+ );
1654
+
1655
+ export { generateCartItemKey };
1656
+ export default useGlobalStore;