+
{children}
);
diff --git a/src/components/widgets/BlurComponent/BlurComponent.module.scss b/src/components/widgets/BlurComponent/BlurComponent.module.scss
index 0b5e7fd..ee40430 100644
--- a/src/components/widgets/BlurComponent/BlurComponent.module.scss
+++ b/src/components/widgets/BlurComponent/BlurComponent.module.scss
@@ -5,6 +5,11 @@
text-align: center;
text-align: -webkit-center;
+ & > * {
+ position: relative;
+ z-index: 10;
+ }
+
.gradientBlur {
position: absolute;
z-index: 5;
diff --git a/src/entities/payment/actions.ts b/src/entities/payment/actions.ts
new file mode 100644
index 0000000..5ebc11a
--- /dev/null
+++ b/src/entities/payment/actions.ts
@@ -0,0 +1,34 @@
+"use server";
+
+import { http } from "@/shared/api/httpClient";
+import { API_ROUTES } from "@/shared/constants/api-routes";
+import { ActionResponse } from "@/types";
+
+import {
+ SingleCheckoutRequest,
+ SingleCheckoutResponse,
+ SingleCheckoutResponseSchema,
+} from "./types";
+
+export async function performSingleCheckout(
+ payload: SingleCheckoutRequest
+): Promise
> {
+ try {
+ const response = await http.post(
+ API_ROUTES.paymentSingleCheckout(),
+ payload,
+ {
+ schema: SingleCheckoutResponseSchema,
+ revalidate: 0,
+ }
+ );
+
+ return { data: response, error: null };
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Failed to perform single checkout:", error);
+ const errorMessage =
+ error instanceof Error ? error.message : "Something went wrong.";
+ return { data: null, error: errorMessage };
+ }
+}
diff --git a/src/entities/payment/api.ts b/src/entities/payment/api.ts
index 222880f..d1635f6 100644
--- a/src/entities/payment/api.ts
+++ b/src/entities/payment/api.ts
@@ -5,6 +5,9 @@ import {
CheckoutRequest,
CheckoutResponse,
CheckoutResponseSchema,
+ SingleCheckoutRequest,
+ SingleCheckoutResponse,
+ SingleCheckoutResponseSchema,
} from "./types";
export async function createPaymentCheckout(payload: CheckoutRequest) {
@@ -13,3 +16,16 @@ export async function createPaymentCheckout(payload: CheckoutRequest) {
revalidate: 0,
});
}
+
+export async function createSinglePaymentCheckout(
+ payload: SingleCheckoutRequest
+) {
+ return http.post(
+ API_ROUTES.paymentSingleCheckout(),
+ payload,
+ {
+ schema: SingleCheckoutResponseSchema,
+ revalidate: 0,
+ }
+ );
+}
diff --git a/src/entities/payment/types.ts b/src/entities/payment/types.ts
index 81a0a49..9c2147d 100644
--- a/src/entities/payment/types.ts
+++ b/src/entities/payment/types.ts
@@ -13,3 +13,37 @@ export const CheckoutResponseSchema = z.object({
paymentUrl: z.string().url(),
});
export type CheckoutResponse = z.infer;
+
+export const PaymentInfoSchema = z.object({
+ productId: z.string(),
+ key: z.string(),
+});
+export type PaymentInfo = z.infer;
+
+export const SingleCheckoutRequestSchema = z.object({
+ paymentInfo: PaymentInfoSchema,
+ return_url: z.string().optional(),
+});
+export type SingleCheckoutRequest = z.infer;
+
+export const SingleCheckoutSuccessSchema = z.object({
+ payment: z.object({
+ status: z.string(),
+ invoiceId: z.string(),
+ }),
+});
+export type SingleCheckoutSuccess = z.infer;
+
+export const SingleCheckoutErrorSchema = z.object({
+ status: z.string(),
+ message: z.string(),
+});
+export type SingleCheckoutError = z.infer;
+
+export const SingleCheckoutResponseSchema = z.union([
+ SingleCheckoutSuccessSchema,
+ SingleCheckoutErrorSchema,
+]);
+export type SingleCheckoutResponse = z.infer<
+ typeof SingleCheckoutResponseSchema
+>;
diff --git a/src/entities/session/funnel/api.ts b/src/entities/session/funnel/api.ts
new file mode 100644
index 0000000..1f75d41
--- /dev/null
+++ b/src/entities/session/funnel/api.ts
@@ -0,0 +1,12 @@
+import { http } from "@/shared/api/httpClient";
+import { API_ROUTES } from "@/shared/constants/api-routes";
+
+import { FunnelRequest, FunnelResponse, FunnelResponseSchema } from "./types";
+
+export const getFunnel = async (payload: FunnelRequest) => {
+ return http.post(API_ROUTES.funnel(), payload, {
+ tags: ["funnel"],
+ schema: FunnelResponseSchema,
+ revalidate: 0,
+ });
+};
diff --git a/src/entities/session/funnel/loaders.ts b/src/entities/session/funnel/loaders.ts
new file mode 100644
index 0000000..875cf3f
--- /dev/null
+++ b/src/entities/session/funnel/loaders.ts
@@ -0,0 +1,41 @@
+import { cache } from "react";
+
+import { getFunnel } from "./api";
+import type { FunnelRequest } from "./types";
+
+export const loadFunnel = cache((payload: FunnelRequest) => getFunnel(payload));
+
+export const loadFunnelData = cache((payload: FunnelRequest) =>
+ loadFunnel(payload).then(d => d.data)
+);
+
+export const loadFunnelStatus = cache((payload: FunnelRequest) =>
+ loadFunnel(payload).then(d => d.status)
+);
+
+export const loadFunnelCurrency = cache((payload: FunnelRequest) =>
+ loadFunnelData(payload).then(d => d.currency)
+);
+
+export const loadFunnelLocale = cache((payload: FunnelRequest) =>
+ loadFunnelData(payload).then(d => d.locale)
+);
+
+export const loadFunnelPayment = cache((payload: FunnelRequest) =>
+ loadFunnelData(payload).then(d => d.payment)
+);
+
+export const loadFunnelPaymentById = cache(
+ (payload: FunnelRequest, paymentId: string) =>
+ loadFunnelData(payload).then(d => d.payment[paymentId])
+);
+
+// export const loadFunnelProducts = cache(
+// (payload: FunnelRequest, paymentId: string) =>
+// loadFunnelPaymentById(payload, paymentId).then(d => d?.variants ?? [])
+// );
+
+// export const loadFunnelProperties = cache(
+// (payload: FunnelRequest, paymentId: string) =>
+// loadFunnelPaymentById(payload, paymentId).then(d => d?.properties ?? [])
+// );
diff --git a/src/entities/session/funnel/types.ts b/src/entities/session/funnel/types.ts
new file mode 100644
index 0000000..9231dcd
--- /dev/null
+++ b/src/entities/session/funnel/types.ts
@@ -0,0 +1,68 @@
+import { z } from "zod";
+
+import { Currency, ELocalesPlacement } from "../../../types";
+
+// Request schemas
+export const FunnelRequestSchema = z.object({
+ funnel: z.nativeEnum(ELocalesPlacement),
+});
+
+// Response schemas
+export const FunnelPaymentPropertySchema = z.object({
+ key: z.string(),
+ value: z.union([z.string(), z.number()]),
+});
+
+export const FunnelPaymentVariantSchema = z.object({
+ id: z.string(),
+ key: z.string(),
+ type: z.string(),
+ price: z.number(),
+ oldPrice: z.number().optional(),
+ trialPrice: z.number().optional(),
+});
+
+export const FunnelPaymentPlacementSchema = z.object({
+ price: z.number().optional(),
+ currency: z.nativeEnum(Currency).optional(),
+ billingPeriod: z.enum(["DAY", "WEEK", "MONTH", "YEAR"]).optional(),
+ billingInterval: z.number().optional(),
+ trialPeriod: z.enum(["DAY", "WEEK", "MONTH", "YEAR"]).optional(),
+ trialInterval: z.number().optional(),
+ placementId: z.string().optional(),
+ paywallId: z.string().optional(),
+ properties: z.array(FunnelPaymentPropertySchema).optional(),
+ variants: z.array(FunnelPaymentVariantSchema).optional(),
+ paymentUrl: z.string().optional(),
+ type: z.string().optional(),
+});
+
+export const FunnelSchema = z.object({
+ currency: z.nativeEnum(Currency),
+ funnel: z.nativeEnum(ELocalesPlacement),
+ locale: z.string(),
+ payment: z.record(
+ z.string(),
+ z.union([
+ FunnelPaymentPlacementSchema.nullable(),
+ z.array(FunnelPaymentPlacementSchema),
+ ])
+ ),
+});
+
+export const FunnelResponseSchema = z.object({
+ status: z.union([z.literal("success"), z.string()]),
+ data: FunnelSchema,
+});
+
+// Type exports
+export type FunnelRequest = z.infer;
+export type IFunnelPaymentProperty = z.infer<
+ typeof FunnelPaymentPropertySchema
+>;
+export type IFunnelPaymentVariant = z.infer;
+export type IFunnelPaymentPlacement = z.infer<
+ typeof FunnelPaymentPlacementSchema
+>;
+export type IFunnel = z.infer;
+export type FunnelResponse = z.infer;
diff --git a/src/hooks/multiPages/useMultiPageNavigation.ts b/src/hooks/multiPages/useMultiPageNavigation.ts
new file mode 100644
index 0000000..71ec504
--- /dev/null
+++ b/src/hooks/multiPages/useMultiPageNavigation.ts
@@ -0,0 +1,142 @@
+"use client";
+
+import { useCallback, useMemo } from "react";
+
+interface PageNavigationOptions {
+ data: T[];
+ currentType: string;
+ getTypeFromItem: (item: T) => string;
+ navigateToItemByType: (type: string) => void;
+ onBeforeNext?: (nextItem: T) => boolean | Promise;
+ onBeforePrevious?: (prevItem: T) => boolean | Promise;
+ onComplete: () => void;
+ onStart?: () => void;
+}
+
+interface PageNavigationReturn {
+ currentItem: T | undefined;
+ currentIndex: number;
+ isFirst: boolean;
+ isLast: boolean;
+ hasNext: boolean;
+ hasPrevious: boolean;
+ goToNext: () => Promise;
+ goToPrevious: () => Promise;
+ goToFirst: () => Promise;
+ goToLast: () => Promise;
+ goToIndex: (index: number) => Promise;
+ totalPages: number;
+}
+
+export function useMultiPageNavigation({
+ data,
+ currentType,
+ getTypeFromItem,
+ navigateToItemByType,
+ onBeforeNext,
+ onBeforePrevious,
+ onComplete,
+ onStart,
+}: PageNavigationOptions): PageNavigationReturn {
+ const currentIndex = useMemo(
+ () => data.findIndex(item => getTypeFromItem(item) === currentType),
+ [data, currentType, getTypeFromItem]
+ );
+
+ const currentItem = useMemo(() => data[currentIndex], [data, currentIndex]);
+
+ const isFirst = currentIndex === 0;
+ const isLast = currentIndex === data.length - 1;
+ const hasNext = !isLast;
+ const hasPrevious = !isFirst;
+ const totalPages = data.length;
+
+ const navigateToItem = useCallback(
+ async (item: T) => {
+ const type = getTypeFromItem(item);
+ navigateToItemByType(type);
+ },
+ [navigateToItemByType, getTypeFromItem]
+ );
+
+ const goToNext = useCallback(async () => {
+ if (!hasNext) return onComplete();
+
+ const nextItem = data[currentIndex + 1];
+
+ if (onBeforeNext) {
+ const shouldProceed = await onBeforeNext(nextItem);
+ if (!shouldProceed) return;
+ }
+
+ await navigateToItem(nextItem);
+ }, [hasNext, data, currentIndex, onBeforeNext, onComplete, navigateToItem]);
+
+ const goToPrevious = useCallback(async () => {
+ if (!hasPrevious) return;
+
+ const prevItem = data[currentIndex - 1];
+
+ if (onBeforePrevious) {
+ const shouldProceed = await onBeforePrevious(prevItem);
+ if (!shouldProceed) return;
+ }
+
+ await navigateToItem(prevItem);
+ }, [hasPrevious, data, currentIndex, onBeforePrevious, navigateToItem]);
+
+ const goToFirst = useCallback(async () => {
+ if (isFirst) return;
+
+ if (onStart) {
+ onStart();
+ return;
+ }
+
+ await navigateToItem(data[0]);
+ }, [isFirst, onStart, navigateToItem, data]);
+
+ const goToLast = useCallback(async () => {
+ if (isLast) return;
+ await navigateToItem(data[data.length - 1]);
+ }, [isLast, navigateToItem, data]);
+
+ const goToIndex = useCallback(
+ async (index: number) => {
+ if (index < 0 || index >= data.length || index === currentIndex) return;
+ await navigateToItem(data[index]);
+ },
+ [data, currentIndex, navigateToItem]
+ );
+
+ return useMemo(
+ () => ({
+ currentItem,
+ currentIndex,
+ isFirst,
+ isLast,
+ hasNext,
+ hasPrevious,
+ goToNext,
+ goToPrevious,
+ goToFirst,
+ goToLast,
+ goToIndex,
+ totalPages,
+ }),
+ [
+ currentItem,
+ currentIndex,
+ isFirst,
+ isLast,
+ hasNext,
+ hasPrevious,
+ goToNext,
+ goToPrevious,
+ goToFirst,
+ goToLast,
+ goToIndex,
+ totalPages,
+ ]
+ );
+}
diff --git a/src/hooks/payment/useSingleCheckout.ts b/src/hooks/payment/useSingleCheckout.ts
new file mode 100644
index 0000000..72295f7
--- /dev/null
+++ b/src/hooks/payment/useSingleCheckout.ts
@@ -0,0 +1,71 @@
+"use client";
+
+import { useCallback, useMemo, useState } from "react";
+
+import { performSingleCheckout } from "@/entities/payment/actions";
+import { PaymentInfo, SingleCheckoutRequest } from "@/entities/payment/types";
+
+interface UseSingleCheckoutOptions {
+ onSuccess?: () => void;
+ onError?: (error: string) => void;
+}
+
+export function useSingleCheckout(options: UseSingleCheckoutOptions = {}) {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { onSuccess, onError } = options;
+
+ const handleSingleCheckout = useCallback(
+ async (paymentInfo: PaymentInfo) => {
+ if (isLoading) return;
+
+ setIsLoading(true);
+
+ try {
+ const payload: SingleCheckoutRequest = {
+ paymentInfo,
+ };
+
+ const response = await performSingleCheckout(payload);
+
+ if (response.error) {
+ onError?.(response.error);
+ return;
+ }
+
+ if (!response.data) {
+ onError?.("Payment failed");
+ return;
+ }
+
+ if ("payment" in response.data) {
+ const { status } = response.data.payment;
+
+ if (status === "paid") {
+ onSuccess?.();
+ } else {
+ onError?.("Payment status is not paid");
+ }
+ } else {
+ const errorMessage = response.data.message || "Payment failed";
+ onError?.(errorMessage);
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Payment failed";
+ onError?.(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [onSuccess, onError, isLoading]
+ );
+
+ return useMemo(
+ () => ({
+ handleSingleCheckout,
+ isLoading,
+ }),
+ [handleSingleCheckout, isLoading]
+ );
+}
diff --git a/src/shared/constants/api-routes.ts b/src/shared/constants/api-routes.ts
index 4dd374e..327fd9d 100644
--- a/src/shared/constants/api-routes.ts
+++ b/src/shared/constants/api-routes.ts
@@ -15,6 +15,7 @@ export const API_ROUTES = {
dashboard: () => createRoute(["dashboard"]),
subscriptions: () => createRoute(["payment", "subscriptions"], ROOT_ROUTE_V3),
paymentCheckout: () => createRoute(["payment", "checkout"], ROOT_ROUTE_V2),
+ paymentSingleCheckout: () => createRoute(["payment", "checkout"]),
usersMe: () => createRoute(["users", "me"], ROOT_ROUTE),
compatibilityActionFields: (id: string) =>
createRoute(["dashboard", "compatibility-actions", id, "fields"]),
@@ -28,4 +29,7 @@ export const API_ROUTES = {
["payment", "subscriptions", subscriptionId, action],
ROOT_ROUTE_V3
),
+
+ // session
+ funnel: () => createRoute(["session", "funnel"], ROOT_ROUTE_V2),
};
diff --git a/src/shared/constants/client-routes.ts b/src/shared/constants/client-routes.ts
index e1e5de2..6f77228 100644
--- a/src/shared/constants/client-routes.ts
+++ b/src/shared/constants/client-routes.ts
@@ -63,6 +63,11 @@ export const ROUTES = {
// Chat
chat: () => createRoute(["chat"]),
+ // Additional Purchases
+ addConsultant: () => createRoute(["add-consultant"]),
+ addGuides: () => createRoute(["add-guides"]),
+ additionalPurchases: (type?: string) => createRoute(["ap", type]),
+
// // Compatibility
// compatibilities: () => createRoute(["compatibilities"]),
diff --git a/src/types/index.ts b/src/types/index.ts
index ec343fa..cf0cbe3 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -60,3 +60,26 @@ export type ActionResponse = {
data: T | null;
error: string | null;
};
+
+export enum ELocalesPlacement {
+ V0 = "v0", // Main site version
+ V1 = "v1",
+ PalmistryV0 = "palmistry-v0",
+ PalmistryV01 = "palmistry-v0_1",
+ PalmistryV1 = "palmistry-v1",
+ PalmistryV11 = "palmistry-v1_1",
+ Chats = "chats",
+ EmailMarketingCompatibilityV1 = "email-marketing-comp-v1",
+ EmailMarketingPalmistryV2 = "email-marketing-palmistry-v2",
+ EmailMarketingCompatibilityV2 = "email-marketing-comp-v2",
+ EmailMarketingCompatibilityV3 = "email-marketing-comp-v3",
+ EmailMarketingCompatibilityV4 = "email-marketing-comp-v4",
+ CompatibilityV2 = "compatibility-v2",
+ CompatibilityV3 = "compatibility-v3",
+ CompatibilityV4 = "compatibility-v4",
+ EmailGenerator = "email-generator",
+ Profile = "profile",
+ RetainingFunnel = "retaining-funnel",
+}
+
+export type PeriodType = "DAY" | "WEEK" | "MONTH" | "YEAR";