@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,1758 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useState, useEffect, useRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
PaymentProcessingState,
|
|
6
|
+
type KountConfigResponse,
|
|
7
|
+
type KountFraudCheckRequest,
|
|
8
|
+
} from "@/graphql/types/checkout";
|
|
9
|
+
import { useRouter } from "next/navigation";
|
|
10
|
+
import { useMutation, useLazyQuery } from "@apollo/client";
|
|
11
|
+
import {
|
|
12
|
+
CHECKOUT_COMPLETE,
|
|
13
|
+
type CheckoutCompleteVars,
|
|
14
|
+
type CheckoutCompleteData,
|
|
15
|
+
} from "@/graphql/mutations/checkoutComplete";
|
|
16
|
+
import {
|
|
17
|
+
CHECKOUT_PAYMENT_CREATE,
|
|
18
|
+
type CheckoutPaymentCreateVars,
|
|
19
|
+
type CheckoutPaymentCreateData,
|
|
20
|
+
} from "@/graphql/mutations/checkoutPaymentCreate";
|
|
21
|
+
import {
|
|
22
|
+
CHECKOUT_CUSTOMER_ATTACH,
|
|
23
|
+
type CheckoutCustomerAttachVars,
|
|
24
|
+
type CheckoutCustomerAttachData,
|
|
25
|
+
} from "@/graphql/mutations/checkoutCustomerAttach";
|
|
26
|
+
import { CHECKOUT_BY_ID } from "@/graphql/mutations/checkoutCreate";
|
|
27
|
+
import { gql } from "@apollo/client";
|
|
28
|
+
import { useGlobalStore } from "@/store/useGlobalStore";
|
|
29
|
+
import { useRecaptcha } from "@/hooks/useRecaptcha";
|
|
30
|
+
import ReCAPTCHA from "react-google-recaptcha";
|
|
31
|
+
import Input from "../reuseableUI/input";
|
|
32
|
+
import LoadingUI from "../reuseableUI/loadingUI";
|
|
33
|
+
import { gtmAddPaymentInfo, Product } from "../../utils/googleTagManager";
|
|
34
|
+
import { useAppConfiguration } from "@/app/components/providers/ServerAppConfigurationProvider";
|
|
35
|
+
import { kountApi, type KountOrderUpdateRequest } from "@/lib/api/kount";
|
|
36
|
+
import { PayPalPayment } from "./paypalPayment";
|
|
37
|
+
import { AffirmPayment } from "./affirmPayment";
|
|
38
|
+
import {
|
|
39
|
+
getUserIP,
|
|
40
|
+
generateTransactionId,
|
|
41
|
+
formatRFC3339Date,
|
|
42
|
+
generateDeviceSessionId,
|
|
43
|
+
detectPaymentType,
|
|
44
|
+
} from "../../utils/ipDetection";
|
|
45
|
+
import {
|
|
46
|
+
UPDATE_CHECKOUT_METADATA,
|
|
47
|
+
type UpdateCheckoutMetadataVariables,
|
|
48
|
+
type UpdateCheckoutMetadataData,
|
|
49
|
+
} from "@/graphql/mutations/updateCheckoutMetadata";
|
|
50
|
+
|
|
51
|
+
interface PaymentGateway {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
config: Array<{
|
|
55
|
+
field: string;
|
|
56
|
+
value: string;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SaleorNativePaymentProps {
|
|
61
|
+
checkoutId: string;
|
|
62
|
+
totalAmount: number;
|
|
63
|
+
onSuccess: () => void;
|
|
64
|
+
onError: (message: string) => void;
|
|
65
|
+
onCheckoutBlocked?: (message: string) => void;
|
|
66
|
+
setIsProcessingPayment: (state: PaymentProcessingState) => void;
|
|
67
|
+
availablePaymentGateways?: PaymentGateway[];
|
|
68
|
+
kountConfig?: KountConfigResponse | null;
|
|
69
|
+
onStartPayment?: () => Promise<void> | void;
|
|
70
|
+
selectedShippingId?: string;
|
|
71
|
+
userEmail?: string;
|
|
72
|
+
guestEmail?: string;
|
|
73
|
+
lineItems?: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
price: number;
|
|
77
|
+
quantity: number;
|
|
78
|
+
category?: string;
|
|
79
|
+
sku?: string;
|
|
80
|
+
}>;
|
|
81
|
+
billingAddress?: {
|
|
82
|
+
firstName: string;
|
|
83
|
+
lastName: string;
|
|
84
|
+
address: string;
|
|
85
|
+
city: string;
|
|
86
|
+
state: string;
|
|
87
|
+
zipCode: string;
|
|
88
|
+
country: string;
|
|
89
|
+
phone?: string;
|
|
90
|
+
};
|
|
91
|
+
shippingAddress?: {
|
|
92
|
+
firstName: string;
|
|
93
|
+
lastName: string;
|
|
94
|
+
address: string;
|
|
95
|
+
city: string;
|
|
96
|
+
state: string;
|
|
97
|
+
zipCode: string;
|
|
98
|
+
country: string;
|
|
99
|
+
phone?: string;
|
|
100
|
+
};
|
|
101
|
+
questionsValid?: boolean;
|
|
102
|
+
termsAccepted?: boolean;
|
|
103
|
+
termsData?: { page?: { isPublished: boolean } | null };
|
|
104
|
+
onTermsModalOpen?: () => void;
|
|
105
|
+
onTermsAcceptedChange?: (accepted: boolean) => void;
|
|
106
|
+
taxInfo?: {
|
|
107
|
+
totalTax: number;
|
|
108
|
+
shippingTax: number;
|
|
109
|
+
subtotalNet: number;
|
|
110
|
+
shippingNet: number;
|
|
111
|
+
currency: string;
|
|
112
|
+
} | null;
|
|
113
|
+
disabled?: boolean;
|
|
114
|
+
onPaymentReady?: (triggerPayment: () => Promise<void>) => void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface CardData {
|
|
118
|
+
cardNumber: string;
|
|
119
|
+
expirationDate: string;
|
|
120
|
+
cardCode: string;
|
|
121
|
+
fullName: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface AuthorizeNetResponse {
|
|
125
|
+
messages: {
|
|
126
|
+
resultCode: string;
|
|
127
|
+
message?: Array<{ text: string }>;
|
|
128
|
+
};
|
|
129
|
+
opaqueData?: {
|
|
130
|
+
dataValue: string;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const CHECKOUT_DELIVERY_METHOD_UPDATE = gql`
|
|
135
|
+
mutation CheckoutDeliveryMethodUpdate($id: ID!, $deliveryMethodId: ID!) {
|
|
136
|
+
checkoutDeliveryMethodUpdate(id: $id, deliveryMethodId: $deliveryMethodId) {
|
|
137
|
+
checkout {
|
|
138
|
+
id
|
|
139
|
+
deliveryMethod {
|
|
140
|
+
... on ShippingMethod {
|
|
141
|
+
id
|
|
142
|
+
name
|
|
143
|
+
}
|
|
144
|
+
... on Warehouse {
|
|
145
|
+
id
|
|
146
|
+
name
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
errors {
|
|
151
|
+
field
|
|
152
|
+
message
|
|
153
|
+
code
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
interface CheckoutDeliveryMethodUpdateVars {
|
|
160
|
+
id: string;
|
|
161
|
+
deliveryMethodId: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface CheckoutDeliveryMethodUpdateData {
|
|
165
|
+
checkoutDeliveryMethodUpdate: {
|
|
166
|
+
checkout: {
|
|
167
|
+
id: string;
|
|
168
|
+
deliveryMethod: {
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
} | null;
|
|
172
|
+
} | null;
|
|
173
|
+
errors: Array<{
|
|
174
|
+
field: string | null;
|
|
175
|
+
message: string;
|
|
176
|
+
code: string;
|
|
177
|
+
}>;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Helper function to get payment gateway icon (using SVG for lightweight icons)
|
|
182
|
+
const getPaymentGatewayIcon = (gatewayId: string): React.ReactElement => {
|
|
183
|
+
if (gatewayId === "mirumee.payments.authorize_net") {
|
|
184
|
+
// Authorize.Net icon
|
|
185
|
+
return (
|
|
186
|
+
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
|
187
|
+
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z" />
|
|
188
|
+
</svg>
|
|
189
|
+
);
|
|
190
|
+
} else if (gatewayId === "saleor.app.payment.stripe") {
|
|
191
|
+
// Stripe icon
|
|
192
|
+
return (
|
|
193
|
+
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="#635BFF">
|
|
194
|
+
<path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z" />
|
|
195
|
+
</svg>
|
|
196
|
+
);
|
|
197
|
+
} else if (gatewayId === "saleor.app.payment.paypal") {
|
|
198
|
+
// PayPal icon
|
|
199
|
+
return (
|
|
200
|
+
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="#00457C">
|
|
201
|
+
<path d="M20.067 8.478c.492.88.556 2.014.3 3.327-.74 3.806-3.276 5.12-6.514 5.12h-.5a.805.805 0 00-.794.68l-.04.22-.63 3.993-.028.15a.806.806 0 01-.795.679H7.72a.483.483 0 01-.477-.558L8.926 14.5h1.513c3.238 0 5.774-1.314 6.514-5.12.145-.746.096-1.433-.204-2.03a2.638 2.638 0 00-.428-.61c-.287.974-.835 1.776-1.637 2.397-1.056.818-2.477 1.223-4.212 1.223H8.66l-.571 3.626H6.456l1.67-10.598h4.346c2.025 0 3.537.404 4.516 1.205.975.8 1.426 1.98 1.346 3.51.039.003.072.013.11.02.29.073.558.184.808.334.473.284.862.675 1.165 1.149z" />
|
|
202
|
+
</svg>
|
|
203
|
+
);
|
|
204
|
+
} else if (gatewayId === "saleor.app.affirm") {
|
|
205
|
+
// Affirm icon
|
|
206
|
+
return (
|
|
207
|
+
<svg
|
|
208
|
+
width="25"
|
|
209
|
+
height="25"
|
|
210
|
+
viewBox="0 0 175 129"
|
|
211
|
+
fill="none"
|
|
212
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
213
|
+
>
|
|
214
|
+
<g clipPath="url(#clip0_2525_341)">
|
|
215
|
+
<path
|
|
216
|
+
d="M28.5299 125.9C21.2699 114.84 17.0399 101.64 17.0399 87.4498C17.0399 48.6898 48.5699 17.1598 87.3299 17.1598C126.09 17.1598 157.62 48.6898 157.62 87.4498C157.62 101.63 153.38 114.84 146.13 125.9H165.81C171.52 114.29 174.74 101.24 174.74 87.4498C174.74 39.2498 135.53 0.0498047 87.3399 0.0498047C39.1499 0.0498047 -0.0600586 39.2598 -0.0600586 87.4498C-0.0600586 101.24 3.15994 114.29 8.86994 125.9H28.5299Z"
|
|
217
|
+
fill="#4A4AF4"
|
|
218
|
+
/>
|
|
219
|
+
<path
|
|
220
|
+
d="M88.5098 45.9199C75.7098 45.9199 60.9798 51.9499 52.9798 58.3299L60.2798 73.6999C66.6898 67.8299 77.0598 62.8199 86.4098 62.8199C95.2998 62.8199 100.2 65.7899 100.2 71.7799C100.2 75.8099 96.9398 78.0799 90.7998 78.6399C67.7398 80.7599 49.8198 87.9599 49.8198 105.66C49.8198 119.7 59.9398 128.18 76.5898 128.18C87.7298 128.18 96.4798 121.99 101.2 113.82V125.89H121.96V75.2999C121.97 54.4099 107.44 45.9199 88.5098 45.9199ZM82.4498 111.96C75.7198 111.96 72.0298 109.08 72.0298 104.35C72.0298 94.4899 84.0798 92.1099 99.7698 92.1099C99.7698 102.43 92.8598 111.96 82.4498 111.96Z"
|
|
221
|
+
fill="black"
|
|
222
|
+
/>
|
|
223
|
+
</g>
|
|
224
|
+
<defs>
|
|
225
|
+
<clipPath id="clip0_2525_341">
|
|
226
|
+
<rect width="174.82" height="128.16" fill="white" />
|
|
227
|
+
</clipPath>
|
|
228
|
+
</defs>
|
|
229
|
+
</svg>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
// Default credit card icon
|
|
233
|
+
return (
|
|
234
|
+
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
|
235
|
+
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z" />
|
|
236
|
+
</svg>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Helper function to get payment gateway display name
|
|
241
|
+
const getPaymentGatewayDisplayName = (gatewayName: string): string => {
|
|
242
|
+
if (gatewayName.toLowerCase().includes("authorize")) {
|
|
243
|
+
return "Authorize.Net";
|
|
244
|
+
} else if (gatewayName.toLowerCase().includes("stripe")) {
|
|
245
|
+
return "Stripe";
|
|
246
|
+
} else if (gatewayName.toLowerCase().includes("paypal")) {
|
|
247
|
+
return "PayPal";
|
|
248
|
+
} else if (gatewayName.toLowerCase().includes("affirm")) {
|
|
249
|
+
return "Affirm";
|
|
250
|
+
}
|
|
251
|
+
return gatewayName;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export function SaleorNativePayment({
|
|
255
|
+
checkoutId,
|
|
256
|
+
totalAmount,
|
|
257
|
+
onSuccess,
|
|
258
|
+
onError,
|
|
259
|
+
onCheckoutBlocked,
|
|
260
|
+
setIsProcessingPayment,
|
|
261
|
+
availablePaymentGateways = [],
|
|
262
|
+
kountConfig,
|
|
263
|
+
onStartPayment,
|
|
264
|
+
selectedShippingId,
|
|
265
|
+
userEmail,
|
|
266
|
+
guestEmail,
|
|
267
|
+
lineItems = [],
|
|
268
|
+
billingAddress,
|
|
269
|
+
shippingAddress,
|
|
270
|
+
questionsValid = true,
|
|
271
|
+
termsAccepted = true,
|
|
272
|
+
termsData,
|
|
273
|
+
onTermsModalOpen,
|
|
274
|
+
onTermsAcceptedChange,
|
|
275
|
+
taxInfo,
|
|
276
|
+
disabled = false,
|
|
277
|
+
onPaymentReady,
|
|
278
|
+
}: SaleorNativePaymentProps) {
|
|
279
|
+
// Filter out dummy payment gateways
|
|
280
|
+
const filteredPaymentGateways = availablePaymentGateways.filter(
|
|
281
|
+
(gateway) =>
|
|
282
|
+
!gateway.id.toLowerCase().includes("dummy") &&
|
|
283
|
+
!gateway.name.toLowerCase().includes("dummy") &&
|
|
284
|
+
!gateway.id.toLowerCase().includes("saleor.io.gift-card-payment-gateway"
|
|
285
|
+
) && !gateway.name.toLowerCase().includes("Gift Card Payment Gateway")
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Avoid noisy logs in templates/production.
|
|
289
|
+
|
|
290
|
+
const router = useRouter();
|
|
291
|
+
const { isLoggedIn, user } = useGlobalStore();
|
|
292
|
+
const { recaptchaRef, resetRecaptcha } = useRecaptcha();
|
|
293
|
+
const config = useAppConfiguration();
|
|
294
|
+
const gtmConfig = config.getGoogleTagManagerConfig();
|
|
295
|
+
|
|
296
|
+
const [cardData, setCardData] = useState<CardData>({
|
|
297
|
+
cardNumber: "",
|
|
298
|
+
expirationDate: "",
|
|
299
|
+
cardCode: "",
|
|
300
|
+
fullName: "",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const [selectedPaymentGateway, setSelectedPaymentGateway] = useState<string>(
|
|
304
|
+
filteredPaymentGateways.find(
|
|
305
|
+
(gateway) => gateway.id === "mirumee.payments.authorize_net"
|
|
306
|
+
)?.id ||
|
|
307
|
+
filteredPaymentGateways[0]?.id ||
|
|
308
|
+
"mirumee.payments.authorize_net"
|
|
309
|
+
);
|
|
310
|
+
const [validationErrors, setValidationErrors] = useState<
|
|
311
|
+
Record<string, string>
|
|
312
|
+
>({});
|
|
313
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
314
|
+
const [recaptchaValue, setRecaptchaValue] = useState<string | null>(null);
|
|
315
|
+
const [_isFraudCheckRunning, setIsFraudCheckRunning] = useState(false);
|
|
316
|
+
const [kountOrderData, setKountOrderData] = useState<{
|
|
317
|
+
kountOrderId: string;
|
|
318
|
+
transactionId: string;
|
|
319
|
+
} | null>(null);
|
|
320
|
+
const [lastFraudCheckData, setLastFraudCheckData] = useState<{
|
|
321
|
+
kountOrderId: string;
|
|
322
|
+
transactionId: string;
|
|
323
|
+
} | null>(null);
|
|
324
|
+
const [isPaymentInProgress, setIsPaymentInProgress] = useState(false);
|
|
325
|
+
const [isCustomerAttached, setIsCustomerAttached] = useState(false);
|
|
326
|
+
const [fraudCheckCompleted, setFraudCheckCompleted] = useState(false);
|
|
327
|
+
|
|
328
|
+
// Reset customer attachment and fraud check state when checkout ID changes
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
setIsCustomerAttached(false);
|
|
331
|
+
setFraudCheckCompleted(false);
|
|
332
|
+
}, [checkoutId]);
|
|
333
|
+
|
|
334
|
+
// Reset fraud check state when email changes
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
const effectiveEmail = user?.email || userEmail || guestEmail;
|
|
337
|
+
if (effectiveEmail) {
|
|
338
|
+
setFraudCheckCompleted(false);
|
|
339
|
+
setKountOrderData(null);
|
|
340
|
+
setLastFraudCheckData(null);
|
|
341
|
+
}
|
|
342
|
+
}, [user?.email, userEmail, guestEmail]);
|
|
343
|
+
|
|
344
|
+
const [checkoutComplete] = useMutation<
|
|
345
|
+
CheckoutCompleteData,
|
|
346
|
+
CheckoutCompleteVars
|
|
347
|
+
>(CHECKOUT_COMPLETE);
|
|
348
|
+
const [checkoutPaymentCreate] = useMutation<
|
|
349
|
+
CheckoutPaymentCreateData,
|
|
350
|
+
CheckoutPaymentCreateVars
|
|
351
|
+
>(CHECKOUT_PAYMENT_CREATE);
|
|
352
|
+
const [attachCustomer] = useMutation<
|
|
353
|
+
CheckoutCustomerAttachData,
|
|
354
|
+
CheckoutCustomerAttachVars
|
|
355
|
+
>(CHECKOUT_CUSTOMER_ATTACH);
|
|
356
|
+
const [updateDeliveryMethod] = useMutation<
|
|
357
|
+
CheckoutDeliveryMethodUpdateData,
|
|
358
|
+
CheckoutDeliveryMethodUpdateVars
|
|
359
|
+
>(CHECKOUT_DELIVERY_METHOD_UPDATE);
|
|
360
|
+
const [updateCheckoutMetadata] = useMutation<
|
|
361
|
+
UpdateCheckoutMetadataData,
|
|
362
|
+
UpdateCheckoutMetadataVariables
|
|
363
|
+
>(UPDATE_CHECKOUT_METADATA);
|
|
364
|
+
const [getCheckoutById] = useLazyQuery(CHECKOUT_BY_ID);
|
|
365
|
+
|
|
366
|
+
const validateForm = useCallback(() => {
|
|
367
|
+
const errors: Record<string, string> = {};
|
|
368
|
+
|
|
369
|
+
if (!cardData.cardNumber.replace(/\s/g, "")) {
|
|
370
|
+
errors.cardNumber = "Card number is required";
|
|
371
|
+
} else if (
|
|
372
|
+
!/^[\d\s]{13,19}$/.test(cardData.cardNumber.replace(/\s/g, ""))
|
|
373
|
+
) {
|
|
374
|
+
errors.cardNumber = "Please enter a valid card number";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!cardData.expirationDate) {
|
|
378
|
+
errors.expirationDate = "Expiration date is required";
|
|
379
|
+
} else if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(cardData.expirationDate)) {
|
|
380
|
+
errors.expirationDate = "Please enter a valid expiration date (MM/YY)";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!cardData.cardCode) {
|
|
384
|
+
errors.cardCode = "Security code is required";
|
|
385
|
+
} else if (!/^\d{3,4}$/.test(cardData.cardCode)) {
|
|
386
|
+
errors.cardCode = "Please enter a valid security code";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!cardData.fullName.trim()) {
|
|
390
|
+
errors.fullName = "Cardholder name is required";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Only require reCAPTCHA if enabled for checkout
|
|
394
|
+
if (config.isRecaptchaEnabledFor("checkout") && !recaptchaValue) {
|
|
395
|
+
errors.recaptcha = "Please complete the reCAPTCHA verification";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
setValidationErrors(errors);
|
|
399
|
+
return Object.keys(errors).length === 0;
|
|
400
|
+
}, [cardData, recaptchaValue, config]);
|
|
401
|
+
|
|
402
|
+
const formatCardNumber = (value: string): string => {
|
|
403
|
+
const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
|
|
404
|
+
const matches = v.match(/\d{4,16}/g);
|
|
405
|
+
const match = (matches && matches[0]) || "";
|
|
406
|
+
const parts = [];
|
|
407
|
+
for (let i = 0, len = match.length; i < len; i += 4) {
|
|
408
|
+
parts.push(match.substring(i, i + 4));
|
|
409
|
+
}
|
|
410
|
+
if (parts.length) {
|
|
411
|
+
return parts.join(" ");
|
|
412
|
+
} else {
|
|
413
|
+
return v;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const formatExpirationDate = (value: string): string => {
|
|
418
|
+
const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
|
|
419
|
+
if (v.length >= 2) {
|
|
420
|
+
return v.substring(0, 2) + "/" + v.substring(2, 4);
|
|
421
|
+
}
|
|
422
|
+
return v;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const handleInputChange = useCallback(
|
|
426
|
+
(field: string, value: string) => {
|
|
427
|
+
let formattedValue = value;
|
|
428
|
+
|
|
429
|
+
if (field === "cardNumber") {
|
|
430
|
+
formattedValue = formatCardNumber(value);
|
|
431
|
+
} else if (field === "expirationDate") {
|
|
432
|
+
formattedValue = formatExpirationDate(value);
|
|
433
|
+
} else if (field === "cardCode") {
|
|
434
|
+
formattedValue = value.replace(/[^0-9]/g, "");
|
|
435
|
+
if (formattedValue.length > 4) return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
setCardData((prev) => ({ ...prev, [field]: formattedValue }));
|
|
439
|
+
|
|
440
|
+
// Clear validation error for this field
|
|
441
|
+
if (validationErrors[field]) {
|
|
442
|
+
setValidationErrors((prev) => {
|
|
443
|
+
const newErrors = { ...prev };
|
|
444
|
+
delete newErrors[field];
|
|
445
|
+
return newErrors;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
[validationErrors]
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// Function to perform Kount fraud check
|
|
453
|
+
const performKountFraudCheck = useCallback(async () => {
|
|
454
|
+
// Prevent duplicate fraud checks
|
|
455
|
+
if (fraudCheckCompleted) {
|
|
456
|
+
return kountOrderData;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!kountConfig?.appConfiguration || !lineItems.length) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const { activePaymentMethods, ipExclusions } = kountConfig.appConfiguration;
|
|
464
|
+
|
|
465
|
+
// Check if selected payment method requires fraud detection
|
|
466
|
+
const shouldPerformFraudCheck = activePaymentMethods.some(
|
|
467
|
+
(method) => method.toLowerCase() === selectedPaymentGateway.toLowerCase()
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (!shouldPerformFraudCheck) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Variable to store fraud check data
|
|
475
|
+
let kountData: { kountOrderId: string; transactionId: string } | null =
|
|
476
|
+
null;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
setIsFraudCheckRunning(true);
|
|
480
|
+
|
|
481
|
+
// Get user IP first
|
|
482
|
+
const userIp = await getUserIP();
|
|
483
|
+
|
|
484
|
+
// Check if user IP is in exclusions list - if so, bypass fraud check
|
|
485
|
+
if (
|
|
486
|
+
ipExclusions &&
|
|
487
|
+
Array.isArray(ipExclusions) &&
|
|
488
|
+
ipExclusions.includes(userIp)
|
|
489
|
+
) {
|
|
490
|
+
setFraudCheckCompleted(true); // Mark as completed to prevent duplicate calls
|
|
491
|
+
return null; // Return null to indicate no fraud check data
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Prepare account info
|
|
495
|
+
const effectiveEmail = user?.email || userEmail || guestEmail;
|
|
496
|
+
if (!effectiveEmail) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const accountId = user?.id || effectiveEmail || `guest-${checkoutId}`;
|
|
501
|
+
const accountUsername = effectiveEmail;
|
|
502
|
+
const accountCreationDate = new Date().toISOString(); // Always use current time for simplicity
|
|
503
|
+
|
|
504
|
+
// Generate a valid device session ID that meets Kount requirements
|
|
505
|
+
const deviceSessionId = generateDeviceSessionId(user?.id, checkoutId);
|
|
506
|
+
|
|
507
|
+
// Prepare request data
|
|
508
|
+
const fraudCheckRequest: KountFraudCheckRequest = {
|
|
509
|
+
merchantOrderId: checkoutId,
|
|
510
|
+
deviceSessionId: deviceSessionId,
|
|
511
|
+
userIp,
|
|
512
|
+
account: {
|
|
513
|
+
id: accountId,
|
|
514
|
+
type: "REGISTERED",
|
|
515
|
+
creationDateTime: formatRFC3339Date(new Date(accountCreationDate)),
|
|
516
|
+
username: accountUsername,
|
|
517
|
+
accountIsActive: true,
|
|
518
|
+
},
|
|
519
|
+
items: lineItems.map((item) => ({
|
|
520
|
+
id: item.id,
|
|
521
|
+
price: Math.round(item.price * 100), // Convert dollars to cents
|
|
522
|
+
description: item.name,
|
|
523
|
+
name: item.name,
|
|
524
|
+
quantity: item.quantity,
|
|
525
|
+
category: item.category || "Products",
|
|
526
|
+
subCategory: "",
|
|
527
|
+
isDigital: false,
|
|
528
|
+
sku: item.sku || "", // Use actual SKU only
|
|
529
|
+
})),
|
|
530
|
+
fulfillment: [
|
|
531
|
+
{
|
|
532
|
+
merchantFulfillmentId: checkoutId,
|
|
533
|
+
type: "LOCAL_DELIVERY",
|
|
534
|
+
status: "UNFULFILLED",
|
|
535
|
+
items: lineItems.map((item) => ({
|
|
536
|
+
id: item.id,
|
|
537
|
+
quantity: item.quantity,
|
|
538
|
+
})),
|
|
539
|
+
shipping: {
|
|
540
|
+
amount: 0, // Will be updated with actual shipping cost
|
|
541
|
+
provider: "Standard",
|
|
542
|
+
method: "STANDARD",
|
|
543
|
+
},
|
|
544
|
+
recipientPerson: {
|
|
545
|
+
name: {
|
|
546
|
+
first: shippingAddress?.firstName || "Guest",
|
|
547
|
+
family: shippingAddress?.lastName || "User",
|
|
548
|
+
},
|
|
549
|
+
emailAddress: effectiveEmail,
|
|
550
|
+
phoneNumber: shippingAddress?.phone || "+15551234567",
|
|
551
|
+
address: {
|
|
552
|
+
line1: shippingAddress?.address || "123 Main St",
|
|
553
|
+
city: shippingAddress?.city || "Unknown",
|
|
554
|
+
region: shippingAddress?.state || "Unknown",
|
|
555
|
+
postalCode: shippingAddress?.zipCode || "00000",
|
|
556
|
+
countryCode: shippingAddress?.country || "US",
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
transactions: [
|
|
562
|
+
{
|
|
563
|
+
merchantTransactionId: generateTransactionId(),
|
|
564
|
+
subtotal: Math.round(totalAmount * 100), // Convert dollars to cents
|
|
565
|
+
orderTotal: Math.round(totalAmount * 100), // Convert dollars to cents
|
|
566
|
+
currency: "USD",
|
|
567
|
+
transactionStatus: "PENDING",
|
|
568
|
+
billedPerson: {
|
|
569
|
+
name: {
|
|
570
|
+
first:
|
|
571
|
+
billingAddress?.firstName ||
|
|
572
|
+
shippingAddress?.firstName ||
|
|
573
|
+
"Guest",
|
|
574
|
+
family:
|
|
575
|
+
billingAddress?.lastName ||
|
|
576
|
+
shippingAddress?.lastName ||
|
|
577
|
+
"User",
|
|
578
|
+
},
|
|
579
|
+
emailAddress: effectiveEmail,
|
|
580
|
+
phoneNumber:
|
|
581
|
+
billingAddress?.phone ||
|
|
582
|
+
shippingAddress?.phone ||
|
|
583
|
+
"+15551234567",
|
|
584
|
+
address: {
|
|
585
|
+
line1:
|
|
586
|
+
billingAddress?.address ||
|
|
587
|
+
shippingAddress?.address ||
|
|
588
|
+
"123 Main St",
|
|
589
|
+
city:
|
|
590
|
+
billingAddress?.city || shippingAddress?.city || "Unknown",
|
|
591
|
+
region:
|
|
592
|
+
billingAddress?.state || shippingAddress?.state || "Unknown",
|
|
593
|
+
postalCode:
|
|
594
|
+
billingAddress?.zipCode ||
|
|
595
|
+
shippingAddress?.zipCode ||
|
|
596
|
+
"00000",
|
|
597
|
+
countryCode:
|
|
598
|
+
billingAddress?.country || shippingAddress?.country || "US",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
items: lineItems.map((item) => ({
|
|
602
|
+
id: item.id,
|
|
603
|
+
quantity: item.quantity,
|
|
604
|
+
})),
|
|
605
|
+
tax: {
|
|
606
|
+
isTaxable: (taxInfo?.totalTax || 0) > 0,
|
|
607
|
+
taxableCountryCode:
|
|
608
|
+
billingAddress?.country || shippingAddress?.country || "US",
|
|
609
|
+
taxAmount: Math.round((taxInfo?.totalTax || 0) * 100), // Convert dollars to cents
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Perform fraud check
|
|
616
|
+
const fraudCheckResponse = await kountApi.performFraudCheck(
|
|
617
|
+
fraudCheckRequest
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Store Kount order data for later update calls
|
|
621
|
+
kountData = {
|
|
622
|
+
kountOrderId: fraudCheckResponse.order.orderId,
|
|
623
|
+
transactionId:
|
|
624
|
+
fraudCheckResponse.order.transactions[0]?.transactionId ||
|
|
625
|
+
generateTransactionId(),
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
setKountOrderData(kountData);
|
|
629
|
+
setLastFraudCheckData(kountData); // Store as backup for immediate use
|
|
630
|
+
setFraudCheckCompleted(true); // Mark fraud check as completed
|
|
631
|
+
|
|
632
|
+
// Save fraud check results to checkout metadata
|
|
633
|
+
const riskInquiry = fraudCheckResponse.order.riskInquiry;
|
|
634
|
+
const persona = riskInquiry.persona;
|
|
635
|
+
|
|
636
|
+
const metadataInput = [
|
|
637
|
+
{ key: "kount_decision", value: riskInquiry.decision },
|
|
638
|
+
{ key: "omniscore", value: riskInquiry.omniscore.toString() },
|
|
639
|
+
{ key: "uniqueCards", value: persona.uniqueCards.toString() },
|
|
640
|
+
{ key: "uniqueDevices", value: persona.uniqueDevices.toString() },
|
|
641
|
+
{ key: "uniqueEmails", value: persona.uniqueEmails.toString() },
|
|
642
|
+
{ key: "riskiestCountry", value: persona.riskiestCountry },
|
|
643
|
+
{
|
|
644
|
+
key: "totalBankApprovedOrders",
|
|
645
|
+
value: persona.totalBankApprovedOrders.toString(),
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
key: "totalBankDeclinedOrders",
|
|
649
|
+
value: persona.totalBankDeclinedOrders.toString(),
|
|
650
|
+
},
|
|
651
|
+
{ key: "maxVelocity", value: persona.maxVelocity.toString() },
|
|
652
|
+
{ key: "riskiestRegion", value: persona.riskiestRegion },
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
await updateCheckoutMetadata({
|
|
656
|
+
variables: {
|
|
657
|
+
id: checkoutId,
|
|
658
|
+
input: metadataInput,
|
|
659
|
+
},
|
|
660
|
+
}).catch((error) => {
|
|
661
|
+
// Handle checkout resolution errors immediately
|
|
662
|
+
if (error.message?.includes("Couldn't resolve to a node")) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
"Your checkout session has expired during fraud check. Please refresh the page and try again."
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
throw error;
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Check if decision is DECLINE and if checkout should be blocked
|
|
671
|
+
const { blockCheckout, blockedCheckoutMessage } =
|
|
672
|
+
kountConfig.appConfiguration;
|
|
673
|
+
const decision = riskInquiry.decision;
|
|
674
|
+
|
|
675
|
+
if (decision === "DECLINE" && blockCheckout) {
|
|
676
|
+
const errorMessage =
|
|
677
|
+
blockedCheckoutMessage ||
|
|
678
|
+
"Your order cannot be processed due to security concerns. Please contact support for assistance.";
|
|
679
|
+
|
|
680
|
+
// Always call the blocked callback to show UI
|
|
681
|
+
if (onCheckoutBlocked) {
|
|
682
|
+
onCheckoutBlocked(errorMessage);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Always throw error to stop payment processing completely
|
|
686
|
+
const blockingError = new Error(errorMessage);
|
|
687
|
+
(
|
|
688
|
+
blockingError as Error & { isKountBlocking: boolean }
|
|
689
|
+
).isKountBlocking = true;
|
|
690
|
+
throw blockingError;
|
|
691
|
+
}
|
|
692
|
+
} catch (error) {
|
|
693
|
+
// If the error is due to blocked checkout, always re-throw to stop payment processing
|
|
694
|
+
if (
|
|
695
|
+
error instanceof Error &&
|
|
696
|
+
(error as Error & { isKountBlocking?: boolean }).isKountBlocking
|
|
697
|
+
) {
|
|
698
|
+
throw error; // Re-throw blocking error to stop payment processing
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Handle specific GraphQL/checkout errors
|
|
702
|
+
if (
|
|
703
|
+
error instanceof Error &&
|
|
704
|
+
error.message.includes("Couldn't resolve to a node")
|
|
705
|
+
) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
"Your checkout session has expired. Please refresh the page and try again."
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Don't block the payment process if fraud check API fails
|
|
712
|
+
} finally {
|
|
713
|
+
setIsFraudCheckRunning(false);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Return the kount data (could be null if fraud check was bypassed or failed)
|
|
717
|
+
return kountData;
|
|
718
|
+
}, [
|
|
719
|
+
kountConfig,
|
|
720
|
+
selectedPaymentGateway,
|
|
721
|
+
lineItems,
|
|
722
|
+
user,
|
|
723
|
+
userEmail,
|
|
724
|
+
guestEmail,
|
|
725
|
+
checkoutId,
|
|
726
|
+
totalAmount,
|
|
727
|
+
shippingAddress,
|
|
728
|
+
billingAddress,
|
|
729
|
+
updateCheckoutMetadata,
|
|
730
|
+
onCheckoutBlocked,
|
|
731
|
+
fraudCheckCompleted,
|
|
732
|
+
setFraudCheckCompleted,
|
|
733
|
+
kountOrderData,
|
|
734
|
+
]);
|
|
735
|
+
|
|
736
|
+
// Function to update Kount order with payment result
|
|
737
|
+
const updateKountOrderStatus = useCallback(
|
|
738
|
+
async (
|
|
739
|
+
isSuccess: boolean,
|
|
740
|
+
paymentToken: string,
|
|
741
|
+
cardNumber: string,
|
|
742
|
+
providedKountData?: { kountOrderId: string; transactionId: string } | null
|
|
743
|
+
) => {
|
|
744
|
+
// Use provided data first, then fallback to state data
|
|
745
|
+
const dataToUse =
|
|
746
|
+
providedKountData || kountOrderData || lastFraudCheckData;
|
|
747
|
+
|
|
748
|
+
if (!dataToUse) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const bin = cardNumber.replace(/\s/g, "").substring(0, 6);
|
|
754
|
+
const detectedPaymentType = detectPaymentType(
|
|
755
|
+
cardNumber,
|
|
756
|
+
selectedPaymentGateway
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const updateRequest: KountOrderUpdateRequest = {
|
|
760
|
+
kountOrderId: dataToUse.kountOrderId,
|
|
761
|
+
transactions: [
|
|
762
|
+
{
|
|
763
|
+
transactionId: dataToUse.transactionId,
|
|
764
|
+
paymentStatus: isSuccess ? "AUTHORIZED" : "REFUSED",
|
|
765
|
+
authorizationStatus: {
|
|
766
|
+
authResult: isSuccess ? "Approved" : "Declined",
|
|
767
|
+
verificationResponse: {
|
|
768
|
+
cvvStatus: "Match", // Could be enhanced to detect actual CVV status
|
|
769
|
+
avsStatus: "Y", // Could be enhanced to detect actual AVS status
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
payment: {
|
|
773
|
+
type: detectedPaymentType,
|
|
774
|
+
paymentToken: paymentToken,
|
|
775
|
+
bin: bin,
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
await kountApi.updateKountOrder(updateRequest);
|
|
782
|
+
} catch {
|
|
783
|
+
// Don't fail the payment process if Kount update fails
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
[kountOrderData, lastFraudCheckData]
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
const processPayment = useCallback(async () => {
|
|
790
|
+
// Prevent duplicate payment processing
|
|
791
|
+
if (isPaymentInProgress) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!validateForm()) return;
|
|
796
|
+
|
|
797
|
+
// Validate checkout ID exists and is accessible before starting payment
|
|
798
|
+
try {
|
|
799
|
+
const checkoutValidation = await getCheckoutById({
|
|
800
|
+
variables: { id: checkoutId },
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
if (!checkoutValidation.data?.checkout) {
|
|
804
|
+
onError(
|
|
805
|
+
"Your checkout session has expired. Please refresh the page and try again."
|
|
806
|
+
);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Check if customer is already attached to this checkout
|
|
811
|
+
const checkout = checkoutValidation.data.checkout;
|
|
812
|
+
if (isLoggedIn && user?.id && checkout.user?.id === user.id) {
|
|
813
|
+
setIsCustomerAttached(true);
|
|
814
|
+
}
|
|
815
|
+
} catch (validationError) {
|
|
816
|
+
if (
|
|
817
|
+
validationError instanceof Error &&
|
|
818
|
+
validationError.message.includes("Couldn't resolve to a node")
|
|
819
|
+
) {
|
|
820
|
+
onError(
|
|
821
|
+
"Your checkout session has expired. Please refresh the page and try again."
|
|
822
|
+
);
|
|
823
|
+
} else {
|
|
824
|
+
onError(
|
|
825
|
+
"Unable to validate checkout. Please refresh the page and try again."
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Check reCAPTCHA verification before payment if enabled
|
|
832
|
+
if (config.isRecaptchaEnabledFor("checkout") && !recaptchaValue) {
|
|
833
|
+
onError(
|
|
834
|
+
"Please complete the reCAPTCHA verification before proceeding with payment."
|
|
835
|
+
);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Set payment in progress flag
|
|
840
|
+
setIsPaymentInProgress(true);
|
|
841
|
+
|
|
842
|
+
// Variable to store fraud check data across the whole payment process
|
|
843
|
+
let currentFraudCheckData: {
|
|
844
|
+
kountOrderId: string;
|
|
845
|
+
transactionId: string;
|
|
846
|
+
} | null = null;
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
// STEP 1: FRAUD CHECK FIRST - before any processing or loading states
|
|
850
|
+
currentFraudCheckData = (await performKountFraudCheck()) || null;
|
|
851
|
+
|
|
852
|
+
// STEP 2: Only if fraud check passes, start payment processing
|
|
853
|
+
setIsProcessing(true);
|
|
854
|
+
setIsProcessingPayment({
|
|
855
|
+
isModalOpen: true,
|
|
856
|
+
paymentProcessingLoading: true,
|
|
857
|
+
error: false,
|
|
858
|
+
success: false,
|
|
859
|
+
});
|
|
860
|
+
} catch (error) {
|
|
861
|
+
// Handle fraud check blocking error
|
|
862
|
+
if (
|
|
863
|
+
error instanceof Error &&
|
|
864
|
+
(error as Error & { isKountBlocking?: boolean }).isKountBlocking
|
|
865
|
+
) {
|
|
866
|
+
// Blocked UI is already shown via onCheckoutBlocked callback
|
|
867
|
+
// Just ensure processing states are reset
|
|
868
|
+
setIsProcessing(false);
|
|
869
|
+
setIsPaymentInProgress(false);
|
|
870
|
+
setIsProcessingPayment({
|
|
871
|
+
isModalOpen: false,
|
|
872
|
+
paymentProcessingLoading: false,
|
|
873
|
+
error: false,
|
|
874
|
+
success: false,
|
|
875
|
+
});
|
|
876
|
+
return; // Stop all processing
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// For other fraud check errors, continue with payment
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Declare paymentToken in broader scope for error handling
|
|
883
|
+
let paymentToken = "";
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
// Call onStartPayment callback if provided
|
|
887
|
+
if (onStartPayment) {
|
|
888
|
+
await onStartPayment();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Store selectedShippingId for payment recovery scenarios
|
|
892
|
+
if (selectedShippingId) {
|
|
893
|
+
try {
|
|
894
|
+
const paymentData = {
|
|
895
|
+
selectedShippingId,
|
|
896
|
+
timestamp: Date.now(),
|
|
897
|
+
};
|
|
898
|
+
localStorage.setItem(
|
|
899
|
+
"pendingPaymentData",
|
|
900
|
+
JSON.stringify(paymentData)
|
|
901
|
+
);
|
|
902
|
+
} catch {
|
|
903
|
+
// Continue if storage fails
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Attach customer if logged in and not already attached
|
|
908
|
+
if (isLoggedIn && user?.email && user?.id && !isCustomerAttached) {
|
|
909
|
+
try {
|
|
910
|
+
await attachCustomer({
|
|
911
|
+
variables: {
|
|
912
|
+
checkoutId,
|
|
913
|
+
customerId: user.id,
|
|
914
|
+
},
|
|
915
|
+
}).catch((error) => {
|
|
916
|
+
// Handle checkout resolution errors immediately
|
|
917
|
+
if (error.message?.includes("Couldn't resolve to a node")) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
"Your checkout session has expired while attaching customer. Please refresh the page and try again."
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
// Handle already attached error specifically
|
|
923
|
+
if (
|
|
924
|
+
error.message?.includes(
|
|
925
|
+
"cannot reassign a checkout that is already attached"
|
|
926
|
+
)
|
|
927
|
+
) {
|
|
928
|
+
setIsCustomerAttached(true);
|
|
929
|
+
return; // Don't throw error, just continue
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// Mark as attached after successful call
|
|
934
|
+
setIsCustomerAttached(true);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
// Re-throw checkout expiration errors, continue for others
|
|
937
|
+
if (
|
|
938
|
+
error instanceof Error &&
|
|
939
|
+
error.message.includes("checkout session has expired")
|
|
940
|
+
) {
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
// Handle already attached error specifically
|
|
944
|
+
if (
|
|
945
|
+
error instanceof Error &&
|
|
946
|
+
error.message.includes(
|
|
947
|
+
"cannot reassign a checkout that is already attached"
|
|
948
|
+
)
|
|
949
|
+
) {
|
|
950
|
+
setIsCustomerAttached(true);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Set delivery method if provided
|
|
956
|
+
if (selectedShippingId) {
|
|
957
|
+
try {
|
|
958
|
+
const deliveryResult = await updateDeliveryMethod({
|
|
959
|
+
variables: {
|
|
960
|
+
id: checkoutId,
|
|
961
|
+
deliveryMethodId: selectedShippingId,
|
|
962
|
+
},
|
|
963
|
+
}).catch((error) => {
|
|
964
|
+
// Handle checkout resolution errors immediately
|
|
965
|
+
if (error.message?.includes("Couldn't resolve to a node")) {
|
|
966
|
+
throw new Error(
|
|
967
|
+
"Your checkout session has expired while setting delivery method. Please refresh the page and try again."
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
throw error;
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
if (
|
|
974
|
+
deliveryResult.data?.checkoutDeliveryMethodUpdate?.errors?.length
|
|
975
|
+
) {
|
|
976
|
+
const deliveryErrors =
|
|
977
|
+
deliveryResult.data.checkoutDeliveryMethodUpdate.errors;
|
|
978
|
+
setIsProcessingPayment({
|
|
979
|
+
isModalOpen: false,
|
|
980
|
+
paymentProcessingLoading: false,
|
|
981
|
+
error: true,
|
|
982
|
+
success: false,
|
|
983
|
+
});
|
|
984
|
+
onError(
|
|
985
|
+
`Failed to set delivery method: ${deliveryErrors[0].message}`
|
|
986
|
+
);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
} catch {
|
|
990
|
+
setIsProcessingPayment({
|
|
991
|
+
isModalOpen: false,
|
|
992
|
+
paymentProcessingLoading: false,
|
|
993
|
+
error: true,
|
|
994
|
+
success: false,
|
|
995
|
+
});
|
|
996
|
+
onError("Failed to set delivery method. Please try again.");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Track payment info event
|
|
1002
|
+
if (lineItems.length > 0) {
|
|
1003
|
+
const products: Product[] = lineItems.map((item, index) => ({
|
|
1004
|
+
item_id: item.id,
|
|
1005
|
+
item_name: item.name,
|
|
1006
|
+
item_category: item.category || "Products",
|
|
1007
|
+
price: item.price,
|
|
1008
|
+
quantity: item.quantity,
|
|
1009
|
+
currency: "USD",
|
|
1010
|
+
index: index,
|
|
1011
|
+
}));
|
|
1012
|
+
|
|
1013
|
+
const totalValue = lineItems.reduce(
|
|
1014
|
+
(sum, item) => sum + item.price * item.quantity,
|
|
1015
|
+
0
|
|
1016
|
+
);
|
|
1017
|
+
const paymentMethodName = selectedPaymentGateway.includes(
|
|
1018
|
+
"authorize_net"
|
|
1019
|
+
)
|
|
1020
|
+
? "authorize_net"
|
|
1021
|
+
: selectedPaymentGateway.includes("stripe")
|
|
1022
|
+
? "stripe"
|
|
1023
|
+
: selectedPaymentGateway.includes("dummy")
|
|
1024
|
+
? "dummy"
|
|
1025
|
+
: "other";
|
|
1026
|
+
|
|
1027
|
+
gtmAddPaymentInfo(
|
|
1028
|
+
products,
|
|
1029
|
+
"USD",
|
|
1030
|
+
totalValue,
|
|
1031
|
+
undefined,
|
|
1032
|
+
paymentMethodName,
|
|
1033
|
+
gtmConfig?.container_id
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Step 1: Get selected payment gateway from available gateways or checkout
|
|
1038
|
+
let selectedGateway;
|
|
1039
|
+
|
|
1040
|
+
if (filteredPaymentGateways.length > 0) {
|
|
1041
|
+
selectedGateway = filteredPaymentGateways.find(
|
|
1042
|
+
(gateway) => gateway.id === selectedPaymentGateway
|
|
1043
|
+
);
|
|
1044
|
+
} else {
|
|
1045
|
+
// Fallback: Get gateway from checkout if not provided via props
|
|
1046
|
+
const checkoutResult = await getCheckoutById({
|
|
1047
|
+
variables: { id: checkoutId },
|
|
1048
|
+
});
|
|
1049
|
+
selectedGateway =
|
|
1050
|
+
checkoutResult.data?.checkout?.availablePaymentGateways?.find(
|
|
1051
|
+
(gateway: {
|
|
1052
|
+
id: string;
|
|
1053
|
+
config?: Array<{ field: string; value: string }>;
|
|
1054
|
+
}) => gateway.id === selectedPaymentGateway
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (!selectedGateway) {
|
|
1059
|
+
setIsProcessingPayment({
|
|
1060
|
+
isModalOpen: false,
|
|
1061
|
+
paymentProcessingLoading: false,
|
|
1062
|
+
error: true,
|
|
1063
|
+
success: false,
|
|
1064
|
+
});
|
|
1065
|
+
onError(
|
|
1066
|
+
`Selected payment gateway "${selectedPaymentGateway}" is not available`
|
|
1067
|
+
);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Step 2: Handle payment based on gateway type
|
|
1072
|
+
|
|
1073
|
+
if (selectedPaymentGateway === "mirumee.payments.authorize_net") {
|
|
1074
|
+
// Extract Authorize.Net credentials
|
|
1075
|
+
const gatewayConfig = selectedGateway.config || [];
|
|
1076
|
+
const clientKey = gatewayConfig.find(
|
|
1077
|
+
(c: { field: string; value: string }) => c.field === "client_key"
|
|
1078
|
+
)?.value;
|
|
1079
|
+
const apiLoginID = gatewayConfig.find(
|
|
1080
|
+
(c: { field: string; value: string }) => c.field === "api_login_id"
|
|
1081
|
+
)?.value;
|
|
1082
|
+
|
|
1083
|
+
if (!clientKey || !apiLoginID) {
|
|
1084
|
+
setIsProcessingPayment({
|
|
1085
|
+
isModalOpen: false,
|
|
1086
|
+
paymentProcessingLoading: false,
|
|
1087
|
+
error: true,
|
|
1088
|
+
success: false,
|
|
1089
|
+
});
|
|
1090
|
+
onError("Authorize.Net credentials not configured properly");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Generate Authorize.Net payment nonce using Accept.js
|
|
1095
|
+
paymentToken = await new Promise<string>((resolve, reject) => {
|
|
1096
|
+
// Check if Accept.js is loaded
|
|
1097
|
+
if (typeof window.Accept === "undefined") {
|
|
1098
|
+
reject(
|
|
1099
|
+
new Error(
|
|
1100
|
+
"Authorize.Net Accept.js not loaded. Please refresh the page and try again."
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const secureData = {
|
|
1107
|
+
cardData: {
|
|
1108
|
+
cardNumber: cardData.cardNumber.replace(/\s/g, ""),
|
|
1109
|
+
month: cardData.expirationDate.split("/")[0],
|
|
1110
|
+
year: "20" + cardData.expirationDate.split("/")[1],
|
|
1111
|
+
cardCode: cardData.cardCode,
|
|
1112
|
+
fullName: cardData.fullName,
|
|
1113
|
+
},
|
|
1114
|
+
authData: {
|
|
1115
|
+
clientKey,
|
|
1116
|
+
apiLoginID,
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
window.Accept.dispatchData(
|
|
1121
|
+
secureData,
|
|
1122
|
+
(response: AuthorizeNetResponse) => {
|
|
1123
|
+
if (response.messages.resultCode === "Error") {
|
|
1124
|
+
const errorMessage =
|
|
1125
|
+
response.messages.message?.[0]?.text ||
|
|
1126
|
+
"Payment tokenization failed";
|
|
1127
|
+
reject(new Error(errorMessage));
|
|
1128
|
+
} else {
|
|
1129
|
+
// Use the OTS token from Authorize.Net
|
|
1130
|
+
resolve(response.opaqueData?.dataValue || "");
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
);
|
|
1134
|
+
});
|
|
1135
|
+
} else if (selectedPaymentGateway === "saleor.app.payment.stripe") {
|
|
1136
|
+
// For Stripe, we would handle Stripe Elements integration here
|
|
1137
|
+
// For now, we'll use a placeholder token
|
|
1138
|
+
paymentToken = "stripe_payment_token_placeholder";
|
|
1139
|
+
} else if (selectedPaymentGateway.includes("dummy")) {
|
|
1140
|
+
// For dummy payment gateways, use a test token
|
|
1141
|
+
paymentToken = "dummy_payment_token";
|
|
1142
|
+
} else {
|
|
1143
|
+
setIsProcessingPayment({
|
|
1144
|
+
isModalOpen: false,
|
|
1145
|
+
paymentProcessingLoading: false,
|
|
1146
|
+
error: true,
|
|
1147
|
+
success: false,
|
|
1148
|
+
});
|
|
1149
|
+
onError(
|
|
1150
|
+
`Payment gateway "${selectedPaymentGateway}" is not supported yet`
|
|
1151
|
+
);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Step 3: Create payment with token
|
|
1156
|
+
|
|
1157
|
+
const paymentResult = await checkoutPaymentCreate({
|
|
1158
|
+
variables: {
|
|
1159
|
+
checkoutId,
|
|
1160
|
+
input: {
|
|
1161
|
+
gateway: selectedPaymentGateway,
|
|
1162
|
+
token: paymentToken,
|
|
1163
|
+
amount: totalAmount,
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
}).catch((error) => {
|
|
1167
|
+
// Handle checkout resolution errors immediately
|
|
1168
|
+
if (error.message?.includes("Couldn't resolve to a node")) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
"Your checkout session has expired. Please refresh the page and try again."
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
throw error;
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
if (paymentResult.data?.checkoutPaymentCreate?.errors?.length) {
|
|
1177
|
+
const paymentErrors = paymentResult.data.checkoutPaymentCreate.errors;
|
|
1178
|
+
setIsProcessingPayment({
|
|
1179
|
+
isModalOpen: false,
|
|
1180
|
+
paymentProcessingLoading: false,
|
|
1181
|
+
error: true,
|
|
1182
|
+
success: false,
|
|
1183
|
+
});
|
|
1184
|
+
onError(`Payment creation failed: ${paymentErrors[0].message}`);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (!paymentResult.data?.checkoutPaymentCreate?.payment) {
|
|
1189
|
+
setIsProcessingPayment({
|
|
1190
|
+
isModalOpen: false,
|
|
1191
|
+
paymentProcessingLoading: false,
|
|
1192
|
+
error: true,
|
|
1193
|
+
success: false,
|
|
1194
|
+
});
|
|
1195
|
+
onError("Payment creation failed: No payment was created");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Step 4: Complete checkout
|
|
1200
|
+
|
|
1201
|
+
const result = await checkoutComplete({
|
|
1202
|
+
variables: {
|
|
1203
|
+
checkoutId,
|
|
1204
|
+
},
|
|
1205
|
+
}).catch((error) => {
|
|
1206
|
+
// Handle checkout resolution errors immediately
|
|
1207
|
+
if (error.message?.includes("Couldn't resolve to a node")) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
"Your checkout session has expired during payment completion. Please refresh the page and try again."
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
throw error;
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const { order, errors } = result.data?.checkoutComplete || {};
|
|
1216
|
+
|
|
1217
|
+
if (errors && errors.length > 0) {
|
|
1218
|
+
const errorMessage = errors.map((e) => e.message).join(", ");
|
|
1219
|
+
setIsProcessingPayment({
|
|
1220
|
+
isModalOpen: false,
|
|
1221
|
+
paymentProcessingLoading: false,
|
|
1222
|
+
error: true,
|
|
1223
|
+
success: false,
|
|
1224
|
+
});
|
|
1225
|
+
onError(`Checkout completion failed: ${errorMessage}`);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (order) {
|
|
1230
|
+
// Update Kount with successful payment
|
|
1231
|
+
// Use the fraud check data returned from the fraud check function since state might not be updated yet
|
|
1232
|
+
await updateKountOrderStatus(
|
|
1233
|
+
true,
|
|
1234
|
+
paymentToken,
|
|
1235
|
+
cardData.cardNumber,
|
|
1236
|
+
currentFraudCheckData
|
|
1237
|
+
);
|
|
1238
|
+
|
|
1239
|
+
setIsProcessingPayment({
|
|
1240
|
+
isModalOpen: false,
|
|
1241
|
+
paymentProcessingLoading: false,
|
|
1242
|
+
error: false,
|
|
1243
|
+
success: true,
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
const orderId = order.id;
|
|
1247
|
+
const orderNumber = order.number;
|
|
1248
|
+
const orderTotal = order.total.gross.amount;
|
|
1249
|
+
|
|
1250
|
+
router.push(
|
|
1251
|
+
`/order-confirmation?orderId=${orderId}&orderNumber=${orderNumber}&total=${orderTotal}`
|
|
1252
|
+
);
|
|
1253
|
+
onSuccess();
|
|
1254
|
+
} else {
|
|
1255
|
+
setIsProcessingPayment({
|
|
1256
|
+
isModalOpen: false,
|
|
1257
|
+
paymentProcessingLoading: false,
|
|
1258
|
+
error: true,
|
|
1259
|
+
success: false,
|
|
1260
|
+
});
|
|
1261
|
+
onError(
|
|
1262
|
+
"Payment completed but no order was created. Please contact support."
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
// Update Kount with failed payment if we have payment token
|
|
1267
|
+
if (paymentToken) {
|
|
1268
|
+
await updateKountOrderStatus(
|
|
1269
|
+
false,
|
|
1270
|
+
paymentToken,
|
|
1271
|
+
cardData.cardNumber,
|
|
1272
|
+
currentFraudCheckData
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
setIsProcessingPayment({
|
|
1277
|
+
isModalOpen: false,
|
|
1278
|
+
paymentProcessingLoading: false,
|
|
1279
|
+
error: true,
|
|
1280
|
+
success: false,
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
if (error instanceof Error) {
|
|
1284
|
+
// Handle specific checkout expiration errors with user-friendly message
|
|
1285
|
+
if (
|
|
1286
|
+
error.message.includes("Couldn't resolve to a node") ||
|
|
1287
|
+
error.message.includes("checkout session has expired")
|
|
1288
|
+
) {
|
|
1289
|
+
onError(
|
|
1290
|
+
"Your checkout session has expired. Please refresh the page and try again."
|
|
1291
|
+
);
|
|
1292
|
+
} else {
|
|
1293
|
+
onError(`Payment failed: ${error.message}`);
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
onError("An unexpected error occurred during payment processing.");
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Reset reCAPTCHA on error
|
|
1300
|
+
setRecaptchaValue(null);
|
|
1301
|
+
resetRecaptcha();
|
|
1302
|
+
} finally {
|
|
1303
|
+
setIsProcessing(false);
|
|
1304
|
+
setIsPaymentInProgress(false);
|
|
1305
|
+
}
|
|
1306
|
+
}, [
|
|
1307
|
+
validateForm,
|
|
1308
|
+
cardData,
|
|
1309
|
+
checkoutId,
|
|
1310
|
+
totalAmount,
|
|
1311
|
+
onStartPayment,
|
|
1312
|
+
isLoggedIn,
|
|
1313
|
+
user,
|
|
1314
|
+
attachCustomer,
|
|
1315
|
+
lineItems,
|
|
1316
|
+
selectedShippingId,
|
|
1317
|
+
checkoutPaymentCreate,
|
|
1318
|
+
checkoutComplete,
|
|
1319
|
+
router,
|
|
1320
|
+
onSuccess,
|
|
1321
|
+
onError,
|
|
1322
|
+
setIsProcessingPayment,
|
|
1323
|
+
updateDeliveryMethod,
|
|
1324
|
+
getCheckoutById,
|
|
1325
|
+
recaptchaValue,
|
|
1326
|
+
resetRecaptcha,
|
|
1327
|
+
setRecaptchaValue,
|
|
1328
|
+
performKountFraudCheck,
|
|
1329
|
+
onCheckoutBlocked,
|
|
1330
|
+
updateKountOrderStatus,
|
|
1331
|
+
isPaymentInProgress,
|
|
1332
|
+
isCustomerAttached,
|
|
1333
|
+
setIsCustomerAttached,
|
|
1334
|
+
setLastFraudCheckData,
|
|
1335
|
+
fraudCheckCompleted,
|
|
1336
|
+
setFraudCheckCompleted,
|
|
1337
|
+
]);
|
|
1338
|
+
|
|
1339
|
+
const handleFormSubmit = useCallback(
|
|
1340
|
+
async (e: React.FormEvent) => {
|
|
1341
|
+
e.preventDefault();
|
|
1342
|
+
await processPayment();
|
|
1343
|
+
},
|
|
1344
|
+
[processPayment]
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
// Store processPayment in a ref to avoid infinite loop
|
|
1348
|
+
const processPaymentRef = useRef(processPayment);
|
|
1349
|
+
useEffect(() => {
|
|
1350
|
+
processPaymentRef.current = processPayment;
|
|
1351
|
+
}, [processPayment]);
|
|
1352
|
+
|
|
1353
|
+
// Expose payment trigger function to parent - only call once
|
|
1354
|
+
useEffect(() => {
|
|
1355
|
+
if (onPaymentReady) {
|
|
1356
|
+
const triggerPayment = () => processPaymentRef.current();
|
|
1357
|
+
onPaymentReady(triggerPayment);
|
|
1358
|
+
}
|
|
1359
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1360
|
+
}, [onPaymentReady]);
|
|
1361
|
+
|
|
1362
|
+
// Show loading state initially to match original behavior
|
|
1363
|
+
if (isProcessing) {
|
|
1364
|
+
return (
|
|
1365
|
+
<div className="space-y-4">
|
|
1366
|
+
<LoadingUI className="h-32" />
|
|
1367
|
+
<p className="text-center text-sm text-[var(--color-secondary-600)]">
|
|
1368
|
+
Processing payment...
|
|
1369
|
+
</p>
|
|
1370
|
+
</div>
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Check if PayPal is selected
|
|
1375
|
+
const isPayPalSelected =
|
|
1376
|
+
selectedPaymentGateway === "saleor.app.payment.paypal";
|
|
1377
|
+
|
|
1378
|
+
// Check if Affirm is selected
|
|
1379
|
+
const isAffirmSelected = selectedPaymentGateway === "saleor.app.affirm";
|
|
1380
|
+
|
|
1381
|
+
// For PayPal, we don't use the form submit approach
|
|
1382
|
+
if (isPayPalSelected) {
|
|
1383
|
+
return (
|
|
1384
|
+
<div className="">
|
|
1385
|
+
{/* Payment Method Selection */}
|
|
1386
|
+
{filteredPaymentGateways.length > 0 && (
|
|
1387
|
+
<div
|
|
1388
|
+
className={`bg-white border border-[var(--color-secondary-200)] p-4 ${disabled ? "opacity-60 pointer-events-none" : ""
|
|
1389
|
+
}`}
|
|
1390
|
+
>
|
|
1391
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
1392
|
+
{filteredPaymentGateways.map((gateway) => (
|
|
1393
|
+
<label
|
|
1394
|
+
key={gateway.id}
|
|
1395
|
+
className={`flex items-center gap-3 ring-1 p-2 transition-all duration-200 ${disabled
|
|
1396
|
+
? "cursor-not-allowed opacity-50 pointer-events-none"
|
|
1397
|
+
: "cursor-pointer"
|
|
1398
|
+
} ${selectedPaymentGateway === gateway.id
|
|
1399
|
+
? "ring-[var(--color-primary-100)] bg-[var(--color-primary-50)] text-[var(--color-primary-700)] accent-[var(--color-primary-600)]"
|
|
1400
|
+
: "ring-gray-300 hover:bg-gray-50"
|
|
1401
|
+
} ${disabled ? "" : "hover:bg-gray-50"}`}
|
|
1402
|
+
>
|
|
1403
|
+
<input
|
|
1404
|
+
type="radio"
|
|
1405
|
+
name="paymentMethod"
|
|
1406
|
+
value={gateway.id}
|
|
1407
|
+
checked={selectedPaymentGateway === gateway.id}
|
|
1408
|
+
onChange={(e) => setSelectedPaymentGateway(e.target.value)}
|
|
1409
|
+
disabled={disabled}
|
|
1410
|
+
/>
|
|
1411
|
+
<div className="flex items-center gap-2 flex-1">
|
|
1412
|
+
{getPaymentGatewayIcon(gateway.id)}
|
|
1413
|
+
<span className="font-medium text-base/none font-secondary">
|
|
1414
|
+
{getPaymentGatewayDisplayName(gateway.name)}
|
|
1415
|
+
</span>
|
|
1416
|
+
</div>
|
|
1417
|
+
</label>
|
|
1418
|
+
))}
|
|
1419
|
+
</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
)}
|
|
1422
|
+
<div
|
|
1423
|
+
className={`bg-white border border-[var(--color-secondary-200)] p-4 ${disabled ? "opacity-60 pointer-events-none" : ""
|
|
1424
|
+
}`}
|
|
1425
|
+
>
|
|
1426
|
+
{/* PayPal Payment Component */}
|
|
1427
|
+
<PayPalPayment
|
|
1428
|
+
checkoutId={checkoutId}
|
|
1429
|
+
totalAmount={totalAmount}
|
|
1430
|
+
currency="USD"
|
|
1431
|
+
onSuccess={onSuccess}
|
|
1432
|
+
onError={onError}
|
|
1433
|
+
setIsProcessingPayment={setIsProcessingPayment}
|
|
1434
|
+
userEmail={userEmail}
|
|
1435
|
+
guestEmail={guestEmail}
|
|
1436
|
+
termsAccepted={termsAccepted}
|
|
1437
|
+
termsData={termsData}
|
|
1438
|
+
onTermsModalOpen={onTermsModalOpen}
|
|
1439
|
+
onTermsAcceptedChange={onTermsAcceptedChange}
|
|
1440
|
+
questionsValid={questionsValid}
|
|
1441
|
+
/>
|
|
1442
|
+
</div>
|
|
1443
|
+
</div>
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// For Affirm, we don't use the form submit approach
|
|
1448
|
+
if (isAffirmSelected) {
|
|
1449
|
+
return (
|
|
1450
|
+
<div className="">
|
|
1451
|
+
{/* Payment Method Selection */}
|
|
1452
|
+
{filteredPaymentGateways.length > 0 && (
|
|
1453
|
+
<div
|
|
1454
|
+
className={`bg-white border border-[var(--color-secondary-200)] p-4 ${disabled ? "opacity-60 pointer-events-none" : ""
|
|
1455
|
+
}`}
|
|
1456
|
+
>
|
|
1457
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
1458
|
+
{filteredPaymentGateways.map((gateway) => (
|
|
1459
|
+
<label
|
|
1460
|
+
key={gateway.id}
|
|
1461
|
+
className={`flex items-center gap-3 ring-1 p-2 transition-all duration-200 ${selectedPaymentGateway === gateway.id
|
|
1462
|
+
? "ring-[var(--color-primary-600)] bg-[var(--color-primary-50)]"
|
|
1463
|
+
: "ring-[var(--color-secondary-200)] hover:ring-[var(--color-secondary-300)]"
|
|
1464
|
+
} cursor-pointer`}
|
|
1465
|
+
>
|
|
1466
|
+
<input
|
|
1467
|
+
type="radio"
|
|
1468
|
+
name="paymentGateway"
|
|
1469
|
+
value={gateway.id}
|
|
1470
|
+
checked={selectedPaymentGateway === gateway.id}
|
|
1471
|
+
onChange={(e) => setSelectedPaymentGateway(e.target.value)}
|
|
1472
|
+
className="sr-only"
|
|
1473
|
+
/>
|
|
1474
|
+
<div className="flex items-center gap-2">
|
|
1475
|
+
{getPaymentGatewayIcon(gateway.id)}
|
|
1476
|
+
<span className="text-sm font-medium text-[var(--color-secondary-800)]">
|
|
1477
|
+
{getPaymentGatewayDisplayName(gateway.name)}
|
|
1478
|
+
</span>
|
|
1479
|
+
</div>
|
|
1480
|
+
</label>
|
|
1481
|
+
))}
|
|
1482
|
+
</div>
|
|
1483
|
+
</div>
|
|
1484
|
+
)}
|
|
1485
|
+
<div
|
|
1486
|
+
className={`bg-white border border-[var(--color-secondary-200)] p-4 ${disabled ? "opacity-60 pointer-events-none" : ""
|
|
1487
|
+
}`}
|
|
1488
|
+
>
|
|
1489
|
+
{/* Affirm Payment Component */}
|
|
1490
|
+
<AffirmPayment
|
|
1491
|
+
checkoutId={checkoutId}
|
|
1492
|
+
totalAmount={totalAmount}
|
|
1493
|
+
currency="USD"
|
|
1494
|
+
onSuccess={onSuccess}
|
|
1495
|
+
onError={onError}
|
|
1496
|
+
setIsProcessingPayment={setIsProcessingPayment}
|
|
1497
|
+
userEmail={userEmail}
|
|
1498
|
+
guestEmail={guestEmail}
|
|
1499
|
+
termsAccepted={termsAccepted}
|
|
1500
|
+
termsData={termsData}
|
|
1501
|
+
onTermsModalOpen={onTermsModalOpen}
|
|
1502
|
+
onTermsAcceptedChange={onTermsAcceptedChange}
|
|
1503
|
+
questionsValid={questionsValid}
|
|
1504
|
+
/>
|
|
1505
|
+
</div>
|
|
1506
|
+
</div>
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// For other payment methods (Authorize.Net, Stripe, etc.), show card form
|
|
1511
|
+
return (
|
|
1512
|
+
<div className="space-y-6">
|
|
1513
|
+
<form onSubmit={handleFormSubmit}>
|
|
1514
|
+
{/* Payment Method Selection */}
|
|
1515
|
+
{filteredPaymentGateways.length > 0 && (
|
|
1516
|
+
<div
|
|
1517
|
+
className={`bg-white border border-[var(--color-secondary-200)] p-4 ${disabled ? "opacity-60 pointer-events-none" : ""
|
|
1518
|
+
}`}
|
|
1519
|
+
>
|
|
1520
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
1521
|
+
{filteredPaymentGateways.map((gateway) => (
|
|
1522
|
+
<label
|
|
1523
|
+
key={gateway.id}
|
|
1524
|
+
className={`flex items-center gap-3 ring-1 p-2 transition-all duration-200 ${disabled
|
|
1525
|
+
? "cursor-not-allowed opacity-50 pointer-events-none"
|
|
1526
|
+
: "cursor-pointer"
|
|
1527
|
+
} ${selectedPaymentGateway === gateway.id
|
|
1528
|
+
? "ring-[var(--color-primary-100)] bg-[var(--color-primary-50)] text-[var(--color-primary-700)] accent-[var(--color-primary-600)]"
|
|
1529
|
+
: "ring-gray-300 hover:bg-gray-50"
|
|
1530
|
+
} ${disabled ? "" : "hover:bg-gray-50"}`}
|
|
1531
|
+
>
|
|
1532
|
+
<input
|
|
1533
|
+
type="radio"
|
|
1534
|
+
name="paymentMethod"
|
|
1535
|
+
value={gateway.id}
|
|
1536
|
+
checked={selectedPaymentGateway === gateway.id}
|
|
1537
|
+
onChange={(e) => setSelectedPaymentGateway(e.target.value)}
|
|
1538
|
+
disabled={disabled}
|
|
1539
|
+
/>
|
|
1540
|
+
<div className="flex items-center gap-2 flex-1">
|
|
1541
|
+
{getPaymentGatewayIcon(gateway.id)}
|
|
1542
|
+
<span className="font-medium text-base/none font-secondary">
|
|
1543
|
+
{getPaymentGatewayDisplayName(gateway.name)}
|
|
1544
|
+
</span>
|
|
1545
|
+
</div>
|
|
1546
|
+
</label>
|
|
1547
|
+
))}
|
|
1548
|
+
</div>
|
|
1549
|
+
</div>
|
|
1550
|
+
)}
|
|
1551
|
+
|
|
1552
|
+
<div
|
|
1553
|
+
className={`bg-white border border-[var(--color-secondary-200)] p-4 ${disabled ? "opacity-60 pointer-events-none" : ""
|
|
1554
|
+
}`}
|
|
1555
|
+
>
|
|
1556
|
+
<div className="space-y-4">
|
|
1557
|
+
{/* Cardholder Name */}
|
|
1558
|
+
<div>
|
|
1559
|
+
<Input
|
|
1560
|
+
name="fullName"
|
|
1561
|
+
label="Cardholder Name"
|
|
1562
|
+
htmlFor="fullName"
|
|
1563
|
+
type="text"
|
|
1564
|
+
id="fullName"
|
|
1565
|
+
value={cardData.fullName}
|
|
1566
|
+
onChange={(e) => handleInputChange("fullName", e.target.value)}
|
|
1567
|
+
className={`w-full ${validationErrors.fullName
|
|
1568
|
+
? "border-red-300 focus:ring-red-500"
|
|
1569
|
+
: "border-[var(--color-secondary-300)]"
|
|
1570
|
+
} py-2`}
|
|
1571
|
+
placeholder="John Doe"
|
|
1572
|
+
disabled={disabled}
|
|
1573
|
+
/>
|
|
1574
|
+
{validationErrors.fullName && (
|
|
1575
|
+
<p className="text-red-500 text-xs mt-1">
|
|
1576
|
+
{validationErrors.fullName}
|
|
1577
|
+
</p>
|
|
1578
|
+
)}
|
|
1579
|
+
</div>
|
|
1580
|
+
|
|
1581
|
+
{/* Card Number */}
|
|
1582
|
+
<div>
|
|
1583
|
+
<Input
|
|
1584
|
+
name="cardNumber"
|
|
1585
|
+
label="Card Number"
|
|
1586
|
+
htmlFor="cardNumber"
|
|
1587
|
+
type="text"
|
|
1588
|
+
id="cardNumber"
|
|
1589
|
+
value={cardData.cardNumber}
|
|
1590
|
+
onChange={(e) =>
|
|
1591
|
+
handleInputChange("cardNumber", e.target.value)
|
|
1592
|
+
}
|
|
1593
|
+
className={`w-full ${validationErrors.cardNumber
|
|
1594
|
+
? "border-red-300 focus:ring-red-500"
|
|
1595
|
+
: "border-[var(--color-secondary-300)]"
|
|
1596
|
+
} py-2`}
|
|
1597
|
+
placeholder="1234 5678 9012 3456"
|
|
1598
|
+
maxLength={19}
|
|
1599
|
+
disabled={disabled}
|
|
1600
|
+
/>
|
|
1601
|
+
{validationErrors.cardNumber && (
|
|
1602
|
+
<p className="text-red-500 text-xs mt-1">
|
|
1603
|
+
{validationErrors.cardNumber}
|
|
1604
|
+
</p>
|
|
1605
|
+
)}
|
|
1606
|
+
</div>
|
|
1607
|
+
|
|
1608
|
+
{/* Expiration Date and Security Code */}
|
|
1609
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1610
|
+
<div>
|
|
1611
|
+
<Input
|
|
1612
|
+
name="expirationDate"
|
|
1613
|
+
label="Expiration Date"
|
|
1614
|
+
htmlFor="expirationDate"
|
|
1615
|
+
type="text"
|
|
1616
|
+
id="expirationDate"
|
|
1617
|
+
value={cardData.expirationDate}
|
|
1618
|
+
onChange={(e) =>
|
|
1619
|
+
handleInputChange("expirationDate", e.target.value)
|
|
1620
|
+
}
|
|
1621
|
+
className={`w-full ${validationErrors.expirationDate
|
|
1622
|
+
? "border-red-300 focus:ring-red-500"
|
|
1623
|
+
: "border-[var(--color-secondary-300)]"
|
|
1624
|
+
} py-2`}
|
|
1625
|
+
placeholder="MM/YY"
|
|
1626
|
+
maxLength={5}
|
|
1627
|
+
disabled={disabled}
|
|
1628
|
+
/>
|
|
1629
|
+
{validationErrors.expirationDate && (
|
|
1630
|
+
<p className="text-red-500 text-xs mt-1">
|
|
1631
|
+
{validationErrors.expirationDate}
|
|
1632
|
+
</p>
|
|
1633
|
+
)}
|
|
1634
|
+
</div>
|
|
1635
|
+
|
|
1636
|
+
<div>
|
|
1637
|
+
<Input
|
|
1638
|
+
name="cardCode"
|
|
1639
|
+
label="Security Code"
|
|
1640
|
+
htmlFor="cardCode"
|
|
1641
|
+
type="text"
|
|
1642
|
+
id="cardCode"
|
|
1643
|
+
value={cardData.cardCode}
|
|
1644
|
+
onChange={(e) =>
|
|
1645
|
+
handleInputChange("cardCode", e.target.value)
|
|
1646
|
+
}
|
|
1647
|
+
className={`w-full ${validationErrors.cardCode
|
|
1648
|
+
? "border-red-300 focus:ring-red-500"
|
|
1649
|
+
: "border-[var(--color-secondary-300)]"
|
|
1650
|
+
} py-2`}
|
|
1651
|
+
placeholder="123"
|
|
1652
|
+
maxLength={4}
|
|
1653
|
+
disabled={disabled}
|
|
1654
|
+
/>
|
|
1655
|
+
{validationErrors.cardCode && (
|
|
1656
|
+
<p className="text-red-500 text-xs mt-1">
|
|
1657
|
+
{validationErrors.cardCode}
|
|
1658
|
+
</p>
|
|
1659
|
+
)}
|
|
1660
|
+
</div>
|
|
1661
|
+
</div>
|
|
1662
|
+
</div>
|
|
1663
|
+
</div>
|
|
1664
|
+
|
|
1665
|
+
{/* Conditional reCAPTCHA for checkout */}
|
|
1666
|
+
{config.isRecaptchaEnabledFor("checkout") && (
|
|
1667
|
+
<div className="flex flex-col items-start py-4">
|
|
1668
|
+
<ReCAPTCHA
|
|
1669
|
+
ref={recaptchaRef}
|
|
1670
|
+
sitekey={config.getGoogleRecaptchaConfig()?.site_key || ""}
|
|
1671
|
+
theme="light"
|
|
1672
|
+
size="normal"
|
|
1673
|
+
onChange={(value) => {
|
|
1674
|
+
setRecaptchaValue(value);
|
|
1675
|
+
// Clear recaptcha error when user completes it
|
|
1676
|
+
if (value && validationErrors.recaptcha) {
|
|
1677
|
+
setValidationErrors((prev) => {
|
|
1678
|
+
const newErrors = { ...prev };
|
|
1679
|
+
delete newErrors.recaptcha;
|
|
1680
|
+
return newErrors;
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}}
|
|
1684
|
+
onExpired={() => {
|
|
1685
|
+
setRecaptchaValue(null);
|
|
1686
|
+
resetRecaptcha();
|
|
1687
|
+
}}
|
|
1688
|
+
onError={() => {
|
|
1689
|
+
setRecaptchaValue(null);
|
|
1690
|
+
resetRecaptcha();
|
|
1691
|
+
}}
|
|
1692
|
+
/>
|
|
1693
|
+
{validationErrors.recaptcha && (
|
|
1694
|
+
<p className="text-red-500 text-xs mt-2 text-center">
|
|
1695
|
+
{validationErrors.recaptcha}
|
|
1696
|
+
</p>
|
|
1697
|
+
)}
|
|
1698
|
+
</div>
|
|
1699
|
+
)}
|
|
1700
|
+
|
|
1701
|
+
{/* reCAPTCHA Notice - only show when reCAPTCHA is enabled */}
|
|
1702
|
+
{config.isRecaptchaEnabledFor("checkout") && (
|
|
1703
|
+
<div className="text-xs text-[var(--color-secondary-600)] text-center py-2">
|
|
1704
|
+
This site is protected by reCAPTCHA and the Google{" "}
|
|
1705
|
+
<a
|
|
1706
|
+
href="https://policies.google.com/privacy"
|
|
1707
|
+
target="_blank"
|
|
1708
|
+
rel="noopener noreferrer"
|
|
1709
|
+
className="text-blue-600 hover:underline"
|
|
1710
|
+
>
|
|
1711
|
+
Privacy Policy
|
|
1712
|
+
</a>{" "}
|
|
1713
|
+
and{" "}
|
|
1714
|
+
<a
|
|
1715
|
+
href="https://policies.google.com/terms"
|
|
1716
|
+
target="_blank"
|
|
1717
|
+
rel="noopener noreferrer"
|
|
1718
|
+
className="text-blue-600 hover:underline"
|
|
1719
|
+
>
|
|
1720
|
+
Terms of Service
|
|
1721
|
+
</a>{" "}
|
|
1722
|
+
apply.
|
|
1723
|
+
</div>
|
|
1724
|
+
)}
|
|
1725
|
+
|
|
1726
|
+
{/* Terms and Conditions */}
|
|
1727
|
+
{termsData?.page?.isPublished && (
|
|
1728
|
+
<div className="flex items-start gap-2 w-full py-2">
|
|
1729
|
+
<input
|
|
1730
|
+
style={{ accentColor: "var(--color-primary-600)" }}
|
|
1731
|
+
type="checkbox"
|
|
1732
|
+
id="termsAcceptedPayment"
|
|
1733
|
+
className="w-5 h-5 cursor-pointer mt-0.5"
|
|
1734
|
+
checked={termsAccepted}
|
|
1735
|
+
onChange={(e) => onTermsAcceptedChange?.(e.target.checked)}
|
|
1736
|
+
/>
|
|
1737
|
+
<label
|
|
1738
|
+
htmlFor="termsAcceptedPayment"
|
|
1739
|
+
style={{ color: "var(--color-secondary-600)" }}
|
|
1740
|
+
className="text-sm lg:text-base tracking-[-0.04px] cursor-pointer"
|
|
1741
|
+
>
|
|
1742
|
+
I agree to the{" "}
|
|
1743
|
+
<button
|
|
1744
|
+
type="button"
|
|
1745
|
+
onClick={onTermsModalOpen}
|
|
1746
|
+
className="font-semibold text-[var(--color-primary-600)] hover:underline focus:underline focus:outline-none"
|
|
1747
|
+
>
|
|
1748
|
+
Terms and Conditions
|
|
1749
|
+
</button>
|
|
1750
|
+
</label>
|
|
1751
|
+
</div>
|
|
1752
|
+
)}
|
|
1753
|
+
|
|
1754
|
+
{/* Payment Button - Moved to OrderSummary */}
|
|
1755
|
+
</form>
|
|
1756
|
+
</div>
|
|
1757
|
+
);
|
|
1758
|
+
}
|