428 lines
16 KiB
JavaScript
428 lines
16 KiB
JavaScript
import React, { useState, useMemo, useEffect } from "react";
|
||
import { useSubscription } from "../../hooks/useAuth";
|
||
import { useParams } from "react-router-dom";
|
||
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
|
||
import {
|
||
formatCurrency,
|
||
formatFigure,
|
||
frequencyLabel,
|
||
} from "../../utils/appUtils";
|
||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||
import { PaymentRepository } from "../../repositories/PaymentRepository";
|
||
import { useMakePayment } from "../../hooks/usePayment";
|
||
import { useDispatch, useSelector } from "react-redux";
|
||
import { setSelfTenant } from "../../slices/localVariablesSlice";
|
||
import { unblockUI } from "../../utils/blockUI";
|
||
import showToast from "../../services/toastService";
|
||
import SelectedPlanSkeleton from "./SelectedPlanSkeleton";
|
||
|
||
const ProcessedPayment = ({
|
||
onNext,
|
||
resetPaymentStep,
|
||
setCurrentStep,
|
||
setStepStatus,
|
||
resetFormStep,
|
||
}) => {
|
||
const { planName } = useParams();
|
||
|
||
const {
|
||
details: client,
|
||
planId: selectedPlanId,
|
||
frequency,
|
||
} = useSelector((store) => store.localVariables.selfTenant);
|
||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||
const [currentPlan, setCurrentPlan] = useState(null);
|
||
const [failPayment, setFailPayment] = useState(null);
|
||
|
||
const {
|
||
data: plans,
|
||
isError: isPlanError,
|
||
isLoading,
|
||
isError,
|
||
isRefetching,
|
||
refetch,
|
||
} = useSubscription(frequency);
|
||
useEffect(() => {
|
||
if (!plans || !selectedPlanId) return;
|
||
const selected = plans.find((p) => p.id === selectedPlanId);
|
||
setSelectedPlan(selected);
|
||
}, [plans, selectedPlanId]);
|
||
|
||
const loadScript = (src) =>
|
||
new Promise((resolve) => {
|
||
const script = document.createElement("script");
|
||
script.src = src;
|
||
script.onload = () => resolve(true);
|
||
script.onerror = () => resolve(false);
|
||
document.body.appendChild(script);
|
||
});
|
||
|
||
const { mutate: MakePayment, isPending } = useMakePayment(
|
||
(response) => {
|
||
unblockUI();
|
||
onNext(response);
|
||
},
|
||
(fail) => {
|
||
unblockUI();
|
||
setFailPayment(fail);
|
||
onNext(fail);
|
||
},
|
||
currentPlan
|
||
);
|
||
|
||
const ProcessToPayment = async () => {
|
||
setStepStatus((prev) => ({ ...prev, 3: "success" }));
|
||
setCurrentStep(4);
|
||
const res = await loadScript(
|
||
"https://checkout.razorpay.com/v1/checkout.js"
|
||
);
|
||
if (!res) {
|
||
alert("Failed to load Razorpay SDK");
|
||
return;
|
||
}
|
||
let price = 0;
|
||
price =
|
||
frequencyLabel(selectedPlan?.frequency, true, true)?.planDurationInInt *
|
||
selectedPlan?.price;
|
||
MakePayment({ amount: price });
|
||
};
|
||
|
||
const handleRetry = () => {
|
||
setFailPayment(null);
|
||
if (typeof resetPaymentStep === "function") resetPaymentStep();
|
||
};
|
||
const handlPrevious = () => {
|
||
setCurrentStep,
|
||
setStepStatus((prev) => ({ ...prev, 2: "pending", 3: "pending" }));
|
||
setCurrentStep(2);
|
||
};
|
||
|
||
// useEffect(() => {
|
||
// if (!client || Object.keys(client).length === 0) {
|
||
// setFailPayment(null);
|
||
// if (typeof resetFormStep === "function") {
|
||
// resetFormStep();
|
||
// }
|
||
// }
|
||
// }, [client]);
|
||
|
||
if (failPayment) {
|
||
return (
|
||
<div className="container-md mt-5 text-center">
|
||
<div className="d-flex flex-column align-items-center justify-content-center">
|
||
<div
|
||
className="bg-danger p-3 rounded-circle mb-3 d-flex align-items-center justify-content-center"
|
||
style={{ width: "70px", height: "70px" }}
|
||
>
|
||
<i className="bx bx-x fs-1 text-white fw-bold"></i>
|
||
</div>
|
||
<h4 className="text-danger mb-2">Payment Failed!</h4>
|
||
<p className="text-muted">
|
||
Unfortunately, your payment could not be completed. Please try again
|
||
or use a different payment method.
|
||
</p>
|
||
|
||
<div className="mt-4 d-flex gap-3 flex-column flex-md-row justify-content-center">
|
||
<button
|
||
className="btn btn-primary px-4 py-2 fw-semibold"
|
||
onClick={handleRetry}
|
||
>
|
||
Retry Payment
|
||
</button>
|
||
<a
|
||
href="/"
|
||
className="btn btn-outline-secondary px-4 py-2 fw-semibold"
|
||
>
|
||
Go Back to Dashboard
|
||
</a>
|
||
</div>
|
||
|
||
{failPayment?.error && (
|
||
<div className="alert alert-light-danger mt-4 w-75 mx-auto text-start">
|
||
<strong>Error Details:</strong>
|
||
<pre className="small mb-0 mt-2 text-wrap">
|
||
{JSON.stringify(failPayment.error, null, 2)}
|
||
</pre>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div className="container-md text-start ">
|
||
<div className="row gx-1 gy-3 justify-content-between">
|
||
<div className="col-12 col-md-6">
|
||
<div className="row">
|
||
<div className="col-12 mb-3 text-start">
|
||
<h4>You’ve Selected the Perfect Plan for Your Organization</h4>
|
||
<p className="text-muted small mb-3">
|
||
Great choice! This plan is tailored to meet your team’s needs
|
||
and help you maximize productivity.
|
||
</p>
|
||
</div>
|
||
{isError && (
|
||
<div className="col-12 col-md text-center">
|
||
<p className="text-muted">{error?.message}</p>
|
||
<small>{error?.name}</small>
|
||
<small
|
||
className={`text-muted ${
|
||
isRefetching ? "cursor-notallowed" : "cursor-pointer"
|
||
}`}
|
||
onClick={refetch}
|
||
>
|
||
{isRefetching ? (
|
||
<>
|
||
<i
|
||
className={`bx bx-loader-alt ${
|
||
isRefetching ? "bx-spin" : ""
|
||
}`}
|
||
></i>{" "}
|
||
Retrying...
|
||
</>
|
||
) : (
|
||
"Try to refetch"
|
||
)}
|
||
</small>
|
||
</div>
|
||
)}
|
||
{isLoading ? (
|
||
<SelectedPlanSkeleton />
|
||
) : (
|
||
<>
|
||
{selectedPlan && (
|
||
<div className="col-12 col-md-8 mb-md-3 mb-2">
|
||
<div className="custom-option custom-option-basic text-start w-100 bg-light-primary border border-primary shadow-md p-3 rounded-3">
|
||
<div className="custom-option-header d-flex justify-content-between align-items-center">
|
||
<span className="h6 mb-0">
|
||
{selectedPlan?.description}
|
||
</span>
|
||
<i className="bx bx-check-circle text-primary fs-4"></i>
|
||
</div>
|
||
|
||
<div className="d-flex justify-content-between mt-1">
|
||
<small>
|
||
Price -{" "}
|
||
<span className="fw-medium">
|
||
{selectedPlan.currency?.symbol} {selectedPlan.price}{" "}
|
||
per {frequencyLabel(frequency)}
|
||
</span>
|
||
</small>
|
||
</div>
|
||
|
||
<div className="d-flex flex-row gap-3 mt-1 align-items-center">
|
||
<small>{selectedPlan?.planName}</small>
|
||
<small className="d-block mt-1 text-xs border border-primary text-center rounded text-secondary w-min text-nowrap px-1">
|
||
billed {frequencyLabel(frequency, true)}
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{selectedPlan && (
|
||
<div className="col-12 text-start">
|
||
<div className="border-warning mt-3">
|
||
{(() => {
|
||
const {
|
||
planName,
|
||
description,
|
||
price,
|
||
frequency,
|
||
trialDays,
|
||
maxUser,
|
||
maxStorage,
|
||
currency,
|
||
features,
|
||
} = selectedPlan;
|
||
return (
|
||
<>
|
||
<div className="row g-2 mb-3">
|
||
<div className="col-sm-6 col-md-4">
|
||
<div className="border rounded-3 p-2 bg-light">
|
||
<i className="bx bx-user me-1 text-primary"></i>
|
||
<strong>Max Users:</strong> {maxUser}
|
||
</div>
|
||
</div>
|
||
<div className="col-sm-6 col-md-4">
|
||
<div className="border rounded-3 p-2 bg-light">
|
||
<i className="bx bx-hdd me-1 text-primary"></i>
|
||
<strong>Max Storage:</strong> {maxStorage} MB
|
||
</div>
|
||
</div>
|
||
<div className="col-sm-6 col-md-4">
|
||
<div className="border rounded-3 p-2 bg-light">
|
||
<i className="bx bx-time-five me-1 text-primary"></i>
|
||
<strong>Trial Days:</strong> {trialDays}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h6 className="fw-bold text-secondary mb-2">
|
||
Included Features
|
||
</h6>
|
||
<div className="row ">
|
||
{features &&
|
||
Object.entries(features?.modules || {})
|
||
.filter(([key]) => key !== "id")
|
||
.map(([key, mod]) => (
|
||
<div
|
||
key={key}
|
||
className="col-4 mb-2 d-flex align-items-center mb-1"
|
||
>
|
||
<i
|
||
className={`fa-regular ${
|
||
mod.enabled
|
||
? "fa-circle-check text-success"
|
||
: "fa-circle-xmark text-danger"
|
||
}`}
|
||
></i>
|
||
<small className="ms-1">{mod.name}</small>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<h6 className="fw-bold text-secondary mt-3 mb-1">
|
||
Support
|
||
</h6>
|
||
<ul className="list-unstyled d-flex flex-wrap gap-3 align-items-center mb-0 small">
|
||
{features?.supports?.emailSupport && (
|
||
<li className="d-flex align-items-center">
|
||
<i className="bx bx-envelope text-primary me-1 fs-5"></i>
|
||
Email Support
|
||
</li>
|
||
)}
|
||
{features?.supports?.phoneSupport && (
|
||
<li className="d-flex align-items-center">
|
||
<i className="bx bx-phone text-primary me-1 fs-5"></i>
|
||
Phone Support
|
||
</li>
|
||
)}
|
||
{features?.supports?.prioritySupport && (
|
||
<li className="d-flex align-items-center">
|
||
<i className="bx bx-star text-warning me-1 fs-5"></i>
|
||
Priority Support
|
||
</li>
|
||
)}
|
||
</ul>
|
||
<hr className="divider border-2 " />
|
||
<div className="d-flex flex-column co-12 ">
|
||
<div className="d-flex justify-content-between">
|
||
<h6 className="fs-6 m-0">Duration</h6>
|
||
<h5 className="fs-6 m-0">
|
||
{frequencyLabel(
|
||
selectedPlan?.frequency,
|
||
true
|
||
)}
|
||
</h5>
|
||
</div>
|
||
|
||
<div className="d-flex justify-content-between">
|
||
<h6 className="fs-4">Total Price</h6>
|
||
<h5 className="fs-3">
|
||
{formatFigure(
|
||
frequencyLabel(
|
||
selectedPlan?.frequency,
|
||
true,
|
||
true
|
||
)?.planDurationInInt * price,
|
||
{
|
||
type: "currency",
|
||
currency:
|
||
selectedPlan?.currency.currencyCode,
|
||
}
|
||
)}
|
||
</h5>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
})()}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="col-12 col-md-4 ">
|
||
{client && (
|
||
<div className="text-start">
|
||
<h6 className="fs-md-4 my-4">
|
||
Confirm your organization details.
|
||
</h6>
|
||
<div className="row g-2">
|
||
<div className="col-sm-6 mb-2">
|
||
<strong>Name:</strong>
|
||
</div>
|
||
<div className="col-sm-6 mb-2">
|
||
{client.firstName} {client.lastName}
|
||
</div>
|
||
|
||
<div className="col-sm-6">
|
||
<strong>Email:</strong>
|
||
</div>
|
||
<div className="col-sm-6 mb-2 text-wrap">{client.email}</div>
|
||
|
||
<div className="col-sm-6 mb-2">
|
||
<strong>Contact Number:</strong>
|
||
</div>
|
||
<div className="col-sm-6 mb-2">{client.contactNumber}</div>
|
||
|
||
<div className="col-sm-6 mb-2">
|
||
<strong>Organization Name:</strong>
|
||
</div>
|
||
<div className="col-sm-6 mb-2">{client.organizationName}</div>
|
||
|
||
<div className="col-sm-6 mb-2">
|
||
<strong>Onboarding Date:</strong>
|
||
</div>
|
||
<div className="col-sm-6">
|
||
{formatUTCToLocalTime(client.onBoardingDate)}
|
||
</div>
|
||
|
||
<div className="col-sm-6 mb-2">
|
||
<strong>Billing Address:</strong>
|
||
</div>
|
||
<div className="col-sm-6 mb-2">{client.billingAddress}</div>
|
||
|
||
<div className="col-sm-6 mb-2">
|
||
<strong>Industry :</strong>
|
||
</div>
|
||
<div className="col-sm-6 mb-2">{client?.industry?.name}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="col-12 d-flex flex-column flex-md-row justify-content-between gap-2 mt-3">
|
||
<button
|
||
type="submit"
|
||
className="btn btn-label-primary d-flex align-items-center justify-content-center w-md-auto"
|
||
onClick={handlPrevious}
|
||
>
|
||
<i className="bx bx-chevron-left me-1"></i> Previous
|
||
</button>
|
||
|
||
<button
|
||
type="submit"
|
||
className="btn btn-label-primary d-flex align-items-center justify-content-center w-md-auto"
|
||
onClick={() => ProcessToPayment(currentPlan?.price)}
|
||
disabled={isPending}
|
||
>
|
||
{isPending ? (
|
||
<>
|
||
<i className="bx bx-loader-alt bx-md bx-spin me-2"></i> Please
|
||
Wait...
|
||
</>
|
||
) : (
|
||
"Proceed To Payment"
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ProcessedPayment;
|