@alphasquad/saleor-template-advance 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/.env.example +57 -0
  2. package/APPLE_PAY_QUICK_START.md +165 -0
  3. package/APPLE_PAY_SETUP.md +331 -0
  4. package/README.md +46 -0
  5. package/SEO_AUDIT_CHECKLIST_STATUS.md +244 -0
  6. package/SEO_AUDIT_REPORT.md +66 -0
  7. package/eslint.config.mjs +16 -0
  8. package/next-env.d.ts +5 -0
  9. package/next.config.ts +109 -0
  10. package/package.json +47 -0
  11. package/postcss.config.mjs +5 -0
  12. package/public/.well-known/apple-developer-merchantid-domain-association +1 -0
  13. package/public/Logo.png +0 -0
  14. package/public/brand-video.mp4 +0 -0
  15. package/public/favicon.ico +0 -0
  16. package/public/file.svg +1 -0
  17. package/public/footer/facebook.tsx +34 -0
  18. package/public/footer/instagram.tsx +27 -0
  19. package/public/footer/mail.tsx +5 -0
  20. package/public/footer/x.tsx +35 -0
  21. package/public/globe.svg +1 -0
  22. package/public/icons/Authorize.net.webp +0 -0
  23. package/public/icons/amex.gif +0 -0
  24. package/public/icons/appIcon.png +0 -0
  25. package/public/icons/discover.gif +0 -0
  26. package/public/icons/master.gif +0 -0
  27. package/public/icons/paypal.png +0 -0
  28. package/public/icons/stripe.png +0 -0
  29. package/public/icons/visa.gif +0 -0
  30. package/public/images/BackgroundNoise.png +0 -0
  31. package/public/images/footer-background.png +0 -0
  32. package/public/next.svg +1 -0
  33. package/public/no-image-avail-large.png +0 -0
  34. package/public/random-car-1.jpeg +0 -0
  35. package/public/random-car-2.png +0 -0
  36. package/public/random-car-3.jpg +0 -0
  37. package/public/random-car-4.jpg +0 -0
  38. package/public/random-car-5.jpg +0 -0
  39. package/public/star.svg +3 -0
  40. package/public/vercel.svg +1 -0
  41. package/public/window.svg +1 -0
  42. package/scripts/seo-audit/generate-checklist.mjs +156 -0
  43. package/src/app/(auth)/account/forgot-password/layout.tsx +16 -0
  44. package/src/app/(auth)/account/forgot-password/page.tsx +135 -0
  45. package/src/app/(auth)/account/login/layout.tsx +16 -0
  46. package/src/app/(auth)/account/login/page.tsx +288 -0
  47. package/src/app/(auth)/account/otp/layout.tsx +16 -0
  48. package/src/app/(auth)/account/otp/page.tsx +108 -0
  49. package/src/app/(auth)/account/register/layout.tsx +16 -0
  50. package/src/app/(auth)/account/register/page.tsx +431 -0
  51. package/src/app/(auth)/account/reset-password/layout.tsx +16 -0
  52. package/src/app/(auth)/account/reset-password/page.tsx +222 -0
  53. package/src/app/[slug]/page.tsx +43 -0
  54. package/src/app/about/loading.tsx +17 -0
  55. package/src/app/about/page.tsx +61 -0
  56. package/src/app/account/address/layout.tsx +15 -0
  57. package/src/app/account/address/page.tsx +166 -0
  58. package/src/app/account/head.tsx +4 -0
  59. package/src/app/account/layout.tsx +62 -0
  60. package/src/app/account/orders/[id]/layout.tsx +17 -0
  61. package/src/app/account/orders/[id]/page.tsx +115 -0
  62. package/src/app/account/orders/components/orderDetailsModal.tsx +410 -0
  63. package/src/app/account/orders/layout.tsx +15 -0
  64. package/src/app/account/orders/page.tsx +146 -0
  65. package/src/app/account/page.tsx +39 -0
  66. package/src/app/account/settings/components/editProfileSuccessModal.tsx +28 -0
  67. package/src/app/account/settings/layout.tsx +15 -0
  68. package/src/app/account/settings/page.tsx +260 -0
  69. package/src/app/api/affirm/check-status/route.ts +94 -0
  70. package/src/app/api/affirm/create-checkout/route.ts +109 -0
  71. package/src/app/api/affirm/get-config/route.ts +108 -0
  72. package/src/app/api/affirm/process-payment/route.ts +244 -0
  73. package/src/app/api/affirm/test-connection/route.ts +45 -0
  74. package/src/app/api/auth/clear/route.ts +16 -0
  75. package/src/app/api/auth/clear-cookies/route.ts +42 -0
  76. package/src/app/api/auth/set/route.ts +47 -0
  77. package/src/app/api/configuration/route.ts +18 -0
  78. package/src/app/api/dynamic-page/[slug]/route.ts +24 -0
  79. package/src/app/api/form-submission/route.ts +237 -0
  80. package/src/app/api/paypal/capture-order/route.ts +303 -0
  81. package/src/app/api/paypal/create-order/route.ts +211 -0
  82. package/src/app/api/paypal/get-config/route.ts +240 -0
  83. package/src/app/api/search-proxy/route.ts +52 -0
  84. package/src/app/authorize-net-success/layout.tsx +19 -0
  85. package/src/app/authorize-net-success/page.tsx +12 -0
  86. package/src/app/authorize-net-success/summary.tsx +486 -0
  87. package/src/app/blog/[slug]/blogContentRenderer.tsx +369 -0
  88. package/src/app/blog/[slug]/layout.tsx +17 -0
  89. package/src/app/blog/[slug]/page.tsx +151 -0
  90. package/src/app/blog/constant.tsx +147 -0
  91. package/src/app/blog/layout.tsx +31 -0
  92. package/src/app/blog/page.tsx +81 -0
  93. package/src/app/brand/[id]/BrandPageClient.tsx +188 -0
  94. package/src/app/brand/[id]/layout.tsx +17 -0
  95. package/src/app/brand/[id]/page.tsx +176 -0
  96. package/src/app/brands/components/brandsListingClient.tsx +97 -0
  97. package/src/app/brands/layout.tsx +31 -0
  98. package/src/app/brands/page.tsx +40 -0
  99. package/src/app/cancellation-policy/page.tsx +53 -0
  100. package/src/app/cart/layout.tsx +19 -0
  101. package/src/app/cart/page.tsx +752 -0
  102. package/src/app/category/[slug]/CategoryPageClient.tsx +377 -0
  103. package/src/app/category/[slug]/layout.tsx +17 -0
  104. package/src/app/category/[slug]/page.tsx +224 -0
  105. package/src/app/category/page.tsx +114 -0
  106. package/src/app/checkout/components/addNewAddressModal.tsx +474 -0
  107. package/src/app/checkout/layout.tsx +19 -0
  108. package/src/app/checkout/page.tsx +3312 -0
  109. package/src/app/components/account/AccountTabs.tsx +40 -0
  110. package/src/app/components/ads/GoogleAdSense.tsx +74 -0
  111. package/src/app/components/analytics/AnalyticsScripts.tsx +78 -0
  112. package/src/app/components/analytics/ConditionalGTMNoscript.tsx +24 -0
  113. package/src/app/components/analytics/ConditionalGoogleAnalytics.tsx +16 -0
  114. package/src/app/components/ancillary/AncillaryContent.tsx +7 -0
  115. package/src/app/components/auth/TokenExpirationHandler.tsx +8 -0
  116. package/src/app/components/blog/BlogList.tsx +112 -0
  117. package/src/app/components/checkout/AddressInformationSection.tsx +34 -0
  118. package/src/app/components/checkout/AddressManagement.tsx +571 -0
  119. package/src/app/components/checkout/CheckoutHeader.tsx +51 -0
  120. package/src/app/components/checkout/CheckoutQuestions.tsx +454 -0
  121. package/src/app/components/checkout/CheckoutTermsModal.tsx +81 -0
  122. package/src/app/components/checkout/ContactDetailsSection.tsx +52 -0
  123. package/src/app/components/checkout/DealerShippingSection.tsx +359 -0
  124. package/src/app/components/checkout/DeliveryMethodSection.tsx +249 -0
  125. package/src/app/components/checkout/OrderSummary.tsx +386 -0
  126. package/src/app/components/checkout/TermsContentRenderer.tsx +147 -0
  127. package/src/app/components/checkout/WillCallSection.tsx +133 -0
  128. package/src/app/components/checkout/affirmPayment.tsx +383 -0
  129. package/src/app/components/checkout/checkoutProcessingModal.tsx +96 -0
  130. package/src/app/components/checkout/googlePayButton.tsx +334 -0
  131. package/src/app/components/checkout/paymentStep.tsx +180 -0
  132. package/src/app/components/checkout/paypalPayment.tsx +1083 -0
  133. package/src/app/components/checkout/saleorNativePayment.tsx +1758 -0
  134. package/src/app/components/dynamicPage/DynamicPageRenderer.tsx +13 -0
  135. package/src/app/components/dynamicPage/HtmlWidgetRenderer.tsx +144 -0
  136. package/src/app/components/filtersCollapsible/index.tsx +365 -0
  137. package/src/app/components/globalSearch/index.tsx +423 -0
  138. package/src/app/components/layout/cartDropDown.tsx +628 -0
  139. package/src/app/components/layout/components/FooterNewsletter.tsx +21 -0
  140. package/src/app/components/layout/footer.tsx +283 -0
  141. package/src/app/components/layout/header/accountMenuDropdown.tsx +53 -0
  142. package/src/app/components/layout/header/components/CartBadge.tsx +18 -0
  143. package/src/app/components/layout/header/components/LoadingState.tsx +17 -0
  144. package/src/app/components/layout/header/components/MenuItemDropdown.tsx +124 -0
  145. package/src/app/components/layout/header/components/MobileNavbar.tsx +123 -0
  146. package/src/app/components/layout/header/components/NavbarActions.tsx +125 -0
  147. package/src/app/components/layout/header/components/NavbarBrand.tsx +29 -0
  148. package/src/app/components/layout/header/components/NavigationLinks.tsx +131 -0
  149. package/src/app/components/layout/header/hamMenuSlide.tsx +318 -0
  150. package/src/app/components/layout/header/header.tsx +44 -0
  151. package/src/app/components/layout/header/hooks/useDropdown.ts +45 -0
  152. package/src/app/components/layout/header/hooks/useNavbarData.ts +138 -0
  153. package/src/app/components/layout/header/hooks/useNavbarState.ts +66 -0
  154. package/src/app/components/layout/header/megaMenuDropdown.tsx +116 -0
  155. package/src/app/components/layout/header/navBar.tsx +121 -0
  156. package/src/app/components/layout/header/search.tsx +418 -0
  157. package/src/app/components/layout/header/styles/navbarStyles.ts +27 -0
  158. package/src/app/components/layout/header/topBar.tsx +214 -0
  159. package/src/app/components/layout/joinNewsletterForm/index.tsx +72 -0
  160. package/src/app/components/layout/mobileAccordian/index.tsx +92 -0
  161. package/src/app/components/layout/paymentMethods.tsx +75 -0
  162. package/src/app/components/layout/rootLayout.tsx +23 -0
  163. package/src/app/components/layout/siteInfo.tsx +103 -0
  164. package/src/app/components/layout/socialLinks.tsx +65 -0
  165. package/src/app/components/newsletterSection/emailListSection.tsx +224 -0
  166. package/src/app/components/newsletterSection/emailSectionServer.tsx +8 -0
  167. package/src/app/components/providers/ApolloWrapper.tsx +12 -0
  168. package/src/app/components/providers/AppConfigurationProvider.tsx +108 -0
  169. package/src/app/components/providers/GoogleAnalyticsProvider.tsx +149 -0
  170. package/src/app/components/providers/GoogleTagManagerProvider.tsx +31 -0
  171. package/src/app/components/providers/RecaptchaProvider.tsx +18 -0
  172. package/src/app/components/providers/ServerAppConfigurationProvider.tsx +133 -0
  173. package/src/app/components/providers/YMMStatusProvider.tsx +15 -0
  174. package/src/app/components/reuseableUI/AboutUs.tsx +115 -0
  175. package/src/app/components/reuseableUI/AddToCartClient.tsx +125 -0
  176. package/src/app/components/reuseableUI/EditorJsRenderer.tsx +219 -0
  177. package/src/app/components/reuseableUI/HeroSectionsearchByVehicle.tsx +188 -0
  178. package/src/app/components/reuseableUI/ImageWithFallback.tsx +41 -0
  179. package/src/app/components/reuseableUI/Toast.tsx +101 -0
  180. package/src/app/components/reuseableUI/blogCard.tsx +52 -0
  181. package/src/app/components/reuseableUI/brandCard.tsx +68 -0
  182. package/src/app/components/reuseableUI/breadcrumb.tsx +38 -0
  183. package/src/app/components/reuseableUI/categoryCard.tsx +37 -0
  184. package/src/app/components/reuseableUI/categorySkeleton.tsx +31 -0
  185. package/src/app/components/reuseableUI/commonButton.tsx +48 -0
  186. package/src/app/components/reuseableUI/defaultInputField/index.tsx +84 -0
  187. package/src/app/components/reuseableUI/emptyState.tsx +29 -0
  188. package/src/app/components/reuseableUI/errorTag.tsx +15 -0
  189. package/src/app/components/reuseableUI/heading/index.tsx +20 -0
  190. package/src/app/components/reuseableUI/input.tsx +117 -0
  191. package/src/app/components/reuseableUI/listCard.tsx +137 -0
  192. package/src/app/components/reuseableUI/loadingUI.tsx +12 -0
  193. package/src/app/components/reuseableUI/modalLayout.tsx +76 -0
  194. package/src/app/components/reuseableUI/newsletter/newsletterClient.tsx +622 -0
  195. package/src/app/components/reuseableUI/newsletter/newslettersHomeModal.tsx +68 -0
  196. package/src/app/components/reuseableUI/offerCard.tsx +42 -0
  197. package/src/app/components/reuseableUI/passwordRules/passwordRules.tsx +56 -0
  198. package/src/app/components/reuseableUI/primaryButton/index.tsx +34 -0
  199. package/src/app/components/reuseableUI/productCard.tsx +118 -0
  200. package/src/app/components/reuseableUI/productSkeleton.tsx +34 -0
  201. package/src/app/components/reuseableUI/searchByVehicle.tsx +187 -0
  202. package/src/app/components/reuseableUI/secondaryButton/index.tsx +34 -0
  203. package/src/app/components/reuseableUI/section.tsx +20 -0
  204. package/src/app/components/reuseableUI/select/index.tsx +98 -0
  205. package/src/app/components/reuseableUI/skeletonLoader.tsx +117 -0
  206. package/src/app/components/reuseableUI/statusTag.tsx +24 -0
  207. package/src/app/components/reuseableUI/tags/saleTag.tsx +19 -0
  208. package/src/app/components/reuseableUI/testimonialCard.tsx +93 -0
  209. package/src/app/components/richText/EditorRenderer.tsx +318 -0
  210. package/src/app/components/search/HierarchicalCategoryFilter.tsx +155 -0
  211. package/src/app/components/search/SearchFilters.tsx +155 -0
  212. package/src/app/components/search/YMMSearchSidebar.tsx +187 -0
  213. package/src/app/components/seo/ServerProductCard.tsx +91 -0
  214. package/src/app/components/seo/ServerProductGrid.tsx +45 -0
  215. package/src/app/components/shop/CategoryFilter.tsx +184 -0
  216. package/src/app/components/shop/ItemsPerPageSelect.tsx +69 -0
  217. package/src/app/components/shop/ItemsPerPageSelectClient.tsx +58 -0
  218. package/src/app/components/shop/MobileFilters.tsx +103 -0
  219. package/src/app/components/shop/ProductGridSkeleton.tsx +16 -0
  220. package/src/app/components/shop/ProductsGrid.tsx +230 -0
  221. package/src/app/components/shop/SearchFilter.tsx +218 -0
  222. package/src/app/components/shop/SearchFilterClient.tsx +122 -0
  223. package/src/app/components/shop/SearchLoadingOverlay.tsx +32 -0
  224. package/src/app/components/shop/ShopMobileFilters.tsx +205 -0
  225. package/src/app/components/showroom/VehicleSearchDropdowns.tsx +187 -0
  226. package/src/app/components/showroom/brandsSwiper.tsx +49 -0
  227. package/src/app/components/showroom/brandsSwiperClient copy.tsx +93 -0
  228. package/src/app/components/showroom/brandsSwiperClient.tsx +122 -0
  229. package/src/app/components/showroom/brandsSwiperServer.tsx +42 -0
  230. package/src/app/components/showroom/bundleProducts.tsx +120 -0
  231. package/src/app/components/showroom/categoryGrid.tsx +51 -0
  232. package/src/app/components/showroom/categoryGridServer.tsx +45 -0
  233. package/src/app/components/showroom/categorySwiper.tsx +115 -0
  234. package/src/app/components/showroom/featureStrip.tsx +139 -0
  235. package/src/app/components/showroom/offersSwiper.tsx +181 -0
  236. package/src/app/components/showroom/productGrid.tsx +56 -0
  237. package/src/app/components/showroom/productSwiper.tsx +119 -0
  238. package/src/app/components/showroom/promotion-slider.tsx +138 -0
  239. package/src/app/components/showroom/promotion.tsx +207 -0
  240. package/src/app/components/showroom/promotionsSwiper.tsx +174 -0
  241. package/src/app/components/showroom/showroomHeroCarousel.tsx +141 -0
  242. package/src/app/components/showroom/testimonialsGrid.tsx +106 -0
  243. package/src/app/components/skeletons/ContentSkeleton.tsx +14 -0
  244. package/src/app/components/sortDropdown/index.tsx +116 -0
  245. package/src/app/components/tertiaryButton/index.tsx +25 -0
  246. package/src/app/components/theme/theme-provider.tsx +82 -0
  247. package/src/app/contact/layout.tsx +32 -0
  248. package/src/app/contact/page.tsx +591 -0
  249. package/src/app/content/[slug]/layout.tsx +17 -0
  250. package/src/app/content/[slug]/page.tsx +159 -0
  251. package/src/app/content/layout.tsx +31 -0
  252. package/src/app/content/page.tsx +88 -0
  253. package/src/app/core-policies/page.tsx +55 -0
  254. package/src/app/discounts/page.tsx +54 -0
  255. package/src/app/frequently-asked-questions/page.tsx +57 -0
  256. package/src/app/globals.css +440 -0
  257. package/src/app/hooks/useDealerLocations.ts +259 -0
  258. package/src/app/hooks/useGTMEngagement.ts +71 -0
  259. package/src/app/hooks/useGoogleAnalytics.ts +145 -0
  260. package/src/app/layout.tsx +149 -0
  261. package/src/app/not-found.tsx +31 -0
  262. package/src/app/order-confirmation/layout.tsx +19 -0
  263. package/src/app/order-confirmation/page.tsx +12 -0
  264. package/src/app/order-confirmation/summary.tsx +1775 -0
  265. package/src/app/page.tsx +194 -0
  266. package/src/app/privacy-policy/loading.tsx +17 -0
  267. package/src/app/privacy-policy/page.tsx +56 -0
  268. package/src/app/product/[id]/ProductDetailClient.tsx +2448 -0
  269. package/src/app/product/[id]/components/itemInquiryModal.tsx +461 -0
  270. package/src/app/product/[id]/layout.tsx +116 -0
  271. package/src/app/product/[id]/page.tsx +200 -0
  272. package/src/app/product/layout.tsx +15 -0
  273. package/src/app/products/all/AllProductsClient.tsx +743 -0
  274. package/src/app/products/all/page.tsx +176 -0
  275. package/src/app/products/components/shopEmptyState.tsx +29 -0
  276. package/src/app/request-return/layout.tsx +36 -0
  277. package/src/app/request-return/page.tsx +597 -0
  278. package/src/app/robots.txt/route.ts +27 -0
  279. package/src/app/search/layout.tsx +16 -0
  280. package/src/app/search/page.tsx +736 -0
  281. package/src/app/shipping-returns/page.tsx +60 -0
  282. package/src/app/site-map/layout.tsx +33 -0
  283. package/src/app/site-map/page.tsx +113 -0
  284. package/src/app/sitemap-index.xml/route.ts +20 -0
  285. package/src/app/sitemap.ts +10 -0
  286. package/src/app/terms-and-conditions/loading.tsx +17 -0
  287. package/src/app/terms-and-conditions/page.tsx +56 -0
  288. package/src/app/utils/appConfiguration.ts +327 -0
  289. package/src/app/utils/branding.ts +52 -0
  290. package/src/app/utils/configurationService.ts +202 -0
  291. package/src/app/utils/constant.tsx +242 -0
  292. package/src/app/utils/editorJsUtils.tsx +249 -0
  293. package/src/app/utils/functions.ts +146 -0
  294. package/src/app/utils/googleAnalytics.ts +168 -0
  295. package/src/app/utils/googleTagManager.ts +475 -0
  296. package/src/app/utils/ipDetection.ts +270 -0
  297. package/src/app/utils/serverConfigurationService.ts +209 -0
  298. package/src/app/utils/svgs/GridIcon.tsx +45 -0
  299. package/src/app/utils/svgs/account/myAccount/listDotIcon.tsx +3 -0
  300. package/src/app/utils/svgs/account/myAccount/tickIcon.tsx +10 -0
  301. package/src/app/utils/svgs/account/orderHistory/InfoIcon.tsx +49 -0
  302. package/src/app/utils/svgs/arrowDownIcon.tsx +17 -0
  303. package/src/app/utils/svgs/arrowIcon.tsx +25 -0
  304. package/src/app/utils/svgs/arrowUpIcon.tsx +16 -0
  305. package/src/app/utils/svgs/brandsSearchIcon.tsx +25 -0
  306. package/src/app/utils/svgs/cart/cartIcon.tsx +31 -0
  307. package/src/app/utils/svgs/cart/plusIcon.tsx +13 -0
  308. package/src/app/utils/svgs/cart/subtractIcon.tsx +13 -0
  309. package/src/app/utils/svgs/cart/successTickIcon.tsx +14 -0
  310. package/src/app/utils/svgs/chevronDownIcon.tsx +21 -0
  311. package/src/app/utils/svgs/closeEyeIcon.tsx +47 -0
  312. package/src/app/utils/svgs/crossIcon.tsx +25 -0
  313. package/src/app/utils/svgs/eyeIcon.tsx +29 -0
  314. package/src/app/utils/svgs/featureTag.tsx +20 -0
  315. package/src/app/utils/svgs/filterIcon.tsx +3 -0
  316. package/src/app/utils/svgs/globleIcon.tsx +41 -0
  317. package/src/app/utils/svgs/infoIcon.tsx +34 -0
  318. package/src/app/utils/svgs/listIcon.tsx +50 -0
  319. package/src/app/utils/svgs/logOutIcon.tsx +35 -0
  320. package/src/app/utils/svgs/menuIcon.tsx +8 -0
  321. package/src/app/utils/svgs/minusIcon.tsx +18 -0
  322. package/src/app/utils/svgs/newsletterIcon.tsx +19 -0
  323. package/src/app/utils/svgs/noDataFoundIcon-.tsx +26 -0
  324. package/src/app/utils/svgs/noProductFoundIcon.tsx +43 -0
  325. package/src/app/utils/svgs/passwordIcons/errorIcon.tsx +31 -0
  326. package/src/app/utils/svgs/passwordIcons/successIcon.tsx +24 -0
  327. package/src/app/utils/svgs/paymentProcessingIcons/hourglassIcon.tsx +43 -0
  328. package/src/app/utils/svgs/paymentProcessingIcons/modalCrossIcon.tsx +23 -0
  329. package/src/app/utils/svgs/paymentProcessingIcons/paymentFailedIcon.tsx +47 -0
  330. package/src/app/utils/svgs/pencilIcon.tsx +11 -0
  331. package/src/app/utils/svgs/plusIcon.tsx +25 -0
  332. package/src/app/utils/svgs/productInquiryIcon.tsx +40 -0
  333. package/src/app/utils/svgs/searchIcon.tsx +31 -0
  334. package/src/app/utils/svgs/shoppingCart.tsx +32 -0
  335. package/src/app/utils/svgs/spinnerIcon.tsx +22 -0
  336. package/src/app/utils/svgs/spinnerLoadingIcon.tsx +26 -0
  337. package/src/app/utils/svgs/successTickIcon.tsx +40 -0
  338. package/src/app/utils/svgs/swiperArrowIconLeft.tsx +18 -0
  339. package/src/app/utils/svgs/swiperArrowIconRight.tsx +18 -0
  340. package/src/app/utils/svgs/userProfileIcon.tsx +31 -0
  341. package/src/app/utils/svgs/warningCircleIcon.tsx +15 -0
  342. package/src/app/warranty/constant.tsx +63 -0
  343. package/src/app/warranty/loading.tsx +17 -0
  344. package/src/app/warranty/page.tsx +56 -0
  345. package/src/graphql/client.ts +288 -0
  346. package/src/graphql/mutations/accountAddressCreate.ts +56 -0
  347. package/src/graphql/mutations/accountAddressDelete.ts +23 -0
  348. package/src/graphql/mutations/accountAddressUpdate.ts +55 -0
  349. package/src/graphql/mutations/accountSetDefaultAddress.ts +32 -0
  350. package/src/graphql/mutations/accountUpdate.ts +34 -0
  351. package/src/graphql/mutations/changePassword.ts +25 -0
  352. package/src/graphql/mutations/checkout.ts +117 -0
  353. package/src/graphql/mutations/checkoutAddVoucher.ts +63 -0
  354. package/src/graphql/mutations/checkoutComplete.ts +79 -0
  355. package/src/graphql/mutations/checkoutCreate.ts +131 -0
  356. package/src/graphql/mutations/checkoutCustomerAttach.ts +50 -0
  357. package/src/graphql/mutations/checkoutEmailUpdate.ts +15 -0
  358. package/src/graphql/mutations/checkoutLineMetadataUpdate.ts +52 -0
  359. package/src/graphql/mutations/checkoutPaymentCreate.ts +82 -0
  360. package/src/graphql/mutations/paymentGatewayInitialize.ts +58 -0
  361. package/src/graphql/mutations/registerAccount.ts +65 -0
  362. package/src/graphql/mutations/requestPasswordReset.ts +32 -0
  363. package/src/graphql/mutations/setPassword.ts +49 -0
  364. package/src/graphql/mutations/signIn.ts +50 -0
  365. package/src/graphql/mutations/tokenRefresh.ts +19 -0
  366. package/src/graphql/mutations/updateCheckoutMetadata.ts +49 -0
  367. package/src/graphql/mutations/updateProfile.ts +18 -0
  368. package/src/graphql/mutations/willCallDeliveryMethod.ts +81 -0
  369. package/src/graphql/queries/checkout.ts +168 -0
  370. package/src/graphql/queries/findProductByOldSlug.ts +58 -0
  371. package/src/graphql/queries/getAboutPage.ts +24 -0
  372. package/src/graphql/queries/getAboutPageId.ts +9 -0
  373. package/src/graphql/queries/getAboutUs.ts +38 -0
  374. package/src/graphql/queries/getAddressInformation.ts +38 -0
  375. package/src/graphql/queries/getAllCategories.ts +41 -0
  376. package/src/graphql/queries/getAllCategoriesTree.ts +67 -0
  377. package/src/graphql/queries/getAllCategoriesWithProducts.ts +29 -0
  378. package/src/graphql/queries/getAllCollectionsWithProducts.ts +16 -0
  379. package/src/graphql/queries/getBlogs.ts +222 -0
  380. package/src/graphql/queries/getBrands.ts +17 -0
  381. package/src/graphql/queries/getBundles.ts +43 -0
  382. package/src/graphql/queries/getCategories.ts +20 -0
  383. package/src/graphql/queries/getChannels.ts +77 -0
  384. package/src/graphql/queries/getCheckoutQuestions.ts +115 -0
  385. package/src/graphql/queries/getCheckoutTermsAndConditions.ts +37 -0
  386. package/src/graphql/queries/getContactPage.ts +117 -0
  387. package/src/graphql/queries/getContentPage.ts +191 -0
  388. package/src/graphql/queries/getDiscountOffers.ts +18 -0
  389. package/src/graphql/queries/getDynamicPageBySlug.ts +251 -0
  390. package/src/graphql/queries/getFeaturedProducts.ts +48 -0
  391. package/src/graphql/queries/getHeroMetadata.ts +23 -0
  392. package/src/graphql/queries/getMenuBySlug.ts +84 -0
  393. package/src/graphql/queries/getMyProfile.ts +23 -0
  394. package/src/graphql/queries/getNewsletter.ts +122 -0
  395. package/src/graphql/queries/getNewsletterPage.ts +111 -0
  396. package/src/graphql/queries/getPageBySlug.ts +52 -0
  397. package/src/graphql/queries/getPageTypeId.ts +27 -0
  398. package/src/graphql/queries/getPaymentMethods.ts +61 -0
  399. package/src/graphql/queries/getProducts.ts +78 -0
  400. package/src/graphql/queries/getPromotions.ts +24 -0
  401. package/src/graphql/queries/getRequestReturnPage.ts +121 -0
  402. package/src/graphql/queries/getSiteInfo.ts +54 -0
  403. package/src/graphql/queries/getSocialLinks.ts +52 -0
  404. package/src/graphql/queries/getTestimonials.ts +25 -0
  405. package/src/graphql/queries/getUserWithCheckout.ts +27 -0
  406. package/src/graphql/queries/getVehicleMakes.ts +21 -0
  407. package/src/graphql/queries/getVehicleModels.ts +21 -0
  408. package/src/graphql/queries/getVehicleYears.ts +21 -0
  409. package/src/graphql/queries/meAddresses.ts +56 -0
  410. package/src/graphql/queries/myOrders.ts +37 -0
  411. package/src/graphql/queries/orderDetail.ts +231 -0
  412. package/src/graphql/queries/productDetailsById.ts +197 -0
  413. package/src/graphql/queries/productInquiry.ts +115 -0
  414. package/src/graphql/queries/productsByCategoriesAndCollections.ts +39 -0
  415. package/src/graphql/queries/willCallCollectionPoints.ts +55 -0
  416. package/src/graphql/server-client.ts +54 -0
  417. package/src/graphql/types/categories.ts +9 -0
  418. package/src/graphql/types/checkout.ts +168 -0
  419. package/src/graphql/types/offer.ts +12 -0
  420. package/src/graphql/types/product.ts +44 -0
  421. package/src/hooks/scrollPageTop.ts +9 -0
  422. package/src/hooks/serverNavbarData.ts +79 -0
  423. package/src/hooks/useCartSync.ts +24 -0
  424. package/src/hooks/useRecaptcha.ts +33 -0
  425. package/src/hooks/useTokenExpiration.ts +81 -0
  426. package/src/hooks/useVehicleData.ts +346 -0
  427. package/src/lib/api/kount.ts +165 -0
  428. package/src/lib/api/shop.ts +1445 -0
  429. package/src/lib/saleor/getSaleorApiUrl.ts +25 -0
  430. package/src/lib/schema.ts +303 -0
  431. package/src/lib/seo/extractTextFromEditorJs.ts +58 -0
  432. package/src/lib/seo/site.ts +10 -0
  433. package/src/lib/urls/normalizeInternalUrl.ts +53 -0
  434. package/src/middleware.ts +134 -0
  435. package/src/sitemaps/README.md +105 -0
  436. package/src/sitemaps/dynamic-pages-sitemap.ts +247 -0
  437. package/src/sitemaps/sitemap-index.ts +21 -0
  438. package/src/sitemaps/static-pages-sitemap.ts +36 -0
  439. package/src/store/useGlobalStore.tsx +1656 -0
  440. package/src/types/global.d.ts +148 -0
  441. package/tsconfig.json +27 -0
@@ -0,0 +1,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
+ }