CheckoutWrapper.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. "use client";
  2. import {useState, useRef, useEffect, useCallback } from "react";
  3. import clsx from "clsx";
  4. import { useCustomToast } from "@/utils/hooks/useToast";
  5. import {useCheckoutPaymentMethod} from "@/utils/hooks/useCheckoutPaymentMethod";
  6. import {useCheckoutShippingMethod} from "@/utils/hooks/useCheckoutShippingMethod";
  7. import { useAppDispatch } from "@/store/hooks";
  8. import { updateCart, clearCart } from "@/store/slices/cart-slice";
  9. import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
  10. import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
  11. import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
  12. import {
  13. PlaceOrderResult,
  14. PlaceOrderFunction,
  15. CreatePaymentInitiateVariables
  16. } from "@/types/checkout/type";
  17. import { CartDetail,CartAddress } from "@/types/cart/type";
  18. import { formatCartDetail } from "@/utils/cartDetailTools";
  19. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  20. import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
  21. import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
  22. import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
  23. import {CheckoutAddress, RefCheckoutAddressHandle} from "./CheckoutAddress/CheckoutAssress";
  24. import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
  25. import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
  26. import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
  27. import CheckoutGiftCardGate from "./GiftCard/CheckoutGiftCardGate";
  28. import PromotionsDetails from "./PromotionsDetails";
  29. import CheckoutVipPlus from "./VipPlus/CheckoutVipPlus";
  30. import CheckoutProducts from "./CheckoutProducts/CheckoutProducts";
  31. import CheckoutCoupon from "./CheckoutCoupon/CheckoutCoupon";
  32. /***
  33. * 保存完地址之后要重新获取运输方式和支付方式
  34. * 变更运输方式后重新获取支付方式
  35. * @todo 购物车为空 跳转到空购物车页面
  36. */
  37. export default function CheckoutWrapper({
  38. loginEmail,
  39. cartDetailData,
  40. }: {
  41. loginEmail: string;
  42. cartDetailData: CartDetail;
  43. }) {
  44. const erudaCount = useRef(0);
  45. const {loadSdk} = usePaymentSDKContext();
  46. const { createOrder } = usePlaceOrder();
  47. const dispatch = useAppDispatch();
  48. const [cartData, setCartData] = useState<CartDetail>(cartDetailData);
  49. const {saveCheckoutCart} = useSaveCheckoutCart();
  50. const {
  51. data: shippingMethodDatas,
  52. loading: shippingMethodLoading,
  53. error: shippingMethodError,
  54. getShippingMethod,
  55. } = useCheckoutShippingMethod();
  56. const selectedPaymentMethod = cartData.paymentMethod || 'paypal_smart_button';
  57. const selectedShippingRate = cartData.selectedShippingRate;
  58. const {
  59. data: paymentMethodDatas,
  60. loading: paymentMethodLoading,
  61. error: paymentMethodError,
  62. getPaymentMethod,
  63. } = useCheckoutPaymentMethod();
  64. const { showToast } = useCustomToast();
  65. const checkoutAddressRef = useRef<RefCheckoutAddressHandle>(null);
  66. const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
  67. const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
  68. const changePaymentMethodToloadSdk = (method:string) => {
  69. if(method === 'klarna' ||
  70. method === 'afterpay' ||
  71. method === 'clearpay' ||
  72. method === 'applepay'
  73. ) {
  74. loadSdk(method);
  75. }
  76. };
  77. useEffect(() => {
  78. changePaymentMethodToloadSdk(selectedPaymentMethod);
  79. }, [selectedPaymentMethod]);
  80. const dispatchCartStore = useCallback((action: "clear" | "update", payload?:Partial<CartDetail>) => {
  81. if(action === "clear") {
  82. dispatch(clearCart());
  83. } else if(action === "update" && payload) {
  84. dispatch(updateCart(payload));
  85. }
  86. },[dispatch]);
  87. const saveAddressCallback = async (resData: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => {
  88. await Promise.all([getShippingMethod(),getPaymentMethod()]);
  89. dispatchCartStore('update',{
  90. billingAddress: resData.billingAddress,
  91. shippingAddress: resData.shippingAddress
  92. });
  93. setCartData(Object.assign({...cartData},{
  94. billingAddress: resData.billingAddress,
  95. shippingAddress: resData.shippingAddress
  96. }));
  97. };
  98. const handlePaymentMethodChange = async (method: string) => {
  99. changePaymentMethodToloadSdk(method);
  100. overlayLoading.start();
  101. const saveRes = await saveCheckoutCart({
  102. shippingMethod: '-1',
  103. paymentMethod: method,
  104. couponCode: '-1',
  105. giftCardNumber: '-1',
  106. memberDiscount: '-1'
  107. });
  108. overlayLoading.stop();
  109. if(!saveRes.error) {
  110. if(saveRes.data) {
  111. const newCartDetail = formatCartDetail(saveRes.data);
  112. dispatchCartStore('update',newCartDetail);
  113. setCartData({...newCartDetail});
  114. }
  115. return true;
  116. } else {
  117. showToast(saveRes.msg, 'danger');
  118. return false;
  119. }
  120. };
  121. const handleShippingMethodChange = useCallback(async (method: string) => {
  122. overlayLoading.start();
  123. const saveRes = await saveCheckoutCart({
  124. shippingMethod: method,
  125. paymentMethod: '-1',
  126. couponCode: '-1',
  127. giftCardNumber: '-1',
  128. memberDiscount: '-1'
  129. });
  130. overlayLoading.stop();
  131. if(!saveRes.error) {
  132. if(saveRes.data) {
  133. const newCartDetail = formatCartDetail(saveRes.data);
  134. dispatchCartStore('update',newCartDetail);
  135. setCartData({...newCartDetail});
  136. }
  137. // 保存成功之后需要重新获取支付方式
  138. getPaymentMethod();
  139. } else {
  140. showToast(saveRes.msg, 'danger');
  141. }
  142. },[saveCheckoutCart,showToast,getPaymentMethod,dispatchCartStore]);
  143. // 校验地址 运输方式,支付方式;
  144. const validateCheckout = useCallback(async () => {
  145. if(checkoutAddressRef.current) {
  146. const addressValid = await checkoutAddressRef.current.validateAddressForm();
  147. if(!addressValid) {
  148. showToast('Please check your address.', 'danger');
  149. return false;
  150. }
  151. } else {
  152. showToast('Please wait for address form loading.', 'danger');
  153. return false;
  154. }
  155. if(methodShiippingRef.current) {
  156. const shippingMethodValid = await methodShiippingRef.current.validateShipMethod();
  157. if(!shippingMethodValid) {
  158. showToast('Please select a shipping method.', 'danger');
  159. return false;
  160. }
  161. } else {
  162. showToast('Please wait for shipping method loading.', 'danger');
  163. return false;
  164. }
  165. if(methodPaymentRef.current) {
  166. const paymentMethodValid = await methodPaymentRef.current.validatePaymentMethod();
  167. const paymentMethod = methodPaymentRef.current.getSelectPaymentMethod();
  168. if(!paymentMethodValid) {
  169. showToast('Please select a payment method.', 'danger');
  170. return false;
  171. }
  172. if(paymentMethod === 'airwallex') {
  173. const paymentMethodValid = methodPaymentRef.current.validateAirwallexCard();
  174. return paymentMethodValid;
  175. }
  176. } else {
  177. showToast('Please wait for payment method loading.', 'danger');
  178. return false;
  179. }
  180. if(!cartData) {
  181. showToast('Please add product to your cart.', 'danger');
  182. return false;
  183. }
  184. //如果购物车详情没有保存过支付方式,则需要保存支付方式
  185. if(cartData.paymentMethod === null) {
  186. const pmethod = methodPaymentRef.current.getSelectPaymentMethod();
  187. const saveRes = await saveCheckoutCart({
  188. shippingMethod: '-1',
  189. paymentMethod: pmethod,
  190. couponCode: '-1',
  191. giftCardNumber: '-1',
  192. memberDiscount: '-1'
  193. });
  194. if(!saveRes.error) {
  195. if(saveRes.data) {
  196. const newCartDetail = formatCartDetail(saveRes.data);
  197. dispatchCartStore('update',newCartDetail);
  198. setCartData({...newCartDetail});
  199. }
  200. } else {
  201. showToast(saveRes.msg, 'danger');
  202. }
  203. }
  204. return true;
  205. },[showToast,saveCheckoutCart,dispatchCartStore]);
  206. // 苹果支付下单
  207. const applepayPlaceOrder = useCallback(async () => {
  208. const res = await createOrder();
  209. if(!res.error) {
  210. //创建订单成功后清空redux 购物车
  211. dispatchCartStore('clear');
  212. return {
  213. orderId: res.data?.gatewayOrderId ?? '',
  214. webOrderId: res.data?.orderId ?? ''
  215. };
  216. } else{
  217. // 抛出错误让第三方支付能够捕获
  218. throw new Error("CREATE_ORDER_ERROR: " + res.msg);
  219. }
  220. },[createOrder,dispatchCartStore]);
  221. // 下单(下单和校验分开)
  222. const handlePlaceOrder: PlaceOrderFunction = async <T,>(params?: CreatePaymentInitiateVariables): Promise<PlaceOrderResult<T>> => {
  223. const res = await createOrder(params);
  224. if(res.error) {
  225. overlayLoading.stop();
  226. confirmDialog({
  227. title: "Warning",
  228. content: res.msg + ". Create order failed. Please try again.",
  229. noCancel: true,
  230. }).then(() => {});
  231. } else {
  232. //创建订单成功后清空redux 购物车
  233. dispatchCartStore('clear');
  234. }
  235. const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
  236. let resultData: PlaceOrderResult<T>;
  237. if(pmethod === 'airwallex' && methodPaymentRef.current) {
  238. const airwallexCardElements = methodPaymentRef.current.getAirwallexCardElements();
  239. const airCardNumber = airwallexCardElements.cardNumber as T;
  240. resultData = {
  241. data: res.data,
  242. otherData: airCardNumber
  243. };
  244. } else if(pmethod === 'klarna') {
  245. const klaData = {
  246. billingAddress: cartData.billingAddress,
  247. shippingAddress: cartData.shippingAddress
  248. } as T;
  249. resultData = {
  250. data: res.data,
  251. otherData: klaData
  252. };
  253. } else {
  254. resultData = {
  255. data: res.data,
  256. otherData: null
  257. };
  258. }
  259. return resultData;
  260. }
  261. const onCartDataChange = async (paylod: CartDetail, dispatch: boolean = true) => {
  262. if(dispatch) {
  263. dispatchCartStore('update',paylod);
  264. }
  265. setCartData({...paylod});
  266. // 还要请求支付方式和运输方式
  267. await Promise.all([getShippingMethod(),getPaymentMethod()]);
  268. };
  269. const testEruda = () => {
  270. erudaCount.current++
  271. if(window.eruda && erudaCount.current > 10) {
  272. window.eruda.init();
  273. }
  274. };
  275. useEffect(() => {
  276. console.log('checkoutWrapper mout==========================');
  277. return () => {
  278. console.log('checkoutWrapper unmout==========================');
  279. }
  280. },[]);
  281. return (<>
  282. <section className="w-full">
  283. <div>
  284. <CheckoutAddress
  285. ref={checkoutAddressRef}
  286. loginEmail={loginEmail}
  287. cartData={cartData}
  288. onSaveAddress={saveAddressCallback}
  289. />
  290. </div>
  291. <div className="mt-6 box-border px-4">
  292. <CheckoutProducts
  293. cartData={cartData}
  294. onCartChange={onCartDataChange}
  295. />
  296. </div>
  297. <div className="mt-6 box-border px-4">
  298. <h3 className="text-ly-24 font-medium" onClick={testEruda}>Shipping Method</h3>
  299. <ShippingMethodCheckout
  300. ref={methodShiippingRef}
  301. shipMethods={shippingMethodDatas}
  302. errorMsg={shippingMethodError}
  303. selectedShippingRate={selectedShippingRate}
  304. shippingMethodLoading={shippingMethodLoading}
  305. onShipMethodChange={handleShippingMethodChange}
  306. />
  307. </div>
  308. <div className="mt-6 box-border px-4">
  309. <CheckoutCoupon
  310. couponCode={cartData.couponCode || ''}
  311. discountAmount={cartData.formattedDiscountAmount}
  312. onCouponChange={onCartDataChange}
  313. />
  314. </div>
  315. {!cartData.isGuest &&
  316. <div className="mt-6 box-border px-4">
  317. <CheckoutGiftCardGate
  318. giftcardNumber={cartData.giftcardNumber || ''}
  319. formattedGiftcardAmount={cartData.formattedGiftcardAmount}
  320. onChangeGiftcard={onCartDataChange}
  321. />
  322. </div>
  323. }
  324. <div className="mt-6 box-border px-4">
  325. <CheckoutVipPlus
  326. cartData={cartData}
  327. onSwitchVip={onCartDataChange}
  328. />
  329. </div>
  330. <div className="mt-6 box-border px-4">
  331. <h3 className="text-ly-24 font-medium">Promotion Details</h3>
  332. <div className="w-full box-border mt-3">
  333. <PromotionsDetails cartData={cartData} />
  334. </div>
  335. </div>
  336. <div className="mt-6 box-border px-4">
  337. <h3 className="text-ly-24 font-medium">Payment Method</h3>
  338. <PaymentMethodCheckout
  339. ref={methodPaymentRef}
  340. paymentMethods={paymentMethodDatas}
  341. selectedPaymentMethod={selectedPaymentMethod}
  342. paymentMethodLoading={paymentMethodLoading}
  343. loadError={paymentMethodError}
  344. onPaymentMethodChange={handlePaymentMethodChange}
  345. />
  346. </div>
  347. <div className="mt-3 box-border px-4 pb-8">
  348. <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
  349. <div className="mt-6 w-full">
  350. { paymentMethodLoading && <LoadingCheckoutPlaceOrder />}
  351. <div className={clsx("w-full",{
  352. "hidden": selectedPaymentMethod !== 'applepay' || paymentMethodLoading
  353. })}>
  354. <PaypalApplepayButton
  355. grandTotal={cartData.grandTotal}
  356. billingContact={cartData.billingAddress}
  357. shippingContact={cartData.shippingAddress}
  358. validateCheckout={validateCheckout}
  359. createOrder={applepayPlaceOrder}
  360. />
  361. </div>
  362. <div className={clsx("w-full",{
  363. "hidden": selectedPaymentMethod === 'applepay' || paymentMethodLoading
  364. })}>
  365. <CheckoutPlaceOrder
  366. paymentMethod={selectedPaymentMethod}
  367. validateCheckout={validateCheckout}
  368. clickPlaceOrder={handlePlaceOrder}
  369. />
  370. </div>
  371. </div>
  372. </div>
  373. </section>
  374. </>);
  375. };