Merge pull request #46 from pennyteenycat/AW-503-additional-purchases
AW-503-additional-purchases
This commit is contained in:
commit
cdeec20928
@ -6,6 +6,7 @@ import { Button, Spinner, Typography } from "@/components/ui";
|
|||||||
import { BlurComponent } from "@/components/widgets";
|
import { BlurComponent } from "@/components/widgets";
|
||||||
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
||||||
import { useToast } from "@/providers/toast-provider";
|
import { useToast } from "@/providers/toast-provider";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import { useMultiPageNavigationContext } from "..";
|
import { useMultiPageNavigationContext } from "..";
|
||||||
|
|
||||||
@ -30,6 +31,10 @@ export default function AddConsultantButton() {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
returnUrl: new URL(
|
||||||
|
navigation.getNextPageUrl() || ROUTES.home(),
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL || ""
|
||||||
|
).toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleGetConsultation = () => {
|
const handleGetConsultation = () => {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Button, Spinner, Typography } from "@/components/ui";
|
|||||||
import { BlurComponent } from "@/components/widgets";
|
import { BlurComponent } from "@/components/widgets";
|
||||||
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
||||||
import { useToast } from "@/providers/toast-provider";
|
import { useToast } from "@/providers/toast-provider";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import { useMultiPageNavigationContext } from "..";
|
import { useMultiPageNavigationContext } from "..";
|
||||||
import { useProductSelection } from "../ProductSelectionProvider";
|
import { useProductSelection } from "../ProductSelectionProvider";
|
||||||
@ -29,6 +30,10 @@ export default function AddGuidesButton() {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
returnUrl: new URL(
|
||||||
|
navigation.getNextPageUrl() || ROUTES.home(),
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL || ""
|
||||||
|
).toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePurchase = () => {
|
const handlePurchase = () => {
|
||||||
|
|||||||
@ -34,9 +34,10 @@ export function MultiPageNavigationProvider({
|
|||||||
data,
|
data,
|
||||||
currentType,
|
currentType,
|
||||||
getTypeFromItem: item => item.type ?? "",
|
getTypeFromItem: item => item.type ?? "",
|
||||||
navigateToItemByType: type => {
|
// navigateToItemByType: type => {
|
||||||
router.push(ROUTES.additionalPurchases(type));
|
// router.push(ROUTES.additionalPurchases(type));
|
||||||
},
|
// },
|
||||||
|
getPageUrlByItem: item => ROUTES.additionalPurchases(item.type ?? ""),
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
router.push(ROUTES.home());
|
router.push(ROUTES.home());
|
||||||
},
|
},
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export default function RefillOptionsModal({
|
|||||||
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
||||||
onSuccess: onPaymentSuccess,
|
onSuccess: onPaymentSuccess,
|
||||||
onError: onPaymentError,
|
onError: onPaymentError,
|
||||||
|
returnUrl: typeof window !== "undefined" ? window.location.href : "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedOption, setSelectedOption] = useState<IRefillModalsProduct>(
|
const [selectedOption, setSelectedOption] = useState<IRefillModalsProduct>(
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export default function RefillTimerModal({
|
|||||||
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
||||||
onSuccess: onPaymentSuccess,
|
onSuccess: onPaymentSuccess,
|
||||||
onError: onPaymentError,
|
onError: onPaymentError,
|
||||||
|
returnUrl: typeof window !== "undefined" ? window.location.href : "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { seconds, isFinished } = useTimer({
|
const { seconds, isFinished } = useTimer({
|
||||||
|
|||||||
@ -25,7 +25,10 @@ export {
|
|||||||
export { default as RefillOptionsModal } from "./CreditsModals/RefillOptionsModal/RefillOptionsModal";
|
export { default as RefillOptionsModal } from "./CreditsModals/RefillOptionsModal/RefillOptionsModal";
|
||||||
export { default as RefillTimerModal } from "./CreditsModals/RefillTimerModal/RefillTimerModal";
|
export { default as RefillTimerModal } from "./CreditsModals/RefillTimerModal/RefillTimerModal";
|
||||||
export { default as GlobalNewMessagesBanner } from "./GlobalNewMessagesBanner/GlobalNewMessagesBanner";
|
export { default as GlobalNewMessagesBanner } from "./GlobalNewMessagesBanner/GlobalNewMessagesBanner";
|
||||||
export { default as LastMessagePreview, type LastMessagePreviewProps } from "./LastMessagePreview/LastMessagePreview";
|
export {
|
||||||
|
default as LastMessagePreview,
|
||||||
|
type LastMessagePreviewProps,
|
||||||
|
} from "./LastMessagePreview/LastMessagePreview";
|
||||||
export { default as MessageInput } from "./MessageInput/MessageInput";
|
export { default as MessageInput } from "./MessageInput/MessageInput";
|
||||||
export { default as MessageInputWrapper } from "./MessageInputWrapper/MessageInputWrapper";
|
export { default as MessageInputWrapper } from "./MessageInputWrapper/MessageInputWrapper";
|
||||||
export { default as NewMessages } from "./NewMessages/NewMessages";
|
export { default as NewMessages } from "./NewMessages/NewMessages";
|
||||||
|
|||||||
@ -4,7 +4,10 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Skeleton } from "@/components/ui";
|
import { Skeleton } from "@/components/ui";
|
||||||
import { getMyChatSettings, updateMyChatSettings } from "@/entities/chats/chatSettings.api";
|
import {
|
||||||
|
getMyChatSettings,
|
||||||
|
updateMyChatSettings,
|
||||||
|
} from "@/entities/chats/chatSettings.api";
|
||||||
import type { IChatSettings } from "@/entities/chats/types";
|
import type { IChatSettings } from "@/entities/chats/types";
|
||||||
|
|
||||||
import styles from "./AutoTopUpToggle.module.scss";
|
import styles from "./AutoTopUpToggle.module.scss";
|
||||||
|
|||||||
@ -14,16 +14,17 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Fetch current user's chat settings (client-side)
|
* Fetch current user's chat settings (client-side)
|
||||||
*/
|
*/
|
||||||
export const getMyChatSettings = async (): Promise<IGetMyChatSettingsResponse> => {
|
export const getMyChatSettings =
|
||||||
return http.get<IGetMyChatSettingsResponse>(
|
async (): Promise<IGetMyChatSettingsResponse> => {
|
||||||
API_ROUTES.getMyChatSettings(),
|
return http.get<IGetMyChatSettingsResponse>(
|
||||||
{
|
API_ROUTES.getMyChatSettings(),
|
||||||
tags: ["profile", "chat-settings"],
|
{
|
||||||
schema: GetMyChatSettingsResponseSchema,
|
tags: ["profile", "chat-settings"],
|
||||||
revalidate: 0,
|
schema: GetMyChatSettingsResponseSchema,
|
||||||
}
|
revalidate: 0,
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update current user's chat settings (client-side)
|
* Update current user's chat settings (client-side)
|
||||||
@ -41,4 +42,3 @@ export const updateMyChatSettings = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -97,4 +97,3 @@ export {
|
|||||||
GetMyChatSettingsResponseSchema,
|
GetMyChatSettingsResponseSchema,
|
||||||
UpdateMyChatSettingsResponseSchema,
|
UpdateMyChatSettingsResponseSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export type PaymentInfo = z.infer<typeof PaymentInfoSchema>;
|
|||||||
export const SingleCheckoutRequestSchema = z.object({
|
export const SingleCheckoutRequestSchema = z.object({
|
||||||
paymentInfo: PaymentInfoSchema,
|
paymentInfo: PaymentInfoSchema,
|
||||||
return_url: z.string().optional(),
|
return_url: z.string().optional(),
|
||||||
|
pageUrl: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type SingleCheckoutRequest = z.infer<typeof SingleCheckoutRequestSchema>;
|
export type SingleCheckoutRequest = z.infer<typeof SingleCheckoutRequestSchema>;
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ export const SingleCheckoutSuccessSchema = z.object({
|
|||||||
payment: z.object({
|
payment: z.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
invoiceId: z.string(),
|
invoiceId: z.string(),
|
||||||
|
paymentUrl: z.string().url().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
export type SingleCheckoutSuccess = z.infer<typeof SingleCheckoutSuccessSchema>;
|
export type SingleCheckoutSuccess = z.infer<typeof SingleCheckoutSuccessSchema>;
|
||||||
|
|||||||
@ -114,15 +114,17 @@ export const useChatSocket = (
|
|||||||
}, [emit, chatId]);
|
}, [emit, chatId]);
|
||||||
|
|
||||||
// Auto top-up: use existing single checkout flow
|
// Auto top-up: use existing single checkout flow
|
||||||
const { handleSingleCheckout, isLoading: isAutoTopUpLoading } = useSingleCheckout({
|
const { handleSingleCheckout, isLoading: isAutoTopUpLoading } =
|
||||||
onSuccess: fetchBalance,
|
useSingleCheckout({
|
||||||
onError: () => {
|
onSuccess: fetchBalance,
|
||||||
// eslint-disable-next-line no-console
|
onError: () => {
|
||||||
console.error("Auto top-up payment failed");
|
// eslint-disable-next-line no-console
|
||||||
// Release in-flight lock on error so a future event can retry
|
console.error("Auto top-up payment failed");
|
||||||
autoTopUpInProgressRef.current = false;
|
// Release in-flight lock on error so a future event can retry
|
||||||
},
|
autoTopUpInProgressRef.current = false;
|
||||||
});
|
},
|
||||||
|
returnUrl: typeof window !== "undefined" ? window.location.href : "",
|
||||||
|
});
|
||||||
|
|
||||||
// Auto top-up: silent flow (no UI prompt)
|
// Auto top-up: silent flow (no UI prompt)
|
||||||
const autoTopUpInProgressRef = useRef(false);
|
const autoTopUpInProgressRef = useRef(false);
|
||||||
@ -222,7 +224,6 @@ export const useChatSocket = (
|
|||||||
handleSingleCheckout(r.data);
|
handleSingleCheckout(r.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.maxFinishedAt) return;
|
if (!session?.maxFinishedAt) return;
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface PageNavigationOptions<T> {
|
interface PageNavigationOptions<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
currentType: string;
|
currentType: string;
|
||||||
getTypeFromItem: (item: T) => string;
|
getTypeFromItem: (item: T) => string;
|
||||||
navigateToItemByType: (type: string) => void;
|
// navigateToItemByType: (type: string) => void;
|
||||||
|
getPageUrlByItem: (item: T) => string;
|
||||||
onBeforeNext?: (nextItem: T) => boolean | Promise<boolean>;
|
onBeforeNext?: (nextItem: T) => boolean | Promise<boolean>;
|
||||||
onBeforePrevious?: (prevItem: T) => boolean | Promise<boolean>;
|
onBeforePrevious?: (prevItem: T) => boolean | Promise<boolean>;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
@ -25,6 +27,7 @@ interface PageNavigationReturn<T> {
|
|||||||
goToFirst: () => Promise<void>;
|
goToFirst: () => Promise<void>;
|
||||||
goToLast: () => Promise<void>;
|
goToLast: () => Promise<void>;
|
||||||
goToIndex: (index: number) => Promise<void>;
|
goToIndex: (index: number) => Promise<void>;
|
||||||
|
getNextPageUrl: () => string | undefined;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,18 +35,22 @@ export function useMultiPageNavigation<T>({
|
|||||||
data,
|
data,
|
||||||
currentType,
|
currentType,
|
||||||
getTypeFromItem,
|
getTypeFromItem,
|
||||||
navigateToItemByType,
|
// navigateToItemByType,
|
||||||
|
getPageUrlByItem,
|
||||||
onBeforeNext,
|
onBeforeNext,
|
||||||
onBeforePrevious,
|
onBeforePrevious,
|
||||||
onComplete,
|
onComplete,
|
||||||
onStart,
|
onStart,
|
||||||
}: PageNavigationOptions<T>): PageNavigationReturn<T> {
|
}: PageNavigationOptions<T>): PageNavigationReturn<T> {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const currentIndex = useMemo(
|
const currentIndex = useMemo(
|
||||||
() => data.findIndex(item => getTypeFromItem(item) === currentType),
|
() => data.findIndex(item => getTypeFromItem(item) === currentType),
|
||||||
[data, currentType, getTypeFromItem]
|
[data, currentType, getTypeFromItem]
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentItem = useMemo(() => data[currentIndex], [data, currentIndex]);
|
const currentItem = useMemo(() => data[currentIndex], [data, currentIndex]);
|
||||||
|
const nextItem = useMemo(() => data[currentIndex + 1], [data, currentIndex]);
|
||||||
|
|
||||||
const isFirst = currentIndex === 0;
|
const isFirst = currentIndex === 0;
|
||||||
const isLast = currentIndex === data.length - 1;
|
const isLast = currentIndex === data.length - 1;
|
||||||
@ -53,10 +60,11 @@ export function useMultiPageNavigation<T>({
|
|||||||
|
|
||||||
const navigateToItem = useCallback(
|
const navigateToItem = useCallback(
|
||||||
async (item: T) => {
|
async (item: T) => {
|
||||||
const type = getTypeFromItem(item);
|
// const type = getTypeFromItem(item);
|
||||||
navigateToItemByType(type);
|
// navigateToItemByType(type);
|
||||||
|
router.push(getPageUrlByItem(item));
|
||||||
},
|
},
|
||||||
[navigateToItemByType, getTypeFromItem]
|
[getPageUrlByItem, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToNext = useCallback(async () => {
|
const goToNext = useCallback(async () => {
|
||||||
@ -109,9 +117,15 @@ export function useMultiPageNavigation<T>({
|
|||||||
[data, currentIndex, navigateToItem]
|
[data, currentIndex, navigateToItem]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getNextPageUrl = useCallback(() => {
|
||||||
|
if (!hasNext || !nextItem) return;
|
||||||
|
return getPageUrlByItem(nextItem);
|
||||||
|
}, [getPageUrlByItem, hasNext, nextItem]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
currentItem,
|
currentItem,
|
||||||
|
nextItem,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
isFirst,
|
isFirst,
|
||||||
isLast,
|
isLast,
|
||||||
@ -122,10 +136,13 @@ export function useMultiPageNavigation<T>({
|
|||||||
goToFirst,
|
goToFirst,
|
||||||
goToLast,
|
goToLast,
|
||||||
goToIndex,
|
goToIndex,
|
||||||
|
getTypeFromItem,
|
||||||
|
getNextPageUrl,
|
||||||
totalPages,
|
totalPages,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
currentItem,
|
currentItem,
|
||||||
|
nextItem,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
isFirst,
|
isFirst,
|
||||||
isLast,
|
isLast,
|
||||||
@ -136,6 +153,8 @@ export function useMultiPageNavigation<T>({
|
|||||||
goToFirst,
|
goToFirst,
|
||||||
goToLast,
|
goToLast,
|
||||||
goToIndex,
|
goToIndex,
|
||||||
|
getTypeFromItem,
|
||||||
|
getNextPageUrl,
|
||||||
totalPages,
|
totalPages,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { performSingleCheckout } from "@/entities/payment/actions";
|
|||||||
import { PaymentInfo, SingleCheckoutRequest } from "@/entities/payment/types";
|
import { PaymentInfo, SingleCheckoutRequest } from "@/entities/payment/types";
|
||||||
|
|
||||||
interface UseSingleCheckoutOptions {
|
interface UseSingleCheckoutOptions {
|
||||||
|
returnUrl?: string;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
}
|
}
|
||||||
@ -13,7 +14,7 @@ interface UseSingleCheckoutOptions {
|
|||||||
export function useSingleCheckout(options: UseSingleCheckoutOptions = {}) {
|
export function useSingleCheckout(options: UseSingleCheckoutOptions = {}) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { onSuccess, onError } = options;
|
const { returnUrl, onSuccess, onError } = options;
|
||||||
|
|
||||||
const handleSingleCheckout = useCallback(
|
const handleSingleCheckout = useCallback(
|
||||||
async (paymentInfo: PaymentInfo) => {
|
async (paymentInfo: PaymentInfo) => {
|
||||||
@ -24,6 +25,8 @@ export function useSingleCheckout(options: UseSingleCheckoutOptions = {}) {
|
|||||||
try {
|
try {
|
||||||
const payload: SingleCheckoutRequest = {
|
const payload: SingleCheckoutRequest = {
|
||||||
paymentInfo,
|
paymentInfo,
|
||||||
|
pageUrl: typeof window !== "undefined" ? window.location.href : "",
|
||||||
|
return_url: returnUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await performSingleCheckout(payload);
|
const response = await performSingleCheckout(payload);
|
||||||
@ -39,7 +42,11 @@ export function useSingleCheckout(options: UseSingleCheckoutOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("payment" in response.data) {
|
if ("payment" in response.data) {
|
||||||
const { status } = response.data.payment;
|
const { status, paymentUrl } = response.data.payment;
|
||||||
|
|
||||||
|
if (paymentUrl) {
|
||||||
|
return window.location.replace(paymentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (status === "paid") {
|
if (status === "paid") {
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
@ -58,7 +65,7 @@ export function useSingleCheckout(options: UseSingleCheckoutOptions = {}) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onSuccess, onError, isLoading]
|
[isLoading, returnUrl, onError, onSuccess]
|
||||||
);
|
);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
|||||||
@ -8,10 +8,10 @@ export async function getServerAccessToken() {
|
|||||||
export function getClientAccessToken(): string | undefined {
|
export function getClientAccessToken(): string | undefined {
|
||||||
if (typeof window === "undefined") return undefined;
|
if (typeof window === "undefined") return undefined;
|
||||||
|
|
||||||
const cookies = document.cookie.split(';');
|
const cookies = document.cookie.split(";");
|
||||||
const accessTokenCookie = cookies.find(cookie =>
|
const accessTokenCookie = cookies.find(cookie =>
|
||||||
cookie.trim().startsWith('accessToken=')
|
cookie.trim().startsWith("accessToken=")
|
||||||
);
|
);
|
||||||
|
|
||||||
return accessTokenCookie?.split('=')[1];
|
return accessTokenCookie?.split("=")[1];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,6 @@ export const API_ROUTES = {
|
|||||||
createRoute(["chats", chatId, "messages"]),
|
createRoute(["chats", chatId, "messages"]),
|
||||||
getUserBalance: () => createRoute(["chats", "balance"]),
|
getUserBalance: () => createRoute(["chats", "balance"]),
|
||||||
getMyChatSettings: () => createRoute(["chats", "profile", "chat-settings"]),
|
getMyChatSettings: () => createRoute(["chats", "profile", "chat-settings"]),
|
||||||
updateMyChatSettings: () => createRoute(["chats", "profile", "chat-settings"]),
|
updateMyChatSettings: () =>
|
||||||
|
createRoute(["chats", "profile", "chat-settings"]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user