From b28946333e9c80e0cfe2efc87d4d37681bcee3c2 Mon Sep 17 00:00:00 2001
From: "pramod.mahajan"
Date: Mon, 27 Oct 2025 02:22:35 +0530
Subject: [PATCH] added subscription form and payment proccess page
---
public/assets/css/core-extend.css | 58 +++
.../UserSubscription/ProcessedPayment.jsx | 295 ++++++++++++++
.../UserSubscription/SubscriptionForm.jsx | 376 ++++++++++++++++++
.../UserSubscription/SubscriptionLayout.jsx | 53 +++
src/hooks/useAuth.jsx | 23 +-
src/hooks/useTenant.js | 4 +-
src/pages/Home/HomeSchema.jsx | 2 +-
src/pages/Home/LandingPage.jsx | 6 +-
src/pages/Home/MakeSubscription.jsx | 309 +-------------
src/pages/Home/SubscriptionPlans.jsx | 84 ++--
src/repositories/AuthRepository.jsx | 7 +-
src/router/AppRoutes.jsx | 2 +-
src/utils/appUtils.js | 17 +-
src/utils/axiosClient.jsx | 18 +-
14 files changed, 884 insertions(+), 370 deletions(-)
create mode 100644 src/components/UserSubscription/ProcessedPayment.jsx
create mode 100644 src/components/UserSubscription/SubscriptionForm.jsx
create mode 100644 src/components/UserSubscription/SubscriptionLayout.jsx
diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css
index 940f2065..32ebbf32 100644
--- a/public/assets/css/core-extend.css
+++ b/public/assets/css/core-extend.css
@@ -40,6 +40,64 @@
.text-royalblue{
color: #1796e3;
}
+.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;
+}
+
+.timeline-point.completed {
+ background-color: var(--bs-success);
+ color: white;
+}
+
+.timeline-point.active {
+ background-color: var(--bs-info);
+ color: white;
+ transform: scale(1.1);
+ padding: 4px;
+}
+
+.timeline-line-horizontal {
+ content: "";
+ position: absolute;
+ top: 10px;
+ left: 50%;
+ width:100% ;
+ height: 2px;
+ background-color: #dee2e6;
+ z-index: 1;
+}
+
+.timeline-item.completed ~ .timeline-line-horizontal {
+ background-color: #28a745;
+}
+
+
.text-md {
font-size: 2rem;
diff --git a/src/components/UserSubscription/ProcessedPayment.jsx b/src/components/UserSubscription/ProcessedPayment.jsx
new file mode 100644
index 00000000..aabaae27
--- /dev/null
+++ b/src/components/UserSubscription/ProcessedPayment.jsx
@@ -0,0 +1,295 @@
+import React, { useState, useMemo } from "react";
+import { useSubscription } from "../../hooks/useAuth";
+import { useParams } from "react-router-dom";
+import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
+import { frequencyLabel } from "../../utils/appUtils";
+import { formatUTCToLocalTime } from "../../utils/dateUtils";
+const ProcessedPayment = () => {
+ const { frequency, planName } = useParams();
+ const [selectedPlan, setSelectedPlan] = useState(planName);
+ const {
+ data: plans,
+ isError: isPlanError,
+ isLoading,
+ } = useSubscription(frequency);
+ const handleChange = (e) => {
+ setSelectedPlan(e.target.value);
+ };
+ const visiblePlans = useMemo(() => {
+ if (!plans) return [];
+
+ const planOrder = ["basic", "pro", "super"];
+ const currentIndex = planOrder.indexOf(planName?.toLowerCase());
+
+ if (currentIndex === -1) return plans; // fallback: show all
+ const visibleNames = planOrder.slice(currentIndex);
+
+ return plans.filter((p) =>
+ visibleNames.includes(p.planName?.toLowerCase())
+ );
+ }, [plans, selectedPlan]);
+ const clients = [
+ {
+ firstName: "Alice",
+ lastName: "Smith",
+ email: "alice.smith@example.com",
+ billingAddress:
+ "456 Elm Street, Metropolis, USA 456 Elm Street, Metropolis, USA 456 Elm Street, Metropolis, USA",
+ organizationName: "BetaTech Ltd.",
+ contactNumber: "+44 20 7946 0958",
+ onBoardingDate: "2025-09-15",
+ organizationSize: "10-50",
+ industryId: "c4d5e6f7-8901-2345-6789-abcdef123456",
+ reference: "d2e3f4a5-6789-0123-4567-89abcdef0123",
+ },
+ ];
+ return (
+
+
+
+
+
+
+ Choose the Perfect Plan for Your Organization
+
+
+ Select a plan that fits your team’s needs and unlock the
+ features that drive productivity.
+
+
+ {visiblePlans?.map((plan) => {
+ let colSize = "8"; // default 1 card full width
+
+ if (visiblePlans.length === 2) colSize = "6";
+ else if (visiblePlans.length === 3) colSize = "4";
+ else if (visiblePlans.length >= 4) colSize = "3";
+
+ return (
+
+
+
+
+
+ );
+ })}
+ {selectedPlan && (
+
+
+ {(() => {
+ const selected = plans?.find(
+ (p) => p.planName === selectedPlan
+ );
+ if (!selected) return null;
+ const {
+ planName,
+ description,
+ price,
+ frequency,
+ trialDays,
+ maxUser,
+ maxStorage,
+ currency,
+ features,
+ } = selected;
+
+ return (
+ <>
+
+
+
+
+ Max Users: {maxUser}
+
+
+
+
+
+ Max Storage: {maxStorage} MB
+
+
+
+
+
+ Trial Days: {trialDays}
+
+
+
+
+
+ Included Features
+
+
+ {Object.entries(features?.modules || {}).map(
+ ([key, mod]) => (
+
+
+
+
+
+ {mod.name}
+
+
+ {mod.enabled ? "Enabled" : "Disabled"}
+
+
+
+
+ )
+ )}
+
+
+
+ Support
+
+
+ {features?.supports?.emailSupport && (
+ -
+
+ Email Support
+
+ )}
+ {features?.supports?.phoneSupport && (
+ -
+
+ Phone Support
+
+ )}
+ {features?.supports?.prioritySupport && (
+ -
+
+ Priority Support
+
+ )}
+
+ >
+ );
+ })()}
+
+
+ )}
+
+
+
+
Client Info
+ {clients.map((client, idx) => (
+
+
+
+ First Name:
+
+
{client.firstName}
+
+
+ Last Name:
+
+
{client.lastName}
+
+
+ Email:
+
+
{client.email}
+
+
+ Contact Number:
+
+
{client.contactNumber}
+
+
+ Organization Name:
+
+
{client.organizationName}
+
+
+ Organization Size:
+
+
{client.organizationSize}
+
+
+ Onboarding Date:
+
+
+ {formatUTCToLocalTime(client.onBoardingDate)}
+
+
+
+ Billing Address:
+
+
{client.billingAddress}
+
+
+ Industry ID:
+
+
{client.industryId}
+
+ {/*
+ Reference:
+
+
{client.reference}
*/}
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default ProcessedPayment;
diff --git a/src/components/UserSubscription/SubscriptionForm.jsx b/src/components/UserSubscription/SubscriptionForm.jsx
new file mode 100644
index 00000000..d917f985
--- /dev/null
+++ b/src/components/UserSubscription/SubscriptionForm.jsx
@@ -0,0 +1,376 @@
+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";
+
+
+
+const SubscriptionForm = ({onNext}) => {
+
+
+
+
+ const { data, isError, isLoading: industryLoading } = useIndustries();
+
+
+
+
+ const {
+ register,
+ handleSubmit,
+ control,
+ formState: { errors },
+ reset,
+ } = useForm({
+ resolver: zodResolver(OrganizationSchema),
+ defaultValues: OrganizationDefaultValue,
+ });
+
+ const { mutate: CreateTenant, isPending } = useCreateTenant(() => {
+ // nextstep
+ if (onNext) onNext();
+ });
+
+ const onSubmit = (data) => {
+ CreateTenant(data)
+ // reset();
+ };
+ return (
+
+
+
+
+ {/*
+
+
+
+
+
+
+
+

+
+
First slide
+
+ Eos mutat malis maluisset et, agam ancillae quo te, in vim
+ congue pertinacia.
+
+
+
+
+

+
+
Second slide
+
In numquam omittam sea.
+
+
+
+

+
+
Third slide
+
+ Lorem ipsum dolor sit amet, virtute consequat ea qui, minim
+ graeco mel no.
+
+
+
+
+
+
+ Previous
+
+
+
+ Next
+
+
*/}
+
+
+ Provide organization information including name, size, industry,
+ and contact details.
+
+
+
+
+
+ );
+};
+
+export default SubscriptionForm;
diff --git a/src/components/UserSubscription/SubscriptionLayout.jsx b/src/components/UserSubscription/SubscriptionLayout.jsx
new file mode 100644
index 00000000..4e598d3e
--- /dev/null
+++ b/src/components/UserSubscription/SubscriptionLayout.jsx
@@ -0,0 +1,53 @@
+import React from "react";
+
+const SubscriptionLayout = ({ configStep = [], currentStep ,setCurrentStep }) => {
+ return (
+
+
+
+
+ {configStep[currentStep - 1]?.component()}
+
+
+
+ );
+};
+
+
+
+export default SubscriptionLayout;
diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx
index 4821565d..6b87ade6 100644
--- a/src/hooks/useAuth.jsx
+++ b/src/hooks/useAuth.jsx
@@ -14,8 +14,6 @@ import {
} from "../slices/localVariablesSlice.jsx";
import { removeSession } from "../utils/authUtils.js";
-
-
// ----------------------------Modal--------------------------
export const useModal = (modalType) => {
@@ -31,6 +29,16 @@ export const useModal = (modalType) => {
return { isOpen, onOpen, onClose, onToggle };
};
+export const useSubscription = (frequency) => {
+ return useQuery({
+ queryKey: ["subscriptionPlans", frequency],
+ queryFn: async () => {
+ const resp = await AuthRepository.getSubscription(frequency);
+ return resp.data;
+ },
+ });
+};
+
export const useTenants = () => {
return useQuery({
queryKey: ["tenantlist"],
@@ -79,18 +87,21 @@ export const useAuthModal = () => {
};
};
-
export const useLogout = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
- let payload = { refreshToken: localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken") };
+ let payload = {
+ refreshToken:
+ localStorage.getItem("refreshToken") ||
+ sessionStorage.getItem("refreshToken"),
+ };
return await AuthRepository.logout(payload);
},
onSuccess: (data) => {
- removeSession()
+ removeSession();
window.location.href = "/auth/login";
if (onSuccessCallBack) onSuccessCallBack();
@@ -98,7 +109,7 @@ export const useLogout = () => {
onError: (error) => {
showToast(error.message || "Error while creating project", "error");
- removeSession()
+ removeSession();
},
});
};
diff --git a/src/hooks/useTenant.js b/src/hooks/useTenant.js
index e8a21e06..d66e5a4b 100644
--- a/src/hooks/useTenant.js
+++ b/src/hooks/useTenant.js
@@ -72,7 +72,7 @@ export const useSubscriptionPlan = (freq) => {
// ------------Mutation---------------------
export const useCreateTenant = (onSuccessCallback) => {
- const clinet = queryClient()
+ const clinet = useQueryClient()
const dispatch = useDispatch();
return useMutation({
mutationFn: async (tenantPayload) => {
@@ -88,7 +88,7 @@ export const useCreateTenant = (onSuccessCallback) => {
} else if (data && !data.subscriptionHistery) {
operationMode = 2; // tenant exists but subscription not added yet
}
-
+debugger
clinet.invalidateQueries({queryKey:["Tenants"]})
diff --git a/src/pages/Home/HomeSchema.jsx b/src/pages/Home/HomeSchema.jsx
index 344ad024..631c9ea0 100644
--- a/src/pages/Home/HomeSchema.jsx
+++ b/src/pages/Home/HomeSchema.jsx
@@ -18,7 +18,7 @@ export const OrganizationSchema = z.object({
.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().optional(),
+ reference: z.string().min(1,{message:"Reference is required"}),
});
diff --git a/src/pages/Home/LandingPage.jsx b/src/pages/Home/LandingPage.jsx
index 67587252..22954736 100644
--- a/src/pages/Home/LandingPage.jsx
+++ b/src/pages/Home/LandingPage.jsx
@@ -6,6 +6,7 @@ import DashboardImage from "/img/hero/bg-01.jpg";
import { Swiper, SwiperSlide } from "swiper/react";
import { EffectFlip, Autoplay, Pagination, Navigation } from "swiper/modules";
import SwaperSlideImages from "./SwaperSlideImages";
+import SubscriptionPlans from "./SubscriptionPlans";
const LandingPage = () => {
return (
@@ -294,7 +295,7 @@ const LandingPage = () => {
No matter which plan you choose, you’ll get access to powerful
features. Choose the best plan to fit your needs.
-
+ {/*
@@ -352,7 +353,8 @@ const LandingPage = () => {
-
+
*/}
+
{/* */}
diff --git a/src/pages/Home/MakeSubscription.jsx b/src/pages/Home/MakeSubscription.jsx
index 1ce873a9..7d7ac231 100644
--- a/src/pages/Home/MakeSubscription.jsx
+++ b/src/pages/Home/MakeSubscription.jsx
@@ -1,306 +1,31 @@
-import React, { useState } from "react";
-import { useForm } from "react-hook-form";
-import { OrganizationDefaultValue, OrganizationSchema } from "./HomeSchema";
-import { zodResolver } from "@hookform/resolvers/zod";
-import Label from "../../components/common/Label";
-import { orgSize, reference } from "../../utils/constants";
-import DatePicker from "../../components/common/DatePicker";
-import { useIndustries } from "../../hooks/useTenant";
+import React, { useState, useMemo } from "react";
+
+import SubscriptionLayout from "../../components/UserSubscription/SubscriptionLayout";
+import SubscriptionForm from "../../components/UserSubscription/SubscriptionForm";
+import ProcessedPayment from "../../components/UserSubscription/ProcessedPayment";
const MakeSubscription = () => {
- const [selectedPlan, setSelectedPlan] = useState("basic");
-
- const handleChange = (e) => {
- const value = e.target.value;
- setSelectedPlan(value);
- };
-
- const { data, isError, isLoading: industryLoading } = useIndustries();
- const options = [
+ const [currentStep, setCurrentStep] = useState(2);
+ const checkOut_Steps = [
{
- id: 1,
- title: "Basic",
- value: "basic", // ✅ Added value
- price: "Free",
- description: "Get 1 project with 1 team member.",
+ name: "Client Info",
+ component: () => setCurrentStep(prev => prev + 1)}/>,
},
{
- id: 2,
- title: "Pro",
- value: "pro", // ✅ Added value
- price: "$10/mo",
- description: "Up to 10 projects and 5 team members.",
+ name: "Payment",
+ component: () => setCurrentStep(prev => prev + 1)}/>,
},
{
- id: 3,
- title: "Enterprise",
- value: "enterprise", // ✅ Added value
- price: "$30/mo",
- description: "Unlimited projects and team members.",
+ name: "Verified",
+ component: () => Verified
,
},
];
- const {
- register,
- handleSubmit,
- control,
- formState: { errors },
- reset,
- } = useForm({
- resolver: zodResolver(OrganizationSchema),
- defaultValues: OrganizationDefaultValue,
- });
-
- const onSubmit = (data) => {
- console.log("Form Submitted:", data);
- alert("Form submitted successfully!");
- reset();
- };
-
return (
-
-
-
-
-
- {/*
Organization Onboarding Form
*/}
-
-
-
-
-
-
- {options.map((opt) => (
-
-
-
-
-
- ))}
-
-
-
+
+
);
};
diff --git a/src/pages/Home/SubscriptionPlans.jsx b/src/pages/Home/SubscriptionPlans.jsx
index ab8a2f06..f7725946 100644
--- a/src/pages/Home/SubscriptionPlans.jsx
+++ b/src/pages/Home/SubscriptionPlans.jsx
@@ -2,62 +2,20 @@ import React, { useState, useEffect } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import PlanCardSkeleton from "./PlanCardSkeleton";
+import { useSubscription } from "../../hooks/useAuth";
+import { frequencyLabel } from "../../utils/appUtils";
const SubscriptionPlans = () => {
const [plans, setPlans] = useState([]);
const [frequency, setFrequency] = useState(1);
+ const { data, isLoading, isError, error } = useSubscription(frequency);
const [loading, setLoading] = useState(false);
- useEffect(() => {
- const fetchPlans = async () => {
- try {
- setLoading(true);
- const res = await axios.get(
- `http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`,
- { headers: { "Content-Type": "application/json" } }
- );
- setPlans(res.data?.data || []);
- } catch (err) {
- console.error("Error fetching plans:", err);
- } finally {
- setLoading(false);
- }
- };
- fetchPlans();
- }, [frequency]);
- const frequencyLabel = (freq) => {
- switch (freq) {
- case 0:
- return "1 mo";
- case 1:
- return "3 mo";
- case 2:
- return "6 mo";
- case 3:
- return "1 yr";
- default:
- return "mo";
- }
- };
+
return (
-
-
- Tailored pricing plans
- {/*
*/}
-
- designed for you
-
-
- No matter which plan you choose, you’ll get access to powerful features.{" "}
- Choose the best plan to fit your needs.
-
{/* Frequency Switcher */}
@@ -80,17 +38,22 @@ const SubscriptionPlans = () => {
{/* Cards */}
- {loading ? (
+ {isLoading ? (
// Show 3 skeletons
<>
>
- ) : plans.length === 0 ? (
+ ) : data.length === 0 ? (
No plans found
+ ) : isError ? (
+
+
{error.message}
+
{error.name}
+
) : (
- plans.map((plan) => (
+ data.map((plan) => (
{/* Header */}
@@ -148,14 +111,21 @@ const SubscriptionPlans = () => {
{/* Button */}
-
-
- Request a Demo
-
-
+
+
+ Subscribe
+
+
+ Request a Demo
+
+
+
))
diff --git a/src/repositories/AuthRepository.jsx b/src/repositories/AuthRepository.jsx
index 5f7e8b9c..7ac9466e 100644
--- a/src/repositories/AuthRepository.jsx
+++ b/src/repositories/AuthRepository.jsx
@@ -10,15 +10,16 @@ const AuthRepository = {
verifyOTP: (data) => api.postPublic("/api/auth/login-otp/v1", data),
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}`),
// Protected routes (require auth token)
logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data),
- appmenu: () => api.get('/api/appmenu/get/menu'),
+ appmenu: () => api.get("/api/appmenu/get/menu"),
selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`),
- getTenantList: () => api.get("/api/Auth/get/user/tenants"),
-
+ getTenantList: () => api.get("/api/Auth/get/user/tenants"),
};
export default AuthRepository;
diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx
index a46d785f..55be4098 100644
--- a/src/router/AppRoutes.jsx
+++ b/src/router/AppRoutes.jsx
@@ -74,7 +74,7 @@ const router = createBrowserRouter(
],
},
{ path: "/auth/switch/org", element:
},
- { path: "/request", element:
},
+ { path: "/auth/subscripe/:frequency/:planName", element:
},
{
element:
,
errorElement:
,
diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js
index 5093522d..d304aa1b 100644
--- a/src/utils/appUtils.js
+++ b/src/utils/appUtils.js
@@ -134,4 +134,19 @@ export const formatFigure = (
}
return new Intl.NumberFormat(locale, formatterOptions).format(amount);
-};
\ No newline at end of file
+};
+
+export const frequencyLabel = (freq) => {
+ switch (freq) {
+ case 0:
+ return "1 mo";
+ case 1:
+ return "3 mo";
+ case 2:
+ return "6 mo";
+ case 3:
+ return "1 yr";
+ default:
+ return "mo";
+ }
+ };
\ No newline at end of file
diff --git a/src/utils/axiosClient.jsx b/src/utils/axiosClient.jsx
index f1ec2d45..d74beec6 100644
--- a/src/utils/axiosClient.jsx
+++ b/src/utils/axiosClient.jsx
@@ -73,15 +73,16 @@ axiosClient.interceptors.response.use(
if (status === 401 && !isRefreshRequest) {
originalRequest._retry = true;
- const refreshToken = localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken");
+ const refreshToken =
+ localStorage.getItem("refreshToken") ||
+ sessionStorage.getItem("refreshToken");
if (
!refreshToken ||
error.response.data?.errors === "Invalid or expired refresh token."
-
) {
redirectToLogin();
- removeSession()
+ removeSession();
return Promise.reject(error);
}
@@ -90,7 +91,9 @@ axiosClient.interceptors.response.use(
try {
// Refresh token call
const res = await axiosClient.post("/api/Auth/refresh-token", {
- token: localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken"),
+ token:
+ localStorage.getItem("jwtToken") ||
+ sessionStorage.getItem("jwtToken"),
refreshToken,
});
@@ -110,7 +113,7 @@ axiosClient.interceptors.response.use(
originalRequest.headers["Authorization"] = `Bearer ${token}`;
return axiosClient(originalRequest);
} catch (refreshError) {
- removeSession()
+ removeSession();
redirectToLogin();
return Promise.reject(refreshError);
}
@@ -148,6 +151,11 @@ export const api = {
headers: { ...customHeaders },
authRequired: false,
}),
+ getPublic: (url, data = {}, customHeaders = {}) =>
+ apiRequest("get", url, data, {
+ headers: { ...customHeaders },
+ authRequired: false,
+ }),
// Authenticated routes
get: (url, params = {}, customHeaders = {}) =>