commit
5e51d2c2cf
@ -10,6 +10,10 @@
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"auto_top_up": {
|
||||
"title": "Automatische Aufladung",
|
||||
"description": "Automatisch Guthaben hinzufügen, wenn es aufgebraucht ist, um Chats ohne Unterbrechung fortzusetzen."
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
|
||||
@ -10,6 +10,10 @@
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"auto_top_up": {
|
||||
"title": "Auto Top-Up",
|
||||
"description": "Automatically add credits when you run out to keep chats uninterrupted."
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
|
||||
@ -10,6 +10,10 @@
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"auto_top_up": {
|
||||
"title": "Recarga automática",
|
||||
"description": "Añade créditos automáticamente cuando te quedes sin saldo para mantener los chats sin interrupciones."
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { GlobalNewMessagesBanner } from "@/components/domains/chat";
|
||||
import { DrawerProvider, Header, NavigationBar } from "@/components/layout";
|
||||
import { ChatStoreProvider } from "@/providers/chat-store-provider";
|
||||
|
||||
@ -12,6 +13,7 @@ export default function CoreLayout({
|
||||
<DrawerProvider>
|
||||
<ChatStoreProvider>
|
||||
<Header className={styles.navBar} />
|
||||
<GlobalNewMessagesBanner />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<NavigationBar />
|
||||
</ChatStoreProvider>
|
||||
|
||||
@ -7,8 +7,6 @@ import {
|
||||
CompatibilitySectionSkeleton,
|
||||
MeditationSection,
|
||||
MeditationSectionSkeleton,
|
||||
NewMessagesSection,
|
||||
NewMessagesSectionSkeleton,
|
||||
PalmSection,
|
||||
PalmSectionSkeleton,
|
||||
} from "@/components/domains/dashboard";
|
||||
@ -27,10 +25,6 @@ export default function Home() {
|
||||
const chatsPromise = loadChatsList();
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Suspense fallback={<NewMessagesSectionSkeleton />}>
|
||||
<NewMessagesSection />
|
||||
</Suspense>
|
||||
|
||||
<Horoscope />
|
||||
|
||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { GlobalNewMessagesBanner } from "@/components/domains/chat";
|
||||
import { DrawerProvider, Header } from "@/components/layout";
|
||||
import { ChatStoreProvider } from "@/providers/chat-store-provider";
|
||||
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
@ -9,8 +11,11 @@ export default function PaymentLayout({
|
||||
}>) {
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<Header className={styles.navBar} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<ChatStoreProvider>
|
||||
<Header className={styles.navBar} />
|
||||
<GlobalNewMessagesBanner />
|
||||
<main className={styles.main}>{children}</main>
|
||||
</ChatStoreProvider>
|
||||
</DrawerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
position: sticky;
|
||||
top: 76px;
|
||||
z-index: 100;
|
||||
margin-bottom: clamp(16px, 2.5vw, 32px);
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useLocale } from "next-intl";
|
||||
|
||||
import NewMessages from "@/components/domains/chat/NewMessages/NewMessages";
|
||||
import ViewAll from "@/components/domains/chat/ViewAll/ViewAll";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { stripLocale } from "@/shared/utils/path";
|
||||
|
||||
import styles from "./GlobalNewMessagesBanner.module.scss";
|
||||
|
||||
export default function GlobalNewMessagesBanner() {
|
||||
const { unreadChats } = useChats();
|
||||
|
||||
// Exclude banner on chat-related and settings (profile) pages
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const pathnameWithoutLocale = stripLocale(pathname, locale);
|
||||
const isExcluded =
|
||||
pathnameWithoutLocale.startsWith(ROUTES.chat()) ||
|
||||
pathnameWithoutLocale.startsWith(ROUTES.profile());
|
||||
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
const { isVisibleAll } = useAppUiStore(state => state.home.newMessages);
|
||||
const setHomeNewMessages = useAppUiStore(state => state.setHomeNewMessages);
|
||||
|
||||
if (!hasHydrated || isExcluded || unreadChats.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{unreadChats.length > 1 && (
|
||||
<ViewAll
|
||||
count={unreadChats.length}
|
||||
isAll={isVisibleAll}
|
||||
onClick={() => setHomeNewMessages({ isVisibleAll: !isVisibleAll })}
|
||||
/>
|
||||
)}
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -24,10 +24,8 @@ export {
|
||||
} from "./CorrespondenceStartedWrapper/CorrespondenceStartedWrapper";
|
||||
export { default as RefillOptionsModal } from "./CreditsModals/RefillOptionsModal/RefillOptionsModal";
|
||||
export { default as RefillTimerModal } from "./CreditsModals/RefillTimerModal/RefillTimerModal";
|
||||
export {
|
||||
default as LastMessagePreview,
|
||||
type LastMessagePreviewProps,
|
||||
} from "./LastMessagePreview/LastMessagePreview";
|
||||
export { default as GlobalNewMessagesBanner } from "./GlobalNewMessagesBanner/GlobalNewMessagesBanner";
|
||||
export { default as LastMessagePreview, type LastMessagePreviewProps } from "./LastMessagePreview/LastMessagePreview";
|
||||
export { default as MessageInput } from "./MessageInput/MessageInput";
|
||||
export { default as MessageInputWrapper } from "./MessageInputWrapper/MessageInputWrapper";
|
||||
export { default as NewMessages } from "./NewMessages/NewMessages";
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.switchButton {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
background-color: #d3d3d3;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.switchButton[aria-checked="true"] {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.switchButton[aria-checked="true"] .thumb {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { getMyChatSettings, updateMyChatSettings } from "@/entities/chats/chatSettings.api";
|
||||
import type { IChatSettings } from "@/entities/chats/types";
|
||||
|
||||
import styles from "./AutoTopUpToggle.module.scss";
|
||||
|
||||
export default function AutoTopUpToggle() {
|
||||
const t = useTranslations("Profile");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [settings, setSettings] = useState<IChatSettings | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await getMyChatSettings();
|
||||
if (mounted) {
|
||||
setSettings(res.settings);
|
||||
}
|
||||
} catch (e) {
|
||||
// silent failure
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to fetch chat settings", e);
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onToggle = async () => {
|
||||
if (!settings || isUpdating) return;
|
||||
const next = { ...settings, autoTopUp: !settings.autoTopUp };
|
||||
setSettings(next); // optimistic
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const res = await updateMyChatSettings(next);
|
||||
setSettings(res.settings);
|
||||
} catch (e) {
|
||||
// revert on error silently
|
||||
setSettings(settings);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to update chat settings", e);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton style={{ height: 72 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.text}>
|
||||
<div className={styles.title}>{t("auto_top_up.title")}</div>
|
||||
<div className={styles.description}>{t("auto_top_up.description")}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.switchButton}
|
||||
role="switch"
|
||||
aria-checked={settings?.autoTopUp ? "true" : "false"}
|
||||
onClick={onToggle}
|
||||
aria-label={t("auto_top_up.title")}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<span className={styles.thumb} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import AutoTopUpToggle from "@/components/domains/profile/AutoTopUpToggle/AutoTopUpToggle";
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
import { useUserBalance } from "@/hooks/balance/useUserBalance";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
@ -48,6 +49,7 @@ function Billing() {
|
||||
{t("credits.description")}
|
||||
</Typography>
|
||||
</div>
|
||||
<AutoTopUpToggle />
|
||||
<Typography
|
||||
as="p"
|
||||
weight="bold"
|
||||
|
||||
44
src/entities/chats/chatSettings.api.ts
Normal file
44
src/entities/chats/chatSettings.api.ts
Normal file
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { http } from "@/shared/api/httpClient";
|
||||
import { API_ROUTES } from "@/shared/constants/api-routes";
|
||||
|
||||
import {
|
||||
GetMyChatSettingsResponseSchema,
|
||||
type IChatSettings,
|
||||
type IGetMyChatSettingsResponse,
|
||||
type IUpdateMyChatSettingsResponse,
|
||||
UpdateMyChatSettingsResponseSchema,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Fetch current user's chat settings (client-side)
|
||||
*/
|
||||
export const getMyChatSettings = async (): Promise<IGetMyChatSettingsResponse> => {
|
||||
return http.get<IGetMyChatSettingsResponse>(
|
||||
API_ROUTES.getMyChatSettings(),
|
||||
{
|
||||
tags: ["profile", "chat-settings"],
|
||||
schema: GetMyChatSettingsResponseSchema,
|
||||
revalidate: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update current user's chat settings (client-side)
|
||||
*/
|
||||
export const updateMyChatSettings = async (
|
||||
settings: IChatSettings
|
||||
): Promise<IUpdateMyChatSettingsResponse> => {
|
||||
return http.put<IUpdateMyChatSettingsResponse>(
|
||||
API_ROUTES.updateMyChatSettings(),
|
||||
settings,
|
||||
{
|
||||
tags: ["profile", "chat-settings"],
|
||||
schema: UpdateMyChatSettingsResponseSchema,
|
||||
revalidate: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@ -52,6 +52,22 @@ const GetChatMessagesResponseSchema = z.object({
|
||||
limit: z.number(),
|
||||
});
|
||||
|
||||
// Chat settings
|
||||
const ChatSettingsSchema = z.object({
|
||||
autoTopUp: z.boolean(),
|
||||
topUpAmount: z.number(),
|
||||
});
|
||||
|
||||
const GetMyChatSettingsResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
settings: ChatSettingsSchema,
|
||||
});
|
||||
|
||||
const UpdateMyChatSettingsResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
settings: ChatSettingsSchema,
|
||||
});
|
||||
|
||||
export type IChat = z.infer<typeof ChatSchema>;
|
||||
export type ICategorizedChats = z.infer<typeof CategorizedChatsSchema>;
|
||||
export type ICreateAllChatsResponse = z.infer<
|
||||
@ -63,11 +79,22 @@ export type IChatMessage = z.infer<typeof ChatMessageSchema>;
|
||||
export type IGetChatMessagesResponse = z.infer<
|
||||
typeof GetChatMessagesResponseSchema
|
||||
>;
|
||||
export type IChatSettings = z.infer<typeof ChatSettingsSchema>;
|
||||
export type IGetMyChatSettingsResponse = z.infer<
|
||||
typeof GetMyChatSettingsResponseSchema
|
||||
>;
|
||||
export type IUpdateMyChatSettingsResponse = z.infer<
|
||||
typeof UpdateMyChatSettingsResponseSchema
|
||||
>;
|
||||
|
||||
export {
|
||||
ChatMessageSchema,
|
||||
ChatSettingsSchema,
|
||||
CreateAllChatsResponseSchema,
|
||||
CreateChatResponseSchema,
|
||||
GetChatMessagesResponseSchema,
|
||||
GetChatsListResponseSchema,
|
||||
GetMyChatSettingsResponseSchema,
|
||||
UpdateMyChatSettingsResponseSchema,
|
||||
};
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { fetchChatMessages } from "@/entities/chats/actions";
|
||||
import type { IChatMessage } from "@/entities/chats/types";
|
||||
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
||||
import { useSocketEvent } from "@/hooks/socket/useSocketEvent";
|
||||
import { useChatStore } from "@/providers/chat-store-provider";
|
||||
import {
|
||||
@ -112,7 +113,25 @@ export const useChatSocket = (
|
||||
emit("fetch_balance", { chatId });
|
||||
}, [emit, chatId]);
|
||||
|
||||
// Auto top-up: use existing single checkout flow
|
||||
const { handleSingleCheckout, isLoading: isAutoTopUpLoading } = useSingleCheckout({
|
||||
onSuccess: fetchBalance,
|
||||
onError: () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Auto top-up payment failed");
|
||||
// Release in-flight lock on error so a future event can retry
|
||||
autoTopUpInProgressRef.current = false;
|
||||
},
|
||||
});
|
||||
|
||||
// Auto top-up: silent flow (no UI prompt)
|
||||
const autoTopUpInProgressRef = useRef(false);
|
||||
|
||||
const balancePollId = useRef<NodeJS.Timeout | null>(null);
|
||||
// Avoid immediate leave_chat right after join in React 18 StrictMode (dev) double-invoke
|
||||
// We debounce leave so that a quick remount cancels it, but real navigation (or chat switch) proceeds.
|
||||
const leaveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastChatIdRef = useRef(chatId);
|
||||
|
||||
const startBalancePolling = useCallback(() => {
|
||||
if (balancePollId.current) return;
|
||||
@ -177,13 +196,32 @@ export const useChatSocket = (
|
||||
options.onNewMessage(data[0]);
|
||||
}
|
||||
});
|
||||
useSocketEvent("current_balance", b => setBalance(b.data));
|
||||
useSocketEvent("current_balance", b => {
|
||||
setBalance(b.data);
|
||||
// If auto top-up was in-flight, release the lock only after balance became positive
|
||||
if (autoTopUpInProgressRef.current && b?.data?.balance > 0) {
|
||||
autoTopUpInProgressRef.current = false;
|
||||
}
|
||||
});
|
||||
useSocketEvent("balance_updated", b => {
|
||||
setBalance(prev => (prev ? { ...prev, balance: b.data.balance } : null));
|
||||
if (autoTopUpInProgressRef.current && b?.data?.balance > 0) {
|
||||
autoTopUpInProgressRef.current = false;
|
||||
}
|
||||
});
|
||||
useSocketEvent("session_started", s => setSession(s.data));
|
||||
useSocketEvent("session_ended", () => setSession(null));
|
||||
useSocketEvent("show_refill_modals", r => setRefillModals(r.data));
|
||||
useSocketEvent("auto_topup_request", r => {
|
||||
if (!r?.data) return;
|
||||
if (isAutoTopUpLoading) return;
|
||||
// Prevent concurrent or rapid duplicate attempts
|
||||
if (autoTopUpInProgressRef.current) return;
|
||||
autoTopUpInProgressRef.current = true;
|
||||
// Trigger checkout silently
|
||||
handleSingleCheckout(r.data);
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.maxFinishedAt) return;
|
||||
@ -204,13 +242,39 @@ export const useChatSocket = (
|
||||
useEffect(() => {
|
||||
if (!socket || status !== ESocketStatus.CONNECTED) return;
|
||||
|
||||
// If a leave was scheduled for the same chat (StrictMode remount), cancel it
|
||||
if (leaveTimeoutRef.current && lastChatIdRef.current === chatId) {
|
||||
clearTimeout(leaveTimeoutRef.current);
|
||||
leaveTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
joinChat();
|
||||
fetchBalance();
|
||||
lastChatIdRef.current = chatId;
|
||||
|
||||
return () => {
|
||||
leaveChat();
|
||||
// Debounce leave to avoid leaving room between StrictMode unmount/mount cycle
|
||||
// For real chat switch (different chatId), we won't cancel this in the next effect
|
||||
leaveTimeoutRef.current = setTimeout(() => {
|
||||
leaveChat();
|
||||
}, 300);
|
||||
};
|
||||
}, [socket, status, joinChat, leaveChat, fetchBalance]);
|
||||
}, [socket, status, joinChat, leaveChat, fetchBalance, chatId]);
|
||||
|
||||
// Re-join chat on socket reconnects while staying on the chat page
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const handleConnect = () => {
|
||||
// Ensure current chat is tracked and we actually reconnect to it
|
||||
lastChatIdRef.current = chatId;
|
||||
joinChat();
|
||||
fetchBalance();
|
||||
};
|
||||
socket.on("connect", handleConnect);
|
||||
return () => {
|
||||
socket.off("connect", handleConnect);
|
||||
};
|
||||
}, [socket, chatId, joinChat, fetchBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
setSuggestions(messages[0]?.suggestions);
|
||||
|
||||
@ -44,6 +44,12 @@ export interface IRefillModals {
|
||||
products?: IRefillModalsProduct[];
|
||||
}
|
||||
|
||||
export interface IAutoTopUpRequest {
|
||||
productId: string;
|
||||
key: string;
|
||||
isAutoTopUp?: boolean;
|
||||
}
|
||||
|
||||
export interface IUnreadMessagesCount {
|
||||
unreadCount: number;
|
||||
}
|
||||
@ -81,6 +87,9 @@ export interface ServerToClientEvents {
|
||||
show_refill_modals: (
|
||||
data: ServerToClientEventsBaseData<IRefillModals>
|
||||
) => void;
|
||||
auto_topup_request: (
|
||||
data: ServerToClientEventsBaseData<IAutoTopUpRequest>
|
||||
) => void;
|
||||
chats_updated: (data: IGetChatsListResponse) => void;
|
||||
unread_messages_count: (data: IUnreadMessagesCount) => void;
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerAccessToken } from "../auth/token";
|
||||
import { devLogger } from "../utils/logger";
|
||||
|
||||
export class ApiError extends Error {
|
||||
@ -41,7 +39,7 @@ class HttpClient {
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: "GET" | "POST" | "PATCH" | "DELETE",
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
||||
rootUrl: string = this.baseUrl,
|
||||
path: string,
|
||||
opts: RequestOpts = {},
|
||||
@ -81,7 +79,18 @@ class HttpClient {
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const accessToken = await getServerAccessToken();
|
||||
let accessToken: string | undefined;
|
||||
if (typeof window === "undefined") {
|
||||
const { getServerAccessToken } = await import("../auth/token");
|
||||
accessToken = await getServerAccessToken();
|
||||
} else {
|
||||
try {
|
||||
const { getClientAccessToken } = await import("../auth/token");
|
||||
accessToken = getClientAccessToken();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
@ -127,7 +136,13 @@ class HttpClient {
|
||||
}
|
||||
|
||||
if (res.status === 401 && !skipAuthRedirect) {
|
||||
redirect(process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "");
|
||||
if (typeof window === "undefined") {
|
||||
const { redirect } = await import("next/navigation");
|
||||
redirect(process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "");
|
||||
} else {
|
||||
const url = process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "/";
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
throw new ApiError(res.status, payload, errorMessage);
|
||||
}
|
||||
@ -180,6 +195,8 @@ class HttpClient {
|
||||
this.request<T>("GET", u, p, o);
|
||||
post = <T>(p: string, b: unknown, o?: RequestOpts, u?: string) =>
|
||||
this.request<T>("POST", u, p, o, b);
|
||||
put = <T>(p: string, b: unknown, o?: RequestOpts, u?: string) =>
|
||||
this.request<T>("PUT", u, p, o, b);
|
||||
patch = <T>(p: string, b: unknown, o?: RequestOpts, u?: string) =>
|
||||
this.request<T>("PATCH", u, p, o, b);
|
||||
delete = <T>(p: string, o?: RequestOpts, u?: string) =>
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
// Server-side token functions (only for Server Components)
|
||||
export async function getServerAccessToken() {
|
||||
const { cookies } = await import("next/headers");
|
||||
return (await cookies()).get("accessToken")?.value;
|
||||
}
|
||||
|
||||
// Client-side token functions
|
||||
export function getClientAccessToken(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
const cookies = document.cookie.split(';');
|
||||
const accessTokenCookie = cookies.find(cookie =>
|
||||
cookie.trim().startsWith('accessToken=')
|
||||
);
|
||||
|
||||
return accessTokenCookie?.split('=')[1];
|
||||
}
|
||||
|
||||
@ -41,4 +41,7 @@ export const API_ROUTES = {
|
||||
getChatMessages: (chatId: string) =>
|
||||
createRoute(["chats", chatId, "messages"]),
|
||||
getUserBalance: () => createRoute(["chats", "balance"]),
|
||||
getMyChatSettings: () => createRoute(["chats", "profile", "chat-settings"]),
|
||||
updateMyChatSettings: () => createRoute(["chats", "profile", "chat-settings"]),
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user