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 { 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 (
<Providers>
<AppRouter />
</Providers>
);
return (
<Providers>
<AppRouter />
<Notification />
</Providers>
);
};

View File

@ -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,

View File

@ -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

View File

@ -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<RootStore>((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),
}));
},
}));

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} */
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: [],
};