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