diff --git a/src/app/index.tsx b/src/app/index.tsx index 6c66e95..4522658 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,27 +1,29 @@ -import "~/app/styles/globals.css"; +import '~/app/styles/globals.css'; import '~/shared/libs/buffer'; -import { useEffect } from "react"; -import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app"; +import { useEffect } from 'react'; +import { useExpand, useWebApp } from '@vkruglikov/react-telegram-web-app'; -import { Providers } from "~/app/providers"; -import { AppRouter } from "~/app/router"; +import { Providers } from '~/app/providers'; +import { AppRouter } from '~/app/router'; +import { Notification } from '~/shared/ui/notification'; export const App = () => { - const WebApp = useWebApp(); - const [, expand] = useExpand(); + const WebApp = useWebApp(); + const [, expand] = useExpand(); - useEffect(() => { - WebApp.enableClosingConfirmation(); - expand(); + useEffect(() => { + WebApp.enableClosingConfirmation(); + expand(); - WebApp.setHeaderColor("#1d1d1b"); - WebApp.setBackgroundColor("#1d1d1b"); - }, []); + WebApp.setHeaderColor('#1d1d1b'); + WebApp.setBackgroundColor('#1d1d1b'); + }, []); - return ( - - - - ); + return ( + + + + + ); }; diff --git a/src/pages/view-content/index.tsx b/src/pages/view-content/index.tsx index 0163501..e1a42ec 100644 --- a/src/pages/view-content/index.tsx +++ b/src/pages/view-content/index.tsx @@ -36,42 +36,47 @@ export const ViewContentPage = () => { const handleBuyContentTON = useCallback(async () => { try { - // Helper function to wait for wallet connection - const waitForConnection = async (timeoutMs = 30000, intervalMs = 500) => { - const startTime = Date.now(); - - while (Date.now() - startTime < timeoutMs) { - if (tonConnectUI.connected) { - return true; - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - - return false; // Timed out - }; - - // If not connected, start connection process + // Если не подключен, начинаем процесс подключения через auth if (!tonConnectUI.connected) { - console.log('DEBUG: Wallet not connected, opening modal'); + console.log('DEBUG: Wallet not connected, using auth flow first'); - // Open connection modal - await tonConnectUI.openModal(); + // Вызываем auth.mutateAsync() до открытия модального окна + // Это настроит параметры подключения с TonProof + try { + await auth.mutateAsync(); - // Wait for connection - const connected = await waitForConnection(); - if (!connected) { - console.log('DEBUG: Connection timed out or was cancelled'); + // Проверяем, установилось ли подключение после auth + if (!tonConnectUI.connected) { + console.log('DEBUG: Auth did not establish connection, returning'); + return; + } + + console.log('DEBUG: Connection and authentication successful'); + } catch (error) { + console.log('Ошибка подключения кошелька', 'danger'); + console.error('DEBUG: Auth failed during connection:', error); return; } - - console.log('DEBUG: Connection successful, authenticating'); - await auth.mutateAsync(); } else { - // Already connected, just authenticate + // Если уже подключены, просто аутентифицируемся + console.log('DEBUG: Already connected, authenticating'); await auth.mutateAsync(); } - // Proceed with purchase + // Проверка наличия TonProof + if ( + tonConnectUI.wallet?.connectItems?.tonProof && + !('error' in tonConnectUI.wallet.connectItems.tonProof) + ) { + console.log( + 'DEBUG: TonProof available:', + tonConnectUI.wallet.connectItems.tonProof + ); + } else { + console.warn('DEBUG: TonProof not available after connection'); + } + + // Теперь продолжаем с покупкой console.log('DEBUG: Proceeding with purchase'); const contentResponse = await purchaseContent({ content_address: WebApp.initDataUnsafe?.start_param, diff --git a/src/shared/services/auth/index.ts b/src/shared/services/auth/index.ts index aed764d..6efbc07 100644 --- a/src/shared/services/auth/index.ts +++ b/src/shared/services/auth/index.ts @@ -129,6 +129,26 @@ export const useAuth = () => { return true; }; + // Helper для ожидания подключения с таймаутом + const waitForConnection = async (timeoutMs = 30000, checkIntervalMs = 500) => { + console.log('DEBUG: Waiting for wallet connection'); + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + if (tonConnectUI.connected) { + console.log('DEBUG: Connection detected'); + // Даем дополнительное время для инициализации connectItems + await new Promise((resolve) => setTimeout(resolve, 1000)); + return true; + } + // Проверяем статус каждые checkIntervalMs + await new Promise((resolve) => setTimeout(resolve, checkIntervalMs)); + } + + console.log('DEBUG: Connection wait timed out'); + return false; + }; + return useMutation(['auth'], async () => { clearInterval(interval.current); let authResult; @@ -150,13 +170,34 @@ export const useAuth = () => { // Start periodic refresh of the payload interval.current = setInterval(prepareConnectParams, payloadTTLMS); - // Open the modal - this will not resolve until connection or cancellation - await tonConnectUI.openModal(); + // Open the modal and wait for connection + try { + console.log('DEBUG: Opening wallet connect modal'); + tonConnectUI.openModal(); - // Check if connection was successful - if (!tonConnectUI.connected) { - console.log('DEBUG: Connection cancelled or failed'); - throw new Error('Wallet connection cancelled or failed'); + // Ждем подключения кошелька + const connected = await waitForConnection(); + + if (!connected || !tonConnectUI.connected) { + console.log('DEBUG: Connection cancelled or failed after waiting'); + throw new Error('Wallet connection cancelled or failed'); + } + + // Даем дополнительное время для полной инициализации wallet + if (!tonConnectUI.wallet || !tonConnectUI.wallet.connectItems) { + console.log('DEBUG: Wallet object not fully initialized, waiting...'); + await new Promise((resolve) => setTimeout(resolve, 1500)); + } + + console.log('DEBUG: Connection successful, wallet details:', { + connected: tonConnectUI.connected, + hasWallet: !!tonConnectUI.wallet, + hasConnectItems: !!tonConnectUI.wallet?.connectItems, + hasTonProof: !!tonConnectUI.wallet?.connectItems?.tonProof, + }); + } catch (error) { + console.error('DEBUG: Error during wallet connection:', error); + throw new Error('Wallet connection process failed'); } // Check if we have a proof after connection diff --git a/src/shared/stores/root/index.ts b/src/shared/stores/root/index.ts index 6baf63e..18b8486 100644 --- a/src/shared/stores/root/index.ts +++ b/src/shared/stores/root/index.ts @@ -1,100 +1,135 @@ -import { create } from "zustand"; +import { create } from 'zustand'; export type Royalty = { - address: string; - value: number; + address: string; + value: number; }; +type NotificationType = 'success' | 'danger' | 'warning' | 'info'; + +interface Notification { + id: string; + message: string; + type: NotificationType; + createdAt: number; +} + type RootStore = { - name: string; - setName: (name: string) => void; + name: string; + setName: (name: string) => void; - author: string; - setAuthor: (author: string) => void; + author: string; + setAuthor: (author: string) => void; - file: File | null; - setFile: (file: File | null) => void; + file: File | null; + setFile: (file: File | null) => void; - fileType: string; - setFileType: (type: string) => void; + fileType: string; + setFileType: (type: string) => void; - fileSrc: string; - setFileSrc: (fileSrc: string) => void; + fileSrc: string; + setFileSrc: (fileSrc: string) => void; - allowCover: boolean; - setAllowCover: (allowCover: boolean) => void; + allowCover: boolean; + setAllowCover: (allowCover: boolean) => void; - allowDwnld: boolean; - setAllowDwnld: (allowDwnld: boolean) => void; + allowDwnld: boolean; + setAllowDwnld: (allowDwnld: boolean) => void; - cover: File | null; - setCover: (cover: File | null) => void; + cover: File | null; + setCover: (cover: File | null) => void; - isPercentHintOpen: boolean; - setPercentHintOpen: (isPercentHintOpen: boolean) => void; + isPercentHintOpen: boolean; + setPercentHintOpen: (isPercentHintOpen: boolean) => void; - authors: string[]; - setAuthors: (authors: string[]) => void; + authors: string[]; + setAuthors: (authors: string[]) => void; - royalty: Royalty[]; - setRoyalty: (authors: Royalty[]) => void; + royalty: Royalty[]; + setRoyalty: (authors: Royalty[]) => void; - price: number; - setPrice: (price: number) => void; + price: number; + setPrice: (price: number) => void; - allowResale: boolean; - setAllowResale: (allowResale: boolean) => void; + allowResale: boolean; + setAllowResale: (allowResale: boolean) => void; - licenseResalePrice: number; - setLicenseResalePrice: (licenseResalePrice: number) => void; + licenseResalePrice: number; + setLicenseResalePrice: (licenseResalePrice: number) => void; - hashtags: string[]; - setHashtags: (hashtags: string[]) => void; + hashtags: string[]; + setHashtags: (hashtags: string[]) => void; + + notifications: Notification[]; + addNotification: (message: string, type: NotificationType) => void; + removeNotification: (id: string) => void; }; export const useRootStore = create((set) => ({ - name: "", - setName: (name) => set({ name }), + name: '', + setName: (name) => set({ name }), - author: "", - setAuthor: (author) => set({ author }), + author: '', + setAuthor: (author) => set({ author }), - file: null, - setFile: (file) => set({ file }), + file: null, + setFile: (file) => set({ file }), - fileType: "", - setFileType: (fileType) => set({ fileType }), + fileType: '', + setFileType: (fileType) => set({ fileType }), - fileSrc: "", - setFileSrc: (fileSrc) => set({ fileSrc }), + fileSrc: '', + setFileSrc: (fileSrc) => set({ fileSrc }), - allowCover: false, - setAllowCover: (allowCover) => set({ allowCover }), + allowCover: false, + setAllowCover: (allowCover) => set({ allowCover }), - allowDwnld: false, - setAllowDwnld: (allowDwnld) => set({ allowDwnld }), - - cover: null, - setCover: (cover) => set({ cover }), + allowDwnld: false, + setAllowDwnld: (allowDwnld) => set({ allowDwnld }), - isPercentHintOpen: true, - setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }), + cover: null, + setCover: (cover) => set({ cover }), - authors: [], - setAuthors: (authors) => set({ authors }), + isPercentHintOpen: true, + setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }), - royalty: [], - setRoyalty: (royalty) => set({ royalty }), + authors: [], + setAuthors: (authors) => set({ authors }), - price: 0.15, - setPrice: (price: number) => set({ price }), + royalty: [], + setRoyalty: (royalty) => set({ royalty }), - allowResale: false, - setAllowResale: (allowResale) => set({ allowResale }), + price: 0.15, + setPrice: (price: number) => set({ price }), - licenseResalePrice: 0, - setLicenseResalePrice: (licenseResalePrice) => set({ licenseResalePrice }), + allowResale: false, + setAllowResale: (allowResale) => set({ allowResale }), - hashtags: [], - setHashtags: (hashtags: string[]) => set({ hashtags }), + licenseResalePrice: 0, + setLicenseResalePrice: (licenseResalePrice) => set({ licenseResalePrice }), + + hashtags: [], + setHashtags: (hashtags: string[]) => set({ hashtags }), + + notifications: [], + + addNotification: (message: string, type: NotificationType = 'info') => { + const id = Date.now().toString(); + const notification = { + id, + message, + type, + createdAt: Date.now(), + }; + + set((state) => ({ + notifications: [...state.notifications, notification], + })); + }, + + removeNotification: (id: string) => { + set((state) => ({ + notifications: state.notifications.filter((notification) => notification.id !== id), + })); + }, })); diff --git a/src/shared/ui/notification/index.tsx b/src/shared/ui/notification/index.tsx new file mode 100644 index 0000000..036ae17 --- /dev/null +++ b/src/shared/ui/notification/index.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import { useRootStore } from '~/shared/stores/root'; + +interface AnimatedNotification { + id: string; + message: string; + type: 'success' | 'danger' | 'warning' | 'info'; + isExiting: boolean; +} + +export const Notification = () => { + const { notifications, removeNotification } = useRootStore(); + const [animatedNotifications, setAnimatedNotifications] = useState([]); + + useEffect(() => { + const currentIds = animatedNotifications.map((n) => n.id); + const newNotifications = notifications + .filter((n) => !currentIds.includes(n.id)) + .map((n) => ({ ...n, isExiting: false })); + + if (newNotifications.length > 0) { + newNotifications.forEach((notification) => { + setTimeout(() => { + handleRemove(notification.id); + }, 4000); + }); + + setAnimatedNotifications((prev) => [...prev, ...newNotifications]); + } + }, [notifications]); + + // Функция для начала анимации удаления + const handleRemove = (id: string) => { + setAnimatedNotifications((prev) => + prev.map((n) => (n.id === id ? { ...n, isExiting: true } : n)) + ); + setTimeout(() => { + setAnimatedNotifications((prev) => prev.filter((n) => n.id !== id)); + removeNotification(id); + }, 300); + }; + + return ( +
+ {animatedNotifications.map((notification) => ( +
handleRemove(notification.id)} + className={` + p-2 rounded shadow-md relative transition-all duration-300 max-w-sm + transform origin-top pointer-events-auto cursor-pointer + ${notification.isExiting ? 'animate-fade-out' : 'animate-slide-in-top'} + ${getNotificationClass(notification.type)} + `} + > +
{notification.message}
+
+ ))} +
+ ); +}; + +const getNotificationClass = (type: 'success' | 'danger' | 'warning' | 'info'): string => { + const baseStyle = 'border border-white bg-opacity-70 backdrop-blur-sm'; + + switch (type) { + case 'success': + return `${baseStyle} bg-primary text-white`; + case 'danger': + return `${baseStyle} bg-primary text-white`; + case 'warning': + return `${baseStyle} bg-primary text-white`; + case 'info': + default: + return `${baseStyle} bg-primary text-white`; + } +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 5d480d9..6ed158b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,13 +1,33 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - extend: { - colors: { - gray: "#1d1d1b", - primary: "#e40615", - }, + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + gray: '#1d1d1b', + primary: '#e40615', + }, + keyframes: { + 'slide-in-top': { + '0%': { + transform: 'translateY(-1rem)', + opacity: '0', + }, + '100%': { + transform: 'translateY(0)', + opacity: '1', + }, + }, + 'fade-out': { + '0%': { opacity: '1' }, + '100%': { opacity: '0' }, + }, + }, + animation: { + 'slide-in-top': 'slide-in-top 0.3s ease-out forwards', + 'fade-out': 'fade-out 0.3s ease-out forwards', + }, + }, }, - }, - plugins: [], + plugins: [], };