diff --git a/index.html b/index.html
index 0ac2d657..69b46b21 100644
--- a/index.html
+++ b/index.html
@@ -95,6 +95,11 @@
+
+
+
+
+
diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css
index 1ecc882c..aec7ebc9 100644
--- a/public/assets/css/core-extend.css
+++ b/public/assets/css/core-extend.css
@@ -3,6 +3,27 @@
--bs-nav-link-font-size: 0.7375rem;
--bg-border-color :#f8f6f6
}
+/* ===========================% Background_Colors %========================================================== */
+.bg-light-primary {
+ background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent);
+ border:var(--bs-primary-border-subtle)
+}
+.bg-light-secondary {
+ background-color: color-mix(in srgb, var(--bs-secondary) 10.4%, transparent);
+}
+.bg-light-danger {
+ background-color: color-mix(in srgb, var(--bs-danger) 10.4%, transparent);
+}
+.bg-light-success {
+ background-color: color-mix(in srgb, var(--bs-success) 10.4%, transparent);
+}
+
+.bg-light-info {
+ background-color: color-mix(in srgb, var(--bs-info) 10.4%, transparent);
+}
+.bg-light-warning {
+ background-color: color-mix(in srgb, var(--bs-warning) 10.4%, transparent);
+}
.card-header {
padding: 0.5rem var(--bs-card-cap-padding-x);
@@ -25,6 +46,101 @@ font-size: 2rem;
.text-md-b {
font-weight: normal;
}
+.stepper-container {
+ position: relative;
+}
+
+.timeline-horizontal {
+ position: relative;
+ padding: 0;
+ margin: 0;
+}
+
+.timeline-item {
+ position: relative;
+ flex: 1;
+}
+
+.timeline-point {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #dee2e6;
+ color: #6c757d;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: 600;
+ z-index: 2;
+ position: relative;
+ padding: 3px;
+ transition: all 0.3s ease;
+}
+
+.timeline-point.completed {
+ background-color: var(--bs-success);
+ color: #fff;
+ box-shadow: 0 0 5px rgba(25, 135, 84, 0.5);
+}
+
+.timeline-point.failed {
+ background-color: var(--bs-danger);
+ color: #fff;
+ box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
+}
+
+.timeline-point.active {
+ background-color: var(--bs-info);
+ color: #fff;
+ transform: scale(1.15);
+ box-shadow: 0 0 6px rgba(13, 202, 240, 0.5);
+}
+
+.timeline-line-horizontal {
+ content: "";
+ position: absolute;
+ top: 10px;
+ left: 50%;
+ width: 100%;
+ height: 2px;
+ background-color: #dee2e6;
+ z-index: 1;
+ transition: background-color 0.3s ease;
+}
+
+/* Make line green for completed sections */
+.timeline-item.completed ~ .timeline-line-horizontal {
+ background-color: var(--bs-success);
+}
+
+/* Optional: subtle pulse for active step */
+.timeline-point.active::after {
+ content: "";
+ position: absolute;
+ width: 25px;
+ height: 25px;
+ border-radius: 50%;
+ border: 2px solid var(--bs-info);
+ animation: pulse 1.5s infinite;
+ opacity: 0.6;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 0.6;
+ }
+ 70% {
+ transform: scale(1.5);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 0;
+ }
+}
+
+
.text-xxs { font-size: 0.55rem; } /* 8px */
.text-xs { font-size: 0.75rem; } /* 12px */
@@ -294,3 +410,36 @@ font-weight: normal;
.w-8-xl{ width: 2rem; }
.w-10-xl{ width: 2.5rem; }
}
+
+
+/* ------------------------Text------------------------- */
+@media (min-width: 576px) {
+ .fs-sm-1 { font-size: calc(1.3rem + 1.6vw) !important; }
+ .fs-sm-2 { font-size: calc(1.2rem + 1.2vw) !important; }
+ .fs-sm-3 { font-size: calc(1.1rem + 0.8vw) !important; }
+ .fs-sm-4 { font-size: calc(1rem + 0.5vw) !important; }
+ .fs-sm-5 { font-size: 1.05rem !important; }
+ .fs-sm-6 { font-size: 0.9rem !important; }
+
+ .fs-sm-tiny { font-size: 72% !important; }
+ .fs-sm-big { font-size: 115% !important; }
+ .fs-sm-large { font-size: 155% !important; }
+ .fs-sm-xlarge { font-size: 175% !important; }
+ .fs-sm-xxlarge { font-size: calc(1.6rem + 3.5vw) !important; }
+}
+
+/* 💻 Medium devices (≥768px) */
+@media (min-width: 768px) {
+ .fs-md-1 { font-size: calc(1.4125rem + 1.95vw) !important; }
+ .fs-md-2 { font-size: calc(1.3625rem + 1.35vw) !important; }
+ .fs-md-3 { font-size: calc(1.3rem + 0.6vw) !important; }
+ .fs-md-4 { font-size: calc(1.275rem + 0.3vw) !important; }
+ .fs-md-5 { font-size: 1.125rem !important; }
+ .fs-md-6 { font-size: 0.9375rem !important; }
+
+ .fs-md-tiny { font-size: 70% !important; }
+ .fs-md-big { font-size: 112% !important; }
+ .fs-md-large { font-size: 150% !important; }
+ .fs-md-xlarge { font-size: 170% !important; }
+ .fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
+}
\ No newline at end of file
diff --git a/public/img/illustrations/undraw_pricing.png b/public/img/illustrations/undraw_pricing.png
new file mode 100644
index 00000000..151bb16a
Binary files /dev/null and b/public/img/illustrations/undraw_pricing.png differ
diff --git a/public/img/illustrations/undraw_pricing.svg b/public/img/illustrations/undraw_pricing.svg
new file mode 100644
index 00000000..8966ea28
--- /dev/null
+++ b/public/img/illustrations/undraw_pricing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/UserSubscription/Invoice.jsx b/src/components/UserSubscription/Invoice.jsx
new file mode 100644
index 00000000..2583a451
--- /dev/null
+++ b/src/components/UserSubscription/Invoice.jsx
@@ -0,0 +1,306 @@
+import React, { useRef, useState } from "react";
+import html2canvas from "html2canvas";
+import jsPDF from "jspdf";
+import { formatFigure } from "../../utils/appUtils";
+
+const Invoice = ({ invoiceData, currencySymbol }) => {
+ const [isGenerating, setIsGenerating] = useState(false);
+ const invoiceRef = useRef(null);
+
+ const data = invoiceData || {
+ razorpayPaymentDetails: {
+ amount: 19999,
+ bankDetails: null,
+ captured: true,
+ cardDetails: {
+ cardId: null,
+ cardType: null,
+ emi: false,
+ international: false,
+ issuer: null,
+ last4Digits: null,
+ network: null,
+ subType: null,
+ },
+ contact: "+919145445127",
+ createdAt: "2025-10-25T06:46:30",
+ currency: "INR",
+ description: "",
+ email: "avn18042001@gmail.com",
+ errorCode: "",
+ errorDescription: "",
+ fee: 707.97,
+ internationalPayment: true,
+ method: "card",
+ orderId: "order_RXbzfh8d1X1SSg",
+ paymentId: "pay_RXc08bJHVpjytP",
+ status: "captured",
+ tax: 108,
+ upiDetails: null,
+ walletDetails: null,
+ },
+ razorpayOrderDetails: {
+ amount: 19999,
+ amountDue: 0,
+ amountPaid: 19999,
+ attempts: 1,
+ createdAt: "2025-10-25T06:46:02",
+ currency: "INR",
+ orderId: "order_RXbzfh8d1X1SSg",
+ receipt: "rec_aae16ec3-9571-4c96-bd77-59950fdce236",
+ status: "paid",
+ },
+ };
+
+ const formatAmount = (amount) => {
+ return currencySymbol + amount.toFixed(2);
+ };
+
+ const formatDate = (dateString) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString("en-IN", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ const downloadPDF = async () => {
+ setIsGenerating(true);
+
+ try {
+ const invoice = invoiceRef.current;
+ const canvas = await html2canvas(invoice, {
+ scale: 2,
+ useCORS: true,
+ logging: false,
+ backgroundColor: "#ffffff",
+ });
+
+ const imgData = canvas.toDataURL("image/png");
+ const pdf = new jsPDF({
+ orientation: "portrait",
+ unit: "mm",
+ format: "a4",
+ });
+
+ const imgWidth = 210;
+ const pageHeight = 297;
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
+
+ pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
+
+ const paymentId = data.razorpayPaymentDetails.paymentId;
+ pdf.save(`Invoice_${paymentId}.pdf`);
+ } catch (error) {
+ console.error("Error generating PDF:", error);
+ alert("Failed to generate PDF. Please try again.");
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ const payment = data.razorpayPaymentDetails;
+ const order = data.razorpayOrderDetails;
+ const subtotal = payment.amount - payment.fee - payment.tax;
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
+
INVOICE
+
Payment Receipt
+
+
+
+ INV-{payment.paymentId.slice(-8).toUpperCase()}
+
+
+
+ Date: {formatDate(payment.createdAt)}
+
+
+ Payment ID: {payment.paymentId}
+
+
+
+ {order.status.toUpperCase()}
+
+
+
+
+
+
+ {/* Billing Details */}
+
+
+
Bill To
+
{payment.customerName || "N/A"}
+
{payment.email || "N/A"}
+
{payment.contact || "N/A"}
+
+
+
+ Payment Information
+
+
+ Order ID: {order.orderId}
+
+
+ Receipt: {order.receipt}
+
+
+ Method:{" "}
+ {payment.method.charAt(0).toUpperCase() +
+ payment.method.slice(1)}
+
+
+
+
+
+
+ {/* Transaction Details */}
+
+
+ Transaction Details
+
+
+
+
+ Payment Status
+
+ {payment.status.charAt(0).toUpperCase() +
+ payment.status.slice(1)}
+
+
+
+
+
+ Currency
+ {payment.currency}
+
+
+
+
+ Card Type
+
+ {payment.cardDetails?.cardType ||
+ payment.method.toUpperCase()}
+
+
+
+
+
+ Last 4 Digits
+
+ {payment.cardDetails?.last4Digits || "N/A"}
+
+
+
+
+
+ International
+
+ {payment.internationalPayment ? "Yes" : "No"}
+
+
+
+
+
+ Captured
+
+ {payment.captured ? "Yes" : "No"}
+
+
+
+
+
+
+
+
+ {/* Amount Breakdown */}
+
+
+ Payment Summary
+
+
+
+
+
+
+ | Subtotal |
+ {formatFigure(subtotal.toFixed(2),{
+ type: "currency",
+ currency: payment.currency,
+ })} |
+
+
+ | Processing Fee |
+ {formatFigure(payment.fee.toFixed(2),{
+ type: "currency",
+ currency: payment.currency,
+ })} |
+
+
+ | Tax |
+ {formatFigure(payment.tax.toFixed(2),{
+ type: "currency",
+ currency: payment.currency,
+ })} |
+
+
+ | Total Paid |
+
+ {formatFigure(payment.amount, {
+ type: "currency",
+ currency: payment.currency,
+ })}
+ |
+
+
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+ Thank you for your payment!
+
+
+ This is a computer-generated invoice and does not require a
+ signature.
+
+
+ Generated on:{" "}
+ {new Date().toLocaleDateString("en-IN", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+
+
+
+ );
+};
+
+export default Invoice;
diff --git a/src/components/UserSubscription/ProcessedPayment.jsx b/src/components/UserSubscription/ProcessedPayment.jsx
new file mode 100644
index 00000000..69f70e3a
--- /dev/null
+++ b/src/components/UserSubscription/ProcessedPayment.jsx
@@ -0,0 +1,358 @@
+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 { formatUTCToLocalTime } from "../../utils/dateUtils";
+import { PaymentRepository } from "../../repositories/PaymentRepository";
+import { useDispatch, useSelector } from "react-redux";
+import { setSelfTenant } from "../../slices/localVariablesSlice";
+import { unblockUI } from "../../utils/blockUI";
+import showToast from "../../services/toastService";
+import { useMakePayment } from "../../hooks/usePayments";
+import { formatFigure, frequencyLabel } from "../../utils/appUtils";
+
+const ProcessedPayment = ({
+ onNext,
+ resetPaymentStep,
+ setCurrentStep,
+ setStepStatus,
+ resetFormStep,
+}) => {
+ const { frequency, planName } = useParams();
+
+ const { details: client, planId: selectedPlanId } = 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,
+ } = 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;
+ }
+ MakePayment({ amount: 1 });
+ };
+
+ 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 (
+
+
+
+
+
+
Payment Failed!
+
+ Unfortunately, your payment could not be completed. Please try again
+ or use a different payment method.
+
+
+
+
+ {failPayment?.error && (
+
+
Error Details:
+
+ {JSON.stringify(failPayment.error, null, 2)}
+
+
+ )}
+
+
+ );
+ }
+ return (
+
+
+
+
+
+
You’ve Selected the Perfect Plan for Your Organization
+
+ Great choice! This plan is tailored to meet your team’s needs
+ and help you maximize productivity.
+
+
+ {selectedPlan && (
+
+
+
+ {selectedPlan?.planName}
+
+
+
+
+
+ {selectedPlan?.currency?.symbol} {selectedPlan?.price} /{" "}
+ {frequencyLabel(frequency)}
+
+
+
+
+ {selectedPlan?.description}
+
+
+
+ )}
+
+ {selectedPlan && (
+
+
+ {(() => {
+ const {
+ planName,
+ description,
+ price,
+ frequency,
+ trialDays,
+ maxUser,
+ maxStorage,
+ currency,
+ features,
+ } = selectedPlan;
+ return (
+ <>
+
+
+
+
+ Max Users: {maxUser}
+
+
+
+
+
+ Max Storage: {maxStorage} MB
+
+
+
+
+
+ Trial Days: {trialDays}
+
+
+
+
+
+ Included Features
+
+
+ {features &&
+ Object.entries(features?.modules || {})
+ .filter(([key]) => key !== "id")
+ .map(([key, mod]) => (
+
+
+ {mod.name}
+
+ ))}
+
+
+
+ Support
+
+
+ {features?.supports?.emailSupport && (
+ -
+
+ Email Support
+
+ )}
+ {features?.supports?.phoneSupport && (
+ -
+
+ Phone Support
+
+ )}
+ {features?.supports?.prioritySupport && (
+ -
+
+ Priority Support
+
+ )}
+
+
+
+
+
Duration
+
+ {frequencyLabel(selectedPlan?.frequency, true)}
+
+
+
+
+
Total Price
+
+ {formatFigure(selectedPlan?.price, {
+ type: "currency",
+ currency: selectedPlan?.currency.currencyCode,
+ })}
+
+
+
+ >
+ );
+ })()}
+
+
+ )}
+
+
+
+
+ {client && (
+
+
Confirm your organization details.
+
+
+ Name:
+
+
{client.firstName} {client.lastName}
+
+
+
+ Email:
+
+
{client.email}
+
+
+ Contact Number:
+
+
{client.contactNumber}
+
+
+ Organization Name:
+
+
{client.organizationName}
+
+
+
+
+ Onboarding Date:
+
+
+ {formatUTCToLocalTime(client.onBoardingDate)}
+
+
+
+ Billing Address:
+
+
{client.billingAddress}
+
+
+ Industry :
+
+
{client?.industry?.name}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default ProcessedPayment;
diff --git a/src/components/UserSubscription/SelectPlan.jsx b/src/components/UserSubscription/SelectPlan.jsx
new file mode 100644
index 00000000..7bc3cd56
--- /dev/null
+++ b/src/components/UserSubscription/SelectPlan.jsx
@@ -0,0 +1,238 @@
+import React, { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useParams } from "react-router-dom";
+import { useSubscription } from "../../hooks/useAuth";
+import { formatFigure, frequencyLabel } from "../../utils/appUtils";
+import { setSelfTenant } from "../../slices/localVariablesSlice";
+
+const SelectPlan = ({ currentStep, setStepStatus, onNext }) => {
+ const { frequency, planName } = useParams();
+ const dispatch = useDispatch();
+
+ const client = useSelector(
+ (store) => store.localVariables.selfTenant.details
+ );
+ const [selectedPlan, setSelectedPlan] = useState(planName);
+ const [currentPlan, setCurrentPlan] = useState(null);
+ const [failPayment, setFailPayment] = useState(null);
+
+ const {
+ data: plans,
+ isError: isPlanError,
+ isLoading,
+ } = useSubscription(frequency);
+
+ const handleChange = (e) => setSelectedPlan(e.target.value);
+
+ useEffect(() => {
+ if (!plans || !selectedPlan) return;
+ const selected = plans.find((p) => p.planName === selectedPlan);
+ if (selected) {
+ setCurrentPlan(selected);
+ dispatch(setSelfTenant({ planId: selected.id }));
+ }
+ }, [plans, selectedPlan, dispatch]);
+
+ const handleNextStep = () => {
+ setStepStatus((prev) => ({ ...prev, 2: "success"}));
+ onNext();
+ };
+
+ return (
+
+
+
+
+
+
Choose the Perfect Plan for Your Organization
+
+ Select a plan that fits your team’s needs and unlock the
+ features that drive productivity.
+
+
+
+ {plans?.map((plan) => (
+
+
+
+
+
+ ))}
+
+ {selectedPlan && (
+
+
+ {(() => {
+ const selected = plans?.find(
+ (p) => p.planName === selectedPlan
+ );
+ if (!selected) return null;
+
+ const {
+ price,
+ frequency,
+ trialDays,
+ maxUser,
+ maxStorage,
+ currency,
+ features,
+ } = selected;
+
+ return (
+ <>
+
+
+
+
+ Max Users: {maxUser}
+
+
+
+
+
+ Max Storage: {maxStorage} MB
+
+
+
+
+
+ Trial Days: {trialDays}
+
+
+
+
+
+ Included Features
+
+
+ {features &&
+ Object.entries(features?.modules || {})
+ .filter(([key]) => key !== "id")
+ .map(([key, mod]) => (
+
+
+ {mod.name}
+
+ ))}
+
+
+
+ Support
+
+
+ {features?.supports?.emailSupport && (
+ -
+
+ Email Support
+
+ )}
+ {features?.supports?.phoneSupport && (
+ -
+
+ Phone Support
+
+ )}
+ {features?.supports?.prioritySupport && (
+ -
+
+ Priority Support
+
+ )}
+
+
+
+
+
+
Duration
+
+ {frequencyLabel(frequency, true)}
+
+
+
+
+
Total Price
+
+ {formatFigure(price, {
+ type: "currency",
+ currency: currency.currencyCode,
+ })}
+
+
+
+ >
+ );
+ })()}
+
+
+ )}
+
+
+
+ {/* Image Section */}
+
+

+
+
+
+
+
+
+
+
+ );
+};
+
+export default SelectPlan;
diff --git a/src/components/UserSubscription/SubscriptionForm.jsx b/src/components/UserSubscription/SubscriptionForm.jsx
new file mode 100644
index 00000000..d9317580
--- /dev/null
+++ b/src/components/UserSubscription/SubscriptionForm.jsx
@@ -0,0 +1,259 @@
+import React, { useState, useMemo } from "react";
+import { useForm } from "react-hook-form";
+import {
+ OrganizationDefaultValue,
+ OrganizationSchema,
+} from "../../pages/Home/HomeSchema";
+import { zodResolver } from "@hookform/resolvers/zod";
+import Label from "../common/Label";
+import { orgSize, reference } from "../../utils/constants";
+import DatePicker from "../common/DatePicker";
+import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
+import { useCreateSelfTenant } from "../../hooks/useAuth";
+import { blockUI } from "../../utils/blockUI";
+
+const SubscriptionForm = ({ currentStep, setCurrentStep, setStepStatus }) => {
+ const { data, isError, isLoading: industryLoading } = useIndustries();
+ const {
+ register,
+ handleSubmit,
+ control,
+ formState: { errors },
+ reset,
+ } = useForm({
+ resolver: zodResolver(OrganizationSchema),
+ defaultValues: OrganizationDefaultValue,
+ });
+
+ const { mutate: CreateTenant, isPending } = useCreateSelfTenant(
+ (resp) => {
+ debugger
+ setStepStatus((prev) => ({ ...prev, [currentStep]: "success" }));
+ setCurrentStep((prev) => prev + 1);
+ },
+ (error) => {
+ setStepStatus((prev) => ({ ...prev, [currentStep]: "failed" }));
+ }
+ );
+
+ const onSubmit = (data) => {
+ CreateTenant(data);
+ // reset();
+ };
+ return (
+
+ );
+};
+
+export default SubscriptionForm;
diff --git a/src/components/UserSubscription/SubscriptionLayout.jsx b/src/components/UserSubscription/SubscriptionLayout.jsx
new file mode 100644
index 00000000..00eab448
--- /dev/null
+++ b/src/components/UserSubscription/SubscriptionLayout.jsx
@@ -0,0 +1,57 @@
+import React from "react";
+
+const SubscriptionLayout = ({
+ configStep = [],
+ currentStep,
+ setCurrentStep,
+ stepStatus = {},
+}) => {
+ return (
+
+
+
+
+ {configStep[currentStep - 1]?.component()}
+
+
+ );
+};
+
+export default SubscriptionLayout;
+
diff --git a/src/components/UserSubscription/VerifiedPayment.jsx b/src/components/UserSubscription/VerifiedPayment.jsx
new file mode 100644
index 00000000..1a8f7d53
--- /dev/null
+++ b/src/components/UserSubscription/VerifiedPayment.jsx
@@ -0,0 +1,81 @@
+import React, { useEffect, useState } from "react";
+import GlobalModel from "../common/GlobalModel";
+import Invoice from "./Invoice";
+
+const VerifiedPayment = ({ onNext, responsePayment }) => {
+ const [isGenerateInvoice, setIsGenerateInvoice] = useState(false);
+useEffect(() => {
+ if (responsePayment?.success) {
+ onNext();
+ }
+}, [responsePayment]);
+ if (responsePayment) {
+ return (
+
+
+
+
Verifying payment...
+
+ Please wait while we verify your transaction. Do not refresh or
+ close this page.
+
+
+
+ );
+ }
+
+ if (!responsePayment?.success) {
+
+ return (
+
+ {isGenerateInvoice && (
+
setIsGenerateInvoice(false)}
+ >
+
+
+ )}
+
+
+
+
+
+ Payment Successful!
+
+
+
+
+ Thank you for your payment. Your subscription has
+ been successfully activated.
+
+
+
+
+ A Set Password link has been sent to your registered email address .
+ Please check your inbox .
+
+
+
+
+
+ );
+ }
+
+ return null;
+};
+
+export default VerifiedPayment;
diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx
index ded67e13..2f29773e 100644
--- a/src/hooks/useAuth.jsx
+++ b/src/hooks/useAuth.jsx
@@ -13,6 +13,7 @@ import {
closeModal,
openAuthModal,
openModal,
+ setSelfTenant,
toggleModal,
} from "../slices/localVariablesSlice.jsx";
import { removeSession } from "../utils/authUtils.js";
@@ -82,6 +83,33 @@ export const useSelectTenant = (onSuccessCallBack) => {
});
};
+
+export const useCreateSelfTenant = (onSuccessCallBack, onFailureCallBack) => {
+ const dispatch = useDispatch();
+ return useMutation({
+ mutationFn: async (payload) => {
+ const resp = await AuthRepository.createSuscription(payload);
+ return resp.data;
+ },
+ onSuccess: (response, variables) => {
+ debugger
+ dispatch(
+ setSelfTenant({
+ tenantEnquireId: response?.id,
+ planId: null,
+ details:response
+ })
+ );
+ debugger
+ if (onSuccessCallBack) onSuccessCallBack(response);
+ },
+ onError: (error) => {
+ showToast("Somthing worng went happend", "error");
+ if (onFailureCallBack) onFailureCallBack();
+ },
+ });
+};
+
export const useAuthModal = () => {
const dispatch = useDispatch();
const { isOpen } = useSelector((state) => state.localVariables.AuthModal);
diff --git a/src/hooks/usePayments.jsx b/src/hooks/usePayments.jsx
index 42ce6558..bfe19fe8 100644
--- a/src/hooks/usePayments.jsx
+++ b/src/hooks/usePayments.jsx
@@ -1,35 +1,181 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { PaymentRepository } from "../repositories/PaymentRepository";
+import showToast from "../services/toastService";
+import { useSelector } from "react-redux";
+import { blockUI, unblockUI } from "../utils/blockUI";
-export const useMakePayment = (onSuccessCallBack) => {
- const client = useQueryClient();
- return useMutation({
- mutationFn: (payload) => PaymentRepository.makePayment(payload),
- onSuccess: (_, varibales) => {
- if (onSuccessCallBack) onSuccessCallBack();
- },
- onError: (error) => {
- showToast(
- error.message ||
- error.response.message ||
- "Something went wrong.Please try again later.",
- "error"
- );
- },
- });
-};
-export const useVerifyPayment = () => {
+export const removeRazorpayArtifacts=()=> {
+ try {
+ document
+ .querySelectorAll("iframe[src*='razorpay'], iframe[name^='__PRIVATE']")
+ .forEach((iframe) => iframe.remove());
+
+ document
+ .querySelectorAll(
+ "div.razorpay-container, div[class*='razorpay-backdrop'], div[style*='z-index: 1040'], div[style*='z-index: 9999']"
+ )
+ .forEach((el) => el.remove());
+ Array.from(document.querySelectorAll("body > div")).forEach((div) => {
+ const html = div.outerHTML || "";
+ if (
+ html.includes("razorpay-container") ||
+ html.includes("Test Mode") ||
+ html.includes("razorpay-backdrop")
+ ) {
+ div.remove();
+ }
+ });
+
+ document.body.removeAttribute("style");
+ document.body.style.overflow = "";
+ document.body.style.position = "";
+ document.body.style.height = "";
+ document.body.style.pointerEvents = "auto";
+
+ document.documentElement.style.overflow = "";
+ document.documentElement.style.removeProperty("overflow");
+
+ window.scrollTo(0, 0);
+ } catch (err) {
+ console.warn(" Error while cleaning Razorpay artifacts:", err);
+ }
+}
+
+const closeRazorpayPopup=()=> {
+ try {
+ if (window.Razorpay && typeof window.Razorpay.close === "function") {
+ window.Razorpay.close();
+ }
+
+ setTimeout(removeRazorpayArtifacts, 600);
+ } catch (err) {
+ console.warn(" Error closing Razorpay popup:", err);
+ removeRazorpayArtifacts();
+ }
+}
+
+export const useVerifyPayment = (onSuccessCallBack, onFailureCallBack) => {
const client = useQueryClient();
+
return useMutation({
mutationFn: (payload) => PaymentRepository.verifyPayment(payload),
- onSuccess: (_, varibales) => {
- if (onSuccessCallBack) onSuccessCallBack();
+
+ onSuccess: (data) => {
+ if (onSuccessCallBack) onSuccessCallBack(data);
},
+
onError: (error) => {
+ if (onFailureCallBack) onFailureCallBack(error);
showToast(
- error.message ||
- error.response.message ||
- "Something went wrong.Please try again later.",
+ error?.message ||
+ error?.response?.message ||
+ "Something went wrong. Please try again later.",
+ "error"
+ );
+ },
+ });
+};
+
+export const useMakePayment = (
+ onSuccessCallBack,
+ onFailureCallBack,
+ currentPlan
+) => {
+ const client = useQueryClient();
+ const { tenantEnquireId, planId } = useSelector(
+ (store) => store.localVariables.selfTenant
+ );
+
+ const { mutate: verifyPayment } = useVerifyPayment(
+ (response) => onSuccessCallBack?.(response),
+ (error) => onFailureCallBack?.(error)
+ );
+
+ return useMutation({
+ mutationFn: (payload) => PaymentRepository.makePayment(payload),
+
+ onSuccess: (data) => {
+ const orderId = data?.data?.orderId;
+ const key = data?.data?.key;
+
+ if (!orderId || !key) {
+ showToast("Invalid Razorpay order details.", "error");
+ return;
+ }
+
+ let manuallyClosed = false;
+
+ const options = {
+ key,
+ amount: (currentPlan?.amount ?? 1) * 100,
+ currency: currentPlan?.currency?.currencyCode || "INR",
+ name: "MarcoAIOT Subscription",
+ order_id: orderId,
+
+ handler: async (response) => {
+ if (manuallyClosed) {
+ unblockUI()
+ return;
+ }
+
+ try {
+ const payload = {
+ tenantEnquireId,
+ planId,
+ orderId: response.razorpay_order_id,
+ paymentId: response.razorpay_payment_id,
+ signature: response.razorpay_signature,
+ };
+ verifyPayment(payload);
+ } finally {
+ closeRazorpayPopup();
+ }
+ },
+
+ prefill: {
+ name: "",
+ email: "",
+ contact: "",
+ },
+
+ theme: { color: "#ea3b0fff" },
+
+ modal: {
+ ondismiss: () => {
+ manuallyClosed = true;
+ unblockUI();
+ closeRazorpayPopup();
+ },
+ },
+ };
+
+ try {
+ const razorpay = new window.Razorpay(options);
+
+ razorpay.on("payment.failed", (response) => {
+ if (manuallyClosed) return;
+ onFailureCallBack?.({
+ status: "failed",
+ message: response.error?.description || "Payment failed.",
+ error: response.error,
+ reason: "transaction_failed",
+ });
+ closeRazorpayPopup();
+ });
+
+ blockUI("Please Wait...");
+ razorpay.open();
+ } catch (err) {
+ alert("This browser is not supported. Please try another browser.");
+ closeRazorpayPopup();
+ }
+ },
+
+ onError: (error) => {
+ showToast(
+ error?.message ||
+ error?.response?.message ||
+ "Something went wrong. Please try again later.",
"error"
);
},
diff --git a/src/pages/Home/HomeSchema.jsx b/src/pages/Home/HomeSchema.jsx
new file mode 100644
index 00000000..867d9d52
--- /dev/null
+++ b/src/pages/Home/HomeSchema.jsx
@@ -0,0 +1,27 @@
+import { z } from "zod";
+
+export const OrganizationSchema = z.object({
+ firstName: z.string().min(1, "First Name is required"),
+ lastName: z.string().min(1, "Last Name is required"),
+ email: z.string().email("Invalid email address"),
+ billingAddress: z.string().min(1, "Billing Address is required"),
+ organizationName: z.string().min(1, "Organization Name is required"),
+ contactNumber: z
+ .string()
+ .min(5, "Contact Number is too short")
+ .regex(
+ /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/,
+ "Invalid phone number format"
+ ),
+ // onBoardingDate: z.coerce
+ // .date()
+ // .refine((d) => !Number.isNaN(d.getTime()), { message: "Invalid date" }),
+ organizationSize: z.string().min(1, "Organization Size is required"),
+ industryId: z.string().uuid("Industry is required"),
+ reference: z.string().min(1,{message:"Reference is required"}),
+});
+
+
+export const OrganizationDefaultValue = {
+
+}
\ No newline at end of file
diff --git a/src/pages/Home/MakeSubscription.jsx b/src/pages/Home/MakeSubscription.jsx
new file mode 100644
index 00000000..d7d6bf4f
--- /dev/null
+++ b/src/pages/Home/MakeSubscription.jsx
@@ -0,0 +1,127 @@
+import React, { useState, useMemo } from "react";
+
+import SubscriptionLayout from "../../components/UserSubscription/SubscriptionLayout";
+import SubscriptionForm from "../../components/UserSubscription/SubscriptionForm";
+import ProcessedPayment from "../../components/UserSubscription/ProcessedPayment";
+import VerifiedPayment from "../../components/UserSubscription/VerifiedPayment";
+import SelectPlan from "../../components/UserSubscription/SelectPlan";
+import { Link } from "react-router-dom";
+
+const MakeSubscription = () => {
+ const [currentStep, setCurrentStep] = useState(2);
+ const [responsePayment, setResponsePayment] = useState(null);
+
+ const [stepStatus, setStepStatus] = useState({
+ 1: "pending",
+ 2: "pending",
+ 3: "pending",
+ 4: "pending",
+ 5: "pending",
+ });
+
+ const handleVerification = (resp) => {
+ setResponsePayment(resp);
+ if (resp?.success) {
+ setStepStatus((prev) => ({ ...prev, 4: "success" }));
+ setCurrentStep(5);
+ } else {
+ setStepStatus((prev) => ({ ...prev, 4: "failed" }));
+ }
+ };
+ const handleNext = () => {
+ setStepStatus((prev) => ({
+ ...prev,
+ [currentStep]: "success",
+ [currentStep + 1]: "pending",
+ }));
+
+ setCurrentStep((prev) => prev + 1);
+ };
+
+ const checkOut_Steps = [
+ {
+ name: "Client Info",
+ component: () => (
+
+ ),
+ },
+ {
+ name: "Select Plan",
+ component: () => (
+
+ ),
+ },
+ {
+ name: "Review",
+ component: () => (
+ handleVerification(resp)}
+ resetPaymentStep={() =>
+ setStepStatus((prev) => ({ ...prev, 4: "pending" }))
+ }
+ setCurrentStep={setCurrentStep}
+ setStepStatus={setStepStatus}
+ resetFormStep={() => {
+ setStepStatus((prev) => ({ ...prev, 1: "pending" }));
+ setCurrentStep(1);
+ }}
+ />
+ ),
+ },
+ {
+ name: "Payment",
+ component: () => (
+ handleVerification(resp)}
+ resetPaymentStep={() =>
+ setStepStatus((prev) => ({ ...prev, 4: "pending" }))
+ }
+ setCurrentStep={setCurrentStep}
+ setStepStatus={setStepStatus}
+ resetFormStep={() => {
+ setStepStatus((prev) => ({ ...prev, 1: "pending" }));
+ setCurrentStep(1);
+ }}
+ />
+ ),
+ },
+ {
+ name: "Verified",
+ component: () => (
+ {
+ setStepStatus((prev) => ({ ...prev, 5: "success" }));
+ }}
+ responsePayment={responsePayment}
+ />
+ ),
+ },
+ ];
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default MakeSubscription;
diff --git a/src/pages/Home/SubscriptionPlans.jsx b/src/pages/Home/SubscriptionPlans.jsx
index 67f873a8..be94f5e0 100644
--- a/src/pages/Home/SubscriptionPlans.jsx
+++ b/src/pages/Home/SubscriptionPlans.jsx
@@ -124,14 +124,20 @@ const SubscriptionPlans = () => {
{/* Button */}
-
-
- Request a Demo
-
-
+
+
+ Subscribe
+
+
+ Request a Demo
+
+
))
diff --git a/src/repositories/AuthRepository.jsx b/src/repositories/AuthRepository.jsx
index ba68946e..57af42a0 100644
--- a/src/repositories/AuthRepository.jsx
+++ b/src/repositories/AuthRepository.jsx
@@ -11,6 +11,7 @@ const AuthRepository = {
register: (data) => api.postPublic("/api/auth/register", data),
sendMail: (data) => api.postPublic("/api/auth/sendmail", data),
getSubscription:(frequency)=> api.getPublic(`/api/market/list/subscription-plan?frequency=${frequency}`),
+ createSuscription:(data)=>api.post(`/api/Tenant/self/create`,data),
// Protected routes (require auth token)
logout: (data) => api.post("/api/auth/logout", data),
diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx
index f97be55f..daf5210c 100644
--- a/src/router/AppRoutes.jsx
+++ b/src/router/AppRoutes.jsx
@@ -55,6 +55,7 @@ import { ComingSoonPage } from "../pages/Misc/ComingSoonPage";
import ImageGalleryPage from "../pages/Gallary/ImageGallaryPage";
import CollectionPage from "../pages/collections/CollectionPage";
import SubscriptionSummary from "../pages/Home/SubscriptionSummary";
+import MakeSubscription from "../pages/Home/MakeSubscription";
const router = createBrowserRouter(
[
{
@@ -75,7 +76,7 @@ const router = createBrowserRouter(
],
},
{ path: "/auth/switch/org", element: },
- { path: "/request", element: },
+ { path: "/auth/subscripe/:frequency/:planName", element: },
{
element: ,
errorElement: ,
diff --git a/src/slices/localVariablesSlice.jsx b/src/slices/localVariablesSlice.jsx
index b520b7e5..b3628a70 100644
--- a/src/slices/localVariablesSlice.jsx
+++ b/src/slices/localVariablesSlice.jsx
@@ -31,6 +31,11 @@ const localVariablesSlice = createSlice({
AuthModal: {
isOpen: false,
},
+ selfTenant: {
+ tenantEnquireId: null,
+ planId: null,
+ details:null,
+ },
},
reducers: {
changeMaster: (state, action) => {
@@ -96,6 +101,14 @@ const localVariablesSlice = createSlice({
const { modalType } = action.payload;
state.modals[modalType].isOpen = !state.modals[modalType].isOpen;
},
+ setSelfTenant: (state, action) => {
+ state.selfTenant.tenantEnquireId =
+ action.payload.tenantEnquireId ?? state.selfTenant.tenantEnquireId;
+ state.selfTenant.planId =
+ action.payload.planId ?? state.selfTenant.planId;
+ state.selfTenant.details =
+ action.payload.details ?? state.selfTenant.details;
+ },
},
});
@@ -110,6 +123,6 @@ export const {
toggleOrgModal,
openAuthModal,
closeAuthModal,
- setOrganization,openModal, closeModal, toggleModal
+ setOrganization,openModal, closeModal, toggleModal , setSelfTenant
} = localVariablesSlice.actions;
export default localVariablesSlice.reducer;
diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js
index bfe9b692..363dccaf 100644
--- a/src/utils/appUtils.js
+++ b/src/utils/appUtils.js
@@ -135,3 +135,17 @@ export const formatFigure = (
return new Intl.NumberFormat(locale, formatterOptions).format(amount);
};
+export const frequencyLabel = (freq,isLong=false) => {
+ switch (freq) {
+ case 0:
+ return isLong ? "1 Month" : "1 mo";
+ case 1:
+ return isLong ? "3 Months" : "3 mo";
+ case 2:
+ return isLong ? "6 Months" : "6 mo";
+ case 3:
+ return isLong ? "1 Year" : "1 yr";
+ default:
+ return "mo";
+ }
+ };
\ No newline at end of file
diff --git a/src/utils/blockUI.js b/src/utils/blockUI.js
new file mode 100644
index 00000000..b516e059
--- /dev/null
+++ b/src/utils/blockUI.js
@@ -0,0 +1,32 @@
+export const blockUI = (message = 'Please wait...') => {
+ if (window.$ && window.$.blockUI) {
+ window.$.blockUI({
+ message: `
+ `,
+ css: {
+ backgroundColor: 'transparent',
+ border: '0',
+ color: '#fff',
+ },
+ overlayCSS: {
+ opacity: 0.5,
+ cursor: 'wait',
+ },
+ });
+ }
+};
+
+export const unblockUI = () => {
+ if (window.$ && window.$.unblockUI) {
+ window.$.unblockUI();
+ }
+};