CheckoutView.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import React, { useState, useEffect } from "react";
  2. import { ViewMode } from "../../services/types";
  3. import { useLocation, useNavigate } from "react-router-dom";
  4. import { useQuery } from "@tanstack/react-query";
  5. import {
  6. Area,
  7. CheckoutDetailResponse,
  8. Package,
  9. PaymentChannel,
  10. } from "../../services/product/type";
  11. import { DataCacheKey, staleTime } from "../../global/constants";
  12. import {
  13. startLoading,
  14. startSmallLoading,
  15. stopLoading,
  16. stopSmallLoading,
  17. } from "../../features/loading/loadingSlice";
  18. import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
  19. import { productApi } from "../../apis/productApi";
  20. import { openPopup } from "../../features/popup/popupSlice";
  21. import { formatNumber } from "../../logic/loigicUtils";
  22. import { useTranslation } from "react-i18next";
  23. const CheckoutView = () => {
  24. const navigate = useNavigate();
  25. const dispatch = useAppDispatch();
  26. const { t } = useTranslation();
  27. const loading = useAppSelector((state) => state.loading);
  28. const accountInfo = localStorage.getItem("accountInfo");
  29. const [paymentMethod, setPaymentMethod] = useState("card");
  30. const [form, setForm] = useState({
  31. firstName: "",
  32. lastName: "",
  33. email: accountInfo != null ? JSON.parse(accountInfo).email : "",
  34. confirmEmail: accountInfo != null ? JSON.parse(accountInfo).email : "",
  35. phone: "",
  36. });
  37. const [agreements, setAgreements] = useState({
  38. terms: false,
  39. esim: false,
  40. vatInvoice: false,
  41. });
  42. // get data from previous step
  43. const location = useLocation();
  44. const state = location.state as {
  45. area: Area;
  46. package: Package;
  47. quantity: number;
  48. simType: "eSIM" | "Physical";
  49. checkoutDetails: CheckoutDetailResponse;
  50. };
  51. const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  52. const { name, value } = e.target;
  53. setForm((prev) => ({ ...prev, [name]: value }));
  54. };
  55. const handleSubmit = async () => {
  56. console.log(form);
  57. if (form.email !== form.confirmEmail) {
  58. dispatch(
  59. openPopup({
  60. isSuccess: false,
  61. title: "Error",
  62. message: "Email and Confirm Email do not match.",
  63. })
  64. );
  65. return;
  66. }
  67. // proceed to payment
  68. dispatch(startSmallLoading());
  69. const res = await productApi.confirmOrder({
  70. surName: form.lastName,
  71. lastName: form.firstName,
  72. email: form.email,
  73. phoneNumber: form.phone,
  74. paymentChannelId: paymentMethod,
  75. packgId: state.package.id,
  76. quantity: state.quantity,
  77. });
  78. console.log("Confirm order response:", res);
  79. dispatch(stopSmallLoading());
  80. if (res.errorCode === "0") {
  81. // open other website to pay
  82. window.location.href = res.data.paymentUrl;
  83. } else {
  84. dispatch(
  85. openPopup({
  86. isSuccess: false,
  87. title: "Error",
  88. message: res.message || "Failed to confirm order.",
  89. })
  90. );
  91. }
  92. };
  93. const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  94. const { name, checked } = e.target;
  95. setAgreements((prev) => ({
  96. ...prev,
  97. [name]: checked,
  98. }));
  99. };
  100. const { data: loadPaymentChannel = [] } = useQuery<PaymentChannel[]>({
  101. queryKey: [DataCacheKey.PAYMENT_CHANNELS],
  102. queryFn: async (): Promise<PaymentChannel[]> => {
  103. try {
  104. dispatch(startLoading({}));
  105. const res = await productApi.loadPaymentChannel();
  106. // save to redux store
  107. return res.data as PaymentChannel[];
  108. } catch (error) {
  109. console.error(error);
  110. return []; // 🔴 bắt buộc
  111. } finally {
  112. dispatch(stopLoading());
  113. }
  114. },
  115. staleTime: staleTime,
  116. });
  117. useEffect(() => {
  118. setPaymentMethod(loadPaymentChannel[0]?.id);
  119. dispatch(stopSmallLoading());
  120. }, [loadPaymentChannel]);
  121. // Styles matching LoginView inputs
  122. const inputClass =
  123. "w-full bg-slate-50 border-2 border-transparent focus:border-[#EE0434]/20 rounded-2xl py-3 md:py-4 px-5 focus:outline-none focus:bg-white transition-all text-slate-700 font-bold placeholder:text-slate-300";
  124. const labelClass =
  125. "block text-slate-400 font-black text-[14px] uppercase tracking-[0.2em] pl-1 mb-2";
  126. if (!state.checkoutDetails) {
  127. return null;
  128. }
  129. return (
  130. <div className="bg-white min-h-screen pb-20">
  131. {/* Header / Breadcrumb */}
  132. <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
  133. <div className="flex items-center space-x-2">
  134. <button
  135. onClick={() => navigate(-1)}
  136. className="flex items-center text-slate-900 font-bold hover:text-[#EE0434] transition-colors text-[18px]"
  137. >
  138. <svg
  139. className="w-5 h-5 mr-1"
  140. fill="none"
  141. stroke="currentColor"
  142. viewBox="0 0 24 24"
  143. >
  144. <path
  145. strokeLinecap="round"
  146. strokeLinejoin="round"
  147. strokeWidth={2.5}
  148. d="M15 19l-7-7 7-7"
  149. />
  150. </svg>
  151. {t("continueShopping")}
  152. </button>
  153. </div>
  154. </div>
  155. <div className="max-w-3xl mx-auto px-4 py-8 md:py-12">
  156. {/* Progress Steps */}
  157. <div className="flex justify-center mb-12">
  158. <div className="flex items-center w-full max-w-sm text-xs font-bold">
  159. <div className="flex flex-col items-center gap-2">
  160. <div className="w-8 h-8 rounded-full bg-[#00c087] text-white flex items-center justify-center">
  161. <svg
  162. className="w-4 h-4"
  163. fill="none"
  164. stroke="currentColor"
  165. viewBox="0 0 24 24"
  166. >
  167. <path
  168. strokeLinecap="round"
  169. strokeLinejoin="round"
  170. strokeWidth={3}
  171. d="M5 13l4 4L19 7"
  172. />
  173. </svg>
  174. </div>
  175. <span className="text-[#00c087] text-[14px]">
  176. {t("chooseProduct")}
  177. </span>
  178. </div>
  179. <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
  180. <div className="flex flex-col items-center gap-2">
  181. <div className="w-8 h-8 rounded-full bg-[#0071e3] text-white flex items-center justify-center">
  182. 2
  183. </div>
  184. <span className="text-[#0071e3] text-center text-[14px]">
  185. {t("orderInformation")}
  186. </span>
  187. </div>
  188. <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
  189. <div className="flex flex-col items-center gap-2">
  190. <div className="w-8 h-8 rounded-full bg-white border border-slate-200 text-slate-300 flex items-center justify-center">
  191. 3
  192. </div>
  193. <span className="text-slate-300 text-[14px]">
  194. {t("paymentPayment")}
  195. </span>
  196. </div>
  197. </div>
  198. </div>
  199. <div className="space-y-10">
  200. {/* Customer Information */}
  201. <div>
  202. <h2 className="text-xl font-black text-[#003459] mb-6">
  203. {t("customerInformation")}
  204. </h2>
  205. <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  206. <div className="space-y-1">
  207. <label className={labelClass}>
  208. {t("lastName")} <span className="text-red-500">*</span>
  209. </label>
  210. <input
  211. type="text"
  212. className={inputClass}
  213. placeholder="Doe"
  214. onChange={handleChange}
  215. value={form.lastName}
  216. name="lastName"
  217. />
  218. </div>
  219. <div className="space-y-1">
  220. <label className={labelClass}>
  221. {t("firstName")} <span className="text-red-500">*</span>
  222. </label>
  223. <input
  224. type="text"
  225. className={inputClass}
  226. placeholder="John"
  227. onChange={handleChange}
  228. value={form.firstName}
  229. name="firstName"
  230. />
  231. </div>
  232. <div className="space-y-1">
  233. <label className={labelClass}>
  234. {t("emailEmail")} <span className="text-red-500">*</span>
  235. </label>
  236. <input
  237. type="email"
  238. className={inputClass}
  239. placeholder="john@example.com"
  240. onChange={handleChange}
  241. value={form.email}
  242. name="email"
  243. />
  244. </div>
  245. <div className="space-y-1">
  246. <label className={labelClass}>
  247. {t("confirmEmail")} <span className="text-red-500">*</span>
  248. </label>
  249. <input
  250. type="email"
  251. className={inputClass}
  252. placeholder="john@example.com"
  253. onChange={handleChange}
  254. value={form.confirmEmail}
  255. name="confirmEmail"
  256. />
  257. </div>
  258. <div className="md:col-span-2 space-y-1">
  259. <label className={labelClass}>{t("phoneNumber")}</label>
  260. <input
  261. type="tel"
  262. className={inputClass}
  263. placeholder="+1 234 567 890"
  264. onChange={handleChange}
  265. value={form.phone}
  266. name="phone"
  267. />
  268. </div>
  269. </div>
  270. </div>
  271. {/* Order Info */}
  272. <div className="bg-slate-50/50 rounded-3xl p-6 border border-slate-100">
  273. <h2 className="text-xl font-black text-[#003459] mb-6">
  274. {t("orderInformation")}
  275. </h2>
  276. <div className="flex justify-between items-center">
  277. <div className="flex items-center gap-4">
  278. <img
  279. src={`${state.area.iconUrl}`}
  280. className="w-10 h-10 rounded-full object-cover border-2 border-white shadow-sm"
  281. />
  282. <div>
  283. <h3 className="font-black text-slate-800 text-lg">
  284. eSIM {state.area.areaName1}
  285. </h3>
  286. <p className="text-xs text-slate-500 font-bold uppercase tracking-wide mt-1">
  287. {/* {state.package.amountData} - */}
  288. {state.package.dayDuration} days • x{state.quantity}
  289. </p>
  290. </div>
  291. </div>
  292. <div className="text-right">
  293. <p className="text-xs text-slate-400 line-through font-bold">
  294. {formatNumber(state.checkoutDetails.totalMoney)}{" "}
  295. {state.checkoutDetails.curency}
  296. </p>
  297. <p className="text-[#0071e3] font-black text-xl">
  298. {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
  299. {state.checkoutDetails.curency}
  300. </p>
  301. </div>
  302. </div>
  303. </div>
  304. {/* Payment Method */}
  305. <div>
  306. <h2 className="text-xl font-black text-[#003459] mb-6">
  307. {t("paymentPayment")}
  308. </h2>
  309. <div className="space-y-3">
  310. {loadPaymentChannel.map((method) => (
  311. <label
  312. key={method.id}
  313. className={`flex items-center p-4 border rounded-2xl cursor-pointer transition-all ${
  314. paymentMethod === method.id
  315. ? "bg-blue-50 border-blue-200"
  316. : "border-slate-100 hover:bg-slate-50"
  317. }`}
  318. >
  319. <input
  320. type="radio"
  321. name="payment"
  322. checked={paymentMethod === method.id}
  323. onChange={() => setPaymentMethod(method.id)}
  324. className="w-5 h-5 text-[#EE0434] border-slate-300 focus:ring-[#EE0434]"
  325. />
  326. <span className="ml-4 font-bold text-slate-700 flex items-center gap-3">
  327. <span className="text-xl w-6 text-center">
  328. {/* {method.imgUrl} */}
  329. </span>{" "}
  330. {method.name}
  331. </span>
  332. </label>
  333. ))}
  334. </div>
  335. </div>
  336. {/* Terms */}
  337. <div className="space-y-3 pt-2 px-1">
  338. <label className="flex items-start cursor-pointer group">
  339. <input
  340. type="checkbox"
  341. name="terms"
  342. checked={agreements.terms}
  343. onChange={handleCheckboxChange}
  344. className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
  345. />
  346. <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
  347. {t("iService")}{" "}
  348. <a
  349. href="#"
  350. className="text-[#0071e3] font-bold hover:underline"
  351. >
  352. {t("termsService")}
  353. </a>
  354. </span>
  355. </label>
  356. <label className="flex items-start cursor-pointer group">
  357. <input
  358. type="checkbox"
  359. className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
  360. name="esim"
  361. checked={agreements.esim}
  362. onChange={handleCheckboxChange}
  363. />
  364. <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
  365. {t("iUnlocked")}
  366. </span>
  367. </label>
  368. <label className="flex items-start cursor-pointer group">
  369. <input
  370. type="checkbox"
  371. className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
  372. name="vatInvoice"
  373. checked={agreements.vatInvoice}
  374. onChange={handleCheckboxChange}
  375. />
  376. <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
  377. {t("iInvoice")}
  378. </span>
  379. </label>
  380. </div>
  381. {/* Overview Card */}
  382. <div className="bg-[#eef6f8] rounded-[32px] p-8 mt-8">
  383. <h2 className="text-xl font-black text-[#003459] mb-6">Overview</h2>
  384. <div className="relative mb-8">
  385. <input
  386. type="text"
  387. placeholder="Promo code"
  388. className="w-full bg-white border-2 border-transparent focus:border-[#0071e3]/20 rounded-2xl py-4 px-6 focus:outline-none transition-all text-slate-700 font-bold placeholder:text-slate-300"
  389. />
  390. <button className="absolute right-3 top-2 bottom-2 text-[#0071e3] font-black text-sm px-4 hover:bg-blue-50 rounded-xl transition-colors">
  391. {t("apply")}
  392. </button>
  393. </div>
  394. <div className="space-y-4 mb-8">
  395. <div className="flex justify-between text-slate-600 font-bold">
  396. <span>{t("subtotal")}:</span>
  397. <span>
  398. {formatNumber(
  399. state.checkoutDetails.totalMoney * state.quantity
  400. )}{" "}
  401. {state.checkoutDetails.curency}
  402. </span>
  403. </div>
  404. <div className="flex justify-between text-green-600 font-bold">
  405. <span>Discount:</span>
  406. <span>
  407. -{formatNumber(0 * state.quantity)}{" "}
  408. {state.checkoutDetails.curency}
  409. </span>
  410. </div>
  411. <div className="flex justify-between items-center pt-4 border-t border-slate-200/50">
  412. <span className="text-[#0071e3] font-black text-xl">
  413. {t("totalTotal")} ({state.quantity}):
  414. </span>
  415. <span className="text-[#0071e3] font-black text-3xl">
  416. {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
  417. {state.checkoutDetails.curency}
  418. </span>
  419. </div>
  420. </div>
  421. <button
  422. className={`w-full py-4 md:py-5 rounded-2xl font-black text-xl md:text-2xl shadow-lg transition-all flex items-center justify-center space-x-3 ${
  423. form.firstName &&
  424. form.lastName &&
  425. form.email &&
  426. agreements.terms &&
  427. agreements.esim &&
  428. !loading.isSmallLoading
  429. ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
  430. : "bg-slate-100 text-slate-300 cursor-not-allowed"
  431. }`}
  432. disabled={
  433. !form.firstName ||
  434. !form.lastName ||
  435. !form.email ||
  436. !agreements.terms ||
  437. !agreements.esim ||
  438. loading.isSmallLoading
  439. }
  440. onClick={handleSubmit}
  441. >
  442. {loading.isSmallLoading && (
  443. <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
  444. )}
  445. <span>{t("checkout")}</span>
  446. </button>
  447. </div>
  448. </div>
  449. </div>
  450. </div>
  451. );
  452. };
  453. export default CheckoutView;