mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Fix/telegram login (#4)
* Add UserProvider to App component and enhance user data fetching logic - Integrated UserProvider into the App component to manage user state. - Updated UserProvider to fetch current user data on token availability and handle token expiration by clearing the token on fetch failure. - Improved user data fetching logic for better error handling and user experience. * Enhance Telegram connection handling and error management - Added logic to handle Telegram account mismatch, providing user feedback and resetting connection state. - Improved user verification during authentication to ensure the logged-in account matches the Telegram account. - Refactored connection status checks and error handling for better user experience. - Introduced a method to clear session and disconnect when account mismatch occurs. - Updated polling mechanism for authentication status to ensure timely updates. This update improves the robustness of the Telegram connection process and enhances user feedback during authentication failures. * ran prettier * Refactor socket and auth state management for user-specific handling - Updated socket state management to support multiple users by introducing user-specific selectors and actions. - Refactored connection status handling in the socket service to dispatch user-specific status updates. - Enhanced onboarding state management to track completion per user, allowing for more granular control. - Introduced new selectors for socket and auth states to improve readability and maintainability. - Updated relevant components to utilize the new user-specific state management. This update improves the application's ability to handle multiple users and enhances the overall user experience during onboarding and socket connections. * Enhance MTProto client session management and introduce user-specific session handling - Updated the MTProtoService to manage sessions per user by storing session data under a user-specific key. - Modified the initialize method to accept a userId parameter, allowing for user-specific session initialization and management. - Improved session loading and saving logic to ensure proper handling of user sessions. - Added a utility function to generate session keys based on userId for better maintainability. This update enhances the application's ability to handle multiple user sessions effectively. * Refactor Telegram state management for user-specific handling - Introduced user-specific state management in the Telegram store, allowing for better handling of multiple users. - Updated selectors to retrieve state based on the current user, improving data encapsulation and reducing global state dependencies. - Enhanced reducers and thunks to ensure actions are dispatched with user context, maintaining user-specific data integrity. - Modified persistence configuration to store Telegram state scoped by user, ensuring isolated state management. This update significantly improves the application's ability to manage multiple user sessions effectively and enhances overall user experience. * Enhance MTProtoService connection handling with user-specific initialization - Updated the checkConnection method to accept an optional userId parameter, allowing for user-specific connection initialization. - Modified the connection logic to ensure proper initialization only occurs when a userId is provided, improving session management. - Refactored related thunks to use a consistent naming convention for userId parameters, enhancing code clarity. This update improves the handling of user sessions in the MTProtoService, aligning with recent enhancements in user-specific state management. * Refactor Telegram connection handling for user-specific management - Updated the TelegramConnectionModal to utilize user-specific identifiers for connection status and authentication processes, enhancing session management. - Refactored related components and selectors to ensure userId is consistently passed and utilized, improving clarity and maintainability. - Enhanced the ConnectionsPanel and ConnectStep to check for saved sessions based on userId, allowing for better handling of multiple user connections. This update significantly improves the application's ability to manage user-specific Telegram connections and enhances the overall user experience. * Refactor Telegram connection logic to simplify session management - Removed unnecessary userId checks and localStorage interactions from ConnectionsPanel and ConnectStep components, streamlining the connection status determination. - Updated MTProtoService to manage session data through Redux instead of localStorage, enhancing consistency and maintainability. - Improved session loading and saving logic to focus solely on Redux state, ensuring better integration with user-specific session handling. This update enhances the clarity and efficiency of the Telegram connection management process, aligning with recent improvements in user-specific state management. * Update CLAUDE.md to clarify localStorage usage and emphasize Redux for state management - Revised documentation to discourage the use of localStorage and sessionStorage for app state, advocating for Redux and Redux Persist instead. - Added guidelines for removing existing localStorage usage and migrating relevant data to Redux. - Updated service layer documentation to reflect changes in session management, specifying that session data is now stored in Redux rather than localStorage. - Enhanced clarity on deep link handling and the prevention of infinite reload loops. This update aligns with recent improvements in user-specific state management and reinforces best practices for state handling. * Enhance onboarding process with error handling and API integration - Updated the Onboarding component to include an API call for marking onboarding as complete, with error handling to provide user feedback. - Refactored the GetStartedStep component to support asynchronous completion handling, including loading state and error messages. - Introduced a new onboardingComplete method in the userApi service to facilitate the onboarding completion process. This update improves the user experience during onboarding by ensuring proper error management and feedback. * Enhance TelegramConnectionModal with socket connection management and improved initialization flow - Integrated socket connection handling in the TelegramConnectionModal to ensure the socket is connected when the modal opens, enhancing real-time features. - Refactored the initialization logic to streamline the connection process, including user verification and error handling. - Introduced a reference to track the QR code flow state, preventing multiple initiations and improving user experience during authentication. This update improves the reliability of the Telegram connection process and enhances user feedback during the onboarding experience. * Refactor Telegram connection checks for improved logic consistency - Updated the ConnectionsPanel and ConnectStep components to refine the logic for determining if a Telegram connection is established, ensuring that both session string and authentication status are required for a valid connection. - Simplified className definitions in the UI components for better readability and maintainability. This update enhances the clarity of connection status checks and improves the overall user experience during the onboarding process. * Refactor Telegram authentication flow and improve error handling - Simplified the TelegramLoginButton component by removing complex authentication logic and replacing it with a direct link to the Telegram bot for login. - Enhanced the Login page to consume a login token from the URL, providing better error handling and user feedback during the authentication process. - Introduced a new API service for consuming login tokens, ensuring secure retrieval of JWT tokens from the backend. This update streamlines the authentication experience and improves error management, enhancing overall user experience during login.
This commit is contained in:
@@ -1,34 +1,35 @@
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectSocketStatus } from "../store/socketSelectors";
|
||||
|
||||
interface ConnectionIndicatorProps {
|
||||
status?: 'connected' | 'disconnected' | 'connecting';
|
||||
status?: "connected" | "disconnected" | "connecting";
|
||||
description?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ConnectionIndicator = ({
|
||||
status: overrideStatus,
|
||||
description = 'Your browser is now connected to the AlphaHuman AI. Keep this tab open to keep the connection alive. You can message your assistant with the button below.',
|
||||
className = '',
|
||||
description = "Your browser is now connected to the AlphaHuman AI. Keep this tab open to keep the connection alive. You can message your assistant with the button below.",
|
||||
className = "",
|
||||
}: ConnectionIndicatorProps) => {
|
||||
// Use socket store status, but allow override via props
|
||||
const storeStatus = useAppSelector((state) => state.socket.status);
|
||||
const storeStatus = useAppSelector(selectSocketStatus);
|
||||
const status = overrideStatus || storeStatus;
|
||||
const statusConfig = {
|
||||
connected: {
|
||||
color: 'bg-sage-500',
|
||||
textColor: 'text-sage-500',
|
||||
text: 'Connected to AlphaHuman AI 🚀',
|
||||
color: "bg-sage-500",
|
||||
textColor: "text-sage-500",
|
||||
text: "Connected to AlphaHuman AI 🚀",
|
||||
},
|
||||
disconnected: {
|
||||
color: 'bg-coral-500',
|
||||
textColor: 'text-coral-500',
|
||||
text: 'Disconnected',
|
||||
color: "bg-coral-500",
|
||||
textColor: "text-coral-500",
|
||||
text: "Disconnected",
|
||||
},
|
||||
connecting: {
|
||||
color: 'bg-amber-500',
|
||||
textColor: 'text-amber-500',
|
||||
text: 'Connecting',
|
||||
color: "bg-amber-500",
|
||||
textColor: "text-amber-500",
|
||||
text: "Connecting",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,7 +38,9 @@ const ConnectionIndicator = ({
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div className={`w-2 h-2 ${config.color} rounded-full ${status === 'connected' ? 'animate-pulse' : ''}`}></div>
|
||||
<div
|
||||
className={`w-2 h-2 ${config.color} rounded-full ${status === "connected" ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
<span className={`text-sm ${config.textColor}`}>{config.text}</span>
|
||||
</div>
|
||||
{description && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
|
||||
/**
|
||||
* Default redirect component that routes users based on their auth and onboarding status
|
||||
@@ -9,7 +10,7 @@ import { useAppSelector } from '../store/hooks';
|
||||
*/
|
||||
const DefaultRedirect = () => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) => state.auth.isOnboarded);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
if (token && isOnboarded) {
|
||||
return <Navigate to="/home" replace />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Design System Showcase Component
|
||||
@@ -46,17 +46,23 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-12 flex flex-wrap gap-4">
|
||||
<button className="btn-premium">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Start Trading
|
||||
</button>
|
||||
<button className="btn-glass">
|
||||
View Portfolio
|
||||
</button>
|
||||
<button className="btn-outline">
|
||||
Market Analysis
|
||||
</button>
|
||||
<button className="btn-glass">View Portfolio</button>
|
||||
<button className="btn-outline">Market Analysis</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -70,19 +76,25 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Primary Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Primary Ocean</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Primary Ocean
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-primary-500 rounded-xl shadow-soft" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-900">Primary 500</p>
|
||||
<p className="text-sm font-medium text-stone-900">
|
||||
Primary 500
|
||||
</p>
|
||||
<p className="text-xs text-stone-500">#5B9BF3</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-primary-600 rounded-xl shadow-soft" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-900">Primary 600</p>
|
||||
<p className="text-sm font-medium text-stone-900">
|
||||
Primary 600
|
||||
</p>
|
||||
<p className="text-xs text-stone-500">#4A83DD</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +103,9 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Success Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Sage Success</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Sage Success
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-sage-500 rounded-xl shadow-soft" />
|
||||
@@ -112,7 +126,9 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Accent Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Accent Palette</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Accent Palette
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-accent-lavender rounded-xl shadow-soft" />
|
||||
@@ -164,14 +180,17 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">Body · 1rem</p>
|
||||
<p className="text-base text-stone-700 leading-relaxed max-w-3xl">
|
||||
The typography system is designed to create clear visual hierarchy while maintaining
|
||||
excellent readability. Each text size has been carefully calibrated with appropriate
|
||||
line heights and letter spacing to ensure optimal legibility across all device sizes.
|
||||
The typography system is designed to create clear visual hierarchy
|
||||
while maintaining excellent readability. Each text size has been
|
||||
carefully calibrated with appropriate line heights and letter
|
||||
spacing to ensure optimal legibility across all device sizes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">Monospace · For prices and data</p>
|
||||
<p className="text-xs text-stone-500 mb-1">
|
||||
Monospace · For prices and data
|
||||
</p>
|
||||
<p className="text-crypto-price text-2xl text-stone-900">
|
||||
$48,392.50 <span className="text-market-bullish">+2.45%</span>
|
||||
</p>
|
||||
@@ -205,7 +224,9 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div className="space-y-2">
|
||||
<p className="price-ticker up">$48,392.50</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-market-bullish font-medium">+$1,185.20</span>
|
||||
<span className="text-sm text-market-bullish font-medium">
|
||||
+$1,185.20
|
||||
</span>
|
||||
<span className="text-sm text-market-bullish">(+2.45%)</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,8 +243,18 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
</p>
|
||||
<div className="flex items-center text-primary-600 font-medium">
|
||||
<span>View Details</span>
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
<svg
|
||||
className="w-4 h-4 ml-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,25 +262,43 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Elevated Card */}
|
||||
<div className="card-elevated">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-stone-900">Quick Stats</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900">
|
||||
Quick Stats
|
||||
</h3>
|
||||
<div className="w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
<svg
|
||||
className="w-4 h-4 text-primary-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">Total Value</span>
|
||||
<span className="font-mono font-semibold text-stone-900">$125,430</span>
|
||||
<span className="font-mono font-semibold text-stone-900">
|
||||
$125,430
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">24h Change</span>
|
||||
<span className="font-mono font-semibold text-market-bullish">+3.2%</span>
|
||||
<span className="font-mono font-semibold text-market-bullish">
|
||||
+3.2%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">Holdings</span>
|
||||
<span className="font-mono font-semibold text-stone-900">12</span>
|
||||
<span className="font-mono font-semibold text-stone-900">
|
||||
12
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,32 +327,82 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Navigation Items */}
|
||||
<nav className="p-2">
|
||||
<a href="#" className="nav-item-premium active">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
Profile Settings
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
Notifications
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
Security
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Help & Support
|
||||
</a>
|
||||
@@ -367,9 +466,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button className="btn-premium w-full">
|
||||
Complete Transaction
|
||||
</button>
|
||||
<button className="btn-premium w-full">Complete Transaction</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -377,4 +474,4 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DesignSystemShowcase;
|
||||
export default DesignSystemShowcase;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import GmailIcon from '../assets/icons/GmailIcon';
|
||||
import GmailIcon from "../assets/icons/GmailIcon";
|
||||
|
||||
interface GmailConnectionIndicatorProps {
|
||||
description?: string;
|
||||
@@ -7,7 +7,7 @@ interface GmailConnectionIndicatorProps {
|
||||
|
||||
const GmailConnectionIndicator = ({
|
||||
description,
|
||||
className = '',
|
||||
className = "",
|
||||
}: GmailConnectionIndicatorProps) => {
|
||||
// Gmail is always offline for now (placeholder)
|
||||
const gmailIsOnline = false;
|
||||
@@ -15,11 +15,17 @@ const GmailConnectionIndicator = ({
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div className={`w-2 h-2 ${gmailIsOnline ? 'bg-red-500' : 'bg-gray-500'} rounded-full ${gmailIsOnline ? 'animate-pulse' : ''}`}></div>
|
||||
<div
|
||||
className={`w-2 h-2 ${gmailIsOnline ? "bg-red-500" : "bg-gray-500"} rounded-full ${gmailIsOnline ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<GmailIcon className={`w-4 h-4 ${gmailIsOnline ? 'text-red-500' : 'text-gray-500'}`} />
|
||||
<span className={`text-sm ${gmailIsOnline ? 'text-red-500' : 'text-gray-500'}`}>
|
||||
{gmailIsOnline ? 'Connected to Gmail' : 'Gmail is Offline'}
|
||||
<GmailIcon
|
||||
className={`w-4 h-4 ${gmailIsOnline ? "text-red-500" : "text-gray-500"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${gmailIsOnline ? "text-red-500" : "text-gray-500"}`}
|
||||
>
|
||||
{gmailIsOnline ? "Connected to Gmail" : "Gmail is Offline"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLottie } from 'lottie-react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLottie } from "lottie-react";
|
||||
|
||||
interface LottieAnimationProps {
|
||||
src: string;
|
||||
@@ -8,14 +8,21 @@ interface LottieAnimationProps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const LottieAnimation = ({ src, className = '', height = 200, width = 200 }: LottieAnimationProps) => {
|
||||
const LottieAnimation = ({
|
||||
src,
|
||||
className = "",
|
||||
height = 200,
|
||||
width = 200,
|
||||
}: LottieAnimationProps) => {
|
||||
const [animationData, setAnimationData] = useState<unknown>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(src)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setAnimationData(data))
|
||||
.catch((error) => console.error('Failed to load Lottie animation:', error));
|
||||
.catch((error) =>
|
||||
console.error("Failed to load Lottie animation:", error),
|
||||
);
|
||||
}, [src]);
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -3,13 +3,18 @@ interface PrivacyFeatureCardProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PrivacyFeatureCard = ({ title, description }: PrivacyFeatureCardProps) => {
|
||||
const PrivacyFeatureCard = ({
|
||||
title,
|
||||
description,
|
||||
}: PrivacyFeatureCardProps) => {
|
||||
return (
|
||||
<div className="bg-stone-800/50 rounded-xl p-3 border border-stone-700">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm mb-2 text-center">{title}</h3>
|
||||
<p className="opacity-70 text-xs leading-relaxed text-center">{description}</p>
|
||||
<p className="opacity-70 text-xs leading-relaxed text-center">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,17 @@ interface ProgressIndicatorProps {
|
||||
totalSteps: number;
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({ currentStep, totalSteps }: ProgressIndicatorProps) => {
|
||||
const ProgressIndicator = ({
|
||||
currentStep,
|
||||
totalSteps,
|
||||
}: ProgressIndicatorProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center space-x-1.5 mb-6">
|
||||
{Array.from({ length: totalSteps }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-8 h-0.5 rounded-full ${
|
||||
index < currentStep ? 'bg-primary-500' : 'bg-stone-700'
|
||||
index < currentStep ? "bg-primary-500" : "bg-stone-700"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -18,16 +19,16 @@ const ProtectedRoute = ({
|
||||
redirectTo,
|
||||
}: ProtectedRouteProps) => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) => state.auth.isOnboarded);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
// If auth is required but user is not logged in
|
||||
if (requireAuth && !token) {
|
||||
return <Navigate to={redirectTo || '/'} replace />;
|
||||
return <Navigate to={redirectTo || "/"} replace />;
|
||||
}
|
||||
|
||||
// If onboarding is required but user is not onboarded
|
||||
if (requireOnboarded && !isOnboarded) {
|
||||
return <Navigate to={redirectTo || '/onboarding'} replace />;
|
||||
return <Navigate to={redirectTo || "/onboarding"} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
|
||||
interface PublicRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -13,11 +14,11 @@ interface PublicRouteProps {
|
||||
*/
|
||||
const PublicRoute = ({ children, redirectTo }: PublicRouteProps) => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) => state.auth.isOnboarded);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
// If user is logged in and onboarded, redirect to home
|
||||
if (token && isOnboarded) {
|
||||
return <Navigate to={redirectTo || '/home'} replace />;
|
||||
return <Navigate to={redirectTo || "/home"} replace />;
|
||||
}
|
||||
|
||||
// If user is logged in but not onboarded, redirect to onboarding
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTelegram } from '../providers/TelegramProvider';
|
||||
import TelegramIcon from '../assets/icons/telegram.svg';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTelegram } from "../providers/TelegramProvider";
|
||||
import TelegramIcon from "../assets/icons/telegram.svg";
|
||||
|
||||
interface TelegramConnectionIndicatorProps {
|
||||
description?: string;
|
||||
@@ -9,7 +9,7 @@ interface TelegramConnectionIndicatorProps {
|
||||
|
||||
const TelegramConnectionIndicator = ({
|
||||
description,
|
||||
className = '',
|
||||
className = "",
|
||||
}: TelegramConnectionIndicatorProps) => {
|
||||
const { isAuthenticated, connectionStatus, checkConnection } = useTelegram();
|
||||
const [telegramIsOnline, setTelegramIsOnline] = useState(false);
|
||||
@@ -26,13 +26,13 @@ const TelegramConnectionIndicator = ({
|
||||
const isConnected = await checkConnection();
|
||||
setTelegramIsOnline(isConnected);
|
||||
} catch (error) {
|
||||
console.warn('Failed to check Telegram connection:', error);
|
||||
console.warn("Failed to check Telegram connection:", error);
|
||||
setTelegramIsOnline(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately if connected
|
||||
if (connectionStatus === 'connected') {
|
||||
if (connectionStatus === "connected") {
|
||||
checkTelegramConnection();
|
||||
} else {
|
||||
setTelegramIsOnline(false);
|
||||
@@ -52,11 +52,19 @@ const TelegramConnectionIndicator = ({
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div className={`w-2 h-2 ${telegramIsOnline ? 'bg-blue-500' : 'bg-gray-500'} rounded-full ${telegramIsOnline ? 'animate-pulse' : ''}`}></div>
|
||||
<div
|
||||
className={`w-2 h-2 ${telegramIsOnline ? "bg-blue-500" : "bg-gray-500"} rounded-full ${telegramIsOnline ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<img src={TelegramIcon} alt="Telegram" className={`w-4 h-4 ${telegramIsOnline ? 'opacity-100' : 'opacity-50'}`} />
|
||||
<span className={`text-sm ${telegramIsOnline ? 'text-blue-500' : 'text-gray-500'}`}>
|
||||
{telegramIsOnline ? 'Connected to Telegram' : 'Telegram is Offline'}
|
||||
<img
|
||||
src={TelegramIcon}
|
||||
alt="Telegram"
|
||||
className={`w-4 h-4 ${telegramIsOnline ? "opacity-100" : "opacity-50"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${telegramIsOnline ? "text-blue-500" : "text-gray-500"}`}
|
||||
>
|
||||
{telegramIsOnline ? "Connected to Telegram" : "Telegram is Offline"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { useAppDispatch, useAppSelector } from "../store/hooks";
|
||||
import {
|
||||
initializeTelegram,
|
||||
connectTelegram,
|
||||
@@ -9,9 +9,17 @@ import {
|
||||
setAuthStatus,
|
||||
setAuthError,
|
||||
setConnectionStatus,
|
||||
} from '../store/telegram';
|
||||
import { selectIsInitialized, selectConnectionStatus } from '../store/telegramSelectors';
|
||||
import { mtprotoService } from '../services/mtprotoService';
|
||||
resetTelegramForUser,
|
||||
} from "../store/telegram";
|
||||
import {
|
||||
selectIsInitialized,
|
||||
selectConnectionStatus,
|
||||
selectTelegramCurrentUserId,
|
||||
} from "../store/telegramSelectors";
|
||||
import { selectSocketStatus } from "../store/socketSelectors";
|
||||
import { mtprotoService } from "../services/mtprotoService";
|
||||
import { socketService } from "../services/socketService";
|
||||
import type { User } from "../types/api";
|
||||
|
||||
interface TelegramConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -19,15 +27,30 @@ interface TelegramConnectionModalProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
type ConnectionStep = 'qr' | '2fa' | 'loading' | 'error';
|
||||
type ConnectionStep = "qr" | "2fa" | "loading" | "error";
|
||||
|
||||
const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnectionModalProps) => {
|
||||
const TELEGRAM_ACCOUNT_MISMATCH_ERROR =
|
||||
"This Telegram account doesn't match your logged-in account. Please use the Telegram account you signed up with.";
|
||||
|
||||
function telegramMeIdMatchesUser(me: { id: unknown }, appUser: User): boolean {
|
||||
return String(me.id) === String(appUser.telegramId);
|
||||
}
|
||||
|
||||
const TelegramConnectionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: TelegramConnectionModalProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const userId = useAppSelector(selectTelegramCurrentUserId);
|
||||
const isInitialized = useAppSelector(selectIsInitialized);
|
||||
const connectionStatus = useAppSelector(selectConnectionStatus);
|
||||
const socketStatus = useAppSelector(selectSocketStatus);
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<ConnectionStep>('qr');
|
||||
const [password, setPassword] = useState('');
|
||||
const [currentStep, setCurrentStep] = useState<ConnectionStep>("qr");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordHint, setPasswordHint] = useState<string | undefined>();
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
||||
const [qrCodeExpires, setQrCodeExpires] = useState<number | null>(null);
|
||||
@@ -35,53 +58,40 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
|
||||
// Store password promise resolver
|
||||
const passwordResolverRef = useRef<((password: string) => void) | null>(null);
|
||||
const qrFlowStartedRef = useRef(false);
|
||||
|
||||
// Initialize and connect when modal opens
|
||||
// Ensure socket is connected when modal opens (e.g. for MCP / real-time features)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen) {
|
||||
qrFlowStartedRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (token && socketStatus !== "connected") {
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
setCurrentStep('loading');
|
||||
if (!isInitialized) {
|
||||
await dispatch(initializeTelegram()).unwrap();
|
||||
}
|
||||
if (connectionStatus !== 'connected') {
|
||||
await dispatch(connectTelegram()).unwrap();
|
||||
}
|
||||
// Check if already authenticated (but don't fail if not authenticated)
|
||||
try {
|
||||
const authCheck = await dispatch(checkAuthStatus()).unwrap();
|
||||
if (authCheck) {
|
||||
onComplete();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
// If checkAuthStatus fails, we're not authenticated - continue with QR flow
|
||||
console.log('Not authenticated, proceeding with QR code flow');
|
||||
}
|
||||
// Start QR code flow
|
||||
startQrCodeFlow();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Telegram';
|
||||
setError(errorMessage);
|
||||
setCurrentStep('error');
|
||||
dispatch(setConnectionStatus('error'));
|
||||
}
|
||||
};
|
||||
socketService.connect(token);
|
||||
}
|
||||
}, [isOpen, token, socketStatus]);
|
||||
|
||||
init();
|
||||
}, [isOpen, isInitialized, connectionStatus, dispatch, onComplete, onClose]);
|
||||
const handleTelegramAccountMismatch = useCallback(async () => {
|
||||
if (!userId) return;
|
||||
setError(TELEGRAM_ACCOUNT_MISMATCH_ERROR);
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
try {
|
||||
await mtprotoService.clearSessionAndDisconnect(userId);
|
||||
} catch (e) {
|
||||
console.warn("clearSessionAndDisconnect failed:", e);
|
||||
}
|
||||
dispatch(resetTelegramForUser({ userId }));
|
||||
}, [dispatch, userId]);
|
||||
|
||||
const startQrCodeFlow = useCallback(async () => {
|
||||
try {
|
||||
setIsAuthenticating(true);
|
||||
setError(null);
|
||||
setCurrentStep('qr');
|
||||
setPassword('');
|
||||
setCurrentStep("qr");
|
||||
setPassword("");
|
||||
setPasswordHint(undefined);
|
||||
setQrCodeUrl(null);
|
||||
setQrCodeExpires(null);
|
||||
@@ -94,15 +104,17 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
// Convert Uint8Array to base64url
|
||||
const binary = Array.from(qrCode.token)
|
||||
.map((byte) => String.fromCharCode(byte))
|
||||
.join('');
|
||||
.join("");
|
||||
tokenBase64 = btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
} else {
|
||||
// If it's a Buffer, use toString
|
||||
const buffer = qrCode.token as { toString: (encoding: string) => string };
|
||||
tokenBase64 = buffer.toString('base64url');
|
||||
const buffer = qrCode.token as {
|
||||
toString: (encoding: string) => string;
|
||||
};
|
||||
tokenBase64 = buffer.toString("base64url");
|
||||
}
|
||||
const url = `tg://login?token=${tokenBase64}`;
|
||||
setQrCodeUrl(url);
|
||||
@@ -110,17 +122,20 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
},
|
||||
async (hint) => {
|
||||
// 2FA password required
|
||||
console.log('Password callback invoked with hint:', hint);
|
||||
console.log("Password callback invoked with hint:", hint);
|
||||
setPasswordHint(hint);
|
||||
setCurrentStep('2fa');
|
||||
setCurrentStep("2fa");
|
||||
setIsAuthenticating(false);
|
||||
|
||||
// Wait for user to enter password
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
passwordResolverRef.current = (password: string) => {
|
||||
console.log('Password resolved in callback, sending to Telegram:', password ? '***' : 'empty');
|
||||
console.log(
|
||||
"Password resolved in callback, sending to Telegram:",
|
||||
password ? "***" : "empty",
|
||||
);
|
||||
if (!password || !password.trim()) {
|
||||
reject(new Error('Password is required'));
|
||||
reject(new Error("Password is required"));
|
||||
return;
|
||||
}
|
||||
resolve(password.trim());
|
||||
@@ -129,8 +144,10 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
// Set a timeout to prevent hanging forever
|
||||
setTimeout(() => {
|
||||
if (passwordResolverRef.current) {
|
||||
console.warn('Password callback timeout - no password provided');
|
||||
reject(new Error('Password input timeout'));
|
||||
console.warn(
|
||||
"Password callback timeout - no password provided",
|
||||
);
|
||||
reject(new Error("Password input timeout"));
|
||||
passwordResolverRef.current = null;
|
||||
}
|
||||
}, 300000); // 5 minutes timeout
|
||||
@@ -138,64 +155,144 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
},
|
||||
async (err) => {
|
||||
// Handle errors
|
||||
const errorMessage = err.message || 'Authentication error';
|
||||
const errorMessage = err.message || "Authentication error";
|
||||
|
||||
// Check if it's a 2FA password needed error
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED') || errorMessage.includes('PASSWORD')) {
|
||||
if (
|
||||
errorMessage.includes("SESSION_PASSWORD_NEEDED") ||
|
||||
errorMessage.includes("PASSWORD")
|
||||
) {
|
||||
// This should trigger the password callback, but if it doesn't, handle it here
|
||||
setPasswordHint(undefined);
|
||||
setCurrentStep('2fa');
|
||||
setCurrentStep("2fa");
|
||||
setIsAuthenticating(false);
|
||||
// Don't stop authentication - let the password callback handle it
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
dispatch(setAuthError(errorMessage));
|
||||
dispatch(setAuthError({ userId, error: errorMessage }));
|
||||
|
||||
// Check if it's a cancellation
|
||||
if (errorMessage.includes('AUTH_USER_CANCEL') || errorMessage.includes('cancel')) {
|
||||
setCurrentStep('qr');
|
||||
if (
|
||||
errorMessage.includes("AUTH_USER_CANCEL") ||
|
||||
errorMessage.includes("cancel")
|
||||
) {
|
||||
setCurrentStep("qr");
|
||||
setIsAuthenticating(false);
|
||||
return true; // Stop authentication
|
||||
}
|
||||
|
||||
return false; // Continue
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Authentication successful
|
||||
setIsAuthenticating(false);
|
||||
let me: { id: unknown } | null = null;
|
||||
try {
|
||||
await dispatch(checkAuthStatus()).unwrap();
|
||||
me = await dispatch(checkAuthStatus(userId)).unwrap();
|
||||
} catch (err) {
|
||||
// Even if checkAuthStatus fails, we know we just authenticated
|
||||
console.warn('checkAuthStatus failed after authentication, but continuing:', err);
|
||||
console.warn("checkAuthStatus failed after authentication:", err);
|
||||
}
|
||||
dispatch(setAuthStatus('authenticated'));
|
||||
if (!user) {
|
||||
setError("Could not verify account. Please try again.");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
return;
|
||||
}
|
||||
if (!me || !telegramMeIdMatchesUser(me, user)) {
|
||||
await handleTelegramAccountMismatch();
|
||||
return;
|
||||
}
|
||||
dispatch(setAuthStatus({ userId, status: "authenticated" }));
|
||||
onComplete();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setIsAuthenticating(false);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Authentication failed";
|
||||
|
||||
// If it's a password needed error, we should already be on the 2FA step
|
||||
// Don't show it as an error, just log it
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED')) {
|
||||
console.log('Password required - user should be on 2FA step');
|
||||
// If we're not on 2FA step, switch to it
|
||||
if (currentStep !== '2fa') {
|
||||
setCurrentStep('2fa');
|
||||
if (errorMessage.includes("SESSION_PASSWORD_NEEDED")) {
|
||||
console.log("Password required - user should be on 2FA step");
|
||||
if (currentStep !== "2fa") {
|
||||
setCurrentStep("2fa");
|
||||
setPasswordHint(undefined);
|
||||
}
|
||||
return; // Don't show error, password callback should handle it
|
||||
return;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
setCurrentStep('error');
|
||||
dispatch(setAuthError(errorMessage));
|
||||
setCurrentStep("error");
|
||||
dispatch(setAuthError({ userId, error: errorMessage }));
|
||||
}
|
||||
}, [dispatch, onComplete, onClose]);
|
||||
}, [
|
||||
dispatch,
|
||||
onComplete,
|
||||
onClose,
|
||||
user,
|
||||
userId,
|
||||
handleTelegramAccountMismatch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !userId) return;
|
||||
if (qrFlowStartedRef.current) return;
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
setCurrentStep("loading");
|
||||
if (!isInitialized) {
|
||||
await dispatch(initializeTelegram(userId)).unwrap();
|
||||
}
|
||||
if (connectionStatus !== "connected") {
|
||||
await dispatch(connectTelegram(userId)).unwrap();
|
||||
}
|
||||
try {
|
||||
const authCheck = await dispatch(checkAuthStatus(userId)).unwrap();
|
||||
if (authCheck) {
|
||||
if (!user) {
|
||||
setError("Could not verify account. Please try again.");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
return;
|
||||
}
|
||||
if (!telegramMeIdMatchesUser(authCheck, user)) {
|
||||
await handleTelegramAccountMismatch();
|
||||
return;
|
||||
}
|
||||
onComplete();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Not authenticated, proceeding with QR code flow");
|
||||
}
|
||||
if (qrFlowStartedRef.current) return;
|
||||
qrFlowStartedRef.current = true;
|
||||
await startQrCodeFlow();
|
||||
} catch (err) {
|
||||
qrFlowStartedRef.current = false;
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to initialize Telegram";
|
||||
setError(errorMessage);
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
}
|
||||
};
|
||||
|
||||
void init();
|
||||
}, [
|
||||
isOpen,
|
||||
isInitialized,
|
||||
connectionStatus,
|
||||
dispatch,
|
||||
onComplete,
|
||||
onClose,
|
||||
user,
|
||||
userId,
|
||||
handleTelegramAccountMismatch,
|
||||
startQrCodeFlow,
|
||||
]);
|
||||
|
||||
// Update countdown timer every second and reload QR code on timeout
|
||||
useEffect(() => {
|
||||
@@ -205,11 +302,14 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
}
|
||||
|
||||
const updateTimer = () => {
|
||||
const remaining = Math.max(0, Math.floor((qrCodeExpires * 1000 - Date.now()) / 1000));
|
||||
const remaining = Math.max(
|
||||
0,
|
||||
Math.floor((qrCodeExpires * 1000 - Date.now()) / 1000),
|
||||
);
|
||||
setTimeRemaining(remaining);
|
||||
|
||||
// If timer reaches 0 and we're on QR step, reload the QR code
|
||||
if (remaining === 0 && currentStep === 'qr' && !isAuthenticating) {
|
||||
if (remaining === 0 && currentStep === "qr" && !isAuthenticating) {
|
||||
startQrCodeFlow();
|
||||
}
|
||||
};
|
||||
@@ -225,68 +325,92 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
|
||||
// Poll authentication status every 5 seconds when QR code is displayed
|
||||
useEffect(() => {
|
||||
if (currentStep !== 'qr' || !qrCodeUrl || isAuthenticating) {
|
||||
if (currentStep !== "qr" || !qrCodeUrl || isAuthenticating) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
const pollAuthStatus = async () => {
|
||||
try {
|
||||
if (!mtprotoService.isReady()) return;
|
||||
const client = mtprotoService.getClient();
|
||||
if (!client) return;
|
||||
|
||||
const isAuthorized = await client.checkAuthorization();
|
||||
|
||||
if (isAuthorized) {
|
||||
// User is authenticated - check if we need password
|
||||
try {
|
||||
// Try to get user info - if this fails with SESSION_PASSWORD_NEEDED, we need password
|
||||
await client.getMe();
|
||||
// If getMe succeeds, authentication is complete
|
||||
console.log('QR code scanned and authenticated successfully');
|
||||
// The signInWithQrCode promise should resolve, but if it doesn't, trigger completion
|
||||
const me = await client.getMe();
|
||||
if (!user) {
|
||||
setError("Could not verify account. Please try again.");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
setIsAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
if (!telegramMeIdMatchesUser(me, user)) {
|
||||
setIsAuthenticating(false);
|
||||
await handleTelegramAccountMismatch();
|
||||
return;
|
||||
}
|
||||
console.log("QR code scanned and authenticated successfully");
|
||||
setIsAuthenticating(false);
|
||||
dispatch(setAuthStatus('authenticated'));
|
||||
dispatch(setAuthStatus({ userId, status: "authenticated" }));
|
||||
onComplete();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED') || errorMessage.includes('PASSWORD')) {
|
||||
// Need password - switch to 2FA step
|
||||
console.log('Password required after QR scan - switching to 2FA step');
|
||||
if (currentStep === 'qr' && !passwordResolverRef.current) {
|
||||
// Trigger password callback manually if it wasn't called
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : String(err);
|
||||
if (
|
||||
errorMessage.includes("SESSION_PASSWORD_NEEDED") ||
|
||||
errorMessage.includes("PASSWORD")
|
||||
) {
|
||||
console.log(
|
||||
"Password required after QR scan - switching to 2FA step",
|
||||
);
|
||||
if (currentStep === "qr" && !passwordResolverRef.current) {
|
||||
setPasswordHint(undefined);
|
||||
setCurrentStep('2fa');
|
||||
setCurrentStep("2fa");
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors during polling - they're expected if not authenticated yet
|
||||
console.debug('Auth status check:', err instanceof Error ? err.message : String(err));
|
||||
console.debug(
|
||||
"Auth status check:",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately, then every 5 seconds
|
||||
checkAuthStatus();
|
||||
const interval = setInterval(checkAuthStatus, 5000);
|
||||
pollAuthStatus();
|
||||
const interval = setInterval(pollAuthStatus, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentStep, qrCodeUrl, isAuthenticating, dispatch, onComplete, onClose]);
|
||||
}, [
|
||||
currentStep,
|
||||
qrCodeUrl,
|
||||
isAuthenticating,
|
||||
dispatch,
|
||||
onComplete,
|
||||
onClose,
|
||||
user,
|
||||
userId,
|
||||
handleTelegramAccountMismatch,
|
||||
]);
|
||||
|
||||
const handle2FASubmit = async () => {
|
||||
const passwordValue = password.trim();
|
||||
|
||||
if (!passwordValue) {
|
||||
setError('Please enter your password');
|
||||
setError("Please enter your password");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordResolverRef.current) {
|
||||
console.error('Password resolver is null - password callback may not be active');
|
||||
setError('Authentication session expired. Please try again.');
|
||||
setCurrentStep('qr');
|
||||
console.error(
|
||||
"Password resolver is null - password callback may not be active",
|
||||
);
|
||||
setError("Authentication session expired. Please try again.");
|
||||
setCurrentStep("qr");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,7 +423,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
const resolver = passwordResolverRef.current;
|
||||
passwordResolverRef.current = null; // Clear before resolving to prevent double calls
|
||||
|
||||
console.log('Resolving password promise - sending password to Telegram');
|
||||
console.log("Resolving password promise - sending password to Telegram");
|
||||
resolver(passwordValue);
|
||||
|
||||
// The authentication will continue in the background via signInWithQrCode
|
||||
@@ -307,20 +431,21 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
// We keep isAuthenticating true until the promise resolves or rejects
|
||||
} catch (err) {
|
||||
setIsAuthenticating(false);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Password verification failed';
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Password verification failed";
|
||||
setError(errorMessage);
|
||||
dispatch(setAuthError(errorMessage));
|
||||
console.error('Error in handle2FASubmit:', err);
|
||||
dispatch(setAuthError({ userId, error: errorMessage }));
|
||||
console.error("Error in handle2FASubmit:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep === '2fa') {
|
||||
setCurrentStep('qr');
|
||||
setPassword('');
|
||||
if (currentStep === "2fa") {
|
||||
setCurrentStep("qr");
|
||||
setPassword("");
|
||||
setPasswordHint(undefined);
|
||||
if (passwordResolverRef.current) {
|
||||
passwordResolverRef.current('');
|
||||
passwordResolverRef.current("");
|
||||
passwordResolverRef.current = null;
|
||||
}
|
||||
} else {
|
||||
@@ -328,12 +453,27 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
const handleRetry = async () => {
|
||||
if (!userId) return;
|
||||
setError(null);
|
||||
setPassword('');
|
||||
setPassword("");
|
||||
setPasswordHint(undefined);
|
||||
setQrCodeUrl(null);
|
||||
setQrCodeExpires(null);
|
||||
setCurrentStep("loading");
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await dispatch(initializeTelegram(userId)).unwrap();
|
||||
}
|
||||
if (connectionStatus !== "connected") {
|
||||
await dispatch(connectTelegram(userId)).unwrap();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to initialize");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
return;
|
||||
}
|
||||
startQrCodeFlow();
|
||||
};
|
||||
|
||||
@@ -343,22 +483,22 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm flex items-center justify-center"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: 9999
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-black/90 shadow-large animate-fade-up max-w-4xl max-h-[90vh] overflow-y-auto flex flex-col items-center justify-center rounded-3xl focus:outline-none"
|
||||
style={{
|
||||
maxWidth: '56rem',
|
||||
maxHeight: '90vh',
|
||||
padding: 0
|
||||
maxWidth: "56rem",
|
||||
maxHeight: "90vh",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{/* Close button */}
|
||||
@@ -366,27 +506,49 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 z-10 w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{currentStep === 'loading' ? (
|
||||
{currentStep === "loading" ? (
|
||||
<div className="text-center py-8 flex flex-col items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mb-4"></div>
|
||||
<p className="opacity-70">Initializing Telegram connection...</p>
|
||||
</div>
|
||||
) : currentStep === 'error' ? (
|
||||
) : currentStep === "error" ? (
|
||||
<>
|
||||
{/* Error Screen */}
|
||||
<div className="text-center flex flex-col items-center justify-center">
|
||||
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
className="w-8 h-8 text-red-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Connection Error</h2>
|
||||
<p className="opacity-70 text-sm mb-6">{error || 'An error occurred'}</p>
|
||||
<p className="opacity-70 text-sm mb-6">
|
||||
{error || "An error occurred"}
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
@@ -403,7 +565,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : currentStep === 'qr' ? (
|
||||
) : currentStep === "qr" ? (
|
||||
<>
|
||||
{/* QR Code Screen */}
|
||||
<div className="text-center flex flex-col items-center justify-center w-full">
|
||||
@@ -428,10 +590,14 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
{isAuthenticating ? (
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600 text-sm">Generating QR code...</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Generating QR code...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-600 text-sm">Loading QR code...</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Loading QR code...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -456,21 +622,27 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-white font-bold text-xs">1</span>
|
||||
</div>
|
||||
<p className="opacity-90 text-sm">Open Telegram on your phone</p>
|
||||
<p className="opacity-90 text-sm">
|
||||
Open Telegram on your phone
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 text-left">
|
||||
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-white font-bold text-xs">2</span>
|
||||
</div>
|
||||
<p className="opacity-90 text-sm">Go to Settings > Devices > Link Desktop Device</p>
|
||||
<p className="opacity-90 text-sm">
|
||||
Go to Settings > Devices > Link Desktop Device
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 text-left">
|
||||
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-white font-bold text-xs">3</span>
|
||||
</div>
|
||||
<p className="opacity-90 text-sm">Point your phone at this screen to confirm login</p>
|
||||
<p className="opacity-90 text-sm">
|
||||
Point your phone at this screen to confirm login
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -483,7 +655,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
<p className="opacity-70 text-sm mb-6">
|
||||
{passwordHint
|
||||
? `Your account is protected with two-step verification. Hint: ${passwordHint}`
|
||||
: 'Your account is protected with two-step verification. Please enter your password to continue.'}
|
||||
: "Your account is protected with two-step verification. Please enter your password to continue."}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
@@ -499,7 +671,11 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && password.trim() && !isAuthenticating) {
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
password.trim() &&
|
||||
!isAuthenticating
|
||||
) {
|
||||
handle2FASubmit();
|
||||
}
|
||||
}}
|
||||
@@ -531,7 +707,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
disabled={!password.trim() || isAuthenticating}
|
||||
className="flex-1 py-2.5 px-4 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
{isAuthenticating ? 'Verifying...' : 'Continue'}
|
||||
{isAuthenticating ? "Verifying..." : "Continue"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,300 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BACKEND_URL, TELEGRAM_BOT_ID } from '../utils/config';
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { setToken } from '../store/authSlice';
|
||||
import { TELEGRAM_BOT_USERNAME } from "../utils/config";
|
||||
import { openUrl } from "../utils/openUrl";
|
||||
|
||||
interface TelegramAuthData {
|
||||
id: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
photo_url?: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
interface TelegramLoginButtonProps {
|
||||
onSuccess?: () => void;
|
||||
className?: string;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const TelegramLoginButton = ({
|
||||
onSuccess,
|
||||
className = '',
|
||||
text = 'Yes, Login with Telegram',
|
||||
className = "",
|
||||
text = "Yes, Login with Telegram",
|
||||
disabled: externalDisabled = false,
|
||||
}: TelegramLoginButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
|
||||
const handleTelegramLogin = async () => {
|
||||
if (isAuthenticating || externalDisabled) return;
|
||||
|
||||
setIsAuthenticating(true);
|
||||
|
||||
try {
|
||||
const origin = window.location.origin;
|
||||
const botId = TELEGRAM_BOT_ID;
|
||||
// Pass origin as query param so backend can use it for postMessage
|
||||
const callbackUrl = `${BACKEND_URL}/auth/telegram/callback?frontend_origin=${encodeURIComponent(origin)}`;
|
||||
const oauthUrl = `https://oauth.telegram.org/auth?bot_id=${botId}&origin=${encodeURIComponent(origin)}&request_access=write&embed=0&return_to=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
// Open popup window
|
||||
const width = 550;
|
||||
const height = 470;
|
||||
const availLeft = 'availLeft' in screen ? (screen as { availLeft: number }).availLeft : 0;
|
||||
const availTop = 'availTop' in screen ? (screen as { availTop: number }).availTop : 0;
|
||||
const left = Math.max(0, (screen.width - width) / 2) + availLeft;
|
||||
const top = Math.max(0, (screen.height - height) / 2) + availTop;
|
||||
|
||||
const popup = window.open(
|
||||
oauthUrl,
|
||||
'telegram_oauth',
|
||||
`width=${width},height=${height},left=${left},top=${top},status=0,location=0,menubar=0,toolbar=0`
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
throw new Error('Failed to open popup window. Please allow popups for this site.');
|
||||
}
|
||||
|
||||
let authCompleted = false;
|
||||
|
||||
// Listen for postMessage from Telegram OAuth
|
||||
const messageHandler = async (event: MessageEvent) => {
|
||||
// 1) Only accept messages from Telegram OAuth
|
||||
if (event.origin !== 'https://oauth.telegram.org') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Parse the payload (Telegram sends a JSON string)
|
||||
let data: any;
|
||||
try {
|
||||
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Handle successful auth_result from Telegram
|
||||
if (data.event === 'auth_result' && data.result && !authCompleted) {
|
||||
authCompleted = true;
|
||||
window.removeEventListener('message', messageHandler);
|
||||
popup.close();
|
||||
|
||||
const telegramData: TelegramAuthData = {
|
||||
id: data.result.id,
|
||||
first_name: data.result.first_name,
|
||||
last_name: data.result.last_name,
|
||||
username: data.result.username,
|
||||
photo_url: data.result.photo_url,
|
||||
auth_date: data.result.auth_date,
|
||||
hash: data.result.hash,
|
||||
};
|
||||
|
||||
try {
|
||||
// Send Telegram auth data to backend to verify and exchange for JWT
|
||||
const webCompleteResponse = await fetch(`${BACKEND_URL}/auth/web-complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'telegram',
|
||||
telegramUser: telegramData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!webCompleteResponse.ok) {
|
||||
const errorData = await webCompleteResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to complete authentication');
|
||||
}
|
||||
|
||||
const { data: completeData } = await webCompleteResponse.json();
|
||||
const { loginToken } = completeData || {};
|
||||
|
||||
if (!loginToken) {
|
||||
throw new Error('No login token received from server');
|
||||
}
|
||||
|
||||
const exchangeResponse = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: loginToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!exchangeResponse.ok) {
|
||||
const errorData = await exchangeResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to exchange token');
|
||||
}
|
||||
|
||||
const exchangeData = await exchangeResponse.json();
|
||||
const { sessionToken } = exchangeData.data || {};
|
||||
|
||||
if (!sessionToken) {
|
||||
throw new Error('No JWT token received from server');
|
||||
}
|
||||
|
||||
// Store JWT token in store (this is the JWT from the backend)
|
||||
dispatch(setToken(sessionToken));
|
||||
|
||||
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
navigate('/onboarding/step1');
|
||||
}
|
||||
setIsAuthenticating(false);
|
||||
} catch (error) {
|
||||
setIsAuthenticating(false);
|
||||
console.error('Failed to complete Telegram authentication:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// Fallback: Try to extract auth data from popup URL if postMessage doesn't work
|
||||
// This will only work if the popup redirects to our origin (unlikely but possible)
|
||||
const checkPopupUrl = setInterval(async () => {
|
||||
if (authCompleted || popup.closed) {
|
||||
clearInterval(checkPopupUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to read popup location (will throw if cross-origin)
|
||||
const popupUrl = popup.location.href;
|
||||
|
||||
// If popup is on our callback URL, extract data and get JWT from backend
|
||||
if (popupUrl.startsWith(callbackUrl.split('?')[0])) {
|
||||
const url = new URL(popupUrl);
|
||||
const telegramData: TelegramAuthData = {
|
||||
id: parseInt(url.searchParams.get('id') || '0', 10),
|
||||
first_name: url.searchParams.get('first_name') || undefined,
|
||||
last_name: url.searchParams.get('last_name') || undefined,
|
||||
username: url.searchParams.get('username') || undefined,
|
||||
photo_url: url.searchParams.get('photo_url') || undefined,
|
||||
auth_date: parseInt(url.searchParams.get('auth_date') || '0', 10),
|
||||
hash: url.searchParams.get('hash') || '',
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (telegramData.id && telegramData.auth_date && telegramData.hash) {
|
||||
authCompleted = true;
|
||||
clearInterval(checkPopupUrl);
|
||||
popup.close();
|
||||
window.removeEventListener('message', messageHandler);
|
||||
|
||||
try {
|
||||
// Send Telegram auth data to backend to get JWT token
|
||||
// Use web-complete to verify and then exchange for JWT
|
||||
const webCompleteResponse = await fetch(`${BACKEND_URL}/auth/web-complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'telegram',
|
||||
telegramUser: telegramData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!webCompleteResponse.ok) {
|
||||
const errorData = await webCompleteResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to complete authentication');
|
||||
}
|
||||
|
||||
const { data } = await webCompleteResponse.json();
|
||||
const { loginToken } = data;
|
||||
|
||||
if (!loginToken) {
|
||||
throw new Error('No login token received from server');
|
||||
}
|
||||
|
||||
// Exchange handoff token for JWT session token
|
||||
const exchangeResponse = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: loginToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!exchangeResponse.ok) {
|
||||
const errorData = await exchangeResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to exchange token');
|
||||
}
|
||||
|
||||
const exchangeData = await exchangeResponse.json();
|
||||
const { sessionToken } = exchangeData.data;
|
||||
|
||||
if (!sessionToken) {
|
||||
throw new Error('No JWT token received from server');
|
||||
}
|
||||
|
||||
// Store JWT token in store (this is the JWT from the backend)
|
||||
dispatch(setToken(sessionToken));
|
||||
|
||||
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
navigate('/onboarding/step1');
|
||||
}
|
||||
setIsAuthenticating(false);
|
||||
} catch (error) {
|
||||
setIsAuthenticating(false);
|
||||
console.error('Failed to complete authentication:', error);
|
||||
// alert(error instanceof Error ? error.message : 'Authentication failed. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin or other error - this is expected, continue polling
|
||||
// The postMessage handler will catch the auth completion
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Monitor popup closure
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed && !authCompleted) {
|
||||
clearInterval(checkClosed);
|
||||
clearInterval(checkPopupUrl);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Timeout after 5 minutes
|
||||
setTimeout(() => {
|
||||
if (!authCompleted) {
|
||||
if (!popup.closed) {
|
||||
popup.close();
|
||||
}
|
||||
clearInterval(checkClosed);
|
||||
clearInterval(checkPopupUrl);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
setIsAuthenticating(false);
|
||||
// alert('Authentication timed out. Please try again.');
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
} catch (error) {
|
||||
setIsAuthenticating(false);
|
||||
console.error('Failed to start Telegram authentication:', error);
|
||||
// alert(error instanceof Error ? error.message : 'Failed to start authentication. Please try again.');
|
||||
}
|
||||
const handleTelegramLogin = () => {
|
||||
if (externalDisabled) return;
|
||||
openUrl(`https://t.me/${TELEGRAM_BOT_USERNAME}?start=login`);
|
||||
};
|
||||
|
||||
const isDisabled = isAuthenticating || externalDisabled;
|
||||
const isDisabled = externalDisabled;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -305,7 +29,7 @@ const TelegramLoginButton = ({
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
<span>{isAuthenticating ? 'Authenticating...' : text}</span>
|
||||
<span>{text}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface TypewriterGreetingProps {
|
||||
greetings: string[];
|
||||
@@ -13,10 +13,10 @@ const TypewriterGreeting = ({
|
||||
typingSpeed = 100,
|
||||
deletingSpeed = 50,
|
||||
pauseTime = 2000,
|
||||
className = '',
|
||||
className = "",
|
||||
}: TypewriterGreetingProps) => {
|
||||
const [currentGreetingIndex, setCurrentGreetingIndex] = useState(0);
|
||||
const [displayedText, setDisplayedText] = useState('');
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
@@ -56,7 +56,16 @@ const TypewriterGreeting = ({
|
||||
}, speed);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [displayedText, isDeleting, isPaused, currentGreetingIndex, greetings, typingSpeed, deletingSpeed, pauseTime]);
|
||||
}, [
|
||||
displayedText,
|
||||
isDeleting,
|
||||
isPaused,
|
||||
currentGreetingIndex,
|
||||
greetings,
|
||||
typingSpeed,
|
||||
deletingSpeed,
|
||||
pauseTime,
|
||||
]);
|
||||
|
||||
return (
|
||||
<h1 className={`text-2xl font-bold mb-4 ${className}`}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAppDispatch } from '../../store/hooks';
|
||||
import { clearToken } from '../../store/authSlice';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
import SettingsHeader from './components/SettingsHeader';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useAppDispatch } from "../../store/hooks";
|
||||
import { clearToken } from "../../store/authSlice";
|
||||
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "./components/SettingsHeader";
|
||||
import SettingsMenuItem from "./components/SettingsMenuItem";
|
||||
|
||||
const SettingsHome = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -15,129 +15,224 @@ const SettingsHome = () => {
|
||||
|
||||
const handleViewEncryptionKey = () => {
|
||||
// TODO: Show encryption key in a secure modal
|
||||
console.log('View encryption key');
|
||||
console.log("View encryption key");
|
||||
};
|
||||
|
||||
const handleDeleteAllData = () => {
|
||||
// TODO: Show confirmation dialog and delete all data
|
||||
console.log('Delete all data');
|
||||
console.log("Delete all data");
|
||||
};
|
||||
|
||||
// Main settings menu items
|
||||
const mainMenuItems = [
|
||||
{
|
||||
id: 'connections',
|
||||
title: 'Connections',
|
||||
description: 'Manage your connected accounts and services',
|
||||
id: "connections",
|
||||
title: "Connections",
|
||||
description: "Manage your connected accounts and services",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('connections'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("connections"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'messaging',
|
||||
title: 'Messaging',
|
||||
description: 'Configure messaging preferences and templates',
|
||||
id: "messaging",
|
||||
title: "Messaging",
|
||||
description: "Configure messaging preferences and templates",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('messaging'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("messaging"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'privacy',
|
||||
title: 'Privacy & Security',
|
||||
description: 'Control your privacy and security settings',
|
||||
id: "privacy",
|
||||
title: "Privacy & Security",
|
||||
description: "Control your privacy and security settings",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('privacy'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("privacy"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'Profile',
|
||||
description: 'Update your profile information and preferences',
|
||||
id: "profile",
|
||||
title: "Profile",
|
||||
description: "Update your profile information and preferences",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('profile'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("profile"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'Advanced',
|
||||
description: 'Advanced configuration and developer options',
|
||||
id: "advanced",
|
||||
title: "Advanced",
|
||||
description: "Advanced configuration and developer options",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('advanced'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("advanced"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'encryption',
|
||||
title: 'View Encryption Key',
|
||||
description: 'Access your encryption key for backup purposes',
|
||||
id: "encryption",
|
||||
title: "View Encryption Key",
|
||||
description: "Access your encryption key for backup purposes",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1721 9z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1721 9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleViewEncryptionKey,
|
||||
dangerous: false
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing',
|
||||
description: 'Manage your subscription and payment methods',
|
||||
id: "billing",
|
||||
title: "Billing",
|
||||
description: "Manage your subscription and payment methods",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('billing'),
|
||||
dangerous: false
|
||||
}
|
||||
onClick: () => navigateToSettings("billing"),
|
||||
dangerous: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Destructive actions menu items
|
||||
const destructiveMenuItems = [
|
||||
{
|
||||
id: 'delete',
|
||||
title: 'Delete All Data',
|
||||
description: 'Permanently delete all your data and reset your account',
|
||||
id: "delete",
|
||||
title: "Delete All Data",
|
||||
description: "Permanently delete all your data and reset your account",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleDeleteAllData,
|
||||
dangerous: true
|
||||
dangerous: true,
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
title: 'Log out',
|
||||
description: 'Sign out of your account',
|
||||
id: "logout",
|
||||
title: "Log out",
|
||||
description: "Sign out of your account",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleLogout,
|
||||
dangerous: true
|
||||
}
|
||||
dangerous: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -12,13 +12,13 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
// Handle escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [onClose]);
|
||||
|
||||
// Handle backdrop click
|
||||
@@ -57,9 +57,9 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
ref={modalRef}
|
||||
className="glass rounded-3xl shadow-large w-full max-w-[520px] h-[800px] overflow-hidden animate-slide-right focus:outline-none focus:ring-0"
|
||||
style={{
|
||||
animationDuration: '300ms',
|
||||
animationTimingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
animationFillMode: 'both'
|
||||
animationDuration: "300ms",
|
||||
animationTimingFunction: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -72,4 +72,4 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
return createPortal(modalContent, document.body);
|
||||
};
|
||||
|
||||
export default SettingsLayout;
|
||||
export default SettingsLayout;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import SettingsLayout from './SettingsLayout';
|
||||
import SettingsHome from './SettingsHome';
|
||||
import ConnectionsPanel from './panels/ConnectionsPanel';
|
||||
import MessagingPanel from './panels/MessagingPanel';
|
||||
import PrivacyPanel from './panels/PrivacyPanel';
|
||||
import ProfilePanel from './panels/ProfilePanel';
|
||||
import AdvancedPanel from './panels/AdvancedPanel';
|
||||
import BillingPanel from './panels/BillingPanel';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsLayout from "./SettingsLayout";
|
||||
import SettingsHome from "./SettingsHome";
|
||||
import ConnectionsPanel from "./panels/ConnectionsPanel";
|
||||
import MessagingPanel from "./panels/MessagingPanel";
|
||||
import PrivacyPanel from "./panels/PrivacyPanel";
|
||||
import ProfilePanel from "./panels/ProfilePanel";
|
||||
import AdvancedPanel from "./panels/AdvancedPanel";
|
||||
import BillingPanel from "./panels/BillingPanel";
|
||||
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";
|
||||
|
||||
const SettingsModal = () => {
|
||||
const location = useLocation();
|
||||
const { closeSettings } = useSettingsNavigation();
|
||||
|
||||
// Only render modal when on settings routes
|
||||
const isSettingsRoute = location.pathname.startsWith('/settings');
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
|
||||
if (!isSettingsRoute) {
|
||||
return null;
|
||||
@@ -36,4 +36,4 @@ const SettingsModal = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
export default SettingsModal;
|
||||
|
||||
@@ -6,8 +6,8 @@ interface SettingsBackButtonProps {
|
||||
|
||||
const SettingsBackButton = ({
|
||||
onClick,
|
||||
title = 'Settings',
|
||||
className = ''
|
||||
title = "Settings",
|
||||
className = "",
|
||||
}: SettingsBackButtonProps) => {
|
||||
return (
|
||||
<div className={`bg-black/30 border-b border-stone-700 p-6 ${className}`}>
|
||||
@@ -35,4 +35,4 @@ const SettingsBackButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsBackButton;
|
||||
export default SettingsBackButton;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
|
||||
interface SettingsHeaderProps {
|
||||
className?: string;
|
||||
@@ -8,15 +8,17 @@ interface SettingsHeaderProps {
|
||||
}
|
||||
|
||||
const SettingsHeader = ({
|
||||
className = '',
|
||||
title = 'Settings',
|
||||
className = "",
|
||||
title = "Settings",
|
||||
showBackButton = false,
|
||||
onBack
|
||||
onBack,
|
||||
}: SettingsHeaderProps) => {
|
||||
const { closeSettings } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<div className={`bg-black/30 border-b border-stone-700 p-6 relative ${className}`}>
|
||||
<div
|
||||
className={`bg-black/30 border-b border-stone-700 p-6 relative ${className}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{/* Back button */}
|
||||
@@ -26,14 +28,27 @@ const SettingsHeader = ({
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors mr-3"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-lg font-semibold text-white" id="settings-modal-title">
|
||||
<h2
|
||||
className="text-lg font-semibold text-white"
|
||||
id="settings-modal-title"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -44,8 +59,18 @@ const SettingsHeader = ({
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,4 +78,4 @@ const SettingsHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsHeader;
|
||||
export default SettingsHeader;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface SettingsMenuItemProps {
|
||||
icon: ReactNode;
|
||||
@@ -17,16 +17,20 @@ const SettingsMenuItem = ({
|
||||
onClick,
|
||||
dangerous = false,
|
||||
isFirst = false,
|
||||
isLast = false
|
||||
isLast = false,
|
||||
}: SettingsMenuItemProps) => {
|
||||
// Color variations for dangerous items (like logout/delete)
|
||||
const titleColor = dangerous ? 'text-amber-400' : 'text-white';
|
||||
const iconColor = dangerous ? 'text-amber-400' : 'text-white';
|
||||
const borderColor = 'border-stone-700'; // Use consistent border color for all items
|
||||
const titleColor = dangerous ? "text-amber-400" : "text-white";
|
||||
const iconColor = dangerous ? "text-amber-400" : "text-white";
|
||||
const borderColor = "border-stone-700"; // Use consistent border color for all items
|
||||
|
||||
// Border classes for first/last items
|
||||
const borderClasses = isLast ? '' : `border-b ${borderColor}`;
|
||||
const roundedClasses = isFirst ? 'first:rounded-t-3xl' : (isLast ? 'last:rounded-b-3xl' : '');
|
||||
const borderClasses = isLast ? "" : `border-b ${borderColor}`;
|
||||
const roundedClasses = isFirst
|
||||
? "first:rounded-t-3xl"
|
||||
: isLast
|
||||
? "last:rounded-b-3xl"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -37,16 +41,9 @@ const SettingsMenuItem = ({
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium text-sm mb-1 ${titleColor}`}>
|
||||
{title}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="opacity-70 text-xs">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className={`font-medium text-sm mb-1 ${titleColor}`}>{title}</div>
|
||||
{description && <p className="opacity-70 text-xs">{description}</p>}
|
||||
</div>
|
||||
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import SettingsBackButton from './SettingsBackButton';
|
||||
import { ReactNode } from "react";
|
||||
import SettingsBackButton from "./SettingsBackButton";
|
||||
|
||||
interface SettingsPanelLayoutProps {
|
||||
title: string;
|
||||
@@ -12,16 +12,16 @@ const SettingsPanelLayout = ({
|
||||
title,
|
||||
onBack,
|
||||
children,
|
||||
className = ''
|
||||
className = "",
|
||||
}: SettingsPanelLayoutProps) => {
|
||||
return (
|
||||
<div className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}>
|
||||
<div
|
||||
className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}
|
||||
>
|
||||
<SettingsBackButton onClick={onBack} title={title} />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPanelLayout;
|
||||
export default SettingsPanelLayout;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export type AnimationState = 'entering' | 'entered' | 'exiting' | 'exited';
|
||||
export type AnimationState = "entering" | "entered" | "exiting" | "exited";
|
||||
|
||||
interface SettingsAnimationHook {
|
||||
isVisible: boolean;
|
||||
@@ -9,24 +9,24 @@ interface SettingsAnimationHook {
|
||||
startExit: () => void;
|
||||
}
|
||||
|
||||
export const useSettingsAnimation = (
|
||||
duration = 300
|
||||
): SettingsAnimationHook => {
|
||||
const [animationState, setAnimationState] = useState<AnimationState>('exited');
|
||||
export const useSettingsAnimation = (duration = 300): SettingsAnimationHook => {
|
||||
const [animationState, setAnimationState] =
|
||||
useState<AnimationState>("exited");
|
||||
|
||||
const isVisible = animationState === 'entering' || animationState === 'entered';
|
||||
const isVisible =
|
||||
animationState === "entering" || animationState === "entered";
|
||||
|
||||
const startEntry = () => {
|
||||
setAnimationState('entering');
|
||||
setAnimationState("entering");
|
||||
setTimeout(() => {
|
||||
setAnimationState('entered');
|
||||
setAnimationState("entered");
|
||||
}, duration);
|
||||
};
|
||||
|
||||
const startExit = () => {
|
||||
setAnimationState('exiting');
|
||||
setAnimationState("exiting");
|
||||
setTimeout(() => {
|
||||
setAnimationState('exited');
|
||||
setAnimationState("exited");
|
||||
}, duration);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useSettingsAnimation = (
|
||||
isVisible,
|
||||
animationState,
|
||||
startEntry,
|
||||
startExit
|
||||
startExit,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,7 +52,8 @@ export const usePanelAnimation = (isActive: boolean, duration = 300) => {
|
||||
}, [isActive, duration]);
|
||||
|
||||
const getPanelClasses = () => {
|
||||
const baseClasses = 'transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]';
|
||||
const baseClasses =
|
||||
"transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]";
|
||||
if (!mounted) return `${baseClasses} opacity-0`;
|
||||
|
||||
return isActive
|
||||
@@ -62,6 +63,6 @@ export const usePanelAnimation = (isActive: boolean, duration = 300) => {
|
||||
|
||||
return {
|
||||
mounted,
|
||||
panelClasses: getPanelClasses()
|
||||
panelClasses: getPanelClasses(),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export type SettingsRoute = 'home' | 'connections' | 'messaging' | 'privacy' | 'profile' | 'advanced' | 'billing';
|
||||
export type SettingsRoute =
|
||||
| "home"
|
||||
| "connections"
|
||||
| "messaging"
|
||||
| "privacy"
|
||||
| "profile"
|
||||
| "advanced"
|
||||
| "billing";
|
||||
|
||||
interface SettingsNavigationHook {
|
||||
currentRoute: SettingsRoute;
|
||||
@@ -17,41 +24,44 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
// Determine current settings route from URL
|
||||
const getCurrentRoute = (): SettingsRoute => {
|
||||
const path = location.pathname;
|
||||
if (path.includes('/settings/connections')) return 'connections';
|
||||
if (path.includes('/settings/messaging')) return 'messaging';
|
||||
if (path.includes('/settings/privacy')) return 'privacy';
|
||||
if (path.includes('/settings/profile')) return 'profile';
|
||||
if (path.includes('/settings/advanced')) return 'advanced';
|
||||
if (path.includes('/settings/billing')) return 'billing';
|
||||
return 'home';
|
||||
if (path.includes("/settings/connections")) return "connections";
|
||||
if (path.includes("/settings/messaging")) return "messaging";
|
||||
if (path.includes("/settings/privacy")) return "privacy";
|
||||
if (path.includes("/settings/profile")) return "profile";
|
||||
if (path.includes("/settings/advanced")) return "advanced";
|
||||
if (path.includes("/settings/billing")) return "billing";
|
||||
return "home";
|
||||
};
|
||||
|
||||
const currentRoute = getCurrentRoute();
|
||||
|
||||
const navigateToSettings = useCallback((route: SettingsRoute = 'home') => {
|
||||
if (route === 'home') {
|
||||
navigate('/settings');
|
||||
} else {
|
||||
navigate(`/settings/${route}`);
|
||||
}
|
||||
}, [navigate]);
|
||||
const navigateToSettings = useCallback(
|
||||
(route: SettingsRoute = "home") => {
|
||||
if (route === "home") {
|
||||
navigate("/settings");
|
||||
} else {
|
||||
navigate(`/settings/${route}`);
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateBack = useCallback(() => {
|
||||
if (currentRoute === 'home') {
|
||||
navigate('/home');
|
||||
if (currentRoute === "home") {
|
||||
navigate("/home");
|
||||
} else {
|
||||
navigate('/settings');
|
||||
navigate("/settings");
|
||||
}
|
||||
}, [navigate, currentRoute]);
|
||||
|
||||
const closeSettings = useCallback(() => {
|
||||
navigate('/home');
|
||||
navigate("/home");
|
||||
}, [navigate]);
|
||||
|
||||
return {
|
||||
currentRoute,
|
||||
navigateToSettings,
|
||||
navigateBack,
|
||||
closeSettings
|
||||
closeSettings,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const AdvancedPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,14 +16,32 @@ const AdvancedPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Advanced Settings</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Advanced Settings
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Configure advanced features, developer options, and system-level settings.
|
||||
Configure advanced features, developer options, and system-level
|
||||
settings.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
@@ -37,4 +55,4 @@ const AdvancedPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPanel;
|
||||
export default AdvancedPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const BillingPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,11 +16,23 @@ const BillingPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Billing & Subscription</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Billing & Subscription
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Manage your subscription, payment methods, and billing history.
|
||||
</p>
|
||||
@@ -36,4 +48,4 @@ const BillingPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BillingPanel;
|
||||
export default BillingPanel;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { selectIsAuthenticated } from '../../../store/telegramSelectors';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import TelegramConnectionModal from '../../TelegramConnectionModal';
|
||||
import { useState } from "react";
|
||||
import { useAppSelector } from "../../../store/hooks";
|
||||
import {
|
||||
selectIsAuthenticated,
|
||||
selectSessionString,
|
||||
} from "../../../store/telegramSelectors";
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
import TelegramConnectionModal from "../../TelegramConnectionModal";
|
||||
|
||||
import BinanceIcon from '../../../assets/icons/binance.svg';
|
||||
import NotionIcon from '../../../assets/icons/notion.svg';
|
||||
import TelegramIcon from '../../../assets/icons/telegram.svg';
|
||||
import MetamaskIcon from '../../../assets/icons/metamask.svg';
|
||||
import GoogleIcon from '../../../assets/icons/GoogleIcon';
|
||||
import BinanceIcon from "../../../assets/icons/binance.svg";
|
||||
import NotionIcon from "../../../assets/icons/notion.svg";
|
||||
import TelegramIcon from "../../../assets/icons/telegram.svg";
|
||||
import MetamaskIcon from "../../../assets/icons/metamask.svg";
|
||||
import GoogleIcon from "../../../assets/icons/GoogleIcon";
|
||||
|
||||
interface ConnectOption {
|
||||
id: string;
|
||||
@@ -19,61 +22,48 @@ interface ConnectOption {
|
||||
comingSoon?: boolean;
|
||||
}
|
||||
|
||||
// Reused from ConnectStep.tsx - helper to check saved session
|
||||
const hasSavedSession = (): boolean => {
|
||||
try {
|
||||
return !!localStorage.getItem('telegram_session');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ConnectionsPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
const [isTelegramModalOpen, setIsTelegramModalOpen] = useState(false);
|
||||
|
||||
// Redux state
|
||||
const isTelegramAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||
const sessionString = useAppSelector((state) => state.telegram.sessionString);
|
||||
const sessionString = useAppSelector(selectSessionString);
|
||||
|
||||
// Check if Telegram account is connected (authenticated or has saved session)
|
||||
const isTelegramConnected = useMemo(() => {
|
||||
return isTelegramAuthenticated || !!sessionString || hasSavedSession();
|
||||
}, [isTelegramAuthenticated, sessionString]);
|
||||
const isTelegramConnected = !!sessionString && isTelegramAuthenticated;
|
||||
|
||||
// Connection options - reused from ConnectStep.tsx
|
||||
const connectOptions: ConnectOption[] = [
|
||||
{
|
||||
id: 'telegram',
|
||||
name: 'Telegram',
|
||||
description: 'Organize chats, automate messages and get insights.',
|
||||
id: "telegram",
|
||||
name: "Telegram",
|
||||
description: "Organize chats, automate messages and get insights.",
|
||||
icon: <img src={TelegramIcon} alt="Telegram" className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
description: 'Manage emails, contacts and calendar events',
|
||||
id: "google",
|
||||
name: "Google",
|
||||
description: "Manage emails, contacts and calendar events",
|
||||
icon: <GoogleIcon />,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
id: 'notion',
|
||||
name: 'Notion',
|
||||
description: 'Manage tasks, documents and everything else in your Notion',
|
||||
id: "notion",
|
||||
name: "Notion",
|
||||
description: "Manage tasks, documents and everything else in your Notion",
|
||||
icon: <img src={NotionIcon} alt="Notion" className="w-5 h-5" />,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
id: 'wallet',
|
||||
name: 'Web3 Wallet',
|
||||
description: 'Trade the trenches in a safe and secure way.',
|
||||
id: "wallet",
|
||||
name: "Web3 Wallet",
|
||||
description: "Trade the trenches in a safe and secure way.",
|
||||
icon: <img src={MetamaskIcon} alt="Metamask" className="w-5 h-5" />,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
id: 'exchange',
|
||||
name: 'Crypto Trading Exchanges',
|
||||
description: 'Connect and make trades with deep insights.',
|
||||
id: "exchange",
|
||||
name: "Crypto Trading Exchanges",
|
||||
description: "Connect and make trades with deep insights.",
|
||||
icon: <img src={BinanceIcon} alt="Binance" className="w-5 h-5" />,
|
||||
comingSoon: true,
|
||||
},
|
||||
@@ -81,7 +71,7 @@ const ConnectionsPanel = () => {
|
||||
|
||||
// Check if an account is connected
|
||||
const isAccountConnected = (accountId: string): boolean => {
|
||||
if (accountId === 'telegram') {
|
||||
if (accountId === "telegram") {
|
||||
return isTelegramConnected;
|
||||
}
|
||||
// Add other account checks here when implemented
|
||||
@@ -89,17 +79,17 @@ const ConnectionsPanel = () => {
|
||||
};
|
||||
|
||||
const handleConnect = (provider: string) => {
|
||||
if (provider === 'telegram') {
|
||||
if (provider === "telegram") {
|
||||
if (isTelegramConnected) {
|
||||
// TODO: Show disconnect confirmation
|
||||
console.log('Disconnect Telegram');
|
||||
console.log("Disconnect Telegram");
|
||||
} else {
|
||||
setIsTelegramModalOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectOptions.find(opt => opt.id === provider)?.comingSoon) {
|
||||
if (connectOptions.find((opt) => opt.id === provider)?.comingSoon) {
|
||||
console.log(`${provider} coming soon`);
|
||||
return;
|
||||
}
|
||||
@@ -131,15 +121,15 @@ const ConnectionsPanel = () => {
|
||||
key={option.id}
|
||||
onClick={() => handleConnect(option.id)}
|
||||
disabled={option.comingSoon}
|
||||
className={`w-full flex items-center justify-between p-3 bg-black/50 ${
|
||||
index === connectOptions.length - 1 ? '' : 'border-b border-stone-700'
|
||||
} hover:bg-stone-800/30 transition-all duration-200 text-left ${
|
||||
index === 0 ? 'first:rounded-t-3xl' : ''
|
||||
} ${
|
||||
index === connectOptions.length - 1 ? 'last:rounded-b-3xl' : ''
|
||||
} focus:outline-none focus:ring-0 focus:border-inherit relative ${
|
||||
option.comingSoon ? 'opacity-60 cursor-not-allowed' : ''
|
||||
}`}
|
||||
className={`w-full flex items-center justify-between p-3 bg-black/50 ${index === connectOptions.length - 1
|
||||
? ""
|
||||
: "border-b border-stone-700"
|
||||
} hover:bg-stone-800/30 transition-all duration-200 text-left ${index === 0 ? "first:rounded-t-3xl" : ""
|
||||
} ${index === connectOptions.length - 1
|
||||
? "last:rounded-b-3xl"
|
||||
: ""
|
||||
} focus:outline-none focus:ring-0 focus:border-inherit relative ${option.comingSoon ? "opacity-60 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Connection status dot - top right corner */}
|
||||
{isConnected && !option.comingSoon && (
|
||||
@@ -153,9 +143,7 @@ const ConnectionsPanel = () => {
|
||||
<div className="font-medium text-sm mb-1 text-white">
|
||||
{option.name}
|
||||
</div>
|
||||
<p className="opacity-70 text-xs">
|
||||
{option.description}
|
||||
</p>
|
||||
<p className="opacity-70 text-xs">{option.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
@@ -188,14 +176,27 @@ const ConnectionsPanel = () => {
|
||||
{/* Security notice */}
|
||||
<div className="p-4 bg-blue-500/10 border border-blue-500/20 rounded-xl">
|
||||
<div className="flex items-start space-x-2">
|
||||
<svg className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-medium text-blue-300 text-sm">🔒 Privacy & Security</p>
|
||||
<p className="font-medium text-blue-300 text-sm">
|
||||
🔒 Privacy & Security
|
||||
</p>
|
||||
<p className="text-blue-200 text-xs mt-1">
|
||||
All data and credentials are stored locally with zero-data retention policy.
|
||||
Your information is encrypted and never shared with third parties.
|
||||
All data and credentials are stored locally with zero-data
|
||||
retention policy. Your information is encrypted and never
|
||||
shared with third parties.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const MessagingPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,13 +16,26 @@ const MessagingPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Messaging Settings</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Messaging Settings
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Configure your messaging preferences, notifications, and communication settings.
|
||||
Configure your messaging preferences, notifications, and
|
||||
communication settings.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
@@ -36,4 +49,4 @@ const MessagingPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MessagingPanel;
|
||||
export default MessagingPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const PrivacyPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,13 +16,26 @@ const PrivacyPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Privacy & Security</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Privacy & Security
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Manage your privacy settings, data retention policies, and security preferences.
|
||||
Manage your privacy settings, data retention policies, and
|
||||
security preferences.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
@@ -36,4 +49,4 @@ const PrivacyPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPanel;
|
||||
export default PrivacyPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const ProfilePanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,11 +16,23 @@ const ProfilePanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Profile Settings</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Profile Settings
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Update your profile information, avatar, and personal preferences.
|
||||
</p>
|
||||
@@ -36,4 +48,4 @@ const ProfilePanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePanel;
|
||||
export default ProfilePanel;
|
||||
|
||||
Reference in New Issue
Block a user