@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,3312 @@
1
+ "use client";
2
+ import AddressManagement from "@/app/components/checkout/AddressManagement";
3
+ import AddressInformationSection from "@/app/components/checkout/AddressInformationSection";
4
+ import CheckoutHeader from "@/app/components/checkout/CheckoutHeader";
5
+ import ContactDetailsSection from "@/app/components/checkout/ContactDetailsSection";
6
+ import DeliveryMethodSection from "@/app/components/checkout/DeliveryMethodSection";
7
+ import DealerShippingSection from "@/app/components/checkout/DealerShippingSection";
8
+ import WillCallSection from "@/app/components/checkout/WillCallSection";
9
+ import OrderSummary from "@/app/components/checkout/OrderSummary";
10
+ import PaymentStep from "@/app/components/checkout/paymentStep";
11
+ import CheckoutQuestions from "@/app/components/checkout/CheckoutQuestions";
12
+ import CheckoutTermsModal from "@/app/components/checkout/CheckoutTermsModal";
13
+ import {
14
+ GET_CHECKOUT_TERMS_AND_CONDITIONS,
15
+ type CheckoutTermsAndConditionsResponse,
16
+ } from "@/graphql/queries/getCheckoutTermsAndConditions";
17
+ import {
18
+ ACCOUNT_SET_DEFAULT_ADDRESS,
19
+ type AccountSetDefaultAddressData,
20
+ type AccountSetDefaultAddressVars,
21
+ } from "@/graphql/mutations/accountSetDefaultAddress";
22
+ import {
23
+ CHECKOUT_BILLING_ADDRESS_UPDATE,
24
+ CHECKOUT_DELIVERY_METHOD_UPDATE,
25
+ } from "@/graphql/mutations/checkout";
26
+ import {
27
+ ADD_VOUCHER_TO_CHECKOUT,
28
+ REMOVE_VOUCHER_FROM_CHECKOUT,
29
+ } from "@/graphql/mutations/checkoutAddVoucher";
30
+ import { CHECKOUT_EMAIL_UPDATE } from "@/graphql/mutations/checkoutEmailUpdate";
31
+ import {
32
+ GET_CHECKOUT_DETAILS,
33
+ GET_CHECKOUT_SHIPPING_METHODS,
34
+ GET_PAYMENT_GATEWAYS,
35
+ } from "@/graphql/queries/checkout";
36
+ import {
37
+ GET_CHECKOUT_COLLECTION_POINTS,
38
+ type GetCheckoutCollectionPointsData,
39
+ type GetCheckoutCollectionPointsVars,
40
+ type CollectionPoint,
41
+ } from "@/graphql/queries/willCallCollectionPoints";
42
+ import {
43
+ CHECKOUT_DELIVERY_METHOD_UPDATE_WILL_CALL,
44
+ type WillCallDeliveryMethodUpdateData,
45
+ type WillCallDeliveryMethodUpdateVars,
46
+ } from "@/graphql/mutations/willCallDeliveryMethod";
47
+ import {
48
+ ME_ADDRESSES_QUERY,
49
+ type MeAddressesData,
50
+ } from "@/graphql/queries/meAddresses";
51
+ import {
52
+ AddressForm,
53
+ type PaymentProcessingState,
54
+ type KountConfigResponse,
55
+ } from "@/graphql/types/checkout";
56
+ import { useGlobalStore } from "@/store/useGlobalStore";
57
+ import { getSaleorApiUrl } from "@/lib/saleor/getSaleorApiUrl";
58
+ import { useMutation, useQuery, useApolloClient, gql } from "@apollo/client";
59
+ import { useRouter } from "next/navigation";
60
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
61
+ import EmptyState from "../components/reuseableUI/emptyState";
62
+ import LoadingUI from "../components/reuseableUI/loadingUI";
63
+ import { gtmAddShippingInfo, Product } from "../utils/googleTagManager";
64
+ import { useAppConfiguration } from "../components/providers/ServerAppConfigurationProvider";
65
+ import { kountApi } from "@/lib/api/kount";
66
+
67
+ /* ----------------- small helpers ----------------- */
68
+ const shallowEq = (
69
+ a: Record<string, unknown> | null,
70
+ b: Record<string, unknown> | null
71
+ ) => {
72
+ if (a === b) return true;
73
+ if (!a || !b) return false;
74
+ const ka = Object.keys(a),
75
+ kb = Object.keys(b);
76
+ if (ka.length !== kb.length) return false;
77
+ for (const k of ka) if (a[k] !== b[k]) return false;
78
+ return true;
79
+ };
80
+ const isMethodAvailable = (id: string | null, list: ShippingMethod[]) =>
81
+ !!(id && list.some((m) => m.id === id));
82
+
83
+ // Product restriction validation helpers
84
+ interface MetadataItem {
85
+ key: string;
86
+ value: string;
87
+ }
88
+
89
+ interface ProductData {
90
+ name: string;
91
+ metadata: MetadataItem[];
92
+ }
93
+
94
+ interface VariantData {
95
+ product: ProductData;
96
+ }
97
+
98
+ interface CheckoutLineData {
99
+ variant: VariantData;
100
+ }
101
+
102
+ interface CheckoutData {
103
+ lines: CheckoutLineData[];
104
+ }
105
+
106
+ const checkProductRestrictions = (
107
+ checkoutData: CheckoutData,
108
+ userState: string,
109
+ userZipCode: string
110
+ ): Array<{ productName: string; checkoutMessage: string }> => {
111
+ const restrictions: Array<{ productName: string; checkoutMessage: string }> =
112
+ [];
113
+
114
+ if (!checkoutData?.lines) return restrictions;
115
+
116
+ checkoutData.lines.forEach((line: CheckoutLineData) => {
117
+ const product = line.variant?.product;
118
+ if (!product?.metadata) return;
119
+
120
+ const metadata = product.metadata;
121
+ let restrictedStates: string[] = [];
122
+ let restrictedZipCodes: (string | number)[] = [];
123
+ let checkoutMessage = "";
124
+ let shippingIsActive = false;
125
+
126
+ // Parse metadata
127
+ metadata.forEach((meta: MetadataItem) => {
128
+ if (meta.key === "restricted_states" && meta.value) {
129
+ try {
130
+ restrictedStates = JSON.parse(meta.value);
131
+ } catch {
132
+ console.warn("Failed to parse restricted_states:", meta.value);
133
+ }
134
+ }
135
+ if (meta.key === "restricted_zip_codes" && meta.value) {
136
+ try {
137
+ restrictedZipCodes = JSON.parse(meta.value);
138
+ } catch {
139
+ console.warn("Failed to parse restricted_zip_codes:", meta.value);
140
+ }
141
+ }
142
+ if (meta.key === "checkout_message" && meta.value) {
143
+ checkoutMessage = meta.value;
144
+ }
145
+ if (meta.key === "shipping_isactive" && meta.value) {
146
+ shippingIsActive = meta.value.toLowerCase() === "true";
147
+ }
148
+ });
149
+
150
+ // Only apply restrictions if shipping_isactive is true
151
+ if (!shippingIsActive) {
152
+ return;
153
+ }
154
+
155
+ // Check if user's state or zip code matches restrictions
156
+ const stateMatches =
157
+ userState && restrictedStates.includes(userState.toUpperCase());
158
+ const zipMatches =
159
+ userZipCode &&
160
+ restrictedZipCodes.some(
161
+ (zip) =>
162
+ String(zip) === userZipCode ||
163
+ String(zip) === userZipCode.split("-")[0]
164
+ );
165
+
166
+ if ((stateMatches || zipMatches) && checkoutMessage) {
167
+ restrictions.push({
168
+ productName: product.name,
169
+ checkoutMessage,
170
+ });
171
+ }
172
+ });
173
+
174
+ return restrictions;
175
+ };
176
+
177
+ /** NEW: timing + retry helpers */
178
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
179
+ async function withRetry<T>(
180
+ fn: () => Promise<T>,
181
+ attempts = 4,
182
+ baseDelayMs = 250
183
+ ): Promise<T> {
184
+ let lastErr: unknown;
185
+ for (let i = 0; i < attempts; i++) {
186
+ try {
187
+ return await fn();
188
+ } catch (e) {
189
+ lastErr = e;
190
+ // exp backoff + small jitter
191
+ const delay =
192
+ baseDelayMs * Math.pow(2, i) + Math.floor(Math.random() * 50);
193
+ await sleep(delay);
194
+ }
195
+ }
196
+ throw lastErr instanceof Error
197
+ ? lastErr
198
+ : new Error("Operation failed after retries");
199
+ }
200
+
201
+ /* ----------------- types ----------------- */
202
+
203
+ interface DeliveryMethodError extends Error {
204
+ isDeliveryMethodError: boolean;
205
+ }
206
+
207
+ function createDeliveryMethodError(message: string): DeliveryMethodError {
208
+ const error = new Error(message) as DeliveryMethodError;
209
+ error.isDeliveryMethodError = true;
210
+ return error;
211
+ }
212
+
213
+ type AddressInputTS = {
214
+ firstName: string;
215
+ lastName: string;
216
+ streetAddress1: string;
217
+ city: string;
218
+ postalCode: string;
219
+ country: string;
220
+ countryArea?: string;
221
+ phone?: string | null;
222
+ };
223
+ type AccountAddressLite = {
224
+ id: string;
225
+ firstName?: string | null;
226
+ lastName?: string | null;
227
+ streetAddress1?: string | null;
228
+ city?: string | null;
229
+ postalCode?: string | null;
230
+ country?: { code?: string | null; country?: string | null } | null;
231
+ countryArea?: string | null;
232
+ phone?: string | null;
233
+ companyName?: string | null;
234
+ };
235
+ type ShippingMethod = {
236
+ id: string;
237
+ name: string;
238
+ price: { amount: number; currency: string };
239
+ minimumDeliveryDays?: number | null;
240
+ maximumDeliveryDays?: number | null;
241
+ };
242
+ interface GraphQLShippingMethod {
243
+ id: string;
244
+ name: string;
245
+ price: { amount: number; currency: string };
246
+ minimumDeliveryDays?: number | null;
247
+ maximumDeliveryDays?: number | null;
248
+ }
249
+ interface GraphQLShippingMethodsResponse {
250
+ data?: { checkout?: { availableShippingMethods?: GraphQLShippingMethod[] } };
251
+ errors?: Array<{ message: string }>;
252
+ }
253
+
254
+ /* ----------------- Checkout Page ----------------- */
255
+ const DEBUG_HALT_AFTER_PAYMENT = false;
256
+
257
+ export default function CheckoutPage() {
258
+ const {
259
+ cartItems: items,
260
+ totalAmount,
261
+ isLoggedIn,
262
+ guestEmail,
263
+ guestShippingInfo,
264
+ checkoutId,
265
+ setCheckoutId,
266
+ checkoutToken,
267
+ setCheckoutToken,
268
+ selectedShippingMethodId: globalSelectedShippingId,
269
+ setSelectedShippingMethodId,
270
+ } = useGlobalStore();
271
+ const user = useGlobalStore((state) => state.user);
272
+ const { getGoogleTagManagerConfig, isWillCallEnabled } =
273
+ useAppConfiguration();
274
+ const gtmConfig = getGoogleTagManagerConfig();
275
+ const route = useRouter();
276
+ const apolloClient = useApolloClient();
277
+ const [isClient, setIsClient] = useState(false);
278
+ const [shippingMethods, setShippingMethods] = useState<ShippingMethod[]>([]);
279
+ const [shippingLoading, setShippingLoading] = useState(false);
280
+ const [shippingError, setShippingError] = useState<string | null>(null);
281
+ const [emailError, setEmailError] = useState<string | null>(null);
282
+ const [selectedShippingId, setSelectedShippingId] = useState<string | null>(
283
+ null
284
+ );
285
+ const [userHasSelectedDelivery, setUserHasSelectedDelivery] =
286
+ useState<boolean>(false);
287
+ const [isProcessingSelection, setIsProcessingSelection] = useState(false);
288
+ const [useShippingAsBilling, setUseShippingAsBilling] =
289
+ useState<boolean>(true);
290
+ const [isProcessingPayment, setIsProcessingPayment] =
291
+ useState<PaymentProcessingState>({
292
+ isModalOpen: false,
293
+ paymentProcessingLoading: false,
294
+ error: false,
295
+ success: false,
296
+ });
297
+ const [saleorTotal, setSaleorTotal] = useState<number | null>(null);
298
+ const [taxInfo, setTaxInfo] = useState<{
299
+ totalTax: number;
300
+ shippingTax: number;
301
+ subtotalNet: number;
302
+ shippingNet: number;
303
+ currency: string;
304
+ } | null>(null);
305
+
306
+ // NEW: fine-grained UX flags + race control
307
+ const [isUpdatingDelivery, setIsUpdatingDelivery] = useState(false);
308
+ const [isCalculatingTotal, setIsCalculatingTotal] = useState(false);
309
+ const [isRecoveringDelivery, setIsRecoveringDelivery] = useState(false);
310
+ const [isCalculatingTax, setIsCalculatingTax] = useState(false);
311
+ const [paymentTriggerFn, setPaymentTriggerFn] = useState<{
312
+ fn: (() => Promise<void>) | null;
313
+ }>({ fn: null });
314
+
315
+ // Dealer shipping state
316
+ const [isShipToDealer, setIsShipToDealer] = useState(false);
317
+ const [selectedDealer, setSelectedDealer] = useState<{
318
+ id: string;
319
+ name: string;
320
+ address: {
321
+ streetAddress1?: string;
322
+ city?: string;
323
+ postalCode?: string;
324
+ countryArea?: string;
325
+ country?: { country?: string; code?: string };
326
+ };
327
+ phone?: string;
328
+ distance?: string;
329
+ hours?: string;
330
+ comments?: string;
331
+ state?: string;
332
+ } | null>(null);
333
+
334
+ // Checkout questions state
335
+ const [, setCheckoutQuestionAnswers] = useState<Record<string, string>>({});
336
+ const [areCheckoutQuestionsValid, setAreCheckoutQuestionsValid] =
337
+ useState(true);
338
+ const [saveCheckoutQuestions, setSaveCheckoutQuestions] = useState<
339
+ (() => Promise<void>) | null
340
+ >(null);
341
+
342
+ // Terms and conditions state
343
+ const [termsAccepted, setTermsAccepted] = useState(false);
344
+ const [isTermsModalOpen, setIsTermsModalOpen] = useState(false);
345
+
346
+ // Product restriction state
347
+ const [productRestrictions, setProductRestrictions] = useState<
348
+ Array<{
349
+ productName: string;
350
+ checkoutMessage: string;
351
+ }>
352
+ >([]);
353
+ const [hasRestrictionViolations, setHasRestrictionViolations] =
354
+ useState(false);
355
+
356
+ // Voucher state
357
+ const [voucherInfo, setVoucherInfo] = useState<{
358
+ voucherCode: string | null;
359
+ discount: { amount: number; currency: string } | null;
360
+ } | null>(null);
361
+ const [isApplyingVoucher, setIsApplyingVoucher] = useState(false);
362
+ const [voucherError, setVoucherError] = useState<string | null>(null);
363
+
364
+ // Kount fraud detection state
365
+ const [kountConfig, setKountConfig] = useState<KountConfigResponse | null>(
366
+ null
367
+ );
368
+ const [_isKountConfigLoading, setIsKountConfigLoading] = useState(false);
369
+ const [_kountConfigError, setKountConfigError] = useState<string | null>(
370
+ null
371
+ );
372
+
373
+ // Will Call state
374
+ const [collectionPoints, setCollectionPoints] = useState<CollectionPoint[]>(
375
+ []
376
+ );
377
+ const [selectedCollectionPointId, setSelectedCollectionPointId] = useState<
378
+ string | null
379
+ >(null);
380
+ const [isWillCallSelected, setIsWillCallSelected] = useState(false);
381
+ const [willCallLoading, setWillCallLoading] = useState(false);
382
+ const [willCallError, setWillCallError] = useState<string | null>(null);
383
+
384
+ // Query for terms and conditions page
385
+ const { data: termsData } = useQuery<CheckoutTermsAndConditionsResponse>(
386
+ GET_CHECKOUT_TERMS_AND_CONDITIONS,
387
+ {
388
+ variables: { slug: "checkout-terms-and-conditions" },
389
+ fetchPolicy: "cache-first",
390
+ }
391
+ );
392
+
393
+ // Request deduplication refs
394
+ const fetchingMethodsRef = useRef(false);
395
+ const updatingDeliveryRef = useRef(false);
396
+ const totalsAbortRef = useRef<AbortController | null>(null);
397
+ const lastAddressHashRef = useRef<string>("");
398
+ const lastFetchedAtRef = useRef<number>(0); // NEW: throttle repeated fetches
399
+ const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null); // NEW: debounce validation
400
+ const addressHashAttemptedRef = useRef<Set<string>>(new Set()); // Track addresses we've attempted
401
+
402
+ const [shippingInfo, setShippingInfo] = useState({
403
+ firstName: "",
404
+ lastName: "",
405
+ address: "",
406
+ city: "",
407
+ state: "",
408
+ zipCode: "",
409
+ email: "",
410
+ phone: "",
411
+ country: "US",
412
+ });
413
+
414
+ // Email validation function
415
+ const validateEmail = useCallback((email: string) => {
416
+ if (!email || email.trim() === "") {
417
+ return "Email address is required.";
418
+ }
419
+
420
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
421
+ if (!emailRegex.test(email)) {
422
+ return "Please enter a valid email address.";
423
+ }
424
+
425
+ return null; // No error
426
+ }, []);
427
+
428
+ // Helper function to handle GraphQL errors and checkout session issues
429
+ const handleGraphQLError = useCallback(
430
+ (error: Error | unknown, operation: string = "operation") => {
431
+ const errorMessage =
432
+ error instanceof Error ? error.message : String(error);
433
+
434
+ // Check for "Couldn't resolve to a node" error which indicates expired/invalid checkout session
435
+ if (errorMessage.includes("Couldn't resolve to a node")) {
436
+ const userFriendlyMessage = `Your checkout session has expired. Please refresh the page to start a new checkout session.`;
437
+ console.error(`${operation} failed: Checkout session expired`, error);
438
+ return new Error(userFriendlyMessage);
439
+ }
440
+
441
+ // Check for other common session/auth errors
442
+ if (
443
+ errorMessage.includes("session") ||
444
+ errorMessage.includes("expired") ||
445
+ errorMessage.includes("401") ||
446
+ errorMessage.includes("403")
447
+ ) {
448
+ const userFriendlyMessage = `Your session has expired. Please refresh the page and try again.`;
449
+ console.error(`${operation} failed: Session expired`, error);
450
+ return new Error(userFriendlyMessage);
451
+ }
452
+
453
+ // Check for network errors
454
+ if (
455
+ errorMessage.includes("network") ||
456
+ errorMessage.includes("Failed to fetch") ||
457
+ errorMessage.includes("NetworkError")
458
+ ) {
459
+ const userFriendlyMessage = `Network connection issue. Please check your connection and try again.`;
460
+ console.error(`${operation} failed: Network error`, error);
461
+ return new Error(userFriendlyMessage);
462
+ }
463
+
464
+ // Default error handling
465
+ console.error(`${operation} failed:`, error);
466
+ return new Error(
467
+ errorMessage || `${operation} failed. Please try again.`
468
+ );
469
+ },
470
+ []
471
+ );
472
+
473
+ // NEW: Add postal code validation
474
+ const isValidPostalCode = useCallback(
475
+ (postalCode: string, country: string) => {
476
+ if (!postalCode || !country) return false;
477
+
478
+ const trimmedCode = postalCode.trim();
479
+ if (trimmedCode.length === 0) return false;
480
+
481
+ // Common postal code patterns
482
+ const patterns = {
483
+ US: /^\d{5}(-\d{4})?$/, // 12345 or 12345-6789
484
+ CA: /^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/, // A1A 1A1 or A1A1A1
485
+ UK: /^[A-Za-z]{1,2}\d[A-Za-z\d]?\s?\d[A-Za-z]{2}$/, // Various UK formats
486
+ AU: /^\d{4}$/, // 1234
487
+ DE: /^\d{5}$/, // 12345
488
+ FR: /^\d{5}$/, // 12345
489
+ JP: /^\d{3}-?\d{4}$/, // 123-4567 or 1234567
490
+ PK: /^\d{5}$/, // 12345
491
+ };
492
+
493
+ const pattern = patterns[country as keyof typeof patterns];
494
+ if (pattern) {
495
+ return pattern.test(trimmedCode);
496
+ }
497
+
498
+ // Generic fallback: allow alphanumeric postal codes 3-10 characters
499
+ return /^[A-Za-z0-9\s-]{3,10}$/.test(trimmedCode);
500
+ },
501
+ []
502
+ );
503
+
504
+ const missingForDelivery = useMemo(() => {
505
+ const miss: string[] = [];
506
+ if (!shippingInfo.firstName) miss.push("First name");
507
+ if (!shippingInfo.lastName) miss.push("Last name");
508
+ if (!shippingInfo.address) miss.push("Street address");
509
+ if (!shippingInfo.city) miss.push("City");
510
+ if (!shippingInfo.zipCode) miss.push("Postal code");
511
+ else if (!isValidPostalCode(shippingInfo.zipCode, shippingInfo.country)) {
512
+ miss.push("Valid postal code");
513
+ }
514
+ if (!shippingInfo.country) miss.push("Country");
515
+ return miss;
516
+ }, [shippingInfo, isValidPostalCode]);
517
+
518
+ const [billingInfo, setBillingInfo] = useState({
519
+ firstName: "",
520
+ lastName: "",
521
+ address: "",
522
+ city: "",
523
+ state: "",
524
+ zipCode: "",
525
+ country: "US",
526
+ phone: "",
527
+ email: "",
528
+ });
529
+
530
+ const [formData, setFormData] = useState<AddressForm>({
531
+ firstName: "",
532
+ lastName: "",
533
+ phone: "",
534
+ companyName: "",
535
+ streetAddress1: "",
536
+ streetAddress2: "",
537
+ city: "",
538
+ postalCode: "",
539
+ country: "US",
540
+ countryArea: "",
541
+ });
542
+
543
+ useEffect(() => setIsClient(true), []);
544
+
545
+ // Fetch Kount configuration on component mount
546
+ useEffect(() => {
547
+ if (!isClient) return;
548
+
549
+ const fetchKountConfig = async () => {
550
+ setIsKountConfigLoading(true);
551
+ setKountConfigError(null);
552
+
553
+ try {
554
+ const config = await kountApi.getKountConfig();
555
+ setKountConfig(config);
556
+ } catch (error) {
557
+ // Silently handle Kount config errors as it's optional in some environments
558
+ const errorMessage =
559
+ error instanceof Error
560
+ ? error.message
561
+ : "Failed to fetch fraud detection configuration";
562
+ setKountConfigError(errorMessage);
563
+ // Only log if it's not a 404 (which is expected when Kount is not configured)
564
+ if (!errorMessage.includes("not configured")) {
565
+ console.error("Failed to fetch Kount configuration:", error);
566
+ }
567
+ } finally {
568
+ setIsKountConfigLoading(false);
569
+ }
570
+ };
571
+
572
+ fetchKountConfig();
573
+ }, [isClient]);
574
+
575
+ const resetCheckoutState = useCallback(() => {
576
+ setShippingMethods([]);
577
+ setShippingLoading(false);
578
+ setShippingError(null);
579
+ setSelectedShippingId(null);
580
+ setUserHasSelectedDelivery(false);
581
+ setIsUpdatingDelivery(false);
582
+ setIsCalculatingTotal(false);
583
+ setIsRecoveringDelivery(false);
584
+ setIsCalculatingTax(false);
585
+ setSaleorTotal(null);
586
+ setTaxInfo(null); // Reset tax information
587
+
588
+ fetchingMethodsRef.current = false;
589
+ updatingDeliveryRef.current = false;
590
+ lastAddressHashRef.current = "";
591
+ lastDeliveryRef.current = null;
592
+ lastShippingRef.current = null;
593
+ lastBillingRef.current = null;
594
+ lastFetchedAtRef.current = 0; // Reset fetch timing
595
+ addressHashAttemptedRef.current.clear(); // Clear attempted addresses
596
+
597
+ setSelectedShippingMethodId(null);
598
+
599
+ if (totalsAbortRef.current) {
600
+ totalsAbortRef.current.abort();
601
+ totalsAbortRef.current = null;
602
+ }
603
+
604
+ // Clear any pending validation timeouts
605
+ if (validationTimeoutRef.current) {
606
+ clearTimeout(validationTimeoutRef.current);
607
+ validationTimeoutRef.current = null;
608
+ }
609
+ }, [setSelectedShippingMethodId]);
610
+
611
+ useEffect(() => {
612
+ if (!isClient) return;
613
+ try {
614
+ const url = new URL(window.location.href);
615
+ const idFromUrl = url.searchParams.get("checkoutId");
616
+ const idFromStorage = localStorage.getItem("checkoutId");
617
+ const tokenFromStorage = localStorage.getItem("checkoutToken");
618
+ const effective = idFromUrl || idFromStorage || null;
619
+
620
+ // If checkout ID is changing, clear shipping method selection
621
+ if (effective && effective !== checkoutId) {
622
+ setCheckoutId(effective);
623
+ setSelectedShippingMethodId(null);
624
+
625
+ // begin_checkout event is now tracked on cart page when user clicks "Proceed to Checkout"
626
+ }
627
+
628
+ if (tokenFromStorage && tokenFromStorage !== checkoutToken)
629
+ setCheckoutToken?.(tokenFromStorage);
630
+ if (idFromUrl && idFromUrl !== idFromStorage)
631
+ localStorage.setItem("checkoutId", idFromUrl);
632
+ } catch {}
633
+ }, [
634
+ isClient,
635
+ checkoutId,
636
+ checkoutToken,
637
+ setCheckoutId,
638
+ setCheckoutToken,
639
+ setSelectedShippingMethodId,
640
+ ]);
641
+
642
+ const endpoint = getSaleorApiUrl();
643
+
644
+ const updateShippingAddress = useCallback(
645
+ async (id: string, addr: AddressInputTS) => {
646
+ const variables = { checkoutId: id, shippingAddress: addr };
647
+
648
+ // Enhanced query to include tax information
649
+ const enhancedQuery = `
650
+ mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
651
+ checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
652
+ checkout {
653
+ id
654
+ shippingAddress {
655
+ streetAddress1
656
+ city
657
+ postalCode
658
+ country {
659
+ code
660
+ }
661
+ countryArea
662
+ }
663
+ totalPrice {
664
+ gross {
665
+ amount
666
+ currency
667
+ }
668
+ net {
669
+ amount
670
+ currency
671
+ }
672
+ tax {
673
+ amount
674
+ currency
675
+ }
676
+ }
677
+ shippingPrice {
678
+ gross {
679
+ amount
680
+ currency
681
+ }
682
+ net {
683
+ amount
684
+ currency
685
+ }
686
+ tax {
687
+ amount
688
+ currency
689
+ }
690
+ }
691
+ subtotalPrice {
692
+ gross {
693
+ amount
694
+ currency
695
+ }
696
+ net {
697
+ amount
698
+ currency
699
+ }
700
+ tax {
701
+ amount
702
+ currency
703
+ }
704
+ }
705
+ lines {
706
+ quantity
707
+ totalPrice {
708
+ gross {
709
+ amount
710
+ currency
711
+ }
712
+ net {
713
+ amount
714
+ currency
715
+ }
716
+ tax {
717
+ amount
718
+ currency
719
+ }
720
+ }
721
+ variant {
722
+ id
723
+ name
724
+ }
725
+ }
726
+ }
727
+ errors {
728
+ field
729
+ message
730
+ }
731
+ }
732
+ }
733
+ `;
734
+
735
+ const res = await fetch(endpoint, {
736
+ method: "POST",
737
+ headers: { "Content-Type": "application/json" },
738
+ body: JSON.stringify({ query: enhancedQuery, variables }),
739
+ });
740
+ if (!res.ok) throw new Error("Failed to update shipping address");
741
+ const json = await res.json();
742
+ const errs = json.data?.checkoutShippingAddressUpdate?.errors as
743
+ | Array<{ message?: string }>
744
+ | undefined;
745
+ if (errs?.length)
746
+ throw new Error(errs[0]?.message || "Shipping address update error");
747
+
748
+ // Extract and store tax information
749
+ const checkout = json.data?.checkoutShippingAddressUpdate?.checkout;
750
+ if (checkout) {
751
+ try {
752
+ const subtotalTax = checkout.subtotalPrice?.tax?.amount || 0;
753
+ const shippingTax = checkout.shippingPrice?.tax?.amount || 0;
754
+ const subtotalNet = checkout.subtotalPrice?.net?.amount || 0;
755
+ const shippingNet = checkout.shippingPrice?.net?.amount || 0;
756
+ const currency = checkout.totalPrice?.gross?.currency || "USD";
757
+
758
+ setTaxInfo({
759
+ totalTax: subtotalTax,
760
+ shippingTax,
761
+ subtotalNet,
762
+ shippingNet,
763
+ currency,
764
+ });
765
+
766
+ // Update the Saleor total with the gross amount including tax
767
+ const grossTotal = checkout.totalPrice?.gross?.amount;
768
+ if (typeof grossTotal === "number") {
769
+ setSaleorTotal(grossTotal);
770
+ }
771
+ } catch (taxError) {
772
+ console.warn("Failed to parse tax information:", taxError);
773
+ // Don't throw error, just log it so address update still succeeds
774
+ }
775
+ }
776
+ },
777
+ [endpoint]
778
+ );
779
+
780
+ const updateBillingAddress = useCallback(
781
+ async (id: string, addr: AddressInputTS) => {
782
+ try {
783
+ const variables = { id, billingAddress: addr };
784
+ const res = await fetch(endpoint, {
785
+ method: "POST",
786
+ headers: { "Content-Type": "application/json" },
787
+ body: JSON.stringify({
788
+ query: CHECKOUT_BILLING_ADDRESS_UPDATE,
789
+ variables,
790
+ }),
791
+ });
792
+ if (!res.ok) throw new Error("Failed to update billing address");
793
+
794
+ const json = await res.json();
795
+
796
+ // Check for GraphQL errors
797
+ if (json.errors && json.errors.length > 0) {
798
+ const graphqlError = json.errors[0];
799
+ throw handleGraphQLError(graphqlError, "Billing address update");
800
+ }
801
+
802
+ const errs = json.data?.checkoutBillingAddressUpdate?.errors as
803
+ | Array<{ message?: string }>
804
+ | undefined;
805
+ if (errs?.length) {
806
+ const errorMessage =
807
+ errs[0]?.message || "Billing address update error";
808
+ throw handleGraphQLError(
809
+ new Error(errorMessage),
810
+ "Billing address update"
811
+ );
812
+ }
813
+ } catch (error) {
814
+ // If it's already a handled error, rethrow it
815
+ if (
816
+ error instanceof Error &&
817
+ (error.message.includes("checkout session") ||
818
+ error.message.includes("session has expired"))
819
+ ) {
820
+ throw error;
821
+ }
822
+ // Otherwise, handle it
823
+ throw handleGraphQLError(error, "Billing address update");
824
+ }
825
+ },
826
+ [endpoint, handleGraphQLError]
827
+ );
828
+
829
+ const updateCheckoutEmail = useCallback(
830
+ async (id: string, email: string, onError?: (message: string) => void) => {
831
+ try {
832
+ const variables = { checkoutId: id, email };
833
+ const res = await fetch(endpoint, {
834
+ method: "POST",
835
+ headers: { "Content-Type": "application/json" },
836
+ body: JSON.stringify({ query: CHECKOUT_EMAIL_UPDATE, variables }),
837
+ });
838
+ if (!res.ok) throw new Error("Failed to update checkout email");
839
+
840
+ const json = await res.json();
841
+
842
+ // Check for GraphQL errors
843
+ if (json.errors && json.errors.length > 0) {
844
+ const graphqlError = json.errors[0];
845
+ const errorMessage = graphqlError.message || "GraphQL error";
846
+ if (onError) onError(errorMessage);
847
+ return;
848
+ }
849
+
850
+ const errs = json.data?.checkoutEmailUpdate?.errors as
851
+ | Array<{ message?: string }>
852
+ | undefined;
853
+ if (errs?.length) {
854
+ const errorMessage = errs[0]?.message || "Email update error";
855
+ if (onError) onError(errorMessage);
856
+ return;
857
+ }
858
+ } catch (error) {
859
+ const errorMessage =
860
+ error instanceof Error ? error.message : "Email update failed";
861
+ if (onError) onError(errorMessage);
862
+ }
863
+ },
864
+ [endpoint]
865
+ );
866
+
867
+ const getCheckoutDetails = useCallback(
868
+ async (id: string, signal?: AbortSignal) => {
869
+ try {
870
+ const res = await fetch(endpoint, {
871
+ method: "POST",
872
+ headers: { "Content-Type": "application/json" },
873
+ body: JSON.stringify({
874
+ query: GET_CHECKOUT_DETAILS,
875
+ variables: { id },
876
+ }),
877
+ signal,
878
+ });
879
+ if (!res.ok) throw new Error("Failed to fetch checkout details");
880
+
881
+ const json = await res.json();
882
+
883
+ // Check for GraphQL errors
884
+ if (json.errors && json.errors.length > 0) {
885
+ const graphqlError = json.errors[0];
886
+ throw handleGraphQLError(graphqlError, "Get checkout details");
887
+ }
888
+
889
+ const checkout = json.data?.checkout;
890
+ if (!checkout) {
891
+ throw handleGraphQLError(
892
+ new Error("Unable to determine checkout details"),
893
+ "Get checkout details"
894
+ );
895
+ }
896
+
897
+ const rawTotal = checkout.totalPrice?.gross?.amount;
898
+ const rawSubtotal = checkout.subtotalPrice?.gross?.amount;
899
+ const lines = checkout.lines || [];
900
+ const deliveryMethod = checkout.deliveryMethod;
901
+
902
+ // Extract voucher information
903
+ const voucherCode = checkout.voucherCode || null;
904
+ const discount = checkout.discount
905
+ ? {
906
+ amount: checkout.discount.amount,
907
+ currency: checkout.discount.currency,
908
+ }
909
+ : null;
910
+
911
+ return {
912
+ total: rawTotal,
913
+ subtotal: rawSubtotal,
914
+ lines,
915
+ deliveryMethod,
916
+ voucherInfo: { voucherCode, discount },
917
+ fullCheckoutData: checkout, // Include full checkout data for validation
918
+ };
919
+ } catch (error) {
920
+ // If it's already a handled error, rethrow it
921
+ if (
922
+ error instanceof Error &&
923
+ (error.message.includes("checkout session") ||
924
+ error.message.includes("session has expired"))
925
+ ) {
926
+ throw error;
927
+ }
928
+ // Otherwise, handle it
929
+ throw handleGraphQLError(error, "Get checkout details");
930
+ }
931
+ },
932
+ [endpoint, handleGraphQLError]
933
+ );
934
+
935
+ const updateDeliveryMethod = useCallback(
936
+ async (id: string, methodId: string) => {
937
+ if (updatingDeliveryRef.current) return;
938
+ updatingDeliveryRef.current = true;
939
+ try {
940
+ const variables = { id, deliveryMethodId: methodId };
941
+ const res = await fetch(endpoint, {
942
+ method: "POST",
943
+ headers: { "Content-Type": "application/json" },
944
+ body: JSON.stringify({
945
+ query: CHECKOUT_DELIVERY_METHOD_UPDATE,
946
+ variables,
947
+ }),
948
+ });
949
+ if (!res.ok) {
950
+ const errorText = await res.text();
951
+ throw new Error(
952
+ `Failed to set delivery method: ${res.status} ${res.statusText} ${errorText}`
953
+ );
954
+ }
955
+
956
+ const json = await res.json();
957
+
958
+ // Check for GraphQL errors
959
+ if (json.errors && json.errors.length > 0) {
960
+ const graphqlError = json.errors[0];
961
+ throw handleGraphQLError(graphqlError, "Delivery method update");
962
+ }
963
+
964
+ const errs = json.data?.checkoutDeliveryMethodUpdate?.errors as
965
+ | Array<{ message?: string; field?: string; code?: string }>
966
+ | undefined;
967
+ if (errs?.length) {
968
+ const errorDetails = errs
969
+ .map((e) => `${e.field || "unknown"}: ${e.message || "unknown"}`)
970
+ .join(", ");
971
+ const errorMessage =
972
+ errs[0]?.message || `Delivery method update error: ${errorDetails}`;
973
+ throw handleGraphQLError(
974
+ new Error(errorMessage),
975
+ "Delivery method update"
976
+ );
977
+ }
978
+
979
+ // Extract and store tax information from delivery method update
980
+ const checkout = json.data?.checkoutDeliveryMethodUpdate?.checkout;
981
+ if (checkout) {
982
+ try {
983
+ const subtotalTax = checkout.subtotalPrice?.tax?.amount || 0;
984
+ const shippingTax = checkout.shippingPrice?.tax?.amount || 0;
985
+ const subtotalNet = checkout.subtotalPrice?.net?.amount || 0;
986
+ const shippingNet = checkout.shippingPrice?.net?.amount || 0;
987
+ const currency = checkout.totalPrice?.gross?.currency || "USD";
988
+
989
+ setTaxInfo({
990
+ totalTax: subtotalTax,
991
+ shippingTax,
992
+ subtotalNet,
993
+ shippingNet,
994
+ currency,
995
+ });
996
+
997
+ // Update the Saleor total with the gross amount including tax
998
+ const grossTotal = checkout.totalPrice?.gross?.amount;
999
+ if (typeof grossTotal === "number") {
1000
+ setSaleorTotal(grossTotal);
1001
+ }
1002
+ } catch (taxError) {
1003
+ console.warn(
1004
+ "Failed to parse tax information from delivery method update:",
1005
+ taxError
1006
+ );
1007
+ // Don't throw error, just log it so delivery method update still succeeds
1008
+ }
1009
+ }
1010
+ } catch (error) {
1011
+ // If it's already a handled error, rethrow it
1012
+ if (
1013
+ error instanceof Error &&
1014
+ (error.message.includes("checkout session") ||
1015
+ error.message.includes("session has expired"))
1016
+ ) {
1017
+ throw error;
1018
+ }
1019
+ // Otherwise, handle it
1020
+ throw handleGraphQLError(error, "Delivery method update");
1021
+ } finally {
1022
+ updatingDeliveryRef.current = false;
1023
+ }
1024
+ },
1025
+ [endpoint, handleGraphQLError]
1026
+ );
1027
+
1028
+ const applyVoucher = useCallback(
1029
+ async (voucherCode: string) => {
1030
+ if (!checkoutId) {
1031
+ setVoucherError("No checkout session found");
1032
+ return;
1033
+ }
1034
+
1035
+ setIsApplyingVoucher(true);
1036
+ setVoucherError(null);
1037
+
1038
+ try {
1039
+ const variables = { checkoutId, promoCode: voucherCode };
1040
+ const res = await fetch(endpoint, {
1041
+ method: "POST",
1042
+ headers: { "Content-Type": "application/json" },
1043
+ body: JSON.stringify({ query: ADD_VOUCHER_TO_CHECKOUT, variables }),
1044
+ });
1045
+
1046
+ if (!res.ok) {
1047
+ throw new Error(
1048
+ `Failed to apply voucher: ${res.status} ${res.statusText}`
1049
+ );
1050
+ }
1051
+
1052
+ const json = await res.json();
1053
+
1054
+ if (json.errors && json.errors.length > 0) {
1055
+ throw new Error(json.errors[0].message || "Failed to apply voucher");
1056
+ }
1057
+
1058
+ const result = json.data?.checkoutAddPromoCode;
1059
+ if (result?.errors && result.errors.length > 0) {
1060
+ const error = result.errors[0];
1061
+ switch (error.code) {
1062
+ case "INVALID":
1063
+ setVoucherError("Promo code is invalid");
1064
+ break;
1065
+ case "VOUCHER_NOT_APPLICABLE":
1066
+ setVoucherError("Voucher is not applicable to this checkout");
1067
+ break;
1068
+ default:
1069
+ setVoucherError(error.message || "Failed to apply voucher");
1070
+ }
1071
+ return;
1072
+ }
1073
+
1074
+ if (result?.checkout) {
1075
+ const checkout = result.checkout;
1076
+ setVoucherInfo({
1077
+ voucherCode: checkout.voucherCode || null,
1078
+ discount: checkout.discount
1079
+ ? {
1080
+ amount: checkout.discount.amount,
1081
+ currency: checkout.discount.currency,
1082
+ }
1083
+ : null,
1084
+ });
1085
+
1086
+ // Update total with new discounted price
1087
+ const grossTotal = checkout.totalPrice?.gross?.amount;
1088
+ if (typeof grossTotal === "number") {
1089
+ setSaleorTotal(grossTotal);
1090
+ }
1091
+ }
1092
+ } catch (error) {
1093
+ console.error("Error applying voucher:", error);
1094
+ setVoucherError(
1095
+ error instanceof Error ? error.message : "Failed to apply voucher"
1096
+ );
1097
+ } finally {
1098
+ setIsApplyingVoucher(false);
1099
+ }
1100
+ },
1101
+ [checkoutId, endpoint]
1102
+ );
1103
+
1104
+ const removeVoucher = useCallback(async () => {
1105
+ if (!checkoutId || !voucherInfo?.voucherCode) {
1106
+ return;
1107
+ }
1108
+
1109
+ setIsApplyingVoucher(true);
1110
+ setVoucherError(null);
1111
+
1112
+ try {
1113
+ const variables = { checkoutId, promoCode: voucherInfo.voucherCode };
1114
+ const res = await fetch(endpoint, {
1115
+ method: "POST",
1116
+ headers: { "Content-Type": "application/json" },
1117
+ body: JSON.stringify({
1118
+ query: REMOVE_VOUCHER_FROM_CHECKOUT,
1119
+ variables,
1120
+ }),
1121
+ });
1122
+
1123
+ if (!res.ok) {
1124
+ throw new Error(
1125
+ `Failed to remove voucher: ${res.status} ${res.statusText}`
1126
+ );
1127
+ }
1128
+
1129
+ const json = await res.json();
1130
+
1131
+ if (json.errors && json.errors.length > 0) {
1132
+ throw new Error(json.errors[0].message || "Failed to remove voucher");
1133
+ }
1134
+
1135
+ const result = json.data?.checkoutRemovePromoCode;
1136
+ if (result?.errors && result.errors.length > 0) {
1137
+ setVoucherError(result.errors[0].message || "Failed to remove voucher");
1138
+ return;
1139
+ }
1140
+
1141
+ if (result?.checkout) {
1142
+ const checkout = result.checkout;
1143
+ setVoucherInfo({
1144
+ voucherCode: checkout.voucherCode || null,
1145
+ discount: checkout.discount
1146
+ ? {
1147
+ amount: checkout.discount.amount,
1148
+ currency: checkout.discount.currency,
1149
+ }
1150
+ : null,
1151
+ });
1152
+
1153
+ // Update total with new price without discount
1154
+ const grossTotal = checkout.totalPrice?.gross?.amount;
1155
+ if (typeof grossTotal === "number") {
1156
+ setSaleorTotal(grossTotal);
1157
+ }
1158
+ }
1159
+ } catch (error) {
1160
+ console.error("Error removing voucher:", error);
1161
+ setVoucherError(
1162
+ error instanceof Error ? error.message : "Failed to remove voucher"
1163
+ );
1164
+ } finally {
1165
+ setIsApplyingVoucher(false);
1166
+ }
1167
+ }, [checkoutId, endpoint, voucherInfo?.voucherCode]);
1168
+
1169
+ const buildAddressFromForm = useCallback(
1170
+ (info: typeof shippingInfo): AddressInputTS => ({
1171
+ firstName: info.firstName?.trim() || "",
1172
+ lastName: info.lastName?.trim() || "",
1173
+ streetAddress1: info.address?.trim() || "",
1174
+ city: info.city?.trim() || "",
1175
+ postalCode: info.zipCode?.trim() || "",
1176
+ country: info.country || "US",
1177
+ countryArea: info.state?.trim() || undefined,
1178
+ phone: info.phone?.trim() || null,
1179
+ }),
1180
+ []
1181
+ );
1182
+
1183
+ const buildAddressFromAccount = useCallback(
1184
+ (addr: AccountAddressLite): AddressInputTS => ({
1185
+ firstName: addr?.firstName || "",
1186
+ lastName: addr?.lastName || "",
1187
+ streetAddress1: addr?.streetAddress1 || "",
1188
+ city: addr?.city || "",
1189
+ postalCode: addr?.postalCode || "",
1190
+ country: addr?.country?.code || "",
1191
+ countryArea: addr?.countryArea || undefined,
1192
+ phone: addr?.phone ?? null,
1193
+ }),
1194
+ []
1195
+ );
1196
+
1197
+ // Init guest form
1198
+ useEffect(() => {
1199
+ if (!isLoggedIn) {
1200
+ setShippingInfo((prev) => ({
1201
+ ...prev,
1202
+ firstName: guestShippingInfo.firstName || "",
1203
+ lastName: guestShippingInfo.lastName || "",
1204
+ address: guestShippingInfo.address || "",
1205
+ city: guestShippingInfo.city || "",
1206
+ state: guestShippingInfo.state || "",
1207
+ zipCode: guestShippingInfo.zipCode || "",
1208
+ email: guestEmail || "",
1209
+ phone: guestShippingInfo.phone || "",
1210
+ country: guestShippingInfo.country || prev.country || "US",
1211
+ }));
1212
+ }
1213
+ }, [isLoggedIn, guestEmail, guestShippingInfo]);
1214
+
1215
+ // Account addresses
1216
+ const { data: meData, refetch: refetchMe } = useQuery<MeAddressesData>(
1217
+ ME_ADDRESSES_QUERY,
1218
+ {
1219
+ skip: !isLoggedIn,
1220
+ fetchPolicy: "cache-and-network",
1221
+ }
1222
+ );
1223
+
1224
+ // Payment gateways
1225
+ const { data: paymentGatewaysData } = useQuery(gql(GET_PAYMENT_GATEWAYS), {
1226
+ variables: { checkoutId },
1227
+ skip: !checkoutId,
1228
+ fetchPolicy: "cache-and-network",
1229
+ });
1230
+
1231
+ const [setDefaultAddress] = useMutation<
1232
+ AccountSetDefaultAddressData,
1233
+ AccountSetDefaultAddressVars
1234
+ >(ACCOUNT_SET_DEFAULT_ADDRESS, {
1235
+ refetchQueries: [{ query: ME_ADDRESSES_QUERY }],
1236
+ });
1237
+
1238
+ const [updateWillCallDeliveryMethod] = useMutation<
1239
+ WillCallDeliveryMethodUpdateData,
1240
+ WillCallDeliveryMethodUpdateVars
1241
+ >(CHECKOUT_DELIVERY_METHOD_UPDATE_WILL_CALL);
1242
+
1243
+ const accountShipping = useMemo(() => {
1244
+ const me = meData?.me;
1245
+ if (!me || !me.addresses?.length) return null;
1246
+ const defId = me.defaultShippingAddress?.id;
1247
+ return (
1248
+ (defId
1249
+ ? me.addresses.find((a: AccountAddressLite) => a.id === defId)
1250
+ : me.addresses[0]) || null
1251
+ );
1252
+ }, [meData]);
1253
+
1254
+ const accountBilling = useMemo(() => {
1255
+ const me = meData?.me;
1256
+ if (!me || !me.addresses?.length) return null;
1257
+ const defId = me.defaultBillingAddress?.id;
1258
+ return (
1259
+ (defId
1260
+ ? me.addresses.find((a: AccountAddressLite) => a.id === defId)
1261
+ : me.addresses[0]) || null
1262
+ );
1263
+ }, [meData]);
1264
+
1265
+ // Initialize selected ids - wait for data to load before setting defaults
1266
+ const [selectedAddressId, setSelectedAddressId] = useState<string | null>(
1267
+ null
1268
+ );
1269
+ const [selectedBillingAddressId, setSelectedBillingAddressId] = useState<
1270
+ string | null
1271
+ >(null);
1272
+ const [addressAutoSelectionComplete, setAddressAutoSelectionComplete] =
1273
+ useState(false);
1274
+
1275
+ // Auto-select addresses but track completion for delivery method loading timing
1276
+ useEffect(() => {
1277
+ if (isLoggedIn && meData?.me) {
1278
+ let hasChanges = false;
1279
+
1280
+ // Auto-select shipping address - default or first available
1281
+ if (!selectedAddressId) {
1282
+ const defaultShippingId = meData.me.defaultShippingAddress?.id;
1283
+ const firstAddressId = meData.me.addresses?.[0]?.id;
1284
+ const addressToUse = defaultShippingId || firstAddressId;
1285
+ if (addressToUse) {
1286
+ setSelectedAddressId(addressToUse);
1287
+ hasChanges = true;
1288
+ }
1289
+ }
1290
+
1291
+ // Set billing address - default or first available
1292
+ if (!selectedBillingAddressId) {
1293
+ const defaultBillingId = meData.me.defaultBillingAddress?.id;
1294
+ const firstAddressId = meData.me.addresses?.[0]?.id;
1295
+ const addressToUse = defaultBillingId || firstAddressId;
1296
+ if (addressToUse) {
1297
+ setSelectedBillingAddressId(addressToUse);
1298
+ }
1299
+ }
1300
+
1301
+ // Mark auto-selection as complete after all address selections are done
1302
+ if (hasChanges || selectedAddressId || !meData.me.addresses?.length) {
1303
+ // Use a small delay to ensure state updates are processed
1304
+ setTimeout(() => setAddressAutoSelectionComplete(true), 50);
1305
+ }
1306
+ } else if (!isLoggedIn) {
1307
+ // For guests, mark auto-selection as complete immediately
1308
+ setAddressAutoSelectionComplete(true);
1309
+ }
1310
+ }, [isLoggedIn, meData?.me, selectedAddressId, selectedBillingAddressId]);
1311
+
1312
+ // This effect is removed as it conflicts with the selectedAddressId effect below
1313
+ // The selectedAddressId effect handles both auto-selected and manually selected addresses
1314
+
1315
+ const handleSetDefaultAddress = async (
1316
+ addressId: string,
1317
+ type: "SHIPPING" | "BILLING"
1318
+ ) => {
1319
+ if (!isLoggedIn || !addressId) return;
1320
+ try {
1321
+ const { data } = await setDefaultAddress({
1322
+ variables: { id: addressId, type },
1323
+ });
1324
+ if (data?.accountSetDefaultAddress?.errors?.length) {
1325
+ throw new Error(
1326
+ data.accountSetDefaultAddress.errors[0]?.message ||
1327
+ "Failed to update default address"
1328
+ );
1329
+ }
1330
+ if (type === "SHIPPING") setSelectedAddressId(addressId);
1331
+ else setSelectedBillingAddressId(addressId);
1332
+ await refetchMe();
1333
+ } catch (error) {
1334
+ console.error(
1335
+ `Failed to set default ${type.toLowerCase()} address:`,
1336
+ error
1337
+ );
1338
+ }
1339
+ };
1340
+
1341
+ // Hydrate shipping form when selectedAddressId changes (logged in)
1342
+ useEffect(() => {
1343
+ if (!isLoggedIn || !meData?.me?.addresses) return;
1344
+ const selectedAddress = meData.me.addresses.find(
1345
+ (addr: AccountAddressLite) => addr.id === selectedAddressId
1346
+ );
1347
+ if (selectedAddress) {
1348
+ setShippingInfo((prev) => ({
1349
+ ...prev,
1350
+ firstName: selectedAddress.firstName || "",
1351
+ lastName: selectedAddress.lastName || "",
1352
+ address: selectedAddress.streetAddress1 || "",
1353
+ city: selectedAddress.city || "",
1354
+ state: selectedAddress.countryArea || "",
1355
+ zipCode: selectedAddress.postalCode || "",
1356
+ email: meData.me?.email || prev.email,
1357
+ phone: selectedAddress.phone || "",
1358
+ country: selectedAddress.country?.code || prev.country || "US",
1359
+ }));
1360
+
1361
+ // Clear address tracking to allow fresh API call with new address
1362
+ lastAddressHashRef.current = "";
1363
+
1364
+ // Clear tax info when address changes to trigger recalculation
1365
+ setTaxInfo(null);
1366
+ setIsCalculatingTax(false);
1367
+
1368
+ // Clear any delivery methods error when address is hydrated
1369
+ if (
1370
+ shippingError &&
1371
+ shippingError.includes("No delivery methods found")
1372
+ ) {
1373
+ setShippingError(null);
1374
+ lastFetchedAtRef.current = 0;
1375
+ fetchingMethodsRef.current = false;
1376
+ setShippingMethods([]);
1377
+ }
1378
+ }
1379
+ }, [isLoggedIn, selectedAddressId, meData, shippingError]);
1380
+
1381
+ // Removed automatic population of shipping info from accountShipping
1382
+ // Users must explicitly select an address to load delivery methods
1383
+
1384
+ const hasCompleteShippingInfo = useMemo(() => {
1385
+ const s = shippingInfo;
1386
+ return !!(
1387
+ s.firstName &&
1388
+ s.lastName &&
1389
+ s.address &&
1390
+ s.city &&
1391
+ s.zipCode &&
1392
+ s.country
1393
+ );
1394
+ }, [shippingInfo]);
1395
+ // ---- Payload builders (idempotent) ----
1396
+ const shippingPayload: AddressInputTS | null = useMemo(() => {
1397
+ // If shipping to dealer, use dealer address
1398
+ if (isShipToDealer && selectedDealer) {
1399
+ return {
1400
+ firstName: "Dealer",
1401
+ lastName: "Pickup",
1402
+ streetAddress1: selectedDealer.address.streetAddress1 || "",
1403
+ city: selectedDealer.address.city || "",
1404
+ postalCode: selectedDealer.address.postalCode || "",
1405
+ country: selectedDealer.address.country?.code || "US",
1406
+ countryArea: selectedDealer.address?.countryArea || "",
1407
+ phone: selectedDealer.phone || null,
1408
+ };
1409
+ }
1410
+
1411
+ // Standard shipping logic
1412
+ if (isLoggedIn && selectedAddressId) {
1413
+ const addr = meData?.me?.addresses?.find(
1414
+ (a: AccountAddressLite) => a.id === selectedAddressId
1415
+ ) as AccountAddressLite | undefined;
1416
+ return addr ? buildAddressFromAccount(addr) : null;
1417
+ }
1418
+ if (isLoggedIn && accountShipping && addressAutoSelectionComplete)
1419
+ return buildAddressFromAccount(
1420
+ accountShipping as unknown as AccountAddressLite
1421
+ );
1422
+ if (hasCompleteShippingInfo) return buildAddressFromForm(shippingInfo);
1423
+ return null;
1424
+ }, [
1425
+ isShipToDealer,
1426
+ selectedDealer,
1427
+ isLoggedIn,
1428
+ selectedAddressId,
1429
+ meData,
1430
+ accountShipping,
1431
+ addressAutoSelectionComplete,
1432
+ hasCompleteShippingInfo,
1433
+ shippingInfo,
1434
+ buildAddressFromAccount,
1435
+ buildAddressFromForm,
1436
+ ]);
1437
+
1438
+ const hasCompleteBillingInfo = useMemo(() => {
1439
+ const b = billingInfo;
1440
+ return !!(
1441
+ b.firstName &&
1442
+ b.lastName &&
1443
+ b.address &&
1444
+ b.city &&
1445
+ b.zipCode &&
1446
+ b.country
1447
+ );
1448
+ }, [billingInfo]);
1449
+
1450
+ const canShowDeliveryMethods = useMemo(() => {
1451
+ // If shipping to dealer, we can show delivery methods once dealer is selected
1452
+ if (isShipToDealer) {
1453
+ return !!selectedDealer;
1454
+ }
1455
+
1456
+ // Don't show delivery methods until auto-selection is complete
1457
+ if (!addressAutoSelectionComplete) return false;
1458
+
1459
+ if (isLoggedIn) {
1460
+ // For logged in users, require address selection (auto or manual)
1461
+ const hasSelectedAddress = !!selectedAddressId;
1462
+ const addressExists = meData?.me?.addresses?.some(
1463
+ (addr: AccountAddressLite) => addr.id === selectedAddressId
1464
+ );
1465
+ return hasSelectedAddress && addressExists;
1466
+ }
1467
+ // For guest users, require complete shipping info
1468
+ return hasCompleteShippingInfo;
1469
+ }, [
1470
+ isShipToDealer,
1471
+ selectedDealer,
1472
+ addressAutoSelectionComplete,
1473
+ isLoggedIn,
1474
+ selectedAddressId,
1475
+ meData,
1476
+ hasCompleteShippingInfo,
1477
+ ]);
1478
+
1479
+ const billingPayload: AddressInputTS | null = useMemo(() => {
1480
+ // Only sync billing if we have valid data
1481
+ if (useShippingAsBilling) {
1482
+ // For dealer shipping, use customer address if available, otherwise use dealer address
1483
+ if (isShipToDealer) {
1484
+ if (isLoggedIn && selectedAddressId) {
1485
+ const addr = meData?.me?.addresses?.find(
1486
+ (a: AccountAddressLite) => a.id === selectedAddressId
1487
+ ) as AccountAddressLite | undefined;
1488
+ return addr ? buildAddressFromAccount(addr) : shippingPayload;
1489
+ }
1490
+ return hasCompleteShippingInfo
1491
+ ? buildAddressFromForm(shippingInfo)
1492
+ : shippingPayload;
1493
+ }
1494
+ // Only return shipping payload if it's complete and valid
1495
+ return shippingPayload && hasCompleteShippingInfo
1496
+ ? shippingPayload
1497
+ : null;
1498
+ }
1499
+ if (isLoggedIn) {
1500
+ const selectedBilling = selectedBillingAddressId
1501
+ ? (meData?.me?.addresses?.find(
1502
+ (a: AccountAddressLite) => a.id === selectedBillingAddressId
1503
+ ) as AccountAddressLite | undefined)
1504
+ : (accountBilling as unknown as AccountAddressLite | null);
1505
+ return selectedBilling ? buildAddressFromAccount(selectedBilling) : null;
1506
+ }
1507
+ return hasCompleteBillingInfo ? buildAddressFromForm(billingInfo) : null;
1508
+ }, [
1509
+ useShippingAsBilling,
1510
+ isShipToDealer,
1511
+ isLoggedIn,
1512
+ selectedAddressId,
1513
+ meData,
1514
+ buildAddressFromAccount,
1515
+ shippingPayload,
1516
+ hasCompleteShippingInfo,
1517
+ shippingInfo,
1518
+ buildAddressFromForm,
1519
+ selectedBillingAddressId,
1520
+ accountBilling,
1521
+ billingInfo,
1522
+ hasCompleteBillingInfo,
1523
+ ]);
1524
+
1525
+ /** UPDATED: fetchShippingMethods now returns methods immediately, not just via state */
1526
+ const fetchShippingMethods = useCallback(
1527
+ async (id: string): Promise<ShippingMethod[]> => {
1528
+ if (fetchingMethodsRef.current) {
1529
+ // brief wait for the in-flight request to complete, then return whatever we have
1530
+ await sleep(100);
1531
+ return shippingMethods;
1532
+ }
1533
+
1534
+ // Basic throttle: avoid hammering endpoint if we called it very recently
1535
+ const now = Date.now();
1536
+ if (now - lastFetchedAtRef.current < 200) {
1537
+ return shippingMethods;
1538
+ }
1539
+
1540
+ fetchingMethodsRef.current = true;
1541
+ setShippingLoading(true);
1542
+ setShippingError(null);
1543
+
1544
+ try {
1545
+ const requestBody = {
1546
+ query: GET_CHECKOUT_SHIPPING_METHODS,
1547
+ variables: { id },
1548
+ };
1549
+ const res = await fetch(endpoint, {
1550
+ method: "POST",
1551
+ headers: { "Content-Type": "application/json" },
1552
+ body: JSON.stringify(requestBody),
1553
+ });
1554
+ if (!res.ok) {
1555
+ let errorMessage = `Failed to fetch shipping methods: ${res.status} ${res.statusText}`;
1556
+
1557
+ // Handle specific error cases
1558
+ if (res.status === 401 || res.status === 403) {
1559
+ errorMessage =
1560
+ "Your session has expired. Please refresh the page to continue.";
1561
+ } else if (res.status === 408 || res.status === 504) {
1562
+ errorMessage =
1563
+ "Request timeout occurred. Please check your connection and try again.";
1564
+ } else if (res.status >= 500) {
1565
+ errorMessage =
1566
+ "Server error occurred. Please try again in a moment.";
1567
+ }
1568
+
1569
+ throw new Error(errorMessage);
1570
+ }
1571
+ const json: GraphQLShippingMethodsResponse = await res.json();
1572
+ if (json.errors) {
1573
+ throw new Error(json.errors[0]?.message || "GraphQL error");
1574
+ }
1575
+
1576
+ const rawMethods = json.data?.checkout?.availableShippingMethods || [];
1577
+ const methods: ShippingMethod[] = rawMethods.map((m) => ({
1578
+ id: m.id,
1579
+ name: m.name,
1580
+ price: { amount: m.price.amount, currency: m.price.currency },
1581
+ minimumDeliveryDays: m.minimumDeliveryDays ?? null,
1582
+ maximumDeliveryDays: m.maximumDeliveryDays ?? null,
1583
+ }));
1584
+ // Update state but also return immediately for the caller to use
1585
+ setShippingMethods(methods);
1586
+ lastFetchedAtRef.current = Date.now();
1587
+
1588
+ // Sync/clean selections against fresh list
1589
+ if (!methods.length) {
1590
+ setSelectedShippingId(null);
1591
+ setSelectedShippingMethodId(null);
1592
+ setUserHasSelectedDelivery(false);
1593
+
1594
+ // Check if empty methods might indicate session expiry
1595
+ // If we had methods before and now have none, it could be session expiry
1596
+ if (shippingMethods.length > 0) {
1597
+ setShippingError(
1598
+ "No shipping methods available. This might be due to session expiry. Please refresh the page if the issue persists."
1599
+ );
1600
+ } else {
1601
+ // Clear any previous shipping errors since we got a valid API response
1602
+ setShippingError(null);
1603
+ }
1604
+
1605
+ // Mark this fetch as completed to prevent immediate retry
1606
+ lastFetchedAtRef.current = Date.now();
1607
+ // IMPORTANT: Also mark the address hash to prevent infinite retries
1608
+ // Use the lastAddressHashRef if available, or create a default
1609
+ if (!lastAddressHashRef.current) {
1610
+ lastAddressHashRef.current = "no-methods-available";
1611
+ }
1612
+ } else {
1613
+ setShippingError(null);
1614
+ // Don't automatically restore previous shipping method selection
1615
+ // Users should explicitly select delivery method for each checkout
1616
+ if (globalSelectedShippingId) {
1617
+ setSelectedShippingMethodId(null);
1618
+ setUserHasSelectedDelivery(false);
1619
+ }
1620
+ if (
1621
+ selectedShippingId &&
1622
+ !isMethodAvailable(selectedShippingId, methods)
1623
+ ) {
1624
+ setSelectedShippingId(null);
1625
+ setSelectedShippingMethodId(null);
1626
+ setUserHasSelectedDelivery(false);
1627
+ }
1628
+ }
1629
+
1630
+ return methods;
1631
+ } catch (error) {
1632
+ let errorMessage = "Failed to load shipping methods";
1633
+
1634
+ if (error instanceof Error) {
1635
+ if (error.name === "AbortError") {
1636
+ return shippingMethods; // Return existing methods if request was aborted
1637
+ } else if (
1638
+ error.message.includes("Failed to fetch") ||
1639
+ error.message.includes("NetworkError")
1640
+ ) {
1641
+ errorMessage =
1642
+ "Network connection issue. Please check your internet and try again.";
1643
+ } else if (error.message.includes("timeout")) {
1644
+ errorMessage = "Request timeout. Please try again.";
1645
+ } else {
1646
+ errorMessage = error.message;
1647
+ }
1648
+ }
1649
+
1650
+ setShippingError(errorMessage);
1651
+ throw error;
1652
+ } finally {
1653
+ setShippingLoading(false);
1654
+ fetchingMethodsRef.current = false;
1655
+ }
1656
+ },
1657
+ [
1658
+ endpoint,
1659
+ // Removed frequently changing dependencies to prevent retriggering:
1660
+ // selectedShippingId, globalSelectedShippingId, shippingMethods
1661
+ ]
1662
+ );
1663
+
1664
+ /** NEW: gets usable methods (state or fresh), with retries if empty/stale */
1665
+ const ensureShippingMethodsAvailable = useCallback(
1666
+ async (id: string): Promise<ShippingMethod[]> => {
1667
+ // If state already has methods, use them
1668
+ if (shippingMethods.length > 0) return shippingMethods;
1669
+
1670
+ // Fetch once and return result (even if empty)
1671
+ // Don't retry if API returns empty array - this is a valid response
1672
+ try {
1673
+ const methods = await fetchShippingMethods(id);
1674
+ // Return whatever we get from the API - empty array is valid
1675
+ return methods;
1676
+ } catch (error) {
1677
+ // Only retry on actual API errors, not empty results
1678
+ console.warn("Failed to fetch shipping methods, retrying...", error);
1679
+
1680
+ const methods = await withRetry(
1681
+ async () => {
1682
+ return await fetchShippingMethods(id);
1683
+ },
1684
+ 2,
1685
+ 1000
1686
+ ); // Reduced retries, only for actual errors
1687
+
1688
+ return methods;
1689
+ }
1690
+ },
1691
+ [fetchShippingMethods]
1692
+ );
1693
+
1694
+ /** NEW: Force retry shipping methods with state reset */
1695
+ const handleRetryShippingMethods = useCallback(async () => {
1696
+ if (!checkoutId || shippingLoading || isUpdatingDelivery) return;
1697
+
1698
+ // Clear error state and force fresh fetch
1699
+ setShippingError(null);
1700
+ setShippingMethods([]);
1701
+ lastFetchedAtRef.current = 0; // Reset throttle
1702
+ lastAddressHashRef.current = ""; // Reset address hash to force refetch
1703
+
1704
+ try {
1705
+ await fetchShippingMethods(checkoutId);
1706
+ } catch (error) {
1707
+ const msg =
1708
+ error instanceof Error
1709
+ ? error.message
1710
+ : "Failed to retry shipping methods";
1711
+ setShippingError(msg);
1712
+ // Re-throw the error so calling functions know it failed
1713
+ throw error;
1714
+ }
1715
+ }, [checkoutId, fetchShippingMethods]);
1716
+
1717
+ // Will Call functions
1718
+ const fetchCollectionPoints = useCallback(
1719
+ async (checkoutId: string) => {
1720
+ if (!checkoutId) return;
1721
+
1722
+ setWillCallLoading(true);
1723
+ setWillCallError(null);
1724
+
1725
+ try {
1726
+ const { data } = await apolloClient.query<
1727
+ GetCheckoutCollectionPointsData,
1728
+ GetCheckoutCollectionPointsVars
1729
+ >({
1730
+ query: GET_CHECKOUT_COLLECTION_POINTS,
1731
+ variables: { checkoutId },
1732
+ fetchPolicy: "network-only",
1733
+ });
1734
+
1735
+ if (data?.checkout?.availableCollectionPoints) {
1736
+ setCollectionPoints(data.checkout.availableCollectionPoints);
1737
+ } else {
1738
+ setCollectionPoints([]);
1739
+ }
1740
+ } catch (error) {
1741
+ console.error("Failed to fetch collection points:", error);
1742
+ setWillCallError("Failed to load pickup locations");
1743
+ setCollectionPoints([]);
1744
+ } finally {
1745
+ setWillCallLoading(false);
1746
+ }
1747
+ },
1748
+ [apolloClient]
1749
+ );
1750
+
1751
+ const handleCollectionPointSelect = useCallback(
1752
+ async (collectionPointId: string) => {
1753
+ if (!checkoutId) return;
1754
+
1755
+ setIsProcessingSelection(true);
1756
+ setSelectedCollectionPointId(collectionPointId);
1757
+ // Clear regular shipping selection when collection point is selected
1758
+ setSelectedShippingId(null);
1759
+ setSelectedShippingMethodId(null);
1760
+ setUserHasSelectedDelivery(false);
1761
+ // Set will call as selected when a collection point is chosen
1762
+ setIsWillCallSelected(true);
1763
+ // Clear shipping tax when switching to local pickup
1764
+ setTaxInfo((prev) =>
1765
+ prev
1766
+ ? {
1767
+ ...prev,
1768
+ shippingTax: 0,
1769
+ shippingNet: 0,
1770
+ }
1771
+ : null
1772
+ );
1773
+ // Clear delivery ref so API will be called when switching back to shipping
1774
+ lastDeliveryRef.current = null;
1775
+ setIsUpdatingDelivery(true);
1776
+
1777
+ try {
1778
+ const response = await updateWillCallDeliveryMethod({
1779
+ variables: {
1780
+ id: checkoutId,
1781
+ deliveryMethodId: collectionPointId,
1782
+ },
1783
+ });
1784
+
1785
+ if (response.data?.checkoutDeliveryMethodUpdate?.errors?.length) {
1786
+ const error = response.data.checkoutDeliveryMethodUpdate.errors[0];
1787
+ throw new Error(error.message);
1788
+ }
1789
+
1790
+ // Mark as user selected delivery
1791
+ setUserHasSelectedDelivery(true);
1792
+
1793
+ // Update total if needed
1794
+ const checkout = response.data?.checkoutDeliveryMethodUpdate?.checkout;
1795
+ if (checkout?.subtotalPrice?.gross?.amount !== undefined) {
1796
+ setSaleorTotal(checkout.subtotalPrice.gross.amount);
1797
+ }
1798
+ } catch (error) {
1799
+ console.error("Failed to set collection point:", error);
1800
+ setWillCallError(
1801
+ error instanceof Error
1802
+ ? error.message
1803
+ : "Failed to set pickup location"
1804
+ );
1805
+ setSelectedCollectionPointId(null);
1806
+ } finally {
1807
+ setIsUpdatingDelivery(false);
1808
+ setIsProcessingSelection(false);
1809
+ }
1810
+ },
1811
+ [checkoutId, updateWillCallDeliveryMethod]
1812
+ );
1813
+
1814
+ // Refs for idempotent pushes
1815
+ const lastShippingRef = useRef<AddressInputTS | null>(null);
1816
+ const lastBillingRef = useRef<AddressInputTS | null>(null);
1817
+ const lastDeliveryRef = useRef<string | null>(null);
1818
+ const lastCheckoutIdRef = useRef<string | null>(null);
1819
+
1820
+ // Consolidated effect for shipping address + delivery methods
1821
+ useEffect(() => {
1822
+ let mounted = true;
1823
+ const syncShippingAndDelivery = async () => {
1824
+ if (!isClient || !checkoutId || !shippingPayload) return;
1825
+
1826
+ // Prevent delivery method API calls if there's already a delivery method or address validation error
1827
+ // But still allow tax calculation for address updates
1828
+ const hasDeliveryError =
1829
+ shippingError &&
1830
+ (shippingError.includes("No delivery methods found") ||
1831
+ shippingError.includes("not valid for the address") ||
1832
+ shippingError.includes("postal code"));
1833
+
1834
+ // If there's a delivery error, we can still update address for tax calculation
1835
+ // but skip delivery method fetching
1836
+
1837
+ // Reduced throttling for more responsive tax calculation
1838
+ const now = Date.now();
1839
+ if (now - lastFetchedAtRef.current < 300) {
1840
+ // 300ms throttle for better responsiveness
1841
+ return;
1842
+ }
1843
+
1844
+ if (
1845
+ lastCheckoutIdRef.current &&
1846
+ lastCheckoutIdRef.current !== checkoutId
1847
+ ) {
1848
+ resetCheckoutState();
1849
+ lastCheckoutIdRef.current = checkoutId;
1850
+ // Clear delivery method selection for new checkout session
1851
+ setSelectedShippingMethodId(null);
1852
+ return;
1853
+ }
1854
+ if (!lastCheckoutIdRef.current) {
1855
+ lastCheckoutIdRef.current = checkoutId;
1856
+ // Clear delivery method selection when starting fresh checkout
1857
+ setSelectedShippingMethodId(null);
1858
+ }
1859
+
1860
+ const addressHash = JSON.stringify({
1861
+ country: shippingPayload.country,
1862
+ postalCode: shippingPayload.postalCode,
1863
+ streetAddress1: shippingPayload.streetAddress1,
1864
+ city: shippingPayload.city,
1865
+ phone: shippingPayload.phone, // Include phone to trigger retry when phone changes
1866
+ });
1867
+
1868
+ const shouldUpdateAddress = !shallowEq(
1869
+ shippingPayload,
1870
+ lastShippingRef.current
1871
+ );
1872
+ const addressChanged = addressHash !== lastAddressHashRef.current;
1873
+ const noMethodsLoaded = shippingMethods.length === 0;
1874
+ const shouldFetchMethods =
1875
+ !hasDeliveryError &&
1876
+ (addressChanged || (noMethodsLoaded && canShowDeliveryMethods));
1877
+
1878
+ // Don't fetch methods if we just failed with the same address hasState/Provinceh (but still allow address updates for tax)
1879
+ if (
1880
+ shouldFetchMethods &&
1881
+ addressHash === lastAddressHashRef.current &&
1882
+ shippingError
1883
+ ) {
1884
+ return;
1885
+ }
1886
+
1887
+ // Don't fetch methods if we've already attempted this address and got empty results (but still allow address updates for tax)
1888
+ if (
1889
+ shouldFetchMethods &&
1890
+ addressHashAttemptedRef.current.has(addressHash) &&
1891
+ shippingMethods.length === 0
1892
+ ) {
1893
+ // Don't return here - still allow address update for tax calculation
1894
+ }
1895
+
1896
+ try {
1897
+ if (shouldUpdateAddress) {
1898
+ setIsCalculatingTax(true);
1899
+ await updateShippingAddress(checkoutId, shippingPayload);
1900
+ lastShippingRef.current = shippingPayload;
1901
+ setIsCalculatingTax(false);
1902
+
1903
+ // Track shipping info event
1904
+ if (items.length > 0) {
1905
+ const products: Product[] = items.map((item, index) => ({
1906
+ item_id: item.id,
1907
+ item_name: item.name,
1908
+ item_category: item.category || "Products",
1909
+ price: item.price,
1910
+ quantity: item.quantity,
1911
+ currency: "USD",
1912
+ index: index,
1913
+ }));
1914
+
1915
+ const totalValue = items.reduce(
1916
+ (sum, item) => sum + item.price * item.quantity,
1917
+ 0
1918
+ );
1919
+
1920
+ // Include shipping address data in the event
1921
+ const shippingAddress = {
1922
+ first_name: shippingPayload.firstName,
1923
+ last_name: shippingPayload.lastName,
1924
+ address_line_1: shippingPayload.streetAddress1,
1925
+ city: shippingPayload.city,
1926
+ state: shippingPayload.countryArea,
1927
+ postal_code: shippingPayload.postalCode,
1928
+ country: shippingPayload.country,
1929
+ };
1930
+
1931
+ gtmAddShippingInfo(
1932
+ products,
1933
+ "USD",
1934
+ totalValue,
1935
+ undefined,
1936
+ undefined,
1937
+ shippingAddress,
1938
+ gtmConfig?.container_id
1939
+ );
1940
+ }
1941
+ }
1942
+ // Only fetch delivery methods if we don't have a delivery error and conditions are met
1943
+ if (shouldFetchMethods && mounted && !hasDeliveryError) {
1944
+ lastAddressHashRef.current = addressHash;
1945
+ // Track that we're attempting this address
1946
+ addressHashAttemptedRef.current.add(addressHash);
1947
+ // use returned methods; don't rely on state right away
1948
+ await fetchShippingMethods(checkoutId);
1949
+ }
1950
+ } catch (e) {
1951
+ if (mounted) {
1952
+ setIsCalculatingTax(false);
1953
+ const msg =
1954
+ e instanceof Error ? e.message : "Failed to sync shipping address";
1955
+ setShippingError(msg);
1956
+ // Mark this address hash as failed to prevent immediate retry
1957
+ lastAddressHashRef.current = addressHash;
1958
+ }
1959
+ }
1960
+ };
1961
+
1962
+ // Allow effect to run for tax calculation even if delivery methods can't be shown
1963
+ if (canShowDeliveryMethods || (shippingPayload && checkoutId)) {
1964
+ const timer = setTimeout(() => {
1965
+ if (mounted) void syncShippingAndDelivery();
1966
+ }, 150); // Reduced timeout for faster tax calculation
1967
+ return () => {
1968
+ clearTimeout(timer);
1969
+ mounted = false;
1970
+ };
1971
+ }
1972
+ return () => {
1973
+ mounted = false;
1974
+ };
1975
+ }, [
1976
+ isClient,
1977
+ checkoutId,
1978
+ canShowDeliveryMethods,
1979
+ shippingPayload,
1980
+ // Removed problematic dependencies that cause loops:
1981
+ // - shippingMethods.length (causes retrigger when methods load)
1982
+ // - shippingError/shippingLoading (change frequently)
1983
+ // - function dependencies (recreated every render)
1984
+ ]);
1985
+
1986
+ // Separate billing sync effect
1987
+ useEffect(() => {
1988
+ (async () => {
1989
+ if (!isClient || !checkoutId) return;
1990
+ if (
1991
+ billingPayload &&
1992
+ !shallowEq(billingPayload, lastBillingRef.current)
1993
+ ) {
1994
+ // Validate required fields before attempting sync
1995
+ const requiredFields = [
1996
+ "firstName",
1997
+ "lastName",
1998
+ "streetAddress1",
1999
+ "city",
2000
+ "postalCode",
2001
+ "country",
2002
+ ];
2003
+ const missingFields = requiredFields.filter(
2004
+ (field) => !billingPayload[field as keyof AddressInputTS]
2005
+ );
2006
+
2007
+ if (missingFields.length > 0) {
2008
+ console.warn(
2009
+ "[Checkout] Billing sync skipped - missing fields:",
2010
+ missingFields.join(", ")
2011
+ );
2012
+ return;
2013
+ }
2014
+
2015
+ try {
2016
+ await updateBillingAddress(checkoutId, billingPayload);
2017
+ lastBillingRef.current = billingPayload;
2018
+ } catch (e) {
2019
+ const msg =
2020
+ e instanceof Error ? e.message : "Failed to sync billing address";
2021
+ console.error("[Checkout] Billing sync error:", msg);
2022
+ // Don't throw error, just log it to avoid breaking the checkout flow
2023
+ }
2024
+ }
2025
+ })();
2026
+ }, [isClient, checkoutId, billingPayload, updateBillingAddress]);
2027
+
2028
+ // Clear stale total when user picks another delivery method
2029
+ useEffect(() => {
2030
+ if (selectedShippingId && lastDeliveryRef.current !== selectedShippingId) {
2031
+ setSaleorTotal(null);
2032
+ }
2033
+ }, [selectedShippingId]);
2034
+
2035
+ // Apply delivery method & get total; now robust against async state
2036
+ useEffect(() => {
2037
+ let mounted = true;
2038
+ const applyDeliveryMethod = async () => {
2039
+ if (!checkoutId || !selectedShippingId) return;
2040
+
2041
+ // Don't apply delivery method if will call is selected
2042
+ if (isWillCallSelected) return;
2043
+
2044
+ // Check if method is available in current state first (no fetch needed)
2045
+ let methodStillAvailable = shippingMethods.find(
2046
+ (m) => m.id === selectedShippingId
2047
+ );
2048
+
2049
+ // Only fetch methods if we don't have any or the selected method isn't available
2050
+ if (!methodStillAvailable && shippingMethods.length === 0) {
2051
+ const methods = await ensureShippingMethodsAvailable(checkoutId);
2052
+ methodStillAvailable = methods.find((m) => m.id === selectedShippingId);
2053
+ }
2054
+
2055
+ if (!methodStillAvailable) {
2056
+ setShippingError(
2057
+ "The selected shipping method is no longer available. Please select a different method."
2058
+ );
2059
+ setSelectedShippingId(null);
2060
+ setSelectedShippingMethodId(null);
2061
+ setUserHasSelectedDelivery(false);
2062
+ return;
2063
+ }
2064
+
2065
+ if (lastDeliveryRef.current === selectedShippingId) return;
2066
+
2067
+ // Check if we actually need to do any work
2068
+ const addressNeedsSync =
2069
+ shippingPayload && !shallowEq(shippingPayload, lastShippingRef.current);
2070
+
2071
+ try {
2072
+ // Only show updating state if we're actually updating address
2073
+ if (addressNeedsSync) {
2074
+ setIsUpdatingDelivery(true);
2075
+ }
2076
+ setIsCalculatingTotal(true);
2077
+
2078
+ // Only sync address if it has actually changed
2079
+ if (addressNeedsSync) {
2080
+ await updateShippingAddress(checkoutId, shippingPayload);
2081
+ lastShippingRef.current = shippingPayload;
2082
+ }
2083
+
2084
+ // Validate that the selected method is still available before calling API
2085
+ const isMethodValid =
2086
+ methodStillAvailable &&
2087
+ shippingMethods.some((m) => m.id === selectedShippingId);
2088
+ if (!isMethodValid) {
2089
+ // If method is no longer valid, fetch fresh methods and validate again
2090
+ console.warn(
2091
+ "Selected shipping method is no longer valid, fetching fresh methods..."
2092
+ );
2093
+ const freshMethods = await ensureShippingMethodsAvailable(checkoutId);
2094
+ const validMethod = freshMethods.find(
2095
+ (m) => m.id === selectedShippingId
2096
+ );
2097
+
2098
+ if (!validMethod) {
2099
+ throw new Error(
2100
+ `Selected shipping method ${selectedShippingId} is no longer available. Please select a different method.`
2101
+ );
2102
+ }
2103
+ }
2104
+
2105
+ await updateDeliveryMethod(checkoutId, selectedShippingId);
2106
+ lastDeliveryRef.current = selectedShippingId;
2107
+
2108
+ if (totalsAbortRef.current) totalsAbortRef.current.abort();
2109
+ totalsAbortRef.current = new AbortController();
2110
+
2111
+ if (mounted) {
2112
+ const details = await getCheckoutDetails(
2113
+ checkoutId,
2114
+ totalsAbortRef.current.signal
2115
+ );
2116
+ if (mounted && !totalsAbortRef.current.signal.aborted) {
2117
+ setSaleorTotal(details.total);
2118
+ setVoucherInfo(details.voucherInfo);
2119
+ setShippingError(null);
2120
+ // Mark as user selected since delivery method was successfully applied
2121
+ setUserHasSelectedDelivery(true);
2122
+ }
2123
+ }
2124
+ } catch (e) {
2125
+ if (mounted) {
2126
+ let errorMessage = "Failed to set delivery method";
2127
+ if (e instanceof Error) {
2128
+ if (
2129
+ e.message.includes("checkout session") ||
2130
+ e.message.includes("session has expired") ||
2131
+ e.message.includes("Couldn't resolve to a node")
2132
+ ) {
2133
+ errorMessage =
2134
+ "Your checkout session has expired. Please refresh the page or restart your checkout to continue.";
2135
+ } else if (e.message.includes("not applicable")) {
2136
+ errorMessage =
2137
+ "This shipping method is not available for your address or items. Please select a different method.";
2138
+ setSelectedShippingId(null);
2139
+ setSelectedShippingMethodId(null);
2140
+ setUserHasSelectedDelivery(false);
2141
+ } else if (
2142
+ e.message.includes("Failed to fetch") ||
2143
+ e.message.includes("NetworkError") ||
2144
+ e.message.includes("timeout")
2145
+ ) {
2146
+ errorMessage =
2147
+ "Network issue occurred. Please check your connection and try again.";
2148
+ } else {
2149
+ errorMessage = e.message;
2150
+ }
2151
+ }
2152
+ setShippingError(errorMessage);
2153
+ }
2154
+ } finally {
2155
+ if (mounted) {
2156
+ setIsUpdatingDelivery(false);
2157
+ setIsCalculatingTotal(false);
2158
+ setIsProcessingSelection(false);
2159
+ }
2160
+ }
2161
+ };
2162
+
2163
+ void applyDeliveryMethod();
2164
+ return () => {
2165
+ mounted = false;
2166
+ };
2167
+ }, [
2168
+ checkoutId,
2169
+ selectedShippingId,
2170
+ isWillCallSelected,
2171
+ shippingPayload,
2172
+ ensureShippingMethodsAvailable,
2173
+ updateDeliveryMethod,
2174
+ updateShippingAddress,
2175
+ getCheckoutDetails,
2176
+ setSelectedShippingMethodId,
2177
+ ]);
2178
+
2179
+ // Product restriction validation effect
2180
+ useEffect(() => {
2181
+ const validateRestrictions = async () => {
2182
+ if (!checkoutId || !hasCompleteShippingInfo) {
2183
+ setProductRestrictions([]);
2184
+ setHasRestrictionViolations(false);
2185
+ return;
2186
+ }
2187
+
2188
+ try {
2189
+ const details = await getCheckoutDetails(checkoutId);
2190
+ const restrictions = checkProductRestrictions(
2191
+ details.fullCheckoutData,
2192
+ shippingInfo.state,
2193
+ shippingInfo.zipCode
2194
+ );
2195
+
2196
+ setProductRestrictions(restrictions);
2197
+ setHasRestrictionViolations(restrictions.length > 0);
2198
+ } catch (error) {
2199
+ console.warn("Failed to validate product restrictions:", error);
2200
+ // Don't block checkout on validation errors, just clear restrictions
2201
+ setProductRestrictions([]);
2202
+ setHasRestrictionViolations(false);
2203
+ }
2204
+ };
2205
+
2206
+ validateRestrictions();
2207
+ }, [
2208
+ checkoutId,
2209
+ hasCompleteShippingInfo,
2210
+ shippingInfo.state,
2211
+ shippingInfo.zipCode,
2212
+ getCheckoutDetails,
2213
+ ]);
2214
+
2215
+ // Will Call collection points effect
2216
+ useEffect(() => {
2217
+ const willCallEnabled = isWillCallEnabled();
2218
+
2219
+ if (!willCallEnabled || !checkoutId || !hasCompleteShippingInfo) {
2220
+ setCollectionPoints([]);
2221
+ return;
2222
+ }
2223
+
2224
+ fetchCollectionPoints(checkoutId);
2225
+ }, [
2226
+ checkoutId,
2227
+ hasCompleteShippingInfo,
2228
+ isWillCallEnabled,
2229
+ fetchCollectionPoints,
2230
+ ]);
2231
+
2232
+ // Initial checkout details load
2233
+ useEffect(() => {
2234
+ const abortController = new AbortController();
2235
+ totalsAbortRef.current = abortController;
2236
+
2237
+ const loadInitialDetails = async () => {
2238
+ if (!checkoutId) return;
2239
+ try {
2240
+ setIsCalculatingTotal(true);
2241
+ const details = await getCheckoutDetails(
2242
+ checkoutId,
2243
+ abortController.signal
2244
+ );
2245
+ if (!abortController.signal.aborted) {
2246
+ setSaleorTotal(details.total);
2247
+ setVoucherInfo(details.voucherInfo);
2248
+
2249
+ // Sync delivery method state with what's actually in Saleor
2250
+ if (details.deliveryMethod) {
2251
+ setSelectedShippingId(details.deliveryMethod.id);
2252
+ setSelectedShippingMethodId(details.deliveryMethod.id);
2253
+ setUserHasSelectedDelivery(true);
2254
+
2255
+ // Add the method to shipping methods if not already there
2256
+ setShippingMethods((prev) => {
2257
+ const exists = prev.find(
2258
+ (m) => m.id === details.deliveryMethod.id
2259
+ );
2260
+ if (exists) return prev;
2261
+ return [
2262
+ ...prev,
2263
+ {
2264
+ id: details.deliveryMethod.id,
2265
+ name: details.deliveryMethod.name,
2266
+ price: {
2267
+ amount: details.deliveryMethod.price?.amount || 0,
2268
+ currency: details.deliveryMethod.price?.currency || "USD",
2269
+ },
2270
+ },
2271
+ ];
2272
+ });
2273
+ } else {
2274
+ // Clear any frontend state if Saleor doesn't have a delivery method
2275
+ setSelectedShippingId(null);
2276
+ setSelectedShippingMethodId(null);
2277
+ setUserHasSelectedDelivery(false);
2278
+ }
2279
+ }
2280
+ } catch (e: unknown) {
2281
+ if (e instanceof Error && e.name !== "AbortError") {
2282
+ setSaleorTotal(null);
2283
+ }
2284
+ } finally {
2285
+ if (!abortController.signal.aborted) setIsCalculatingTotal(false);
2286
+ }
2287
+ };
2288
+
2289
+ void loadInitialDetails();
2290
+ return () => {
2291
+ abortController.abort(new Error("Component unmounted"));
2292
+ };
2293
+ }, [checkoutId, getCheckoutDetails, setSelectedShippingMethodId]);
2294
+
2295
+ // Validate before payment — robust against stale state
2296
+ const persistGuestInfoAndValidateShipping = useCallback(
2297
+ async (bypassShippingValidation = false) => {
2298
+ // Validate email for both logged-in and guest users
2299
+ const emailToValidate = isLoggedIn ? user?.email : shippingInfo.email;
2300
+
2301
+ if (!emailToValidate || emailToValidate.trim() === "") {
2302
+ throw new Error("Email address is required to complete your order.");
2303
+ }
2304
+
2305
+ // Basic email format validation
2306
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2307
+ if (!emailRegex.test(emailToValidate)) {
2308
+ throw new Error("Please enter a valid email address.");
2309
+ }
2310
+
2311
+ if (!isLoggedIn) {
2312
+ useGlobalStore.getState().setGuestEmail(shippingInfo.email);
2313
+ useGlobalStore.getState().setGuestShippingInfo({
2314
+ firstName: shippingInfo.firstName,
2315
+ lastName: shippingInfo.lastName,
2316
+ address: shippingInfo.address,
2317
+ city: shippingInfo.city,
2318
+ state: shippingInfo.state,
2319
+ zipCode: shippingInfo.zipCode,
2320
+ phone: shippingInfo.phone,
2321
+ country: shippingInfo.country,
2322
+ });
2323
+ }
2324
+ if (!checkoutId) {
2325
+ throw new Error(
2326
+ "Checkout ID is missing. Please refresh the page and try again."
2327
+ );
2328
+ }
2329
+ // Check if either regular shipping or will-call is selected
2330
+ if (!selectedShippingId && !selectedCollectionPointId) {
2331
+ throw createDeliveryMethodError(
2332
+ "Please select a delivery method or pickup location before completing your order."
2333
+ );
2334
+ }
2335
+
2336
+ // If bypassing shipping validation (for payment redirects), skip the validation
2337
+ if (bypassShippingValidation) {
2338
+ return;
2339
+ }
2340
+
2341
+ // If will-call is selected, skip regular shipping validation
2342
+ if (selectedCollectionPointId) {
2343
+ return;
2344
+ }
2345
+
2346
+ // For payment flow, don't refresh methods if we already have valid ones
2347
+ // This prevents issues where methods become unavailable during payment processing
2348
+ let methods = shippingMethods;
2349
+ let selectedMethod = methods.find((m) => m.id === selectedShippingId);
2350
+
2351
+ // Only fetch fresh methods if we don't have any or the selected method is missing
2352
+ if (methods.length === 0 || !selectedMethod) {
2353
+ try {
2354
+ methods = await ensureShippingMethodsAvailable(checkoutId);
2355
+ selectedMethod = methods.find((m) => m.id === selectedShippingId);
2356
+
2357
+ // If still no methods after fresh fetch, this might be a temporary API issue
2358
+ if (methods.length === 0) {
2359
+ console.error(
2360
+ "No shipping methods available after fresh fetch - this might be a temporary API issue"
2361
+ );
2362
+
2363
+ // For payment flow, we can proceed if the user already had a valid method selected
2364
+ // since they already went through proper validation earlier
2365
+ if (selectedShippingId && shippingMethods.length > 0) {
2366
+ console.warn(
2367
+ "Using previously validated shipping methods for payment completion"
2368
+ );
2369
+ methods = shippingMethods;
2370
+ selectedMethod = methods.find((m) => m.id === selectedShippingId);
2371
+
2372
+ if (selectedMethod) {
2373
+ } else {
2374
+ throw new Error(
2375
+ "No delivery methods are available and cached method is invalid. Please refresh the page and try again."
2376
+ );
2377
+ }
2378
+ } else {
2379
+ throw new Error(
2380
+ "No delivery methods are available. This may be due to checkout session issues. Please refresh the page and try again."
2381
+ );
2382
+ }
2383
+ }
2384
+ } catch (error) {
2385
+ // If we can't even get shipping methods, the checkout might be expired/invalid
2386
+ if (error instanceof Error) {
2387
+ if (error.message.includes("No delivery methods")) {
2388
+ throw error; // Re-throw our specific error
2389
+ } else if (
2390
+ error.message.includes("session") ||
2391
+ error.message.includes("expired") ||
2392
+ error.message.includes("401") ||
2393
+ error.message.includes("403")
2394
+ ) {
2395
+ throw new Error(
2396
+ "Your checkout session has expired. Please refresh the page to start a new checkout session."
2397
+ );
2398
+ } else if (
2399
+ error.message.includes("network") ||
2400
+ error.message.includes("Failed to fetch")
2401
+ ) {
2402
+ throw new Error(
2403
+ "Network connection issue during checkout validation. Please check your connection and try again."
2404
+ );
2405
+ }
2406
+ }
2407
+ throw new Error(
2408
+ "Unable to validate delivery methods during checkout. Please refresh the page and try again."
2409
+ );
2410
+ }
2411
+ } else {
2412
+ }
2413
+
2414
+ // If selected method is not available, refresh methods and try again
2415
+ if (!selectedMethod) {
2416
+ try {
2417
+ setIsRecoveringDelivery(true);
2418
+ // Force a fresh fetch of shipping methods
2419
+ setShippingError(null);
2420
+ setShippingMethods([]);
2421
+ lastFetchedAtRef.current = 0;
2422
+ lastAddressHashRef.current = "";
2423
+
2424
+ methods = await fetchShippingMethods(checkoutId);
2425
+ selectedMethod = methods.find((m) => m.id === selectedShippingId);
2426
+
2427
+ if (!selectedMethod) {
2428
+ // Clear the invalid selection and show updated methods
2429
+ setSelectedShippingId(null);
2430
+ setSelectedShippingMethodId(null);
2431
+ setUserHasSelectedDelivery(false);
2432
+
2433
+ if (methods.length === 0) {
2434
+ throw createDeliveryMethodError(
2435
+ "No delivery methods are currently available for your address. Please verify your shipping address or contact support."
2436
+ );
2437
+ } else {
2438
+ throw createDeliveryMethodError(
2439
+ `Your previously selected delivery method is no longer available. Please choose from the ${
2440
+ methods.length
2441
+ } available method${
2442
+ methods.length > 1 ? "s" : ""
2443
+ } below and try again.`
2444
+ );
2445
+ }
2446
+ }
2447
+ } catch (refreshError) {
2448
+ // If refresh also fails, provide helpful guidance
2449
+ if (
2450
+ refreshError instanceof Error &&
2451
+ refreshError.message.includes("No delivery methods")
2452
+ ) {
2453
+ throw refreshError; // Re-throw the no methods error
2454
+ } else if (
2455
+ refreshError instanceof Error &&
2456
+ refreshError.message.includes("available method")
2457
+ ) {
2458
+ throw refreshError; // Re-throw the selection guidance error
2459
+ }
2460
+ throw createDeliveryMethodError(
2461
+ "The delivery method became unavailable during payment processing. Please select a new delivery method and try again."
2462
+ );
2463
+ } finally {
2464
+ setIsRecoveringDelivery(false);
2465
+ }
2466
+ }
2467
+
2468
+ // Wait briefly if a delivery update is in flight
2469
+ if (isUpdatingDelivery) {
2470
+ await sleep(300);
2471
+ }
2472
+
2473
+ // Confirm selection server-side with retry for payment stability (only for regular shipping)
2474
+ if (selectedShippingId) {
2475
+ try {
2476
+ await updateDeliveryMethod(checkoutId, selectedShippingId);
2477
+
2478
+ // Double-check the method was actually set by querying back
2479
+ const verification = await fetch(
2480
+ endpoint,
2481
+ {
2482
+ method: "POST",
2483
+ headers: { "Content-Type": "application/json" },
2484
+ body: JSON.stringify({
2485
+ query: `
2486
+ query VerifyDeliveryMethod($checkoutId: ID!) {
2487
+ checkout(id: $checkoutId) {
2488
+ id
2489
+ deliveryMethod {
2490
+ ... on ShippingMethod {
2491
+ id
2492
+ name
2493
+ }
2494
+ }
2495
+ }
2496
+ }
2497
+ `,
2498
+ variables: { checkoutId },
2499
+ }),
2500
+ }
2501
+ );
2502
+
2503
+ if (verification.ok) {
2504
+ const verifyData = await verification.json();
2505
+ const setMethod = verifyData.data?.checkout?.deliveryMethod;
2506
+
2507
+ if (!setMethod || setMethod.id !== selectedShippingId) {
2508
+ console.warn("Delivery method verification failed, retrying...");
2509
+ try {
2510
+ // Retry once more
2511
+ await updateDeliveryMethod(checkoutId, selectedShippingId);
2512
+ } catch (retryError) {
2513
+ console.warn(
2514
+ "Retry also failed, but proceeding with payment since method was originally valid:",
2515
+ retryError
2516
+ );
2517
+ // Don't throw - proceed with payment since user originally had valid method
2518
+ }
2519
+ }
2520
+ } else {
2521
+ console.warn(
2522
+ "Verification request failed, but proceeding with payment since method was originally valid"
2523
+ );
2524
+ // Don't throw - proceed with payment since user originally had valid method
2525
+ }
2526
+ } catch (error) {
2527
+ if (error instanceof Error) {
2528
+ if (error.message.includes("Couldn't resolve to a node")) {
2529
+ // Try to refresh methods one more time before failing
2530
+ try {
2531
+ await handleRetryShippingMethods();
2532
+ setSelectedShippingId(null);
2533
+ setSelectedShippingMethodId(null);
2534
+ setUserHasSelectedDelivery(false);
2535
+ throw createDeliveryMethodError(
2536
+ "The delivery method became invalid during checkout. Fresh delivery options have been loaded - please select one and try again."
2537
+ );
2538
+ } catch (refreshError) {
2539
+ // Clear selection and provide recovery instructions
2540
+ setSelectedShippingId(null);
2541
+ setSelectedShippingMethodId(null);
2542
+ setUserHasSelectedDelivery(false);
2543
+
2544
+ // Provide more specific error based on what failed
2545
+ if (refreshError instanceof Error) {
2546
+ if (
2547
+ refreshError.message.includes("session") ||
2548
+ refreshError.message.includes("expired")
2549
+ ) {
2550
+ throw new Error(
2551
+ "Your session expired during checkout. The page will reload automatically to restore your session."
2552
+ );
2553
+ } else if (
2554
+ refreshError.message.includes("network") ||
2555
+ refreshError.message.includes("Failed to fetch")
2556
+ ) {
2557
+ throw createDeliveryMethodError(
2558
+ "Network connection issue during checkout. Please check your connection and try selecting a delivery method again."
2559
+ );
2560
+ }
2561
+ }
2562
+ throw createDeliveryMethodError(
2563
+ "The delivery method became unavailable during payment processing. Please select a new delivery method and try again."
2564
+ );
2565
+ }
2566
+ } else if (error.message.includes("not applicable")) {
2567
+ // Clear selection and let user choose again
2568
+ setSelectedShippingId(null);
2569
+ setSelectedShippingMethodId(null);
2570
+ setUserHasSelectedDelivery(false);
2571
+ throw createDeliveryMethodError(
2572
+ "The selected shipping method is not available for your address or items. Please choose a different delivery method and try again."
2573
+ );
2574
+ }
2575
+ throw createDeliveryMethodError(
2576
+ `Unable to confirm delivery method: ${error.message}`
2577
+ );
2578
+ }
2579
+ throw createDeliveryMethodError(
2580
+ "Failed to confirm delivery method. Please refresh and try again."
2581
+ );
2582
+ }
2583
+ }
2584
+ },
2585
+ [
2586
+ isLoggedIn,
2587
+ shippingInfo,
2588
+ checkoutId,
2589
+ selectedShippingId,
2590
+ selectedCollectionPointId,
2591
+ isUpdatingDelivery,
2592
+ updateDeliveryMethod,
2593
+ ensureShippingMethodsAvailable,
2594
+ ]
2595
+ );
2596
+
2597
+ const selectedShipping = useMemo(
2598
+ () => shippingMethods.find((m) => m.id === selectedShippingId) || null,
2599
+ [shippingMethods, selectedShippingId]
2600
+ );
2601
+
2602
+ const grandTotal = useMemo(() => {
2603
+ // If we have Saleor total (includes tax and shipping), use it
2604
+ if (typeof saleorTotal === "number" && saleorTotal > 0) {
2605
+ return saleorTotal;
2606
+ }
2607
+
2608
+ // Fallback: calculate from store total + shipping (but this won't include tax)
2609
+ return (selectedShipping?.price?.amount || 0) + totalAmount;
2610
+ }, [saleorTotal, totalAmount, selectedShipping]);
2611
+
2612
+ // Field handlers (shipping) with debounced validation and tax calculation
2613
+ // Add debounce timer ref
2614
+ const emailUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
2615
+
2616
+ const handleFieldChange = (
2617
+ e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
2618
+ ) => {
2619
+ const { name, value } = e.target;
2620
+ setShippingInfo((prev) => ({ ...prev, [name]: value }));
2621
+
2622
+ // Validate email field as user types
2623
+ if (name === "email") {
2624
+ const emailValidationError = validateEmail(value);
2625
+ setEmailError(emailValidationError);
2626
+
2627
+ // Clear previous timeout
2628
+ if (emailUpdateTimeoutRef.current) {
2629
+ clearTimeout(emailUpdateTimeoutRef.current);
2630
+ }
2631
+
2632
+ // Update checkout email when valid email is entered (for guest users)
2633
+ if (!emailValidationError && !isLoggedIn && checkoutId && value.trim()) {
2634
+ // Clear previous timeout
2635
+ if (emailUpdateTimeoutRef.current) {
2636
+ clearTimeout(emailUpdateTimeoutRef.current);
2637
+ }
2638
+
2639
+ // Debounce the email update to avoid too many API calls
2640
+ emailUpdateTimeoutRef.current = setTimeout(() => {
2641
+ // Call the function and handle the callback
2642
+ updateCheckoutEmail(checkoutId, value, (errorMessage) => {
2643
+ setEmailError(errorMessage);
2644
+ });
2645
+ }, 1000);
2646
+ } else {
2647
+ }
2648
+ }
2649
+
2650
+ // Mark auto-selection as complete when guest starts entering address
2651
+ if (!isLoggedIn && !addressAutoSelectionComplete) {
2652
+ setAddressAutoSelectionComplete(true);
2653
+ }
2654
+
2655
+ // Clear any existing shipping errors and tax info when user changes address fields
2656
+ // This allows the delivery method and tax calculation APIs to be called with new address
2657
+ if (
2658
+ [
2659
+ "firstName",
2660
+ "lastName",
2661
+ "address",
2662
+ "city",
2663
+ "zipCode",
2664
+ "state",
2665
+ "country",
2666
+ ].includes(name)
2667
+ ) {
2668
+ // Clear local pickup selection when address changes (since pickup locations are tied to address)
2669
+ if (isWillCallSelected) {
2670
+ setIsWillCallSelected(false);
2671
+ setSelectedCollectionPointId(null);
2672
+ setWillCallError(null);
2673
+ // Clear any will call related shipping errors
2674
+ if (
2675
+ shippingError &&
2676
+ (shippingError.includes("click and collect") ||
2677
+ shippingError.includes("warehouse address"))
2678
+ ) {
2679
+ setShippingError(null);
2680
+ }
2681
+ }
2682
+
2683
+ // Clear tax info on address changes to trigger recalculation
2684
+ setTaxInfo(null);
2685
+ setIsCalculatingTax(false);
2686
+
2687
+ // Reset address tracking to force fresh API calls
2688
+ lastAddressHashRef.current = "";
2689
+ lastFetchedAtRef.current = 0;
2690
+ fetchingMethodsRef.current = false;
2691
+
2692
+ // If there's a delivery method or address validation error, clear it and allow new API call
2693
+ if (
2694
+ shippingError &&
2695
+ (shippingError.includes("No delivery methods found") ||
2696
+ shippingError.includes("not valid for the address") ||
2697
+ shippingError.includes("Delivery Method Error"))
2698
+ ) {
2699
+ setShippingError(null);
2700
+ // Clear existing methods to trigger fresh fetch
2701
+ setShippingMethods([]);
2702
+ }
2703
+
2704
+ // Also clear postal code validation errors when user changes any address fields
2705
+ if (shippingError && shippingError.includes("postal code")) {
2706
+ setShippingError(null);
2707
+ lastAddressHashRef.current = "";
2708
+ lastFetchedAtRef.current = 0;
2709
+ }
2710
+
2711
+ // Clear any pending postal code validation
2712
+ if (validationTimeoutRef.current) {
2713
+ clearTimeout(validationTimeoutRef.current);
2714
+ validationTimeoutRef.current = null;
2715
+ }
2716
+
2717
+ // When user changes state/country, trigger a re-validation after form updates
2718
+ if (name === "state" || name === "country") {
2719
+ // Set a flag or trigger re-validation via a separate effect
2720
+ setTimeout(() => {
2721
+ const event = new CustomEvent("revalidatePostalCode", {
2722
+ detail: { changedField: name, newValue: value },
2723
+ });
2724
+ window.dispatchEvent(event);
2725
+ }, 100);
2726
+ }
2727
+ }
2728
+
2729
+ // Debounce validation for postal code to prevent race conditions
2730
+ if (name === "zipCode") {
2731
+ // Set new validation timeout - but only if no delivery method API is running
2732
+ validationTimeoutRef.current = setTimeout(() => {
2733
+ // Only validate if not currently loading delivery methods
2734
+ if (!shippingLoading && !isUpdatingDelivery) {
2735
+ const isValid = isValidPostalCode(value, shippingInfo.country);
2736
+ if (!isValid && value.length >= 3) {
2737
+ setShippingError(
2738
+ "Please enter a valid postal code for the selected country."
2739
+ );
2740
+ } else if (
2741
+ isValid &&
2742
+ shippingError &&
2743
+ shippingError.includes("postal code")
2744
+ ) {
2745
+ // Clear postal code error when it becomes valid
2746
+ setShippingError(null);
2747
+ // Reset to allow delivery methods to be fetched with valid postal code
2748
+ lastAddressHashRef.current = "";
2749
+ lastFetchedAtRef.current = 0;
2750
+ }
2751
+ }
2752
+ }, 1500); // Increased debounce to allow delivery API calls to complete first
2753
+ }
2754
+ };
2755
+
2756
+ // Billing handlers
2757
+ const handleBillingFieldChange = (
2758
+ e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
2759
+ ) => {
2760
+ const { name, value } = e.target;
2761
+ setBillingInfo((prev) => ({ ...prev, [name]: value }));
2762
+ };
2763
+
2764
+ const handleUseShippingAsBillingChange = (checked: boolean) => {
2765
+ setUseShippingAsBilling(checked);
2766
+ if (checked) {
2767
+ setBillingInfo({
2768
+ ...shippingInfo,
2769
+ email: shippingInfo.email,
2770
+ });
2771
+ }
2772
+ };
2773
+
2774
+ const handlePlaceOrder = async () => {
2775
+ try {
2776
+ if (DEBUG_HALT_AFTER_PAYMENT) return;
2777
+ const billingAddress = useShippingAsBilling
2778
+ ? buildAddressFromForm(shippingInfo)
2779
+ : buildAddressFromForm(billingInfo);
2780
+ if (checkoutId) {
2781
+ await updateBillingAddress(checkoutId, billingAddress);
2782
+ await updateShippingAddress(
2783
+ checkoutId,
2784
+ buildAddressFromForm(shippingInfo)
2785
+ );
2786
+ }
2787
+ window.location.href = `/order-confirmation${
2788
+ checkoutId ? `?checkoutId=${encodeURIComponent(checkoutId)}` : ""
2789
+ }`;
2790
+ } catch (e) {
2791
+ console.error("Error during checkout:", e);
2792
+ }
2793
+ };
2794
+
2795
+ const payAmount = useMemo(() => {
2796
+ if (typeof saleorTotal === "number" && saleorTotal > 0) return saleorTotal;
2797
+ if (selectedShipping && totalAmount > 0) {
2798
+ const shippingCost = selectedShipping.price?.amount || 0;
2799
+ return totalAmount + shippingCost;
2800
+ }
2801
+ if (selectedShippingId && shippingMethods.length > 0 && totalAmount > 0) {
2802
+ const foundMethod = shippingMethods.find(
2803
+ (m) => m.id === selectedShippingId
2804
+ );
2805
+ if (foundMethod) return totalAmount + (foundMethod.price?.amount || 0);
2806
+ }
2807
+ return null;
2808
+ }, [
2809
+ saleorTotal,
2810
+ selectedShipping,
2811
+ totalAmount,
2812
+ selectedShippingId,
2813
+ shippingMethods,
2814
+ ]);
2815
+
2816
+ // Re-validate postal code when state/country changes
2817
+ useEffect(() => {
2818
+ const handleRevalidation = (event: CustomEvent) => {
2819
+ const {} = event.detail;
2820
+
2821
+ if (shippingInfo.zipCode && shippingInfo.zipCode.length >= 3) {
2822
+ const isValid = isValidPostalCode(
2823
+ shippingInfo.zipCode,
2824
+ shippingInfo.country
2825
+ );
2826
+
2827
+ if (
2828
+ isValid &&
2829
+ shippingError &&
2830
+ (shippingError.includes("postal code") ||
2831
+ shippingError.includes("not valid for the address") ||
2832
+ shippingError.includes("Delivery Method Error"))
2833
+ ) {
2834
+ setShippingError(null);
2835
+ lastAddressHashRef.current = "";
2836
+ lastFetchedAtRef.current = 0;
2837
+ fetchingMethodsRef.current = false;
2838
+ // Clear methods to trigger fresh fetch with corrected address
2839
+ setShippingMethods([]);
2840
+ } else if (!isValid) {
2841
+ setShippingError(
2842
+ "Please enter a valid postal code for the selected country."
2843
+ );
2844
+ }
2845
+ }
2846
+ };
2847
+
2848
+ window.addEventListener(
2849
+ "revalidatePostalCode",
2850
+ handleRevalidation as EventListener
2851
+ );
2852
+
2853
+ return () => {
2854
+ window.removeEventListener(
2855
+ "revalidatePostalCode",
2856
+ handleRevalidation as EventListener
2857
+ );
2858
+ };
2859
+ }, [
2860
+ shippingInfo.zipCode,
2861
+ shippingInfo.country,
2862
+ shippingError,
2863
+ isValidPostalCode,
2864
+ ]);
2865
+
2866
+ // Cleanup on unmount
2867
+ useEffect(() => {
2868
+ return () => {
2869
+ if (totalsAbortRef.current) totalsAbortRef.current.abort();
2870
+ if (validationTimeoutRef.current) {
2871
+ clearTimeout(validationTimeoutRef.current);
2872
+ }
2873
+ };
2874
+ }, []);
2875
+
2876
+ // Memoize the payment ready callback to prevent infinite loops
2877
+ const handlePaymentReady = useCallback((trigger: () => Promise<void>) => {
2878
+ setPaymentTriggerFn({ fn: trigger });
2879
+ }, []);
2880
+
2881
+ if (!isClient) return <LoadingUI className="h-[80vh]" />;
2882
+ if (items.length === 0 && !checkoutId)
2883
+ return (
2884
+ <EmptyState
2885
+ className="h-[80vh]"
2886
+ text="Your cart is empty"
2887
+ buttonLabel="Continue Shopping"
2888
+ onClick={() => route.push("/")}
2889
+ />
2890
+ );
2891
+
2892
+ return (
2893
+ <div className="px-4 md:px-6 md:py-8 py-6 lg:max-w-7xl mx-auto lg:py-10">
2894
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-14">
2895
+ <div className="lg:col-span-2 space-y-4 lg:border-r lg:border-[var(--color-secondary-200)] lg:pr-14">
2896
+ <CheckoutHeader isLoggedIn={isLoggedIn} />
2897
+
2898
+ <ContactDetailsSection
2899
+ isLoggedIn={isLoggedIn}
2900
+ userEmail={user?.email}
2901
+ guestEmail={shippingInfo.email}
2902
+ onEmailChange={handleFieldChange}
2903
+ emailError={emailError}
2904
+ />
2905
+
2906
+ <DealerShippingSection
2907
+ isShipToDealer={isShipToDealer}
2908
+ onShippingTypeChange={setIsShipToDealer}
2909
+ selectedDealer={selectedDealer}
2910
+ onDealerSelect={setSelectedDealer}
2911
+ />
2912
+
2913
+ {!isShipToDealer && (
2914
+ <AddressManagement
2915
+ isLoggedIn={isLoggedIn}
2916
+ shippingInfo={shippingInfo}
2917
+ billingInfo={billingInfo}
2918
+ useShippingAsBilling={useShippingAsBilling}
2919
+ onShippingChange={handleFieldChange}
2920
+ onBillingChange={handleBillingFieldChange}
2921
+ onUseShippingAsBillingChange={handleUseShippingAsBillingChange}
2922
+ onShippingPhoneChange={(phone) =>
2923
+ setShippingInfo((f) => ({
2924
+ ...f,
2925
+ phone,
2926
+ }))
2927
+ }
2928
+ onBillingPhoneChange={(phone) => {
2929
+ setBillingInfo((prev) => ({
2930
+ ...prev,
2931
+ phone,
2932
+ }));
2933
+ }}
2934
+ meData={meData}
2935
+ formData={formData}
2936
+ setFormData={setFormData}
2937
+ selectedAddressId={selectedAddressId}
2938
+ setSelectedAddressId={(id) => {
2939
+ setSelectedAddressId(id);
2940
+ setAddressAutoSelectionComplete(true); // Mark as complete when manually selected
2941
+
2942
+ // Clear tax info when address selection changes
2943
+ setTaxInfo(null);
2944
+ setIsCalculatingTax(false);
2945
+
2946
+ // Clear delivery methods error when user selects different address
2947
+ if (
2948
+ shippingError &&
2949
+ shippingError.includes("No delivery methods found")
2950
+ ) {
2951
+ setShippingError(null);
2952
+ lastAddressHashRef.current = "";
2953
+ lastFetchedAtRef.current = 0;
2954
+ fetchingMethodsRef.current = false;
2955
+ setShippingMethods([]);
2956
+ }
2957
+ }}
2958
+ selectedBillingAddressId={selectedBillingAddressId}
2959
+ setSelectedBillingAddressId={setSelectedBillingAddressId}
2960
+ onAddressAdded={async () => {
2961
+ await refetchMe();
2962
+ }}
2963
+ onSetDefaultAddress={handleSetDefaultAddress}
2964
+ />
2965
+ )}
2966
+
2967
+ <AddressInformationSection />
2968
+
2969
+ {/* Product Restriction Messages */}
2970
+ {productRestrictions.length > 0 && (
2971
+ <div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
2972
+ <div className="flex items-start gap-3">
2973
+ <div className="flex-shrink-0 mt-0.5">
2974
+ <svg
2975
+ className="w-5 h-5 text-orange-600"
2976
+ fill="currentColor"
2977
+ viewBox="0 0 20 20"
2978
+ >
2979
+ <path
2980
+ fillRule="evenodd"
2981
+ d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
2982
+ clipRule="evenodd"
2983
+ />
2984
+ </svg>
2985
+ </div>
2986
+ <div className="flex-1">
2987
+ <h3 className="text-sm font-semibold text-orange-800 mb-3 uppercase tracking-wide">
2988
+ Shipping Restriction
2989
+ {productRestrictions.length > 1 ? "s" : ""} Detected
2990
+ </h3>
2991
+ <div className="space-y-4">
2992
+ {productRestrictions.map((restriction, index) => (
2993
+ <div
2994
+ key={index}
2995
+ className="bg-white border border-orange-100 rounded p-3"
2996
+ >
2997
+ <p className="font-medium text-orange-900 text-sm">
2998
+ {restriction.productName}
2999
+ </p>
3000
+ <p className="text-orange-700 text-sm mt-1">
3001
+ {restriction.checkoutMessage}
3002
+ </p>
3003
+ </div>
3004
+ ))}
3005
+ </div>
3006
+ <div className="mt-4 p-3 bg-orange-100 rounded border border-orange-200">
3007
+ <p className="text-sm text-orange-800 font-medium">
3008
+ Please update your shipping address or remove the
3009
+ restricted item(s) to continue.
3010
+ </p>
3011
+ </div>
3012
+ </div>
3013
+ </div>
3014
+ </div>
3015
+ )}
3016
+
3017
+ {!hasRestrictionViolations && (
3018
+ <>
3019
+ <DeliveryMethodSection
3020
+ checkoutId={checkoutId}
3021
+ canShowDeliveryMethods={!!canShowDeliveryMethods}
3022
+ hasCompleteShippingInfo={!!hasCompleteShippingInfo}
3023
+ missingForDelivery={missingForDelivery}
3024
+ shippingLoading={shippingLoading}
3025
+ shippingMethods={shippingMethods}
3026
+ shippingError={shippingError}
3027
+ selectedShippingId={selectedShippingId}
3028
+ isUpdatingDelivery={isUpdatingDelivery || isRecoveringDelivery}
3029
+ isProcessingSelection={isProcessingSelection}
3030
+ isWillCallSelected={isWillCallSelected}
3031
+ onShippingMethodSelect={async (methodId) => {
3032
+ setIsProcessingSelection(true);
3033
+ try {
3034
+ // If switching from will call, check if methods need refresh
3035
+ if (isWillCallSelected && checkoutId) {
3036
+ try {
3037
+ // First check if the method exists in current methods
3038
+ const existingMethod = shippingMethods.find(
3039
+ (m) => m.id === methodId
3040
+ );
3041
+
3042
+ if (!existingMethod || shippingMethods.length === 0) {
3043
+ // Only fetch fresh methods if needed
3044
+ setShippingLoading(true);
3045
+ setShippingError(null);
3046
+ lastDeliveryRef.current = null;
3047
+
3048
+ // Fetch fresh shipping methods without clearing existing ones
3049
+ const freshMethods = await fetchShippingMethods(
3050
+ checkoutId
3051
+ );
3052
+
3053
+ // Validate that the selected method exists in fresh methods
3054
+ const validMethod = freshMethods.find(
3055
+ (m) => m.id === methodId
3056
+ );
3057
+ if (!validMethod) {
3058
+ setShippingError(
3059
+ "The selected shipping method is no longer available. Please select a different method."
3060
+ );
3061
+ setIsProcessingSelection(false);
3062
+ return;
3063
+ }
3064
+ }
3065
+ } catch (error) {
3066
+ console.error(
3067
+ "Failed to fetch fresh shipping methods:",
3068
+ error
3069
+ );
3070
+ setShippingError(
3071
+ "Failed to load shipping methods. Please try again."
3072
+ );
3073
+ setIsProcessingSelection(false);
3074
+ return;
3075
+ } finally {
3076
+ setShippingLoading(false);
3077
+ }
3078
+ } else {
3079
+ // Even if not switching from will call, validate method exists
3080
+ const validMethod = shippingMethods.find(
3081
+ (m) => m.id === methodId
3082
+ );
3083
+ if (!validMethod) {
3084
+ setShippingError(
3085
+ "The selected shipping method is no longer available. Please refresh the page and select a different method."
3086
+ );
3087
+ setIsProcessingSelection(false);
3088
+ return;
3089
+ }
3090
+ }
3091
+
3092
+ // Batch state updates to reduce re-renders
3093
+ setSelectedShippingId(methodId);
3094
+ setSelectedShippingMethodId(methodId);
3095
+ setUserHasSelectedDelivery(true);
3096
+ // Clear will call selection when regular shipping is selected
3097
+ setIsWillCallSelected(false);
3098
+ setSelectedCollectionPointId(null);
3099
+ // Clear any will call related shipping errors
3100
+ if (
3101
+ shippingError &&
3102
+ (shippingError.includes("click and collect") ||
3103
+ shippingError.includes("warehouse address"))
3104
+ ) {
3105
+ setShippingError(null);
3106
+ }
3107
+ } finally {
3108
+ // Always reset processing state when done
3109
+ setIsProcessingSelection(false);
3110
+ }
3111
+ }}
3112
+ onRetryShippingMethods={handleRetryShippingMethods}
3113
+ />
3114
+
3115
+ <WillCallSection
3116
+ checkoutId={checkoutId}
3117
+ willCallEnabled={isWillCallEnabled()}
3118
+ collectionPoints={collectionPoints}
3119
+ selectedCollectionPointId={selectedCollectionPointId}
3120
+ isUpdatingDelivery={isUpdatingDelivery || isRecoveringDelivery}
3121
+ isProcessingSelection={isProcessingSelection}
3122
+ onCollectionPointSelect={handleCollectionPointSelect}
3123
+ userState={shippingInfo.state}
3124
+ willCallLoading={willCallLoading}
3125
+ willCallError={willCallError}
3126
+ />
3127
+ </>
3128
+ )}
3129
+
3130
+ {checkoutId &&
3131
+ payAmount &&
3132
+ !isCalculatingTotal &&
3133
+ ((selectedShippingId && userHasSelectedDelivery) ||
3134
+ (isWillCallSelected && selectedCollectionPointId)) &&
3135
+ !hasRestrictionViolations && (
3136
+ <CheckoutQuestions
3137
+ isLoggedIn={isLoggedIn}
3138
+ grandTotal={grandTotal}
3139
+ checkoutId={checkoutId}
3140
+ onQuestionsChange={setCheckoutQuestionAnswers}
3141
+ onValidationChange={setAreCheckoutQuestionsValid}
3142
+ onSaveQuestions={(saveFn) =>
3143
+ setSaveCheckoutQuestions(() => saveFn)
3144
+ }
3145
+ />
3146
+ )}
3147
+
3148
+ {checkoutId && (
3149
+ <PaymentStep
3150
+ onBack={() => {}}
3151
+ onComplete={handlePlaceOrder}
3152
+ totalAmount={payAmount || 0}
3153
+ checkoutId={checkoutId}
3154
+ availablePaymentGateways={
3155
+ paymentGatewaysData?.checkout?.availablePaymentGateways
3156
+ }
3157
+ kountConfig={kountConfig}
3158
+ taxInfo={taxInfo}
3159
+ isCalculatingTotal={isCalculatingTotal}
3160
+ disabled={
3161
+ isCalculatingTotal ||
3162
+ hasRestrictionViolations ||
3163
+ !(
3164
+ (selectedShippingId && userHasSelectedDelivery) ||
3165
+ (isWillCallSelected && selectedCollectionPointId)
3166
+ )
3167
+ }
3168
+ onPaymentReady={handlePaymentReady}
3169
+ onStartPayment={async () => {
3170
+ // Validate terms and conditions if required
3171
+ if (termsData?.page?.isPublished && !termsAccepted) {
3172
+ throw new Error(
3173
+ "Please accept the Terms and Conditions to continue."
3174
+ );
3175
+ }
3176
+
3177
+ await persistGuestInfoAndValidateShipping(true);
3178
+ // Save checkout questions when payment is initiated
3179
+ if (
3180
+ saveCheckoutQuestions &&
3181
+ typeof saveCheckoutQuestions === "function"
3182
+ ) {
3183
+ try {
3184
+ await saveCheckoutQuestions();
3185
+ } catch (error) {
3186
+ console.error("Failed to save checkout questions:", error);
3187
+ // Don't throw error to prevent payment from being blocked
3188
+ }
3189
+ }
3190
+ }}
3191
+ isProcessingPayment={isProcessingPayment}
3192
+ setIsProcessingPayment={setIsProcessingPayment}
3193
+ selectedShippingId={selectedShippingId || undefined}
3194
+ userEmail={user?.email}
3195
+ guestEmail={shippingInfo.email}
3196
+ lineItems={items}
3197
+ questionsValid={areCheckoutQuestionsValid}
3198
+ termsAccepted={termsAccepted}
3199
+ termsData={termsData}
3200
+ onTermsModalOpen={() => setIsTermsModalOpen(true)}
3201
+ onTermsAcceptedChange={setTermsAccepted}
3202
+ billingAddress={(() => {
3203
+ const addressInfo = useShippingAsBilling
3204
+ ? shippingInfo
3205
+ : billingInfo;
3206
+ // Only send billing address if we have minimum required fields
3207
+ if (
3208
+ addressInfo.firstName &&
3209
+ addressInfo.lastName &&
3210
+ addressInfo.address &&
3211
+ addressInfo.city &&
3212
+ addressInfo.zipCode
3213
+ ) {
3214
+ return {
3215
+ firstName: addressInfo.firstName || "",
3216
+ lastName: addressInfo.lastName || "",
3217
+ address: addressInfo.address || "",
3218
+ city: addressInfo.city || "",
3219
+ state: addressInfo.state || "",
3220
+ zipCode: addressInfo.zipCode || "",
3221
+ country: addressInfo.country || "US",
3222
+ phone: addressInfo.phone || undefined,
3223
+ };
3224
+ }
3225
+ return undefined;
3226
+ })()}
3227
+ shippingAddress={(() => {
3228
+ // For dealer shipping, use dealer address
3229
+ if (isShipToDealer && selectedDealer) {
3230
+ return {
3231
+ firstName: "Dealer",
3232
+ lastName: "Pickup",
3233
+ address: selectedDealer.address.streetAddress1 || "",
3234
+ city: selectedDealer.address.city || "",
3235
+ state: selectedDealer.address.countryArea || "",
3236
+ zipCode: selectedDealer.address.postalCode || "",
3237
+ country: selectedDealer.address.country?.code || "US",
3238
+ phone: selectedDealer.phone || undefined,
3239
+ dealerName: selectedDealer.name,
3240
+ };
3241
+ }
3242
+
3243
+ // Only send shipping address if we have minimum required fields
3244
+ if (
3245
+ shippingInfo.firstName &&
3246
+ shippingInfo.lastName &&
3247
+ shippingInfo.address &&
3248
+ shippingInfo.city &&
3249
+ shippingInfo.zipCode
3250
+ ) {
3251
+ return {
3252
+ firstName: shippingInfo.firstName || "",
3253
+ lastName: shippingInfo.lastName || "",
3254
+ address: shippingInfo.address || "",
3255
+ city: shippingInfo.city || "",
3256
+ state: shippingInfo.state || "",
3257
+ zipCode: shippingInfo.zipCode || "",
3258
+ country: shippingInfo.country || "US",
3259
+ phone: shippingInfo.phone || undefined,
3260
+ };
3261
+ }
3262
+ return undefined;
3263
+ })()}
3264
+ />
3265
+ )}
3266
+ </div>
3267
+
3268
+ <OrderSummary
3269
+ lineItems={items}
3270
+ totalAmount={totalAmount}
3271
+ selectedShipping={selectedShipping}
3272
+ grandTotal={grandTotal}
3273
+ saleorTotal={saleorTotal}
3274
+ isUpdatingDelivery={isUpdatingDelivery}
3275
+ shippingLoading={shippingLoading}
3276
+ isCalculatingTotal={isCalculatingTotal}
3277
+ taxInfo={taxInfo}
3278
+ isCalculatingTax={isCalculatingTax}
3279
+ voucherInfo={voucherInfo}
3280
+ onApplyVoucher={applyVoucher}
3281
+ onRemoveVoucher={removeVoucher}
3282
+ isApplyingVoucher={isApplyingVoucher}
3283
+ voucherError={voucherError}
3284
+ selectedCollectionPointId={selectedCollectionPointId}
3285
+ onCompletePayment={paymentTriggerFn.fn || undefined}
3286
+ isPaymentProcessing={isProcessingPayment.isModalOpen}
3287
+ paymentDisabled={
3288
+ isCalculatingTotal ||
3289
+ hasRestrictionViolations ||
3290
+ !(
3291
+ (selectedShippingId && userHasSelectedDelivery) ||
3292
+ (isWillCallSelected && selectedCollectionPointId)
3293
+ )
3294
+ }
3295
+ paymentDisabledReason={
3296
+ isCalculatingTotal
3297
+ ? "Calculating total..."
3298
+ : hasRestrictionViolations
3299
+ ? "Please resolve product restrictions"
3300
+ : "Select a delivery method"
3301
+ }
3302
+ />
3303
+ </div>
3304
+
3305
+ {/* Terms and Conditions Modal */}
3306
+ <CheckoutTermsModal
3307
+ isModalOpen={isTermsModalOpen}
3308
+ onClose={() => setIsTermsModalOpen(false)}
3309
+ />
3310
+ </div>
3311
+ );
3312
+ }