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:
Steven Enamakel
2026-01-29 07:38:34 +05:30
committed by GitHub
parent f6d5f94caa
commit 520bfdb74c
139 changed files with 4365 additions and 2901 deletions
+18 -15
View File
@@ -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 && (
+4 -3
View File
@@ -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 />;
+138 -41
View File
@@ -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;
+12 -6
View File
@@ -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>
+11 -4
View File
@@ -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 = {
+7 -2
View File
@@ -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>
+5 -2
View File
@@ -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"
}`}
/>
))}
+6 -5
View File
@@ -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}</>;
+5 -4
View File
@@ -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
+18 -10
View File
@@ -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>
+323 -147
View File
@@ -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 &gt; Devices &gt; Link Desktop Device</p>
<p className="opacity-90 text-sm">
Go to Settings &gt; Devices &gt; 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>
+9 -285
View File
@@ -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>
);
};
+13 -4
View File
@@ -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}`}>
+165 -70
View File
@@ -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 (
+9 -9
View File
@@ -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;
+13 -13
View File
@@ -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;