tonproof fix, notification
This commit is contained in:
parent
1b96d049c7
commit
91d9c916e2
|
|
@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
}));
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -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`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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: [],
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue