| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- import React, { useState, useEffect } from "react";
- import { ViewMode } from "../../services/types";
- import { useLocation, useNavigate } from "react-router-dom";
- import { useQuery } from "@tanstack/react-query";
- import {
- Area,
- CheckoutDetailResponse,
- Package,
- PaymentChannel,
- } from "../../services/product/type";
- import { DataCacheKey, staleTime } from "../../global/constants";
- import {
- startLoading,
- startSmallLoading,
- stopLoading,
- stopSmallLoading,
- } from "../../features/loading/loadingSlice";
- import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
- import { productApi } from "../../apis/productApi";
- import { openPopup } from "../../features/popup/popupSlice";
- import { formatNumber } from "../../logic/loigicUtils";
- import { useTranslation } from "react-i18next";
- const CheckoutView = () => {
- const navigate = useNavigate();
- const dispatch = useAppDispatch();
- const { t } = useTranslation();
- const loading = useAppSelector((state) => state.loading);
- const accountInfo = localStorage.getItem("accountInfo");
- const [paymentMethod, setPaymentMethod] = useState("card");
- const [form, setForm] = useState({
- firstName: "",
- lastName: "",
- email: accountInfo != null ? JSON.parse(accountInfo).email : "",
- confirmEmail: accountInfo != null ? JSON.parse(accountInfo).email : "",
- phone: "",
- });
- const [agreements, setAgreements] = useState({
- terms: false,
- esim: false,
- vatInvoice: false,
- });
- // get data from previous step
- const location = useLocation();
- const state = location.state as {
- area: Area;
- package: Package;
- quantity: number;
- simType: "eSIM" | "Physical";
- checkoutDetails: CheckoutDetailResponse;
- };
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const { name, value } = e.target;
- setForm((prev) => ({ ...prev, [name]: value }));
- };
- const handleSubmit = async () => {
- console.log(form);
- if (form.email !== form.confirmEmail) {
- dispatch(
- openPopup({
- isSuccess: false,
- title: "Error",
- message: "Email and Confirm Email do not match.",
- })
- );
- return;
- }
- // proceed to payment
- dispatch(startSmallLoading());
- const res = await productApi.confirmOrder({
- surName: form.lastName,
- lastName: form.firstName,
- email: form.email,
- phoneNumber: form.phone,
- paymentChannelId: paymentMethod,
- packgId: state.package.id,
- quantity: state.quantity,
- });
- console.log("Confirm order response:", res);
- dispatch(stopSmallLoading());
- if (res.errorCode === "0") {
- // open other website to pay
- window.location.href = res.data.paymentUrl;
- } else {
- dispatch(
- openPopup({
- isSuccess: false,
- title: "Error",
- message: res.message || "Failed to confirm order.",
- })
- );
- }
- };
- const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const { name, checked } = e.target;
- setAgreements((prev) => ({
- ...prev,
- [name]: checked,
- }));
- };
- const { data: loadPaymentChannel = [] } = useQuery<PaymentChannel[]>({
- queryKey: [DataCacheKey.PAYMENT_CHANNELS],
- queryFn: async (): Promise<PaymentChannel[]> => {
- try {
- dispatch(startLoading({}));
- const res = await productApi.loadPaymentChannel();
- // save to redux store
- return res.data as PaymentChannel[];
- } catch (error) {
- console.error(error);
- return []; // 🔴 bắt buộc
- } finally {
- dispatch(stopLoading());
- }
- },
- staleTime: staleTime,
- });
- useEffect(() => {
- setPaymentMethod(loadPaymentChannel[0]?.id);
- dispatch(stopSmallLoading());
- }, [loadPaymentChannel]);
- // Styles matching LoginView inputs
- const inputClass =
- "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";
- const labelClass =
- "block text-slate-400 font-black text-[14px] uppercase tracking-[0.2em] pl-1 mb-2";
- if (!state.checkoutDetails) {
- return null;
- }
- return (
- <div className="bg-white min-h-screen pb-20">
- {/* Header / Breadcrumb */}
- <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
- <div className="flex items-center space-x-2">
- <button
- onClick={() => navigate(-1)}
- className="flex items-center text-slate-900 font-bold hover:text-[#EE0434] transition-colors text-[18px]"
- >
- <svg
- className="w-5 h-5 mr-1"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2.5}
- d="M15 19l-7-7 7-7"
- />
- </svg>
- {t("continueShopping")}
- </button>
- </div>
- </div>
- <div className="max-w-3xl mx-auto px-4 py-8 md:py-12">
- {/* Progress Steps */}
- <div className="flex justify-center mb-12">
- <div className="flex items-center w-full max-w-sm text-xs font-bold">
- <div className="flex flex-col items-center gap-2">
- <div className="w-8 h-8 rounded-full bg-[#00c087] text-white flex items-center justify-center">
- <svg
- className="w-4 h-4"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={3}
- d="M5 13l4 4L19 7"
- />
- </svg>
- </div>
- <span className="text-[#00c087] text-[14px]">
- {t("chooseProduct")}
- </span>
- </div>
- <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
- <div className="flex flex-col items-center gap-2">
- <div className="w-8 h-8 rounded-full bg-[#0071e3] text-white flex items-center justify-center">
- 2
- </div>
- <span className="text-[#0071e3] text-center text-[14px]">
- {t("orderInformation")}
- </span>
- </div>
- <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
- <div className="flex flex-col items-center gap-2">
- <div className="w-8 h-8 rounded-full bg-white border border-slate-200 text-slate-300 flex items-center justify-center">
- 3
- </div>
- <span className="text-slate-300 text-[14px]">
- {t("paymentPayment")}
- </span>
- </div>
- </div>
- </div>
- <div className="space-y-10">
- {/* Customer Information */}
- <div>
- <h2 className="text-xl font-black text-[#003459] mb-6">
- {t("customerInformation")}
- </h2>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-1">
- <label className={labelClass}>
- {t("lastName")} <span className="text-red-500">*</span>
- </label>
- <input
- type="text"
- className={inputClass}
- placeholder="Doe"
- onChange={handleChange}
- value={form.lastName}
- name="lastName"
- />
- </div>
- <div className="space-y-1">
- <label className={labelClass}>
- {t("firstName")} <span className="text-red-500">*</span>
- </label>
- <input
- type="text"
- className={inputClass}
- placeholder="John"
- onChange={handleChange}
- value={form.firstName}
- name="firstName"
- />
- </div>
- <div className="space-y-1">
- <label className={labelClass}>
- {t("emailEmail")} <span className="text-red-500">*</span>
- </label>
- <input
- type="email"
- className={inputClass}
- placeholder="john@example.com"
- onChange={handleChange}
- value={form.email}
- name="email"
- />
- </div>
- <div className="space-y-1">
- <label className={labelClass}>
- {t("confirmEmail")} <span className="text-red-500">*</span>
- </label>
- <input
- type="email"
- className={inputClass}
- placeholder="john@example.com"
- onChange={handleChange}
- value={form.confirmEmail}
- name="confirmEmail"
- />
- </div>
- <div className="md:col-span-2 space-y-1">
- <label className={labelClass}>{t("phoneNumber")}</label>
- <input
- type="tel"
- className={inputClass}
- placeholder="+1 234 567 890"
- onChange={handleChange}
- value={form.phone}
- name="phone"
- />
- </div>
- </div>
- </div>
- {/* Order Info */}
- <div className="bg-slate-50/50 rounded-3xl p-6 border border-slate-100">
- <h2 className="text-xl font-black text-[#003459] mb-6">
- {t("orderInformation")}
- </h2>
- <div className="flex justify-between items-center">
- <div className="flex items-center gap-4">
- <img
- src={`${state.area.iconUrl}`}
- className="w-10 h-10 rounded-full object-cover border-2 border-white shadow-sm"
- />
- <div>
- <h3 className="font-black text-slate-800 text-lg">
- eSIM {state.area.areaName1}
- </h3>
- <p className="text-xs text-slate-500 font-bold uppercase tracking-wide mt-1">
- {/* {state.package.amountData} - */}
- {state.package.dayDuration} days • x{state.quantity}
- </p>
- </div>
- </div>
- <div className="text-right">
- <p className="text-xs text-slate-400 line-through font-bold">
- {formatNumber(state.checkoutDetails.totalMoney)}{" "}
- {state.checkoutDetails.curency}
- </p>
- <p className="text-[#0071e3] font-black text-xl">
- {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
- {state.checkoutDetails.curency}
- </p>
- </div>
- </div>
- </div>
- {/* Payment Method */}
- <div>
- <h2 className="text-xl font-black text-[#003459] mb-6">
- {t("paymentPayment")}
- </h2>
- <div className="space-y-3">
- {loadPaymentChannel.map((method) => (
- <label
- key={method.id}
- className={`flex items-center p-4 border rounded-2xl cursor-pointer transition-all ${
- paymentMethod === method.id
- ? "bg-blue-50 border-blue-200"
- : "border-slate-100 hover:bg-slate-50"
- }`}
- >
- <input
- type="radio"
- name="payment"
- checked={paymentMethod === method.id}
- onChange={() => setPaymentMethod(method.id)}
- className="w-5 h-5 text-[#EE0434] border-slate-300 focus:ring-[#EE0434]"
- />
- <span className="ml-4 font-bold text-slate-700 flex items-center gap-3">
- <span className="text-xl w-6 text-center">
- {/* {method.imgUrl} */}
- </span>{" "}
- {method.name}
- </span>
- </label>
- ))}
- </div>
- </div>
- {/* Terms */}
- <div className="space-y-3 pt-2 px-1">
- <label className="flex items-start cursor-pointer group">
- <input
- type="checkbox"
- name="terms"
- checked={agreements.terms}
- onChange={handleCheckboxChange}
- className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
- />
- <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
- {t("iService")}{" "}
- <a
- href="#"
- className="text-[#0071e3] font-bold hover:underline"
- >
- {t("termsService")}
- </a>
- </span>
- </label>
- <label className="flex items-start cursor-pointer group">
- <input
- type="checkbox"
- className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
- name="esim"
- checked={agreements.esim}
- onChange={handleCheckboxChange}
- />
- <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
- {t("iUnlocked")}
- </span>
- </label>
- <label className="flex items-start cursor-pointer group">
- <input
- type="checkbox"
- className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
- name="vatInvoice"
- checked={agreements.vatInvoice}
- onChange={handleCheckboxChange}
- />
- <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
- {t("iInvoice")}
- </span>
- </label>
- </div>
- {/* Overview Card */}
- <div className="bg-[#eef6f8] rounded-[32px] p-8 mt-8">
- <h2 className="text-xl font-black text-[#003459] mb-6">Overview</h2>
- <div className="relative mb-8">
- <input
- type="text"
- placeholder="Promo code"
- 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"
- />
- <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">
- {t("apply")}
- </button>
- </div>
- <div className="space-y-4 mb-8">
- <div className="flex justify-between text-slate-600 font-bold">
- <span>{t("subtotal")}:</span>
- <span>
- {formatNumber(
- state.checkoutDetails.totalMoney * state.quantity
- )}{" "}
- {state.checkoutDetails.curency}
- </span>
- </div>
- <div className="flex justify-between text-green-600 font-bold">
- <span>Discount:</span>
- <span>
- -{formatNumber(0 * state.quantity)}{" "}
- {state.checkoutDetails.curency}
- </span>
- </div>
- <div className="flex justify-between items-center pt-4 border-t border-slate-200/50">
- <span className="text-[#0071e3] font-black text-xl">
- {t("totalTotal")} ({state.quantity}):
- </span>
- <span className="text-[#0071e3] font-black text-3xl">
- {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
- {state.checkoutDetails.curency}
- </span>
- </div>
- </div>
- <button
- 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 ${
- form.firstName &&
- form.lastName &&
- form.email &&
- agreements.terms &&
- agreements.esim &&
- !loading.isSmallLoading
- ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
- : "bg-slate-100 text-slate-300 cursor-not-allowed"
- }`}
- disabled={
- !form.firstName ||
- !form.lastName ||
- !form.email ||
- !agreements.terms ||
- !agreements.esim ||
- loading.isSmallLoading
- }
- onClick={handleSubmit}
- >
- {loading.isSmallLoading && (
- <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
- )}
- <span>{t("checkout")}</span>
- </button>
- </div>
- </div>
- </div>
- </div>
- );
- };
- export default CheckoutView;
|