diff --git a/src/components/domains/chat/CategoryChats/CategoryChats.tsx b/src/components/domains/chat/CategoryChats/CategoryChats.tsx index 0c5c30c..0c4b3c0 100644 --- a/src/components/domains/chat/CategoryChats/CategoryChats.tsx +++ b/src/components/domains/chat/CategoryChats/CategoryChats.tsx @@ -13,11 +13,13 @@ import styles from "./CategoryChats.module.scss"; interface CategoryChatsProps { chats: IChat[]; maxVisibleChats?: number; + currentBalance?: number; } export default function CategoryChats({ chats, maxVisibleChats = 3, + currentBalance = 0, }: CategoryChatsProps) { const router = useRouter(); const setCurrentChat = useChatStore(state => state.setCurrentChat); @@ -40,7 +42,9 @@ export default function CategoryChats({ message: { type: chat.lastMessage.type, content: chat.lastMessage.text, + sentWithZeroBalance: chat.lastMessage.sentWithZeroBalance, }, + currentBalance: currentBalance, } : null } diff --git a/src/components/domains/chat/ChatCategories/ChatCategories.tsx b/src/components/domains/chat/ChatCategories/ChatCategories.tsx index 8dadc63..08e5d3b 100644 --- a/src/components/domains/chat/ChatCategories/ChatCategories.tsx +++ b/src/components/domains/chat/ChatCategories/ChatCategories.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Skeleton } from "@/components/ui"; import { Chips } from "@/components/widgets"; +import { useBalance } from "@/hooks/balance/useBalance"; import { useChats } from "@/providers/chats-provider"; import { CategoryChats, ChatItemsList } from ".."; @@ -12,6 +13,7 @@ const MAX_HIDE_VISIBLE_COUNT = 3; export default function ChatCategories() { const { categorizedChats } = useChats(); + const { balance } = useBalance(); const [activeChip, setActiveChip] = useState("All"); const [maxVisibleChats, setMaxVisibleChats] = useState< @@ -59,6 +61,7 @@ export default function ChatCategories() { ))} diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.tsx b/src/components/domains/chat/ChatMessage/ChatMessage.tsx index c6b0b51..c6f1376 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.tsx +++ b/src/components/domains/chat/ChatMessage/ChatMessage.tsx @@ -3,7 +3,8 @@ import { useEffect } from "react"; import clsx from "clsx"; -import { IChatMessage } from "@/entities/chats/types"; +import type { IChatMessage } from "@/entities/chats/types"; +import { useShowRefillModal } from "@/hooks/refill/useShowRefillModal"; import { useChat } from "@/providers/chat-provider"; import { formatTime } from "@/shared/utils/date"; @@ -31,9 +32,22 @@ export interface ChatMessageProps { } export default function ChatMessage({ message }: ChatMessageProps) { - const { isConnected, read } = useChat(); + const { isConnected, read, balance } = useChat(); + const { showRefillModal } = useShowRefillModal(); const isOwn = message.role === "user"; + // Применяем блюр к ОТВЕТАМ АССИСТЕНТА если они были отправлены при нулевом балансе И текущий баланс все еще нулевой + const shouldBlur = + !isOwn && // Блюрим только сообщения ассистента (НЕ пользователя) + message.sentWithZeroBalance === true && + (balance?.balance ?? 0) <= 0; + + const handleBlurClick = () => { + if (message.chatId) { + showRefillModal(message.chatId); + } + }; + useEffect(() => { if ( !!message.id && @@ -50,7 +64,12 @@ export default function ChatMessage({ message }: ChatMessageProps) {
{message.type === "text" && message.id !== "typing" && ( - + )} {message.id === "typing" && } diff --git a/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss b/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss index 92c4a0f..0eb7a6b 100644 --- a/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss +++ b/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss @@ -6,4 +6,17 @@ &.own { color: #ffffff; } + + &.blurred { + filter: blur(4px); + opacity: 0.6; + transition: + filter 0.3s ease, + opacity 0.3s ease; + pointer-events: none; + user-select: none; // Запрещаем выделение текста + -webkit-user-select: none; // Safari + -moz-user-select: none; // Firefox + -ms-user-select: none; // IE/Edge + } } diff --git a/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx b/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx index e917056..e6c7bd6 100644 --- a/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx +++ b/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx @@ -8,24 +8,40 @@ interface MessageTextProps { text?: string; isOwn: boolean; className?: string; + isBlurred?: boolean; + onBlurClick?: () => void; } export default function MessageText({ text, isOwn, className, + isBlurred = false, + onBlurClick, }: MessageTextProps) { + const handleClick = () => { + if (isBlurred && onBlurClick) { + onBlurClick(); + } + }; + return ( - - {text} - + + {text} + +
); } diff --git a/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx index 0d2852e..f281da2 100644 --- a/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx +++ b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx @@ -6,38 +6,65 @@ import { useTranslations } from "next-intl"; import { ModalSheet } from "@/components/ui"; import { useChat } from "@/providers/chat-provider"; import { useToast } from "@/providers/toast-provider"; +import { useSocketEmit } from "@/services/socket"; import { RefillOptionsModal, RefillTimerModal } from ".."; export default function ChatModalsWrapper() { const t = useTranslations("Chat"); - const { refillModals } = useChat(); + const { refillModals, clearRefillModals } = useChat(); const { addToast } = useToast(); + const emit = useSocketEmit(); const [isModalOpen, setIsModalOpen] = useState(false); const [modalChild, setModalChild] = useState< "refill-timer" | "refill-options" | null >(null); + const [isClosing, setIsClosing] = useState(false); + // Теперь hasShownTimerModal приходит с сервера + const hasShownTimerModal = refillModals?.hasShownTimerModal ?? false; const handleModalClose = () => { + setIsClosing(true); setIsModalOpen(false); const timeout = setTimeout(() => { setModalChild(null); + setIsClosing(false); + clearRefillModals(); // Очищаем состояние refillModals }, 300); return () => clearTimeout(timeout); }; + const handleTimerModalClose = () => { + // При закрытии крестика в timer modal - переходим к options modal + setModalChild("refill-options"); + }; + useEffect(() => { - if (!!refillModals?.oneClick) { + // Не обрабатываем новые события если модальное окно в процессе закрытия + if (isClosing) return; + + if (!!refillModals?.oneClick && !hasShownTimerModal) { setIsModalOpen(true); - return setModalChild("refill-timer"); + setModalChild("refill-timer"); + // Подтверждаем серверу что показали timer modal + emit("confirm_timer_modal_shown"); + return; } if (!!refillModals?.products) { setIsModalOpen(true); return setModalChild("refill-options"); } - }, [refillModals]); + // Если получили событие повторно и timer modal уже показывался, + // сразу показываем options modal + if (!!refillModals?.oneClick && hasShownTimerModal) { + if (!isModalOpen) { + setIsModalOpen(true); + return setModalChild("refill-options"); + } + } + }, [refillModals, hasShownTimerModal, isModalOpen, emit, isClosing]); const handlePaymentSuccess = () => { handleModalClose(); @@ -54,8 +81,11 @@ export default function ChatModalsWrapper() { return ( {modalChild === "refill-timer" && !!refillModals?.oneClick && ( { interface CorrespondenceStartedProps { chats: IChat[]; - isVisibleAll?: boolean; + isVisibleAll: boolean; maxHideVisibleCount?: number; + currentBalance?: number; } export default function CorrespondenceStarted({ chats, isVisibleAll = false, maxHideVisibleCount = 3, + currentBalance = 0, }: CorrespondenceStartedProps) { const router = useRouter(); const setCurrentChat = useChatStore(state => state.setCurrentChat); @@ -61,7 +63,9 @@ export default function CorrespondenceStarted({ message: { type: chat.lastMessage.type, content: chat.lastMessage.text, + sentWithZeroBalance: chat.lastMessage.sentWithZeroBalance, }, + currentBalance: currentBalance, } : null } diff --git a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx index b488bab..066f193 100644 --- a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx +++ b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx @@ -3,6 +3,7 @@ import { useTranslations } from "next-intl"; import { Skeleton } from "@/components/ui"; +import { useBalance } from "@/hooks/balance/useBalance"; import { useAppUiStore } from "@/providers/app-ui-store-provider"; import { useChats } from "@/providers/chats-provider"; @@ -11,6 +12,7 @@ import { ChatItemsList, CorrespondenceStarted } from ".."; export default function CorrespondenceStartedWrapper() { const t = useTranslations("Chat"); const { startedChats } = useChats(); + const { balance } = useBalance(); const { isVisibleAll } = useAppUiStore( state => state.chats.correspondenceStarted @@ -41,6 +43,7 @@ export default function CorrespondenceStartedWrapper() { )} diff --git a/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx b/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx index f4880ab..15b7e56 100644 --- a/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx +++ b/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx @@ -5,6 +5,7 @@ import { useLocale } from "next-intl"; import NewMessages from "@/components/domains/chat/NewMessages/NewMessages"; import ViewAll from "@/components/domains/chat/ViewAll/ViewAll"; +import { useBalance } from "@/hooks/balance/useBalance"; import { useAppUiStore } from "@/providers/app-ui-store-provider"; import { useChats } from "@/providers/chats-provider"; import { ROUTES } from "@/shared/constants/client-routes"; @@ -14,6 +15,7 @@ import styles from "./GlobalNewMessagesBanner.module.scss"; export default function GlobalNewMessagesBanner() { const { unreadChats } = useChats(); + const { balance } = useBalance(); // Exclude banner on chat-related, settings (profile), and retention funnel pages const pathname = usePathname(); @@ -39,7 +41,11 @@ export default function GlobalNewMessagesBanner() { onClick={() => setHomeNewMessages({ isVisibleAll: !isVisibleAll })} /> )} - + ); } diff --git a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss index 42ddb38..0edcc00 100644 --- a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss +++ b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss @@ -17,4 +17,16 @@ -webkit-box-orient: vertical; white-space: normal; word-break: break-word; + + &.blurred { + filter: blur(4px); + opacity: 0.6; + transition: + filter 0.3s ease, + opacity 0.3s ease; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } } diff --git a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx index d3fd91c..bea56b8 100644 --- a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx +++ b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx @@ -8,18 +8,25 @@ export interface LastMessagePreviewProps { message: { type: "text" | "voice" | "image"; content?: string; + sentWithZeroBalance?: boolean; }; isTyping?: boolean; isRead?: boolean; + currentBalance?: number; } export default function LastMessagePreview({ message, isTyping, isRead, + currentBalance = 0, }: LastMessagePreviewProps) { const t = useTranslations("Chat"); + // Определяем нужно ли блюрить сообщение + const shouldBlur = + message.sentWithZeroBalance === true && currentBalance <= 0; + const getMessageIcon = () => { switch (message.type) { case "voice": @@ -72,7 +79,7 @@ export default function LastMessagePreview({ size="sm" color="secondary" align="left" - className={styles.text} + className={`${styles.text} ${shouldBlur ? styles.blurred : ""}`} > {getMessageText()} diff --git a/src/components/domains/chat/MessageInput/MessageInput.module.scss b/src/components/domains/chat/MessageInput/MessageInput.module.scss index a703a61..5cfbb84 100644 --- a/src/components/domains/chat/MessageInput/MessageInput.module.scss +++ b/src/components/domains/chat/MessageInput/MessageInput.module.scss @@ -20,3 +20,18 @@ align-items: center; justify-content: center; } + +.sendButtonWrapper { + position: relative; + width: 40px; + height: 40px; +} + +.disabledOverlay { + position: absolute; + inset: 0; + border-radius: 50%; + z-index: 1; + background: transparent; + cursor: pointer; +} diff --git a/src/components/domains/chat/MessageInput/MessageInput.tsx b/src/components/domains/chat/MessageInput/MessageInput.tsx index eac5336..621742f 100644 --- a/src/components/domains/chat/MessageInput/MessageInput.tsx +++ b/src/components/domains/chat/MessageInput/MessageInput.tsx @@ -9,14 +9,20 @@ import styles from "./MessageInput.module.scss"; interface MessageInputProps { onSend: (message: string) => void; + disabled?: boolean; + onDisabledClick?: () => void; } -export default function MessageInput({ onSend }: MessageInputProps) { +export default function MessageInput({ + onSend, + disabled = false, + onDisabledClick, +}: MessageInputProps) { const t = useTranslations("Chat"); const [message, setMessage] = useState(""); const handleSend = () => { - if (message.trim()) { + if (message.trim() && !disabled) { onSend(message.trim()); setMessage(""); } @@ -24,6 +30,12 @@ export default function MessageInput({ onSend }: MessageInputProps) { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { + // If disabled (e.g., zero balance), open refill modal instead of sending + if (!e.ctrlKey && disabled) { + e.preventDefault(); + onDisabledClick?.(); + return; + } if (e.ctrlKey) { // Ctrl + Enter - добавляем перенос строки e.preventDefault(); @@ -55,14 +67,24 @@ export default function MessageInput({ onSend }: MessageInputProps) { onKeyDown={handleKeyDown} maxRows={5} /> - +
+ {/* Capture clicks when disabled to open refill modal */} + {disabled && typeof onDisabledClick === "function" && ( +
+ )} + +
); } diff --git a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx index 20e32a1..5759bc6 100644 --- a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx +++ b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx @@ -1,5 +1,6 @@ "use client"; +import { useShowRefillModal } from "@/hooks/refill/useShowRefillModal"; import { useChat } from "@/providers/chat-provider"; import { MessageInput } from ".."; @@ -7,12 +8,26 @@ import { MessageInput } from ".."; import styles from "./MessageInputWrapper.module.scss"; export default function MessageInputWrapper() { - const { send } = useChat(); + const { send, balance, chatId } = useChat(); + const { showRefillModal } = useShowRefillModal(); + + // Блокируем отправку сообщений при нулевом или отрицательном балансе + const disabled = (balance?.balance ?? 0) <= 0; + + const handleDisabledClick = () => { + if (disabled && chatId) { + showRefillModal(chatId); + } + }; return (
- +
); diff --git a/src/components/domains/chat/NewMessages/NewMessages.tsx b/src/components/domains/chat/NewMessages/NewMessages.tsx index 7fefd64..4aa4e19 100644 --- a/src/components/domains/chat/NewMessages/NewMessages.tsx +++ b/src/components/domains/chat/NewMessages/NewMessages.tsx @@ -21,12 +21,14 @@ interface NewMessagesProps { chats: IChat[]; isVisibleAll: boolean; maxHideVisibleCount?: number; + currentBalance?: number; } export default function NewMessages({ chats, isVisibleAll = false, maxHideVisibleCount = 3, + currentBalance = 0, }: NewMessagesProps) { const router = useRouter(); const setCurrentChat = useChatStore(state => state.setCurrentChat); @@ -60,7 +62,9 @@ export default function NewMessages({ message: { type: chat.lastMessage.type, content: chat.lastMessage.text, + sentWithZeroBalance: chat.lastMessage.sentWithZeroBalance, }, + currentBalance: currentBalance, } : null } diff --git a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx index 282a007..fbe2899 100644 --- a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx +++ b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx @@ -3,6 +3,7 @@ import { useTranslations } from "next-intl"; import { Skeleton } from "@/components/ui"; +import { useBalance } from "@/hooks/balance/useBalance"; import { useAppUiStore } from "@/providers/app-ui-store-provider"; import { useChats } from "@/providers/chats-provider"; @@ -11,6 +12,7 @@ import { ChatItemsList, NewMessages } from ".."; export default function NewMessagesWrapper() { const t = useTranslations("Chat"); const { unreadChats } = useChats(); + const { balance } = useBalance(); const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages); const hasHydrated = useAppUiStore(state => state._hasHydrated); @@ -32,7 +34,11 @@ export default function NewMessagesWrapper() { }} isVisibleViewAll={unreadChats.length > 1} > - + )} diff --git a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx index 3b8d670..2728fa6 100644 --- a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx +++ b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx @@ -2,6 +2,7 @@ import { NewMessages, ViewAll } from "@/components/domains/chat"; import { Skeleton } from "@/components/ui"; +import { useBalance } from "@/hooks/balance/useBalance"; import { useAppUiStore } from "@/providers/app-ui-store-provider"; import { useChats } from "@/providers/chats-provider"; @@ -9,6 +10,7 @@ import styles from "./NewMessagesSection.module.scss"; export default function NewMessagesSection() { const { unreadChats } = useChats(); + const { balance } = useBalance(); const { isVisibleAll } = useAppUiStore(state => state.home.newMessages); const hasHydrated = useAppUiStore(state => state._hasHydrated); @@ -29,7 +31,11 @@ export default function NewMessagesSection() { }} /> )} - + )} diff --git a/src/components/ui/ModalSheet/ModalSheet.tsx b/src/components/ui/ModalSheet/ModalSheet.tsx index 7d0664e..f1484d1 100644 --- a/src/components/ui/ModalSheet/ModalSheet.tsx +++ b/src/components/ui/ModalSheet/ModalSheet.tsx @@ -12,6 +12,7 @@ import styles from "./ModalSheet.module.scss"; interface ModalSheetProps extends Omit { showCloseButton?: boolean; variant?: "white" | "gray"; + disableBackdropClose?: boolean; } export default function ModalSheet({ @@ -22,6 +23,7 @@ export default function ModalSheet({ modalClassName, showCloseButton = true, variant = "white", + disableBackdropClose = false, ref, }: ModalSheetProps) { const [isOpen, setIsOpen] = useState(open); @@ -37,7 +39,13 @@ export default function ModalSheet({ return ( { + /* Backdrop close disabled */ + } + : onClose + } className={clsx(className, styles.overlay, { [styles.closed]: !open && isOpen, })} diff --git a/src/entities/chats/types.ts b/src/entities/chats/types.ts index 323111f..af10dda 100644 --- a/src/entities/chats/types.ts +++ b/src/entities/chats/types.ts @@ -11,6 +11,7 @@ const ChatMessageSchema = z.object({ text: z.string().optional(), isLast: z.boolean().optional(), suggestions: z.array(z.string()).optional(), + sentWithZeroBalance: z.boolean().optional(), }); const ChatSchema = z.object({ diff --git a/src/hooks/balance/useBalance.ts b/src/hooks/balance/useBalance.ts new file mode 100644 index 0000000..59f6d1a --- /dev/null +++ b/src/hooks/balance/useBalance.ts @@ -0,0 +1,27 @@ +"use client"; + +import { useState } from "react"; + +import { useSocketEvent } from "@/hooks/socket/useSocketEvent"; + +import { useUserBalance } from "./useUserBalance"; + +export const useBalance = () => { + const [socketBalance, setSocketBalance] = useState(null); + + // Получаем начальный баланс через API + const { balance: apiBalance, isLoading } = useUserBalance(); + + // Слушаем обновления баланса через WebSocket + useSocketEvent("balance_updated", b => { + setSocketBalance(b.data.balance); + }); + + // Приоритет: WebSocket > API > 0 + const currentBalance = socketBalance ?? apiBalance ?? 0; + + return { + balance: currentBalance, + isLoading, + }; +}; diff --git a/src/hooks/chats/useChatSocket.ts b/src/hooks/chats/useChatSocket.ts index 394ff6a..29efd5d 100644 --- a/src/hooks/chats/useChatSocket.ts +++ b/src/hooks/chats/useChatSocket.ts @@ -113,6 +113,10 @@ export const useChatSocket = ( emit("fetch_balance", { chatId }); }, [emit, chatId]); + const clearRefillModals = useCallback(() => { + setRefillModals(null); + }, []); + // Auto top-up: silent flow (no UI prompt) const autoTopUpInProgressRef = useRef(false); // Timer for delayed unlock after error @@ -341,6 +345,7 @@ export const useChatSocket = ( return useMemo( () => ({ + chatId, messages, balance, session, @@ -352,6 +357,7 @@ export const useChatSocket = ( read, startSession, endSession, + clearRefillModals, isLoadingSelfMessage, isLoadingAdvisorMessage, @@ -363,6 +369,7 @@ export const useChatSocket = ( hasMoreOlderMessages, }), [ + chatId, messages, balance, session, @@ -380,6 +387,7 @@ export const useChatSocket = ( read, startSession, endSession, + clearRefillModals, ] ); }; diff --git a/src/hooks/refill/useShowRefillModal.ts b/src/hooks/refill/useShowRefillModal.ts new file mode 100644 index 0000000..3df5275 --- /dev/null +++ b/src/hooks/refill/useShowRefillModal.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useSocketEmit } from "@/services/socket"; + +export const useShowRefillModal = () => { + const emit = useSocketEmit(); + + const showRefillModal = (chatId: string) => { + // Запрашиваем у сервера показать модальное окно пополнения + emit("request_refill_modal", { chatId }); + }; + + return { showRefillModal }; +}; diff --git a/src/services/socket/events.ts b/src/services/socket/events.ts index c9cfb11..5b0111e 100644 --- a/src/services/socket/events.ts +++ b/src/services/socket/events.ts @@ -42,6 +42,7 @@ export interface IRefillModals { }; }; products?: IRefillModalsProduct[]; + hasShownTimerModal?: boolean; } export interface IAutoTopUpRequest { @@ -63,6 +64,8 @@ export interface ClientToServerEvents { end_session: (data: { chatId: string }) => void; fetch_balance: (data: { chatId: string }) => void; deposit: (data: { amount: number }) => void; + confirm_timer_modal_shown: () => void; + request_refill_modal: (data: { chatId: string }) => void; } export interface ServerToClientEventsBaseData {