@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,1775 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ApolloClient, HttpLink, InMemoryCache, gql } from "@apollo/client";
|
|
4
|
+
|
|
5
|
+
type GraphQLError = {
|
|
6
|
+
message: string;
|
|
7
|
+
code?: string;
|
|
8
|
+
field?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// type GraphQLResponseError = {
|
|
12
|
+
// extensions?: {
|
|
13
|
+
// exception?: {
|
|
14
|
+
// code: string;
|
|
15
|
+
// };
|
|
16
|
+
// };
|
|
17
|
+
// message: string;
|
|
18
|
+
// code?: string;
|
|
19
|
+
// };
|
|
20
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
21
|
+
import { useEffect, useMemo, useState } from "react";
|
|
22
|
+
import CommonButton from "../components/reuseableUI/commonButton";
|
|
23
|
+
import EmptyState from "../components/reuseableUI/emptyState";
|
|
24
|
+
import LoadingUI from "../components/reuseableUI/loadingUI";
|
|
25
|
+
import { ArrowIcon } from "../utils/svgs/arrowIcon";
|
|
26
|
+
import { SuccessTickIcon } from "../utils/svgs/cart/successTickIcon";
|
|
27
|
+
import { useGlobalStore } from "@/store/useGlobalStore";
|
|
28
|
+
import Image from "next/image";
|
|
29
|
+
import { getSaleorApiUrl } from "@/lib/saleor/getSaleorApiUrl";
|
|
30
|
+
import {
|
|
31
|
+
gtmPurchase,
|
|
32
|
+
gtmEnhancedConversion,
|
|
33
|
+
Product,
|
|
34
|
+
PurchaseData,
|
|
35
|
+
EnhancedConversionData,
|
|
36
|
+
} from "../utils/googleTagManager";
|
|
37
|
+
import { useAppConfiguration } from "../components/providers/ServerAppConfigurationProvider";
|
|
38
|
+
|
|
39
|
+
/** GraphQL ops */
|
|
40
|
+
const GET_CHECKOUT_SUMMARY = gql`
|
|
41
|
+
query getCheckoutSummary($checkoutId: ID!) {
|
|
42
|
+
checkout(id: $checkoutId) {
|
|
43
|
+
id
|
|
44
|
+
created
|
|
45
|
+
email
|
|
46
|
+
lines {
|
|
47
|
+
id
|
|
48
|
+
quantity
|
|
49
|
+
metadata {
|
|
50
|
+
key
|
|
51
|
+
value
|
|
52
|
+
}
|
|
53
|
+
variant {
|
|
54
|
+
id
|
|
55
|
+
sku
|
|
56
|
+
name
|
|
57
|
+
metadata {
|
|
58
|
+
key
|
|
59
|
+
value
|
|
60
|
+
}
|
|
61
|
+
product {
|
|
62
|
+
id
|
|
63
|
+
name
|
|
64
|
+
category {
|
|
65
|
+
name
|
|
66
|
+
}
|
|
67
|
+
thumbnail {
|
|
68
|
+
url
|
|
69
|
+
alt
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
totalPrice {
|
|
74
|
+
gross {
|
|
75
|
+
amount
|
|
76
|
+
currency
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
subtotalPrice {
|
|
81
|
+
gross {
|
|
82
|
+
amount
|
|
83
|
+
currency
|
|
84
|
+
}
|
|
85
|
+
net {
|
|
86
|
+
amount
|
|
87
|
+
currency
|
|
88
|
+
}
|
|
89
|
+
tax {
|
|
90
|
+
amount
|
|
91
|
+
currency
|
|
92
|
+
}
|
|
93
|
+
currency
|
|
94
|
+
}
|
|
95
|
+
shippingPrice {
|
|
96
|
+
gross {
|
|
97
|
+
amount
|
|
98
|
+
currency
|
|
99
|
+
}
|
|
100
|
+
net {
|
|
101
|
+
amount
|
|
102
|
+
currency
|
|
103
|
+
}
|
|
104
|
+
tax {
|
|
105
|
+
amount
|
|
106
|
+
currency
|
|
107
|
+
}
|
|
108
|
+
currency
|
|
109
|
+
}
|
|
110
|
+
totalPrice {
|
|
111
|
+
gross {
|
|
112
|
+
amount
|
|
113
|
+
currency
|
|
114
|
+
}
|
|
115
|
+
net {
|
|
116
|
+
amount
|
|
117
|
+
currency
|
|
118
|
+
}
|
|
119
|
+
tax {
|
|
120
|
+
amount
|
|
121
|
+
currency
|
|
122
|
+
}
|
|
123
|
+
currency
|
|
124
|
+
}
|
|
125
|
+
shippingMethod {
|
|
126
|
+
name
|
|
127
|
+
}
|
|
128
|
+
shippingAddress {
|
|
129
|
+
streetAddress1
|
|
130
|
+
city
|
|
131
|
+
countryArea
|
|
132
|
+
postalCode
|
|
133
|
+
country {
|
|
134
|
+
country
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
billingAddress {
|
|
138
|
+
firstName
|
|
139
|
+
lastName
|
|
140
|
+
phone
|
|
141
|
+
streetAddress1
|
|
142
|
+
city
|
|
143
|
+
countryArea
|
|
144
|
+
postalCode
|
|
145
|
+
country {
|
|
146
|
+
country
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
const GET_ORDER_SUMMARY = gql`
|
|
154
|
+
query getOrderSummary($orderId: ID!) {
|
|
155
|
+
order(id: $orderId) {
|
|
156
|
+
id
|
|
157
|
+
number
|
|
158
|
+
created
|
|
159
|
+
userEmail
|
|
160
|
+
lines {
|
|
161
|
+
id
|
|
162
|
+
quantity
|
|
163
|
+
productName
|
|
164
|
+
metadata {
|
|
165
|
+
key
|
|
166
|
+
value
|
|
167
|
+
}
|
|
168
|
+
totalPrice {
|
|
169
|
+
gross {
|
|
170
|
+
amount
|
|
171
|
+
currency
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
thumbnail(size: 200) {
|
|
175
|
+
url
|
|
176
|
+
alt
|
|
177
|
+
}
|
|
178
|
+
variant {
|
|
179
|
+
id
|
|
180
|
+
sku
|
|
181
|
+
name
|
|
182
|
+
metadata {
|
|
183
|
+
key
|
|
184
|
+
value
|
|
185
|
+
}
|
|
186
|
+
product {
|
|
187
|
+
id
|
|
188
|
+
name
|
|
189
|
+
category {
|
|
190
|
+
name
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
subtotal {
|
|
196
|
+
gross {
|
|
197
|
+
amount
|
|
198
|
+
currency
|
|
199
|
+
}
|
|
200
|
+
net {
|
|
201
|
+
amount
|
|
202
|
+
currency
|
|
203
|
+
}
|
|
204
|
+
tax {
|
|
205
|
+
amount
|
|
206
|
+
currency
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
shippingPrice {
|
|
210
|
+
gross {
|
|
211
|
+
amount
|
|
212
|
+
currency
|
|
213
|
+
}
|
|
214
|
+
net {
|
|
215
|
+
amount
|
|
216
|
+
currency
|
|
217
|
+
}
|
|
218
|
+
tax {
|
|
219
|
+
amount
|
|
220
|
+
currency
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
total {
|
|
224
|
+
gross {
|
|
225
|
+
amount
|
|
226
|
+
currency
|
|
227
|
+
}
|
|
228
|
+
net {
|
|
229
|
+
amount
|
|
230
|
+
currency
|
|
231
|
+
}
|
|
232
|
+
tax {
|
|
233
|
+
amount
|
|
234
|
+
currency
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
shippingMethod {
|
|
238
|
+
id
|
|
239
|
+
name
|
|
240
|
+
}
|
|
241
|
+
shippingAddress {
|
|
242
|
+
streetAddress1
|
|
243
|
+
city
|
|
244
|
+
countryArea
|
|
245
|
+
postalCode
|
|
246
|
+
country {
|
|
247
|
+
country
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
billingAddress {
|
|
251
|
+
firstName
|
|
252
|
+
lastName
|
|
253
|
+
phone
|
|
254
|
+
streetAddress1
|
|
255
|
+
city
|
|
256
|
+
countryArea
|
|
257
|
+
postalCode
|
|
258
|
+
country {
|
|
259
|
+
country
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
const COMPLETE_CHECKOUT = gql`
|
|
267
|
+
mutation CompleteCheckout($checkoutId: ID!) {
|
|
268
|
+
checkoutComplete(id: $checkoutId) {
|
|
269
|
+
order {
|
|
270
|
+
id
|
|
271
|
+
}
|
|
272
|
+
errors {
|
|
273
|
+
field
|
|
274
|
+
message
|
|
275
|
+
code
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
const TRANSACTION_PROCESS = gql`
|
|
282
|
+
mutation TransactionProcess($transactionId: ID!, $data: JSON) {
|
|
283
|
+
transactionProcess(id: $transactionId, data: $data) {
|
|
284
|
+
transaction {
|
|
285
|
+
id
|
|
286
|
+
}
|
|
287
|
+
data
|
|
288
|
+
errors {
|
|
289
|
+
field
|
|
290
|
+
message
|
|
291
|
+
code
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
/** minimal types used in UI */
|
|
298
|
+
type Money = { amount: number; currency: string };
|
|
299
|
+
type TaxedMoney = { gross: Money; net?: Money; tax?: Money };
|
|
300
|
+
|
|
301
|
+
type MetadataItem = {
|
|
302
|
+
key: string;
|
|
303
|
+
value: string;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
type OptionSetMetadata = {
|
|
307
|
+
name: string;
|
|
308
|
+
label: string;
|
|
309
|
+
type?: string;
|
|
310
|
+
hidden?: boolean;
|
|
311
|
+
deselect?: string;
|
|
312
|
+
required?: boolean;
|
|
313
|
+
base_product_required?: boolean;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
type CheckoutLine = {
|
|
317
|
+
id: string;
|
|
318
|
+
quantity: number;
|
|
319
|
+
metadata?: MetadataItem[] | null;
|
|
320
|
+
variant: {
|
|
321
|
+
id: string;
|
|
322
|
+
sku?: string | null;
|
|
323
|
+
name: string;
|
|
324
|
+
metadata?: MetadataItem[] | null;
|
|
325
|
+
product: {
|
|
326
|
+
id: string;
|
|
327
|
+
name: string;
|
|
328
|
+
category?: {
|
|
329
|
+
name: string;
|
|
330
|
+
} | null;
|
|
331
|
+
thumbnail?: {
|
|
332
|
+
url: string;
|
|
333
|
+
alt: string;
|
|
334
|
+
} | null;
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
totalPrice: TaxedMoney;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
type OrderLine = {
|
|
341
|
+
id: string;
|
|
342
|
+
quantity: number;
|
|
343
|
+
productName: string;
|
|
344
|
+
metadata?: MetadataItem[] | null;
|
|
345
|
+
thumbnail?: {
|
|
346
|
+
url: string;
|
|
347
|
+
alt: string;
|
|
348
|
+
} | null;
|
|
349
|
+
variant?: {
|
|
350
|
+
id: string;
|
|
351
|
+
sku?: string | null;
|
|
352
|
+
name: string;
|
|
353
|
+
metadata?: MetadataItem[] | null;
|
|
354
|
+
product: {
|
|
355
|
+
id: string;
|
|
356
|
+
name: string;
|
|
357
|
+
category?: {
|
|
358
|
+
name: string;
|
|
359
|
+
} | null;
|
|
360
|
+
};
|
|
361
|
+
} | null;
|
|
362
|
+
totalPrice: TaxedMoney;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
type OptionItem = {
|
|
366
|
+
lineId: string;
|
|
367
|
+
variantId: string;
|
|
368
|
+
variantName: string;
|
|
369
|
+
optionSetLabel: string;
|
|
370
|
+
price: number;
|
|
371
|
+
currency: string;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
type CustomInput = {
|
|
375
|
+
key: string;
|
|
376
|
+
value: string;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
type GroupedOrderItem = {
|
|
380
|
+
id: string;
|
|
381
|
+
name: string;
|
|
382
|
+
category: string;
|
|
383
|
+
thumbnail?: { url: string; alt: string } | null;
|
|
384
|
+
quantity: number;
|
|
385
|
+
basePrice: number;
|
|
386
|
+
totalPrice: TaxedMoney;
|
|
387
|
+
currency: string;
|
|
388
|
+
options: OptionItem[];
|
|
389
|
+
customInputs: CustomInput[];
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
type Address = {
|
|
393
|
+
streetAddress1?: string | null;
|
|
394
|
+
city?: string | null;
|
|
395
|
+
countryArea?: string | null;
|
|
396
|
+
postalCode?: string | null;
|
|
397
|
+
country: { country: string };
|
|
398
|
+
firstName?: string | null;
|
|
399
|
+
lastName?: string | null;
|
|
400
|
+
phone?: string | null;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
type Checkout = {
|
|
404
|
+
id: string;
|
|
405
|
+
created: string;
|
|
406
|
+
email?: string | null;
|
|
407
|
+
lines: CheckoutLine[];
|
|
408
|
+
subtotalPrice: { gross: Money; net?: Money; tax?: Money; currency: string };
|
|
409
|
+
shippingPrice: { gross: Money; net?: Money; tax?: Money; currency: string };
|
|
410
|
+
totalPrice: { gross: Money; net?: Money; tax?: Money; currency: string };
|
|
411
|
+
shippingAddress?: Address | null;
|
|
412
|
+
billingAddress?: Address | null;
|
|
413
|
+
deliveryMethod?: {
|
|
414
|
+
id: string;
|
|
415
|
+
name: string;
|
|
416
|
+
price: { gross: Money; currency: string };
|
|
417
|
+
} | null;
|
|
418
|
+
shippingMethod?: {
|
|
419
|
+
name: string;
|
|
420
|
+
} | null;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
type Order = {
|
|
424
|
+
id: string;
|
|
425
|
+
number?: string | null;
|
|
426
|
+
created: string;
|
|
427
|
+
userEmail?: string | null;
|
|
428
|
+
lines: OrderLine[];
|
|
429
|
+
subtotal: { gross: Money; net?: Money; tax?: Money; currency: string };
|
|
430
|
+
shippingPrice: { gross: Money; net?: Money; tax?: Money; currency: string };
|
|
431
|
+
total: { gross: Money; net?: Money; tax?: Money; currency: string };
|
|
432
|
+
shippingAddress?: Address | null;
|
|
433
|
+
billingAddress?: Address | null;
|
|
434
|
+
shippingMethod?: {
|
|
435
|
+
name: string;
|
|
436
|
+
} | null;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
type OrderData = Checkout | Order;
|
|
440
|
+
|
|
441
|
+
function formatDateTime(ts: string | Date) {
|
|
442
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
443
|
+
const yyyy = d.getFullYear();
|
|
444
|
+
const MM = String(d.getMonth() + 1).padStart(2, "0");
|
|
445
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
446
|
+
const HH = String(d.getHours()).padStart(2, "0");
|
|
447
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
448
|
+
return `${yyyy}-${MM}-${dd} ${HH}:${mm}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function errMsg(e: unknown): string {
|
|
452
|
+
if (e instanceof Error) return e.message;
|
|
453
|
+
if (typeof e === "string") return e;
|
|
454
|
+
try {
|
|
455
|
+
return JSON.stringify(e);
|
|
456
|
+
} catch {
|
|
457
|
+
return "Unknown error";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Helper to parse option_set metadata from a variant
|
|
462
|
+
function parseOptionSetMetadata(
|
|
463
|
+
metadata: MetadataItem[] | null | undefined
|
|
464
|
+
): OptionSetMetadata | null {
|
|
465
|
+
if (!metadata) return null;
|
|
466
|
+
const optionMeta = metadata.find((m) => m.key === "option_set");
|
|
467
|
+
if (!optionMeta?.value) return null;
|
|
468
|
+
try {
|
|
469
|
+
return JSON.parse(optionMeta.value) as OptionSetMetadata;
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Helper to extract custom inputs from line metadata (non-SKU option sets)
|
|
476
|
+
// These are user-provided values stored on the order/checkout line itself
|
|
477
|
+
function extractCustomInputs(
|
|
478
|
+
lineMetadata: MetadataItem[] | null | undefined
|
|
479
|
+
): CustomInput[] {
|
|
480
|
+
if (!lineMetadata || lineMetadata.length === 0) return [];
|
|
481
|
+
|
|
482
|
+
// Filter out system metadata keys that aren't custom inputs
|
|
483
|
+
const systemKeys = new Set([
|
|
484
|
+
"__typename",
|
|
485
|
+
"option_set",
|
|
486
|
+
"wsm_availability",
|
|
487
|
+
"wsm_brand",
|
|
488
|
+
"wsm_condition",
|
|
489
|
+
"wsm_cost",
|
|
490
|
+
"wsm_dealer_id",
|
|
491
|
+
"wsm_height",
|
|
492
|
+
"wsm_id",
|
|
493
|
+
"wsm_inventory",
|
|
494
|
+
"wsm_length",
|
|
495
|
+
"wsm_price",
|
|
496
|
+
"wsm_sale_price",
|
|
497
|
+
"wsm_upscode",
|
|
498
|
+
"wsm_width",
|
|
499
|
+
]);
|
|
500
|
+
|
|
501
|
+
return lineMetadata
|
|
502
|
+
.filter((m) => !systemKeys.has(m.key))
|
|
503
|
+
.map((m) => ({ key: m.key, value: m.value }));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Group order/checkout lines by product, separating base products from options
|
|
507
|
+
function groupOrderLines(
|
|
508
|
+
lines: Array<{
|
|
509
|
+
id: string;
|
|
510
|
+
quantity: number;
|
|
511
|
+
productName?: string;
|
|
512
|
+
lineMetadata?: MetadataItem[] | null;
|
|
513
|
+
thumbnail?: { url: string; alt: string } | null;
|
|
514
|
+
variant?: {
|
|
515
|
+
id: string;
|
|
516
|
+
sku?: string | null;
|
|
517
|
+
name: string;
|
|
518
|
+
metadata?: MetadataItem[] | null;
|
|
519
|
+
product: {
|
|
520
|
+
id: string;
|
|
521
|
+
name: string;
|
|
522
|
+
category?: { name: string } | null;
|
|
523
|
+
thumbnail?: { url: string; alt: string } | null;
|
|
524
|
+
};
|
|
525
|
+
} | null;
|
|
526
|
+
totalPrice: TaxedMoney;
|
|
527
|
+
}>
|
|
528
|
+
): GroupedOrderItem[] {
|
|
529
|
+
// Build a map of product ID -> lines for that product
|
|
530
|
+
const productMap = new Map<
|
|
531
|
+
string,
|
|
532
|
+
{
|
|
533
|
+
baseLine: (typeof lines)[0] | null;
|
|
534
|
+
optionLines: Array<{
|
|
535
|
+
line: (typeof lines)[0];
|
|
536
|
+
optionMeta: OptionSetMetadata;
|
|
537
|
+
}>;
|
|
538
|
+
}
|
|
539
|
+
>();
|
|
540
|
+
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
const variant = line.variant;
|
|
543
|
+
if (!variant) {
|
|
544
|
+
// Line has no variant info - treat as standalone item
|
|
545
|
+
const standaloneId = `standalone-${line.id}`;
|
|
546
|
+
productMap.set(standaloneId, { baseLine: line, optionLines: [] });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const productId = variant.product.id;
|
|
551
|
+
const optionMeta = parseOptionSetMetadata(variant.metadata);
|
|
552
|
+
|
|
553
|
+
if (!productMap.has(productId)) {
|
|
554
|
+
productMap.set(productId, { baseLine: null, optionLines: [] });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const entry = productMap.get(productId)!;
|
|
558
|
+
|
|
559
|
+
if (optionMeta) {
|
|
560
|
+
// This is an option variant (SKU-based option set)
|
|
561
|
+
entry.optionLines.push({ line, optionMeta });
|
|
562
|
+
} else {
|
|
563
|
+
// This is a base product variant
|
|
564
|
+
entry.baseLine = line;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Convert the map into grouped items
|
|
569
|
+
const groupedItems: GroupedOrderItem[] = [];
|
|
570
|
+
|
|
571
|
+
for (const [, entry] of productMap) {
|
|
572
|
+
const { baseLine, optionLines } = entry;
|
|
573
|
+
|
|
574
|
+
// Determine the display line (prefer base, fall back to first option)
|
|
575
|
+
const displayLine = baseLine ?? optionLines[0]?.line;
|
|
576
|
+
if (!displayLine) continue;
|
|
577
|
+
|
|
578
|
+
const variant = displayLine.variant;
|
|
579
|
+
const productName =
|
|
580
|
+
displayLine.productName ??
|
|
581
|
+
variant?.product.name ??
|
|
582
|
+
"Unknown Product";
|
|
583
|
+
const category =
|
|
584
|
+
variant?.product.category?.name ?? "Uncategorized";
|
|
585
|
+
const thumbnail =
|
|
586
|
+
"thumbnail" in displayLine
|
|
587
|
+
? displayLine.thumbnail
|
|
588
|
+
: variant?.product.thumbnail;
|
|
589
|
+
|
|
590
|
+
// Calculate base price (0 if no base line or if base_product_required is false for all options)
|
|
591
|
+
let basePrice = 0;
|
|
592
|
+
if (baseLine) {
|
|
593
|
+
basePrice = baseLine.totalPrice.gross.amount;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Build SKU-based options array
|
|
597
|
+
const options: OptionItem[] = optionLines.map(({ line, optionMeta }) => ({
|
|
598
|
+
lineId: line.id,
|
|
599
|
+
variantId: line.variant?.id ?? "",
|
|
600
|
+
variantName: line.variant?.name ?? "",
|
|
601
|
+
optionSetLabel: optionMeta.label || optionMeta.name,
|
|
602
|
+
price: line.totalPrice.gross.amount,
|
|
603
|
+
currency: line.totalPrice.gross.currency,
|
|
604
|
+
}));
|
|
605
|
+
|
|
606
|
+
// Extract non-SKU custom inputs from line metadata
|
|
607
|
+
const customInputs = extractCustomInputs(displayLine.lineMetadata);
|
|
608
|
+
|
|
609
|
+
// Calculate combined total price
|
|
610
|
+
const combinedAmount =
|
|
611
|
+
basePrice + options.reduce((sum, opt) => sum + opt.price, 0);
|
|
612
|
+
const currency =
|
|
613
|
+
displayLine.totalPrice.gross.currency ||
|
|
614
|
+
options[0]?.currency ||
|
|
615
|
+
"USD";
|
|
616
|
+
|
|
617
|
+
groupedItems.push({
|
|
618
|
+
id: displayLine.id,
|
|
619
|
+
name: productName,
|
|
620
|
+
category,
|
|
621
|
+
thumbnail,
|
|
622
|
+
quantity: displayLine.quantity,
|
|
623
|
+
basePrice,
|
|
624
|
+
totalPrice: {
|
|
625
|
+
gross: { amount: combinedAmount, currency },
|
|
626
|
+
},
|
|
627
|
+
currency,
|
|
628
|
+
options,
|
|
629
|
+
customInputs,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return groupedItems;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export default function Summary() {
|
|
637
|
+
const [orderData, setOrderData] = useState<OrderData | null>(null);
|
|
638
|
+
const [orderId, setOrderId] = useState<string | null>(null);
|
|
639
|
+
const [loading, setLoading] = useState(true);
|
|
640
|
+
const [completing, setCompleting] = useState(false);
|
|
641
|
+
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
642
|
+
const route = useRouter();
|
|
643
|
+
const [flowRan, setFlowRan] = useState(false);
|
|
644
|
+
const [processingStatus, setProcessingStatus] = useState<
|
|
645
|
+
"idle" | "running" | "success" | "fail"
|
|
646
|
+
>("idle");
|
|
647
|
+
|
|
648
|
+
const { finalizeCheckoutCleanup } = useGlobalStore(); // ✅ use the cleanup
|
|
649
|
+
const { getGoogleTagManagerConfig } = useAppConfiguration();
|
|
650
|
+
const gtmConfig = getGoogleTagManagerConfig();
|
|
651
|
+
|
|
652
|
+
const searchParams = useSearchParams();
|
|
653
|
+
const saleorTransactionId = searchParams.get("saleorTransactionId");
|
|
654
|
+
const urlOrderId = searchParams.get("orderId");
|
|
655
|
+
const orderNumber = searchParams.get("orderNumber");
|
|
656
|
+
|
|
657
|
+
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
|
658
|
+
const client = useMemo(() => {
|
|
659
|
+
if (!apiUrl) throw new Error("NEXT_PUBLIC_API_URL is not defined");
|
|
660
|
+
return new ApolloClient({
|
|
661
|
+
link: new HttpLink({ uri: apiUrl, fetch }),
|
|
662
|
+
cache: new InMemoryCache(),
|
|
663
|
+
});
|
|
664
|
+
}, [apiUrl]);
|
|
665
|
+
|
|
666
|
+
// Single orchestrated flow: Load -> (Process) -> Complete
|
|
667
|
+
useEffect(() => {
|
|
668
|
+
if (flowRan) return;
|
|
669
|
+
let cancelled = false;
|
|
670
|
+
|
|
671
|
+
(async () => {
|
|
672
|
+
try {
|
|
673
|
+
setErrorMsg(null);
|
|
674
|
+
setLoading(true);
|
|
675
|
+
|
|
676
|
+
// 1) Try to load existing order OR checkout
|
|
677
|
+
let fetchedOrderData: OrderData | null = null;
|
|
678
|
+
|
|
679
|
+
// Prefer explicit orderId from URL (return-from-PSP or direct link)
|
|
680
|
+
if (urlOrderId) {
|
|
681
|
+
try {
|
|
682
|
+
const { data } = await client.query<{ order: Order | null }>({
|
|
683
|
+
query: GET_ORDER_SUMMARY,
|
|
684
|
+
variables: { orderId: urlOrderId },
|
|
685
|
+
fetchPolicy: "no-cache",
|
|
686
|
+
});
|
|
687
|
+
if (data.order) {
|
|
688
|
+
fetchedOrderData = data.order;
|
|
689
|
+
setOrderId(urlOrderId);
|
|
690
|
+
// Clean up after successful order confirmation
|
|
691
|
+
await finalizeCheckoutCleanup();
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
// Don't cleanup on error - might still need checkout data
|
|
695
|
+
console.error("Order fetch failed:", error);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// If we still have nothing, try checkoutId (pre-completion)
|
|
700
|
+
if (!fetchedOrderData) {
|
|
701
|
+
let checkoutId = localStorage.getItem("checkoutId");
|
|
702
|
+
if (!checkoutId) {
|
|
703
|
+
const urlCheckoutId = searchParams.get("checkoutId");
|
|
704
|
+
if (urlCheckoutId) {
|
|
705
|
+
checkoutId = urlCheckoutId;
|
|
706
|
+
localStorage.setItem("checkoutId", urlCheckoutId);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (checkoutId) {
|
|
711
|
+
try {
|
|
712
|
+
const { data } = await client.query<{
|
|
713
|
+
checkout: Checkout | null;
|
|
714
|
+
}>({
|
|
715
|
+
query: GET_CHECKOUT_SUMMARY,
|
|
716
|
+
variables: { checkoutId },
|
|
717
|
+
fetchPolicy: "no-cache",
|
|
718
|
+
});
|
|
719
|
+
if (data.checkout) {
|
|
720
|
+
fetchedOrderData = data.checkout;
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
// If checkout fetch fails, we’ll error below.
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!fetchedOrderData) {
|
|
729
|
+
setErrorMsg("No order or checkout data found.");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (cancelled) return;
|
|
734
|
+
setOrderData(fetchedOrderData);
|
|
735
|
+
|
|
736
|
+
// Track purchase completion with GTM for all successful order confirmations
|
|
737
|
+
if (fetchedOrderData) {
|
|
738
|
+
const lines = getLines(fetchedOrderData);
|
|
739
|
+
const pricing = getPricing(fetchedOrderData);
|
|
740
|
+
|
|
741
|
+
const products: Product[] = lines.map((line, index) => ({
|
|
742
|
+
item_id: line.id,
|
|
743
|
+
item_name: line.name,
|
|
744
|
+
item_category: line.category || "Products",
|
|
745
|
+
price: line.totalPrice.gross.amount / line.quantity,
|
|
746
|
+
quantity: line.quantity,
|
|
747
|
+
currency: line.totalPrice.gross.currency || "USD",
|
|
748
|
+
index: index,
|
|
749
|
+
}));
|
|
750
|
+
|
|
751
|
+
// Use orderNumber from URL if available, fallback to order number from data, then to orderId
|
|
752
|
+
let transactionId = orderNumber;
|
|
753
|
+
if (
|
|
754
|
+
!transactionId &&
|
|
755
|
+
isOrder(fetchedOrderData) &&
|
|
756
|
+
fetchedOrderData.number
|
|
757
|
+
) {
|
|
758
|
+
transactionId = fetchedOrderData.number;
|
|
759
|
+
}
|
|
760
|
+
if (!transactionId) {
|
|
761
|
+
transactionId = urlOrderId || fetchedOrderData.id;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const purchaseData: PurchaseData = {
|
|
765
|
+
transaction_id: transactionId,
|
|
766
|
+
value: pricing.total.gross.amount,
|
|
767
|
+
currency: pricing.total.gross.currency || "USD",
|
|
768
|
+
tax: 0,
|
|
769
|
+
shipping: pricing.shipping.gross.amount,
|
|
770
|
+
items: products,
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
gtmPurchase(purchaseData, gtmConfig?.container_id);
|
|
774
|
+
|
|
775
|
+
// Enhanced conversion tracking with customer data if available
|
|
776
|
+
const orderData = fetchedOrderData as Order;
|
|
777
|
+
const checkoutData = fetchedOrderData as Checkout;
|
|
778
|
+
const userEmail = orderData.userEmail || checkoutData.email;
|
|
779
|
+
const billingAddress =
|
|
780
|
+
orderData.billingAddress || checkoutData.billingAddress;
|
|
781
|
+
|
|
782
|
+
if (userEmail || billingAddress) {
|
|
783
|
+
const enhancedData: EnhancedConversionData = {};
|
|
784
|
+
|
|
785
|
+
if (userEmail) {
|
|
786
|
+
enhancedData.email = userEmail;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (billingAddress) {
|
|
790
|
+
if (billingAddress.firstName)
|
|
791
|
+
enhancedData.first_name = billingAddress.firstName;
|
|
792
|
+
if (billingAddress.lastName)
|
|
793
|
+
enhancedData.last_name = billingAddress.lastName;
|
|
794
|
+
if (billingAddress.streetAddress1)
|
|
795
|
+
enhancedData.street = billingAddress.streetAddress1;
|
|
796
|
+
if (billingAddress.city) enhancedData.city = billingAddress.city;
|
|
797
|
+
if (billingAddress.countryArea)
|
|
798
|
+
enhancedData.region = billingAddress.countryArea;
|
|
799
|
+
if (billingAddress.postalCode)
|
|
800
|
+
enhancedData.postal_code = billingAddress.postalCode;
|
|
801
|
+
if (billingAddress.country?.country)
|
|
802
|
+
enhancedData.country = billingAddress.country.country;
|
|
803
|
+
if (billingAddress.phone)
|
|
804
|
+
enhancedData.phone_number = billingAddress.phone;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
gtmEnhancedConversion(
|
|
808
|
+
enhancedData,
|
|
809
|
+
pricing.total.gross.amount,
|
|
810
|
+
pricing.total.gross.currency || "USD",
|
|
811
|
+
gtmConfig?.container_id,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// 2) If returning from a PSP with a transaction, process it first
|
|
817
|
+
if (saleorTransactionId) {
|
|
818
|
+
setProcessingStatus("running");
|
|
819
|
+
const tryProcess = async (attempt: number): Promise<void> => {
|
|
820
|
+
const { data: procData } = await client.mutate<{
|
|
821
|
+
transactionProcess: {
|
|
822
|
+
transaction: { id: string } | null;
|
|
823
|
+
errors: {
|
|
824
|
+
field?: string | null;
|
|
825
|
+
message: string;
|
|
826
|
+
code: string;
|
|
827
|
+
}[];
|
|
828
|
+
} | null;
|
|
829
|
+
}>({
|
|
830
|
+
mutation: TRANSACTION_PROCESS,
|
|
831
|
+
variables: { transactionId: saleorTransactionId },
|
|
832
|
+
fetchPolicy: "no-cache",
|
|
833
|
+
});
|
|
834
|
+
const resp = procData?.transactionProcess;
|
|
835
|
+
const errs = resp?.errors ?? [];
|
|
836
|
+
if (errs.length) {
|
|
837
|
+
if (attempt < 3) {
|
|
838
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
839
|
+
return tryProcess(attempt + 1);
|
|
840
|
+
}
|
|
841
|
+
throw new Error(
|
|
842
|
+
"Payment processing failed: " +
|
|
843
|
+
errs.map((e) => e.message || e.code).join(", "),
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
await tryProcess(1);
|
|
848
|
+
if (cancelled) return;
|
|
849
|
+
setProcessingStatus("success");
|
|
850
|
+
|
|
851
|
+
// Track purchase completion with GTM
|
|
852
|
+
if (fetchedOrderData) {
|
|
853
|
+
const lines = getLines(fetchedOrderData);
|
|
854
|
+
const pricing = getPricing(fetchedOrderData);
|
|
855
|
+
|
|
856
|
+
const products: Product[] = lines.map((line, index) => ({
|
|
857
|
+
item_id: line.id,
|
|
858
|
+
item_name: line.name,
|
|
859
|
+
item_category: line.category || "Products",
|
|
860
|
+
price: line.totalPrice.gross.amount / line.quantity, // Calculate unit price
|
|
861
|
+
quantity: line.quantity,
|
|
862
|
+
currency: line.totalPrice.gross.currency || "USD",
|
|
863
|
+
index: index,
|
|
864
|
+
}));
|
|
865
|
+
|
|
866
|
+
// Use orderNumber from URL if available, fallback to order number from data, then to orderId
|
|
867
|
+
let transactionId = orderNumber;
|
|
868
|
+
if (
|
|
869
|
+
!transactionId &&
|
|
870
|
+
isOrder(fetchedOrderData) &&
|
|
871
|
+
fetchedOrderData.number
|
|
872
|
+
) {
|
|
873
|
+
transactionId = fetchedOrderData.number;
|
|
874
|
+
}
|
|
875
|
+
if (!transactionId) {
|
|
876
|
+
transactionId =
|
|
877
|
+
urlOrderId || saleorTransactionId || fetchedOrderData.id;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const purchaseData: PurchaseData = {
|
|
881
|
+
transaction_id: transactionId,
|
|
882
|
+
value: pricing.total.gross.amount,
|
|
883
|
+
currency: pricing.total.gross.currency || "USD",
|
|
884
|
+
tax: 0, // You may want to extract tax if available
|
|
885
|
+
shipping: pricing.shipping.gross.amount,
|
|
886
|
+
items: products,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
gtmPurchase(purchaseData, gtmConfig?.container_id);
|
|
890
|
+
|
|
891
|
+
// Enhanced conversion tracking with customer data if available
|
|
892
|
+
const checkoutData = fetchedOrderData as Checkout;
|
|
893
|
+
if (checkoutData.email || checkoutData.billingAddress) {
|
|
894
|
+
const enhancedData: EnhancedConversionData = {};
|
|
895
|
+
|
|
896
|
+
if (checkoutData.email) {
|
|
897
|
+
enhancedData.email = checkoutData.email;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (checkoutData.billingAddress) {
|
|
901
|
+
const billing = checkoutData.billingAddress;
|
|
902
|
+
if (billing.firstName)
|
|
903
|
+
enhancedData.first_name = billing.firstName;
|
|
904
|
+
if (billing.lastName) enhancedData.last_name = billing.lastName;
|
|
905
|
+
if (billing.streetAddress1)
|
|
906
|
+
enhancedData.street = billing.streetAddress1;
|
|
907
|
+
if (billing.city) enhancedData.city = billing.city;
|
|
908
|
+
if (billing.countryArea)
|
|
909
|
+
enhancedData.region = billing.countryArea;
|
|
910
|
+
if (billing.postalCode)
|
|
911
|
+
enhancedData.postal_code = billing.postalCode;
|
|
912
|
+
if (billing.country?.country)
|
|
913
|
+
enhancedData.country = billing.country.country;
|
|
914
|
+
if (billing.phone) enhancedData.phone_number = billing.phone;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
gtmEnhancedConversion(
|
|
918
|
+
enhancedData,
|
|
919
|
+
pricing.total.gross.amount,
|
|
920
|
+
pricing.total.gross.currency || "USD",
|
|
921
|
+
gtmConfig?.container_id,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 3) If we’re still on a checkout (not an order yet), complete it
|
|
928
|
+
const isCheckoutData =
|
|
929
|
+
!("number" in fetchedOrderData) &&
|
|
930
|
+
Array.isArray((fetchedOrderData as Checkout).lines) &&
|
|
931
|
+
(fetchedOrderData as Checkout).lines.length > 0;
|
|
932
|
+
|
|
933
|
+
if (isCheckoutData) {
|
|
934
|
+
const checkoutData = fetchedOrderData as Checkout;
|
|
935
|
+
|
|
936
|
+
setCompleting(true);
|
|
937
|
+
const completeRes = await client.mutate<{
|
|
938
|
+
checkoutComplete: {
|
|
939
|
+
order: { id: string } | null;
|
|
940
|
+
errors: { message: string; code: string }[];
|
|
941
|
+
} | null;
|
|
942
|
+
}>({
|
|
943
|
+
mutation: COMPLETE_CHECKOUT,
|
|
944
|
+
variables: { checkoutId: checkoutData.id },
|
|
945
|
+
fetchPolicy: "no-cache",
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
if (cancelled) return;
|
|
949
|
+
|
|
950
|
+
const errs = completeRes.data?.checkoutComplete?.errors ?? [];
|
|
951
|
+
if (errs.length) {
|
|
952
|
+
// Special handling for shipping method not set error
|
|
953
|
+
const shippingError = errs.find(
|
|
954
|
+
(e) => e.code === "SHIPPING_METHOD_NOT_SET",
|
|
955
|
+
);
|
|
956
|
+
if (shippingError) {
|
|
957
|
+
console.error(
|
|
958
|
+
"Shipping method not set during checkout completion. This commonly happens with PayPal redirects.",
|
|
959
|
+
);
|
|
960
|
+
console.error("Checkout ID:", checkoutData.id, "Errors:", errs);
|
|
961
|
+
// For PayPal redirects or guest users, try auto-recovery - complete checkout without setting shipping method first
|
|
962
|
+
if (!saleorTransactionId) {
|
|
963
|
+
// Not a PayPal redirect; attempting auto-recovery for guest checkout.
|
|
964
|
+
// Don't immediately throw error for guest users, try auto-recovery first
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Try to complete checkout directly since user already selected shipping method during checkout
|
|
968
|
+
const flowType = saleorTransactionId
|
|
969
|
+
? "PayPal redirect"
|
|
970
|
+
: "guest checkout";
|
|
971
|
+
try {
|
|
972
|
+
const directComplete = await client.mutate({
|
|
973
|
+
mutation: gql(`
|
|
974
|
+
mutation DirectCompleteCheckout($checkoutId: ID!) {
|
|
975
|
+
checkoutComplete(id: $checkoutId) {
|
|
976
|
+
order {
|
|
977
|
+
id
|
|
978
|
+
number
|
|
979
|
+
created
|
|
980
|
+
status
|
|
981
|
+
total { gross { amount currency } }
|
|
982
|
+
deliveryMethod {
|
|
983
|
+
... on ShippingMethod {
|
|
984
|
+
id
|
|
985
|
+
name
|
|
986
|
+
price { amount currency }
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
errors { field message code }
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
`),
|
|
994
|
+
variables: { checkoutId: checkoutData.id },
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const directErrors =
|
|
998
|
+
directComplete.data?.checkoutComplete?.errors || [];
|
|
999
|
+
if (
|
|
1000
|
+
directErrors.length === 0 &&
|
|
1001
|
+
directComplete.data?.checkoutComplete?.order
|
|
1002
|
+
) {
|
|
1003
|
+
// Direct checkout completion successful.
|
|
1004
|
+
const newOrderId =
|
|
1005
|
+
directComplete.data.checkoutComplete.order.id;
|
|
1006
|
+
setOrderId(newOrderId);
|
|
1007
|
+
await finalizeCheckoutCleanup();
|
|
1008
|
+
// Completed successfully without shipping method auto-fix.
|
|
1009
|
+
return; // Success! Skip the auto-fix entirely
|
|
1010
|
+
} else {
|
|
1011
|
+
// Falling back to shipping method auto-fix.
|
|
1012
|
+
}
|
|
1013
|
+
} catch (directError) {
|
|
1014
|
+
// Falling back to shipping method auto-fix.
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Try to auto-fix delivery method for PayPal redirects and guest checkouts
|
|
1018
|
+
console.warn(`Attempting auto-recovery for ${flowType}...`);
|
|
1019
|
+
try {
|
|
1020
|
+
const shippingMethodsQuery = `
|
|
1021
|
+
query GetShippingMethods($checkoutId: ID!) {
|
|
1022
|
+
checkout(id: $checkoutId) {
|
|
1023
|
+
id
|
|
1024
|
+
deliveryMethod {
|
|
1025
|
+
... on ShippingMethod {
|
|
1026
|
+
id
|
|
1027
|
+
name
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
availableShippingMethods {
|
|
1031
|
+
id
|
|
1032
|
+
name
|
|
1033
|
+
}
|
|
1034
|
+
shippingAddress {
|
|
1035
|
+
country {
|
|
1036
|
+
code
|
|
1037
|
+
}
|
|
1038
|
+
postalCode
|
|
1039
|
+
city
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
`;
|
|
1044
|
+
|
|
1045
|
+
const { data: methodsData } = await client.query({
|
|
1046
|
+
query: gql(shippingMethodsQuery),
|
|
1047
|
+
variables: { checkoutId: checkoutData.id },
|
|
1048
|
+
fetchPolicy: "no-cache",
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
const checkout = methodsData?.checkout;
|
|
1052
|
+
const availableMethods =
|
|
1053
|
+
checkout?.availableShippingMethods || [];
|
|
1054
|
+
|
|
1055
|
+
// Avoid logging checkout state details in templates/production.
|
|
1056
|
+
|
|
1057
|
+
// If no methods available, don't show UI error - just fail gracefully
|
|
1058
|
+
if (availableMethods.length === 0) {
|
|
1059
|
+
console.error(
|
|
1060
|
+
"No shipping methods available during PayPal auto-recovery",
|
|
1061
|
+
);
|
|
1062
|
+
throw new Error(
|
|
1063
|
+
"No shipping methods are available for this order. " +
|
|
1064
|
+
"This may be due to address or product restrictions. " +
|
|
1065
|
+
"Please contact support to complete your order.",
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (availableMethods.length > 0) {
|
|
1070
|
+
// Try to use the stored shipping method first, then fall back to first available
|
|
1071
|
+
let methodToUse = availableMethods[0];
|
|
1072
|
+
let methodSource = "first available";
|
|
1073
|
+
|
|
1074
|
+
// Check if we have a stored shipping method from before PayPal redirect or guest checkout
|
|
1075
|
+
try {
|
|
1076
|
+
const pendingPaymentData =
|
|
1077
|
+
localStorage.getItem("pendingPaymentData");
|
|
1078
|
+
// Avoid logging raw stored data.
|
|
1079
|
+
if (pendingPaymentData) {
|
|
1080
|
+
const paymentData = JSON.parse(pendingPaymentData);
|
|
1081
|
+
if (paymentData.selectedShippingId) {
|
|
1082
|
+
// Try to find the previously selected shipping method
|
|
1083
|
+
const storedMethod = availableMethods.find(
|
|
1084
|
+
(m: { id: string }) =>
|
|
1085
|
+
m.id === paymentData.selectedShippingId,
|
|
1086
|
+
);
|
|
1087
|
+
if (storedMethod) {
|
|
1088
|
+
methodToUse = storedMethod;
|
|
1089
|
+
methodSource = saleorTransactionId
|
|
1090
|
+
? "previously selected before PayPal redirect"
|
|
1091
|
+
: "previously selected during checkout";
|
|
1092
|
+
} else {
|
|
1093
|
+
console.warn(
|
|
1094
|
+
"Previously selected shipping method not available:",
|
|
1095
|
+
paymentData.selectedShippingId,
|
|
1096
|
+
);
|
|
1097
|
+
console.warn(
|
|
1098
|
+
"Available methods:",
|
|
1099
|
+
availableMethods.map(
|
|
1100
|
+
(m: { id: string; name: string }) => ({
|
|
1101
|
+
id: m.id,
|
|
1102
|
+
name: m.name,
|
|
1103
|
+
}),
|
|
1104
|
+
),
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
} else {
|
|
1109
|
+
// For guest users without stored payment data, check if there are any other storage mechanisms
|
|
1110
|
+
// No stored payment data found.
|
|
1111
|
+
|
|
1112
|
+
// Check for any other stored checkout data
|
|
1113
|
+
const allStorageKeys = Object.keys(localStorage);
|
|
1114
|
+
const checkoutKeys = allStorageKeys.filter(
|
|
1115
|
+
(key) =>
|
|
1116
|
+
key.toLowerCase().includes("checkout") ||
|
|
1117
|
+
key.toLowerCase().includes("shipping"),
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
checkoutKeys.forEach((key) => {
|
|
1121
|
+
// Avoid logging localStorage keys/values.
|
|
1122
|
+
void key;
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
console.warn("Failed to parse stored payment data:", e);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Validate that the delivery method ID is properly formatted
|
|
1130
|
+
if (!methodToUse.id || typeof methodToUse.id !== "string") {
|
|
1131
|
+
console.error("Invalid delivery method ID:", methodToUse);
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
"Invalid delivery method data received from server",
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
console.warn(
|
|
1138
|
+
`Auto-selecting ${methodSource} shipping method for ${flowType} completion:`,
|
|
1139
|
+
{
|
|
1140
|
+
id: methodToUse.id,
|
|
1141
|
+
name: methodToUse.name,
|
|
1142
|
+
idLength: methodToUse.id.length,
|
|
1143
|
+
idIsBase64Like: /^[A-Za-z0-9+/]+=*$/.test(methodToUse.id),
|
|
1144
|
+
},
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
// Use the same REST-style approach as the main checkout page to avoid GraphQL variable encoding issues
|
|
1148
|
+
const setShippingMethodMutation = `
|
|
1149
|
+
mutation CheckoutDeliveryMethodUpdate($id: ID!, $deliveryMethodId: ID!) {
|
|
1150
|
+
checkoutDeliveryMethodUpdate(id: $id, deliveryMethodId: $deliveryMethodId) {
|
|
1151
|
+
checkout {
|
|
1152
|
+
id
|
|
1153
|
+
deliveryMethod {
|
|
1154
|
+
... on ShippingMethod {
|
|
1155
|
+
id
|
|
1156
|
+
name
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
errors { field message code }
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
`;
|
|
1164
|
+
|
|
1165
|
+
try {
|
|
1166
|
+
// Setting delivery method for recovery.
|
|
1167
|
+
const endpoint = getSaleorApiUrl();
|
|
1168
|
+
const res = await fetch(endpoint, {
|
|
1169
|
+
method: "POST",
|
|
1170
|
+
headers: { "Content-Type": "application/json" },
|
|
1171
|
+
body: JSON.stringify({
|
|
1172
|
+
query: setShippingMethodMutation,
|
|
1173
|
+
variables: {
|
|
1174
|
+
id: checkoutData.id,
|
|
1175
|
+
deliveryMethodId: methodToUse.id,
|
|
1176
|
+
},
|
|
1177
|
+
}),
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
if (!res.ok) {
|
|
1181
|
+
const errorText = await res.text();
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`Failed to set delivery method: ${res.status} ${res.statusText} ${errorText}`,
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const setMethodResult = await res.json();
|
|
1188
|
+
|
|
1189
|
+
const setMethodErrors =
|
|
1190
|
+
setMethodResult.data?.checkoutDeliveryMethodUpdate
|
|
1191
|
+
?.errors || [];
|
|
1192
|
+
if (setMethodErrors.length > 0) {
|
|
1193
|
+
console.error("Failed to set shipping method:", {
|
|
1194
|
+
checkoutId: checkoutData.id,
|
|
1195
|
+
deliveryMethodId: methodToUse.id,
|
|
1196
|
+
errors: setMethodErrors,
|
|
1197
|
+
fullResult: setMethodResult.data,
|
|
1198
|
+
availableMethodsCount: availableMethods.length,
|
|
1199
|
+
methodData: methodToUse,
|
|
1200
|
+
methodSource,
|
|
1201
|
+
requestBody: JSON.stringify({
|
|
1202
|
+
query: setShippingMethodMutation,
|
|
1203
|
+
variables: {
|
|
1204
|
+
id: checkoutData.id,
|
|
1205
|
+
deliveryMethodId: methodToUse.id,
|
|
1206
|
+
},
|
|
1207
|
+
}),
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// Check for specific error types
|
|
1211
|
+
const nodeError = setMethodErrors.find(
|
|
1212
|
+
(e: GraphQLError) =>
|
|
1213
|
+
e.message?.includes("Couldn't resolve to a node") ||
|
|
1214
|
+
e.code === "NOT_FOUND",
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
const notApplicableError = setMethodErrors.find(
|
|
1218
|
+
(e: GraphQLError) =>
|
|
1219
|
+
e.message?.includes("not applicable") ||
|
|
1220
|
+
e.message?.includes(
|
|
1221
|
+
"shipping method is not applicable",
|
|
1222
|
+
) ||
|
|
1223
|
+
e.code === "SHIPPING_METHOD_NOT_APPLICABLE",
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
if (nodeError) {
|
|
1227
|
+
console.warn(
|
|
1228
|
+
"Delivery method ID is stale, will proceed without setting method",
|
|
1229
|
+
);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (notApplicableError) {
|
|
1234
|
+
console.warn(
|
|
1235
|
+
"First delivery method is not applicable, trying others...",
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
// Try other available methods
|
|
1239
|
+
let methodSet = false;
|
|
1240
|
+
for (let i = 1; i < availableMethods.length; i++) {
|
|
1241
|
+
const alternateMethod = availableMethods[i];
|
|
1242
|
+
// Trying alternate method.
|
|
1243
|
+
|
|
1244
|
+
try {
|
|
1245
|
+
const altResult = await client.mutate({
|
|
1246
|
+
mutation: gql(setShippingMethodMutation),
|
|
1247
|
+
variables: {
|
|
1248
|
+
checkoutId: checkoutData.id,
|
|
1249
|
+
deliveryMethodId: alternateMethod.id,
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const altErrors =
|
|
1254
|
+
altResult.data?.checkoutDeliveryMethodUpdate
|
|
1255
|
+
?.errors || [];
|
|
1256
|
+
if (altErrors.length === 0) {
|
|
1257
|
+
// Successfully set alternate shipping method.
|
|
1258
|
+
methodSet = true;
|
|
1259
|
+
break;
|
|
1260
|
+
} else {
|
|
1261
|
+
console.warn(
|
|
1262
|
+
`Alternate method ${alternateMethod.name} also failed:`,
|
|
1263
|
+
altErrors,
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
} catch (altError) {
|
|
1267
|
+
console.warn(
|
|
1268
|
+
`Failed to try alternate method ${alternateMethod.name}:`,
|
|
1269
|
+
altError,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (!methodSet) {
|
|
1275
|
+
console.error(
|
|
1276
|
+
"No applicable shipping methods found for this checkout",
|
|
1277
|
+
);
|
|
1278
|
+
return; // Let the main error flow handle this
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// If we successfully set an alternate method, continue
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
setMethodErrors
|
|
1287
|
+
.map((e: GraphQLError) => e.message || e.code)
|
|
1288
|
+
.join(", "),
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
} catch (mutationError) {
|
|
1292
|
+
console.error(
|
|
1293
|
+
"Mutation failed when setting delivery method:",
|
|
1294
|
+
mutationError,
|
|
1295
|
+
);
|
|
1296
|
+
// Don't throw here - let the main flow handle the error
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Successfully set shipping method; retrying checkout completion.
|
|
1301
|
+
|
|
1302
|
+
// Retry checkout completion
|
|
1303
|
+
const retryRes = await client.mutate({
|
|
1304
|
+
mutation: COMPLETE_CHECKOUT,
|
|
1305
|
+
variables: { checkoutId: checkoutData.id },
|
|
1306
|
+
fetchPolicy: "no-cache",
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
const retryErrs =
|
|
1310
|
+
retryRes.data?.checkoutComplete?.errors ?? [];
|
|
1311
|
+
if (retryErrs.length) {
|
|
1312
|
+
console.error("Retry completion failed:", retryErrs);
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
"Checkout completion failed after setting shipping method: " +
|
|
1315
|
+
retryErrs
|
|
1316
|
+
.map((e: GraphQLError) => e.message)
|
|
1317
|
+
.join(", "),
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const newOrderId =
|
|
1322
|
+
retryRes.data?.checkoutComplete?.order?.id ?? null;
|
|
1323
|
+
if (newOrderId) {
|
|
1324
|
+
setOrderId(newOrderId);
|
|
1325
|
+
await finalizeCheckoutCleanup();
|
|
1326
|
+
// Order completed successfully after auto-fixing shipping method.
|
|
1327
|
+
return; // Success, continue with normal flow
|
|
1328
|
+
} else {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
"No order ID returned after successful completion",
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
} else {
|
|
1334
|
+
console.error(
|
|
1335
|
+
"No shipping methods available for this checkout",
|
|
1336
|
+
);
|
|
1337
|
+
// Don't throw here - let the natural error flow handle it
|
|
1338
|
+
}
|
|
1339
|
+
} catch (retryError) {
|
|
1340
|
+
console.error(
|
|
1341
|
+
"Failed to auto-fix shipping method:",
|
|
1342
|
+
retryError,
|
|
1343
|
+
);
|
|
1344
|
+
// Don't re-throw here, let the original error be thrown below
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// If we reach here, auto-fix failed, provide helpful error message
|
|
1348
|
+
const enhancedError = new Error(
|
|
1349
|
+
`Order completion failed: ${errs
|
|
1350
|
+
.map((e) => e.message)
|
|
1351
|
+
.join(", ")}. ` +
|
|
1352
|
+
`This commonly happens when delivery methods become unavailable after payment. ` +
|
|
1353
|
+
`Please contact support with your payment confirmation to complete your order.`,
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
throw enhancedError;
|
|
1357
|
+
}
|
|
1358
|
+
throw new Error(errs.map((e) => e.message).join(", "));
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const newOrderId =
|
|
1362
|
+
completeRes.data?.checkoutComplete?.order?.id ?? null;
|
|
1363
|
+
if (newOrderId) {
|
|
1364
|
+
setOrderId(newOrderId);
|
|
1365
|
+
|
|
1366
|
+
// Clean up cart and checkout data after order completion
|
|
1367
|
+
await finalizeCheckoutCleanup();
|
|
1368
|
+
} else {
|
|
1369
|
+
setErrorMsg("Checkout completion returned no order id.");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
if (!cancelled) {
|
|
1374
|
+
setErrorMsg(errMsg(e));
|
|
1375
|
+
setProcessingStatus((s) => (s === "running" ? "fail" : s));
|
|
1376
|
+
}
|
|
1377
|
+
} finally {
|
|
1378
|
+
if (!cancelled) {
|
|
1379
|
+
setLoading(false);
|
|
1380
|
+
setCompleting(false);
|
|
1381
|
+
setFlowRan(true);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
})();
|
|
1385
|
+
|
|
1386
|
+
return () => {
|
|
1387
|
+
cancelled = true;
|
|
1388
|
+
};
|
|
1389
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1390
|
+
}, [client, saleorTransactionId]);
|
|
1391
|
+
|
|
1392
|
+
const isOrder = (data: OrderData): data is Order =>
|
|
1393
|
+
"number" in data || "userEmail" in data;
|
|
1394
|
+
|
|
1395
|
+
const getBillingFirstName = (data: OrderData) =>
|
|
1396
|
+
data.billingAddress?.firstName;
|
|
1397
|
+
|
|
1398
|
+
const getLines = (data: OrderData): GroupedOrderItem[] => {
|
|
1399
|
+
if (isOrder(data)) {
|
|
1400
|
+
// Normalize order lines to common format for grouping
|
|
1401
|
+
const normalizedLines = data.lines.map((line) => ({
|
|
1402
|
+
id: line.id,
|
|
1403
|
+
quantity: line.quantity,
|
|
1404
|
+
productName: line.productName,
|
|
1405
|
+
lineMetadata: line.metadata,
|
|
1406
|
+
thumbnail: line.thumbnail,
|
|
1407
|
+
variant: line.variant
|
|
1408
|
+
? {
|
|
1409
|
+
id: line.variant.id,
|
|
1410
|
+
sku: line.variant.sku,
|
|
1411
|
+
name: line.variant.name,
|
|
1412
|
+
metadata: line.variant.metadata,
|
|
1413
|
+
product: {
|
|
1414
|
+
id: line.variant.product.id,
|
|
1415
|
+
name: line.variant.product.name,
|
|
1416
|
+
category: line.variant.product.category,
|
|
1417
|
+
},
|
|
1418
|
+
}
|
|
1419
|
+
: null,
|
|
1420
|
+
totalPrice: line.totalPrice,
|
|
1421
|
+
}));
|
|
1422
|
+
return groupOrderLines(normalizedLines);
|
|
1423
|
+
} else {
|
|
1424
|
+
// Normalize checkout lines to common format for grouping
|
|
1425
|
+
const normalizedLines = data.lines.map((line) => ({
|
|
1426
|
+
id: line.id,
|
|
1427
|
+
quantity: line.quantity,
|
|
1428
|
+
productName: line.variant.product.name,
|
|
1429
|
+
lineMetadata: line.metadata,
|
|
1430
|
+
thumbnail: line.variant.product.thumbnail,
|
|
1431
|
+
variant: {
|
|
1432
|
+
id: line.variant.id,
|
|
1433
|
+
sku: line.variant.sku,
|
|
1434
|
+
name: line.variant.name,
|
|
1435
|
+
metadata: line.variant.metadata,
|
|
1436
|
+
product: {
|
|
1437
|
+
id: line.variant.product.id,
|
|
1438
|
+
name: line.variant.product.name,
|
|
1439
|
+
category: line.variant.product.category,
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
totalPrice: line.totalPrice,
|
|
1443
|
+
}));
|
|
1444
|
+
return groupOrderLines(normalizedLines);
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const getPricing = (data: OrderData) => {
|
|
1449
|
+
if (isOrder(data)) {
|
|
1450
|
+
return {
|
|
1451
|
+
subtotal: data.subtotal,
|
|
1452
|
+
shipping: data.shippingPrice,
|
|
1453
|
+
total: data.total,
|
|
1454
|
+
};
|
|
1455
|
+
} else {
|
|
1456
|
+
return {
|
|
1457
|
+
subtotal: data.subtotalPrice,
|
|
1458
|
+
shipping: data.shippingPrice,
|
|
1459
|
+
total: data.totalPrice,
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
if (loading || completing) {
|
|
1465
|
+
return <LoadingUI className="h-[80vh]" />;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (!orderData && !loading) {
|
|
1469
|
+
return (
|
|
1470
|
+
<EmptyState
|
|
1471
|
+
text="No order found."
|
|
1472
|
+
className="h-[80vh]"
|
|
1473
|
+
buttonLabel="GO TO HOME"
|
|
1474
|
+
buttonVariant="secondary"
|
|
1475
|
+
onClick={() => route.push("/")}
|
|
1476
|
+
/>
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (errorMsg && !loading && !completing) {
|
|
1481
|
+
return (
|
|
1482
|
+
<EmptyState
|
|
1483
|
+
text={errorMsg}
|
|
1484
|
+
className="h-[80vh]"
|
|
1485
|
+
buttonLabel="GO TO HOME"
|
|
1486
|
+
buttonVariant="secondary"
|
|
1487
|
+
onClick={() => route.push("/")}
|
|
1488
|
+
/>
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return (
|
|
1493
|
+
<div className="container mx-auto px-4 py-10 lg:py-24 grid grid-cols-1 lg:grid-cols-3 gap-14">
|
|
1494
|
+
<div className="lg:col-span-2 lg:border-r lg:border-[var(--color-secondary-200)] lg:pr-14">
|
|
1495
|
+
<div className="flex flex-col md:flex-row md:items-center w-full justify-between pb-8 gap-6">
|
|
1496
|
+
<div className="flex flex-col md:flex-row md:items-center gap-2">
|
|
1497
|
+
<span className="[&>svg]:size-10">{SuccessTickIcon}</span>
|
|
1498
|
+
<div className="space-y-1">
|
|
1499
|
+
<p className="uppercase font-medium text-xl font-secondary text-[var(--color-secondary-800)]">
|
|
1500
|
+
THANK YOU,{" "}
|
|
1501
|
+
{orderData ? getBillingFirstName(orderData) : "Customer"}!
|
|
1502
|
+
</p>
|
|
1503
|
+
<p className="font-normal text-sm font-secondary text-[var(--color-secondary-600)]">
|
|
1504
|
+
YOUR ORDER HAS BEEN CONFIRMED.
|
|
1505
|
+
</p>
|
|
1506
|
+
</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
<div className="flex items-center gap-1 cursor-pointer">
|
|
1509
|
+
<CommonButton
|
|
1510
|
+
onClick={() => route.push("/")}
|
|
1511
|
+
className="p-0"
|
|
1512
|
+
content="CONTINUE SHOPPING"
|
|
1513
|
+
variant="tertiary"
|
|
1514
|
+
/>
|
|
1515
|
+
<span className="size-5 text-[var(--color-primary-600)]">
|
|
1516
|
+
{ArrowIcon}
|
|
1517
|
+
</span>
|
|
1518
|
+
</div>
|
|
1519
|
+
</div>
|
|
1520
|
+
|
|
1521
|
+
<div className="p-4 lg:p-10 border border-[var(--color-secondary-200)]">
|
|
1522
|
+
{orderData && (
|
|
1523
|
+
<>
|
|
1524
|
+
<div className="grid gap-3">
|
|
1525
|
+
<div className="space-y-5">
|
|
1526
|
+
<p className="text-xl font-semibold leading-7 tracking-[-0.05px] text-[var(--color-secondary-800)]">
|
|
1527
|
+
ORDER DETAILS
|
|
1528
|
+
</p>
|
|
1529
|
+
<div className="flex flex-col items-start gap-3 uppercase text-[var(--color-secondary-600)] font-normal text-sm font-secondary">
|
|
1530
|
+
<div className="flex items-center gap-1">
|
|
1531
|
+
<p className=" text-sm font-normal leading-5 tracking-[-0.035px] text-[var(--color-secondary-600)]">
|
|
1532
|
+
Order Number
|
|
1533
|
+
</p>
|
|
1534
|
+
<p className="text-[var(--color-secondary-800)] text-sm font-semibold leading-5 tracking-[-0.035px]">
|
|
1535
|
+
{isOrder(orderData) && orderData.number
|
|
1536
|
+
? orderData.number
|
|
1537
|
+
: orderData.id}
|
|
1538
|
+
</p>
|
|
1539
|
+
</div>
|
|
1540
|
+
<div className="flex items-center gap-1">
|
|
1541
|
+
<p className=" text-sm font-normal leading-5 tracking-[-0.035px] text-[var(--color-secondary-600)]">
|
|
1542
|
+
Placed on
|
|
1543
|
+
</p>
|
|
1544
|
+
<p className="text-[var(--color-secondary-800)] text-sm font-semibold leading-5 tracking-[-0.035px]">
|
|
1545
|
+
{formatDateTime(orderData.created)}
|
|
1546
|
+
</p>
|
|
1547
|
+
</div>
|
|
1548
|
+
</div>
|
|
1549
|
+
</div>
|
|
1550
|
+
|
|
1551
|
+
<hr className="border border-[var(--color-secondary-200)]" />
|
|
1552
|
+
|
|
1553
|
+
<div className="flex flex-col gap-4">
|
|
1554
|
+
{getLines(orderData).map((item) => (
|
|
1555
|
+
<div
|
|
1556
|
+
className="flex flex-col md:flex-row lg:items-start justify-between gap-5 pb-4 border-b border-[var(--color-secondary-100)] last:border-b-0 last:pb-0"
|
|
1557
|
+
key={item.id}
|
|
1558
|
+
>
|
|
1559
|
+
<div className="flex gap-4">
|
|
1560
|
+
<div className="relative w-20 h-20 flex-shrink-0 rounded-md overflow-hidden">
|
|
1561
|
+
<Image
|
|
1562
|
+
src={
|
|
1563
|
+
item?.thumbnail?.url || "/no-image-avail-large.png"
|
|
1564
|
+
}
|
|
1565
|
+
alt={item?.thumbnail?.alt || "Product Image"}
|
|
1566
|
+
className="object-contain w-full h-full"
|
|
1567
|
+
width={100}
|
|
1568
|
+
height={100}
|
|
1569
|
+
/>
|
|
1570
|
+
</div>
|
|
1571
|
+
<div className="flex flex-col gap-1">
|
|
1572
|
+
<span className="text-sm font-normal leading-5 tracking-[-0.035px] text-[var(--color-secondary-600)]">
|
|
1573
|
+
{item.category}
|
|
1574
|
+
</span>
|
|
1575
|
+
<span className="font-medium text-lg md:text-xl leading-7 tracking[-0.05px] text-[var(--color-secondary-800)]">
|
|
1576
|
+
{item.name}
|
|
1577
|
+
</span>
|
|
1578
|
+
{/* Display selected SKU-based options */}
|
|
1579
|
+
{item.options.length > 0 && (
|
|
1580
|
+
<div className="mt-1 space-y-1">
|
|
1581
|
+
{item.options.map((option) => (
|
|
1582
|
+
<div
|
|
1583
|
+
key={option.lineId}
|
|
1584
|
+
className="text-sm text-[var(--color-secondary-600)] flex items-center gap-2"
|
|
1585
|
+
>
|
|
1586
|
+
<span className="text-[var(--color-primary-600)]">+</span>
|
|
1587
|
+
<span className="font-medium">{option.optionSetLabel}:</span>
|
|
1588
|
+
<span>{option.variantName}</span>
|
|
1589
|
+
{option.price > 0 && (
|
|
1590
|
+
<span className="text-[var(--color-primary-600)]">
|
|
1591
|
+
(+{new Intl.NumberFormat(undefined, {
|
|
1592
|
+
style: "currency",
|
|
1593
|
+
currency: option.currency,
|
|
1594
|
+
}).format(option.price)})
|
|
1595
|
+
</span>
|
|
1596
|
+
)}
|
|
1597
|
+
</div>
|
|
1598
|
+
))}
|
|
1599
|
+
</div>
|
|
1600
|
+
)}
|
|
1601
|
+
{/* Display non-SKU custom inputs */}
|
|
1602
|
+
{item.customInputs.length > 0 && (
|
|
1603
|
+
<div className="mt-1 space-y-1">
|
|
1604
|
+
{item.customInputs.map((input, idx) => (
|
|
1605
|
+
<div
|
|
1606
|
+
key={`${input.key}-${idx}`}
|
|
1607
|
+
className="text-sm text-[var(--color-secondary-600)] flex items-center gap-2"
|
|
1608
|
+
>
|
|
1609
|
+
<span className="font-medium">{input.key}:</span>
|
|
1610
|
+
<span>{input.value}</span>
|
|
1611
|
+
</div>
|
|
1612
|
+
))}
|
|
1613
|
+
</div>
|
|
1614
|
+
)}
|
|
1615
|
+
<span className="mt-2">
|
|
1616
|
+
<span
|
|
1617
|
+
style={{ color: "var(--color-secondary-600)" }}
|
|
1618
|
+
className="text-sm font-normal leading-5 tracking-[-0.035px]"
|
|
1619
|
+
>
|
|
1620
|
+
QTY
|
|
1621
|
+
</span>{" "}
|
|
1622
|
+
<span className="text-sm font-semibold leading-5 tracking-[-0.035px] uppercase text-[var(--color-secondary-800)]">
|
|
1623
|
+
{item.quantity}
|
|
1624
|
+
</span>
|
|
1625
|
+
</span>
|
|
1626
|
+
</div>
|
|
1627
|
+
</div>
|
|
1628
|
+
<span className="text-xl font-semibold leading-7 tracking-[-0.05px] text-[var(--color-secondary-800)] whitespace-nowrap">
|
|
1629
|
+
{new Intl.NumberFormat(undefined, {
|
|
1630
|
+
style: "currency",
|
|
1631
|
+
currency: item.totalPrice.gross.currency,
|
|
1632
|
+
}).format(item.totalPrice.gross.amount)}
|
|
1633
|
+
</span>
|
|
1634
|
+
</div>
|
|
1635
|
+
))}
|
|
1636
|
+
</div>
|
|
1637
|
+
</div>
|
|
1638
|
+
|
|
1639
|
+
<hr className="my-4 border border-[var(--color-secondary-200)]" />
|
|
1640
|
+
|
|
1641
|
+
<div>
|
|
1642
|
+
<h2 className="text-lg lg:text-xl not-italic font-semibold leading-7 tracking-[-0.05px] text-[var(--color-secondary-800)] uppercase mb-4">
|
|
1643
|
+
Shipping Address
|
|
1644
|
+
</h2>
|
|
1645
|
+
<p className="flex flex-col md:flex-row md:items-center gap-2 text-base md:text-lg lg:text-xl not-italic font-medium leading-7 tracking-[-0.05px] font-secondary text-[var(--color-secondary-800)]">
|
|
1646
|
+
<span>{orderData.shippingAddress?.streetAddress1}</span>
|
|
1647
|
+
<span>
|
|
1648
|
+
{orderData.shippingAddress?.city},{" "}
|
|
1649
|
+
{orderData.shippingAddress?.countryArea}{" "}
|
|
1650
|
+
{orderData.shippingAddress?.postalCode}
|
|
1651
|
+
</span>
|
|
1652
|
+
<span>{orderData.shippingAddress?.country.country}</span>
|
|
1653
|
+
</p>
|
|
1654
|
+
<p className="text-lg not-italic font-normal leading-7 tracking-[-0.045px] text-[var(--color-secondary-500)] mt-1">
|
|
1655
|
+
{orderData.shippingAddress?.phone}
|
|
1656
|
+
</p>
|
|
1657
|
+
</div>
|
|
1658
|
+
|
|
1659
|
+
<hr className="my-4 border border-[var(--color-secondary-200)]" />
|
|
1660
|
+
<div>
|
|
1661
|
+
<h2 className="text-lg lg:text-xl font-secondary text-[var(--color-secondary-800)] uppercase font-semibold mb-4">
|
|
1662
|
+
Billing Address
|
|
1663
|
+
</h2>
|
|
1664
|
+
<p className="flex flex-col md:flex-row md:items-center gap-2 text-medium text-base md:text-lg lg:text-xl font-secondary text-[var(--color-secondary-800)]">
|
|
1665
|
+
<span>{orderData.billingAddress?.streetAddress1}</span>
|
|
1666
|
+
<span>
|
|
1667
|
+
{orderData.billingAddress?.city},{" "}
|
|
1668
|
+
{orderData.billingAddress?.countryArea}{" "}
|
|
1669
|
+
{orderData.billingAddress?.postalCode}
|
|
1670
|
+
</span>
|
|
1671
|
+
<span>{orderData.billingAddress?.country.country}</span>
|
|
1672
|
+
</p>
|
|
1673
|
+
<p className="text-medium text-lg font-secondary text-[var(--color-secondary-500)] mt-1">
|
|
1674
|
+
{orderData.billingAddress?.phone}
|
|
1675
|
+
</p>
|
|
1676
|
+
</div>
|
|
1677
|
+
{orderData?.shippingMethod?.name && (
|
|
1678
|
+
<>
|
|
1679
|
+
<hr className="my-4 border border-[var(--color-secondary-200)]" />
|
|
1680
|
+
<div>
|
|
1681
|
+
<h2 className="text-lg lg:text-xl font-secondary text-[var(--color-secondary-800)] uppercase font-semibold mb-4">
|
|
1682
|
+
Delivery Method
|
|
1683
|
+
</h2>
|
|
1684
|
+
<p className="text-base md:text-lg not-italic font-normal leading-7 tracking-[-0.045px] uppercase mt-1 text-[var(--color-secondary-500)]">
|
|
1685
|
+
{orderData?.shippingMethod?.name}
|
|
1686
|
+
</p>
|
|
1687
|
+
</div>
|
|
1688
|
+
</>
|
|
1689
|
+
)}
|
|
1690
|
+
</>
|
|
1691
|
+
)}
|
|
1692
|
+
</div>
|
|
1693
|
+
</div>
|
|
1694
|
+
|
|
1695
|
+
<div className="lg:col-span-1 flex flex-col">
|
|
1696
|
+
<h2 className="font-medium font-secondary text-base text-[var(--color-secondary-800)] text-start pb-4 uppercase">
|
|
1697
|
+
Summary
|
|
1698
|
+
</h2>
|
|
1699
|
+
|
|
1700
|
+
<div className="w-full text-normal text-[var(--color-secondary-600)] text-base">
|
|
1701
|
+
{orderData &&
|
|
1702
|
+
(() => {
|
|
1703
|
+
const pricing = getPricing(orderData);
|
|
1704
|
+
return (
|
|
1705
|
+
<>
|
|
1706
|
+
<div className="flex justify-between mb-2">
|
|
1707
|
+
<span>Sub-Total</span>
|
|
1708
|
+
<span className="font-medium">
|
|
1709
|
+
{pricing.subtotal.net?.amount ||
|
|
1710
|
+
pricing.subtotal.gross.amount}{" "}
|
|
1711
|
+
{pricing.subtotal.currency}
|
|
1712
|
+
</span>
|
|
1713
|
+
</div>
|
|
1714
|
+
<div className="flex justify-between mb-2">
|
|
1715
|
+
<span>Subtotal Tax</span>
|
|
1716
|
+
<span className="font-medium">
|
|
1717
|
+
{(() => {
|
|
1718
|
+
const subtotalTax =
|
|
1719
|
+
pricing.subtotal.tax?.amount ||
|
|
1720
|
+
pricing.subtotal.gross.amount -
|
|
1721
|
+
(pricing.subtotal.net?.amount ||
|
|
1722
|
+
pricing.subtotal.gross.amount);
|
|
1723
|
+
|
|
1724
|
+
if (subtotalTax > 0) {
|
|
1725
|
+
return new Intl.NumberFormat(undefined, {
|
|
1726
|
+
style: "currency",
|
|
1727
|
+
currency: pricing.total.gross.currency,
|
|
1728
|
+
}).format(subtotalTax);
|
|
1729
|
+
}
|
|
1730
|
+
return "N/A";
|
|
1731
|
+
})()}
|
|
1732
|
+
</span>
|
|
1733
|
+
</div>
|
|
1734
|
+
<div className="flex justify-between mb-2">
|
|
1735
|
+
<span>Shipping Tax</span>
|
|
1736
|
+
<span className="font-medium">
|
|
1737
|
+
{(() => {
|
|
1738
|
+
const shippingTax =
|
|
1739
|
+
pricing.shipping.tax?.amount ||
|
|
1740
|
+
pricing.shipping.gross.amount -
|
|
1741
|
+
(pricing.shipping.net?.amount ||
|
|
1742
|
+
pricing.shipping.gross.amount);
|
|
1743
|
+
|
|
1744
|
+
if (shippingTax > 0) {
|
|
1745
|
+
return new Intl.NumberFormat(undefined, {
|
|
1746
|
+
style: "currency",
|
|
1747
|
+
currency: pricing.total.gross.currency,
|
|
1748
|
+
}).format(shippingTax);
|
|
1749
|
+
}
|
|
1750
|
+
return "N/A";
|
|
1751
|
+
})()}
|
|
1752
|
+
</span>
|
|
1753
|
+
</div>
|
|
1754
|
+
<div className="flex justify-between mb-2">
|
|
1755
|
+
<span>Shipping Cost</span>
|
|
1756
|
+
<span className="font-medium">
|
|
1757
|
+
{pricing.shipping.net?.amount ||
|
|
1758
|
+
pricing.shipping.gross.amount}{" "}
|
|
1759
|
+
{pricing.shipping.currency}
|
|
1760
|
+
</span>
|
|
1761
|
+
</div>
|
|
1762
|
+
<div className="border-t border-gray-200 pt-4 flex justify-between text-xl text-[var(--color-secondary-600)] font-medium ">
|
|
1763
|
+
<span>TOTAL</span>
|
|
1764
|
+
<span className="font-semibold text-[var(--color-secondary-800)]">
|
|
1765
|
+
{pricing.total.gross.amount} {pricing.total.currency}
|
|
1766
|
+
</span>
|
|
1767
|
+
</div>
|
|
1768
|
+
</>
|
|
1769
|
+
);
|
|
1770
|
+
})()}
|
|
1771
|
+
</div>
|
|
1772
|
+
</div>
|
|
1773
|
+
</div>
|
|
1774
|
+
);
|
|
1775
|
+
}
|