tonproof fix, notification

This commit is contained in:
Verticool 2025-03-28 21:21:38 +06:00
parent 1b96d049c7
commit 91d9c916e2
6 changed files with 304 additions and 124 deletions

View File

@ -1,27 +1,29 @@
import "~/app/styles/globals.css"; import '~/app/styles/globals.css';
import '~/shared/libs/buffer'; import '~/shared/libs/buffer';
import { useEffect } from "react"; import { useEffect } from 'react';
import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app"; import { useExpand, useWebApp } from '@vkruglikov/react-telegram-web-app';
import { Providers } from "~/app/providers"; import { Providers } from '~/app/providers';
import { AppRouter } from "~/app/router"; import { AppRouter } from '~/app/router';
import { Notification } from '~/shared/ui/notification';
export const App = () => { export const App = () => {
const WebApp = useWebApp(); const WebApp = useWebApp();
const [, expand] = useExpand(); const [, expand] = useExpand();
useEffect(() => { useEffect(() => {
WebApp.enableClosingConfirmation(); WebApp.enableClosingConfirmation();
expand(); expand();
WebApp.setHeaderColor("#1d1d1b"); WebApp.setHeaderColor('#1d1d1b');
WebApp.setBackgroundColor("#1d1d1b"); WebApp.setBackgroundColor('#1d1d1b');
}, []); }, []);
return ( return (
<Providers> <Providers>
<AppRouter /> <AppRouter />
</Providers> <Notification />
); </Providers>
);
}; };

View File

@ -36,42 +36,47 @@ export const ViewContentPage = () => {
const handleBuyContentTON = useCallback(async () => { const handleBuyContentTON = useCallback(async () => {
try { try {
// Helper function to wait for wallet connection // Если не подключен, начинаем процесс подключения через auth
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
if (!tonConnectUI.connected) { if (!tonConnectUI.connected) {
console.log('DEBUG: Wallet not connected, opening modal'); console.log('DEBUG: Wallet not connected, using auth flow first');
// Open connection modal // Вызываем auth.mutateAsync() до открытия модального окна
await tonConnectUI.openModal(); // Это настроит параметры подключения с TonProof
try {
await auth.mutateAsync();
// Wait for connection // Проверяем, установилось ли подключение после auth
const connected = await waitForConnection(); if (!tonConnectUI.connected) {
if (!connected) { console.log('DEBUG: Auth did not establish connection, returning');
console.log('DEBUG: Connection timed out or was cancelled'); return;
}
console.log('DEBUG: Connection and authentication successful');
} catch (error) {
console.log('Ошибка подключения кошелька', 'danger');
console.error('DEBUG: Auth failed during connection:', error);
return; return;
} }
console.log('DEBUG: Connection successful, authenticating');
await auth.mutateAsync();
} else { } else {
// Already connected, just authenticate // Если уже подключены, просто аутентифицируемся
console.log('DEBUG: Already connected, authenticating');
await auth.mutateAsync(); 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'); console.log('DEBUG: Proceeding with purchase');
const contentResponse = await purchaseContent({ const contentResponse = await purchaseContent({
content_address: WebApp.initDataUnsafe?.start_param, content_address: WebApp.initDataUnsafe?.start_param,

View File

@ -129,6 +129,26 @@ export const useAuth = () => {
return true; 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 () => { return useMutation(['auth'], async () => {
clearInterval(interval.current); clearInterval(interval.current);
let authResult; let authResult;
@ -150,13 +170,34 @@ export const useAuth = () => {
// Start periodic refresh of the payload // Start periodic refresh of the payload
interval.current = setInterval(prepareConnectParams, payloadTTLMS); interval.current = setInterval(prepareConnectParams, payloadTTLMS);
// Open the modal - this will not resolve until connection or cancellation // Open the modal and wait for connection
await tonConnectUI.openModal(); try {
console.log('DEBUG: Opening wallet connect modal');
tonConnectUI.openModal();
// Check if connection was successful // Ждем подключения кошелька
if (!tonConnectUI.connected) { const connected = await waitForConnection();
console.log('DEBUG: Connection cancelled or failed');
throw new Error('Wallet connection cancelled or failed'); 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 // Check if we have a proof after connection

View File

@ -1,100 +1,135 @@
import { create } from "zustand"; import { create } from 'zustand';
export type Royalty = { export type Royalty = {
address: string; address: string;
value: number; value: number;
}; };
type NotificationType = 'success' | 'danger' | 'warning' | 'info';
interface Notification {
id: string;
message: string;
type: NotificationType;
createdAt: number;
}
type RootStore = { type RootStore = {
name: string; name: string;
setName: (name: string) => void; setName: (name: string) => void;
author: string; author: string;
setAuthor: (author: string) => void; setAuthor: (author: string) => void;
file: File | null; file: File | null;
setFile: (file: File | null) => void; setFile: (file: File | null) => void;
fileType: string; fileType: string;
setFileType: (type: string) => void; setFileType: (type: string) => void;
fileSrc: string; fileSrc: string;
setFileSrc: (fileSrc: string) => void; setFileSrc: (fileSrc: string) => void;
allowCover: boolean; allowCover: boolean;
setAllowCover: (allowCover: boolean) => void; setAllowCover: (allowCover: boolean) => void;
allowDwnld: boolean; allowDwnld: boolean;
setAllowDwnld: (allowDwnld: boolean) => void; setAllowDwnld: (allowDwnld: boolean) => void;
cover: File | null; cover: File | null;
setCover: (cover: File | null) => void; setCover: (cover: File | null) => void;
isPercentHintOpen: boolean; isPercentHintOpen: boolean;
setPercentHintOpen: (isPercentHintOpen: boolean) => void; setPercentHintOpen: (isPercentHintOpen: boolean) => void;
authors: string[]; authors: string[];
setAuthors: (authors: string[]) => void; setAuthors: (authors: string[]) => void;
royalty: Royalty[]; royalty: Royalty[];
setRoyalty: (authors: Royalty[]) => void; setRoyalty: (authors: Royalty[]) => void;
price: number; price: number;
setPrice: (price: number) => void; setPrice: (price: number) => void;
allowResale: boolean; allowResale: boolean;
setAllowResale: (allowResale: boolean) => void; setAllowResale: (allowResale: boolean) => void;
licenseResalePrice: number; licenseResalePrice: number;
setLicenseResalePrice: (licenseResalePrice: number) => void; setLicenseResalePrice: (licenseResalePrice: number) => void;
hashtags: string[]; hashtags: string[];
setHashtags: (hashtags: string[]) => void; setHashtags: (hashtags: string[]) => void;
notifications: Notification[];
addNotification: (message: string, type: NotificationType) => void;
removeNotification: (id: string) => void;
}; };
export const useRootStore = create<RootStore>((set) => ({ export const useRootStore = create<RootStore>((set) => ({
name: "", name: '',
setName: (name) => set({ name }), setName: (name) => set({ name }),
author: "", author: '',
setAuthor: (author) => set({ author }), setAuthor: (author) => set({ author }),
file: null, file: null,
setFile: (file) => set({ file }), setFile: (file) => set({ file }),
fileType: "", fileType: '',
setFileType: (fileType) => set({ fileType }), setFileType: (fileType) => set({ fileType }),
fileSrc: "", fileSrc: '',
setFileSrc: (fileSrc) => set({ fileSrc }), setFileSrc: (fileSrc) => set({ fileSrc }),
allowCover: false, allowCover: false,
setAllowCover: (allowCover) => set({ allowCover }), setAllowCover: (allowCover) => set({ allowCover }),
allowDwnld: false, allowDwnld: false,
setAllowDwnld: (allowDwnld) => set({ allowDwnld }), setAllowDwnld: (allowDwnld) => set({ allowDwnld }),
cover: null,
setCover: (cover) => set({ cover }),
isPercentHintOpen: true, cover: null,
setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }), setCover: (cover) => set({ cover }),
authors: [], isPercentHintOpen: true,
setAuthors: (authors) => set({ authors }), setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }),
royalty: [], authors: [],
setRoyalty: (royalty) => set({ royalty }), setAuthors: (authors) => set({ authors }),
price: 0.15, royalty: [],
setPrice: (price: number) => set({ price }), setRoyalty: (royalty) => set({ royalty }),
allowResale: false, price: 0.15,
setAllowResale: (allowResale) => set({ allowResale }), setPrice: (price: number) => set({ price }),
licenseResalePrice: 0, allowResale: false,
setLicenseResalePrice: (licenseResalePrice) => set({ licenseResalePrice }), setAllowResale: (allowResale) => set({ allowResale }),
hashtags: [], licenseResalePrice: 0,
setHashtags: (hashtags: string[]) => set({ hashtags }), 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),
}));
},
})); }));

View File

@ -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<AnimatedNotification[]>([]);
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 (
<div className="fixed inset-x-0 top-2 z-50 flex flex-col items-center gap-2 pointer-events-none">
{animatedNotifications.map((notification) => (
<div
key={notification.id}
onClick={() => 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)}
`}
>
<div className="text-center">{notification.message}</div>
</div>
))}
</div>
);
};
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`;
}
};

View File

@ -1,13 +1,33 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {
colors: { colors: {
gray: "#1d1d1b", gray: '#1d1d1b',
primary: "#e40615", 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: [],
}; };