Skip to content

Commit 5b6c9ad

Browse files
committed
load user only when needed
1 parent f2ed0c6 commit 5b6c9ad

File tree

5 files changed

+99
-144
lines changed

5 files changed

+99
-144
lines changed

src/App.tsx

Lines changed: 2 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import Terms from './pages/Terms';
66
import Privacy from './pages/Privacy';
77
import LearnMore from './pages/LearnMore';
88
import Pricing from './pages/Pricing';
9-
import {AuthProvider, useAuth} from './contexts/AuthContext';
9+
import {AuthProvider} from './contexts/AuthContext';
1010

1111
const AppContent: React.FC = () => {
1212
const [currentPage, setCurrentPage] = useState('welcome');
13-
const {user, isLoading} = useAuth();
1413

1514
useEffect(() => {
1615
const handleHashChange = () => {
@@ -42,92 +41,7 @@ const AppContent: React.FC = () => {
4241
};
4342
}, []);
4443

45-
// Show loading while checking auth state
46-
if (isLoading) {
47-
return (
48-
<div style={{
49-
minHeight: '100vh',
50-
display: 'flex',
51-
alignItems: 'center',
52-
justifyContent: 'center',
53-
backgroundColor: '#111827',
54-
padding: '20px',
55-
fontFamily: '"Fira Code", "JetBrains Mono", "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace'
56-
}}>
57-
<div style={{
58-
background: '#1f2937',
59-
border: '1px solid #374151',
60-
borderRadius: '16px',
61-
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
62-
padding: '48px 40px',
63-
textAlign: 'center',
64-
maxWidth: '400px',
65-
width: '100%'
66-
}}>
67-
<div style={{
68-
width: '48px',
69-
height: '48px',
70-
margin: '0 auto 24px',
71-
background: '#ff6b35',
72-
borderRadius: '8px',
73-
display: 'flex',
74-
alignItems: 'center',
75-
justifyContent: 'center',
76-
fontSize: '24px',
77-
color: 'white',
78-
fontWeight: 'bold'
79-
}}>
80-
S
81-
</div>
82-
<h1 style={{
83-
fontSize: '24px',
84-
fontWeight: '600',
85-
color: '#f9fafb',
86-
margin: '0 0 8px 0'
87-
}}>
88-
Salamander
89-
</h1>
90-
<p style={{
91-
fontSize: '14px',
92-
color: '#d1d5db',
93-
margin: '0 0 32px 0',
94-
fontWeight: '500'
95-
}}>
96-
Never be AFK
97-
</p>
98-
<div style={{
99-
width: '32px',
100-
height: '32px',
101-
border: '3px solid #374151',
102-
borderTop: '3px solid #ff6b35',
103-
borderRadius: '50%',
104-
animation: 'spin 1s linear infinite',
105-
margin: '0 auto'
106-
}}></div>
107-
</div>
108-
</div>
109-
);
110-
}
111-
112-
// If user is logged in and trying to access auth page, redirect to account
113-
// UNLESS there's a callback/redirect_url parameter (CLI auth flow)
114-
if (user && currentPage === 'auth') {
115-
// Parse URL parameters from the hash (e.g., #auth?callback=...)
116-
const hashParts = window.location.hash.split('?');
117-
const urlParams = new URLSearchParams(hashParts[1] || '');
118-
const hasCallback = urlParams.get('callback') || urlParams.get('redirect_url');
119-
120-
if (!hasCallback) {
121-
window.location.hash = 'account';
122-
return null;
123-
}
124-
}
125-
126-
// If user is not logged in and trying to access account page, redirect to welcome
127-
if (!user && currentPage === 'account') {
128-
window.location.hash = 'welcome';
129-
return null;
130-
}
44+
// No global auth checks - let individual pages handle their own auth requirements
13145

13246
if (currentPage === 'auth') {
13347
return <Auth/>;

src/contexts/AuthContext.tsx

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import React, {createContext, ReactNode, useCallback, useContext, useEffect, useState} from 'react';
2-
import {GoogleAuthProvider, OAuthProvider, signInWithCredential, signInWithPopup, onAuthStateChanged} from 'firebase/auth';
1+
import React, {createContext, ReactNode, useCallback, useContext, useState} from 'react';
2+
import {GoogleAuthProvider, OAuthProvider, signInWithCredential, signInWithPopup} from 'firebase/auth';
33
import {auth} from '../config/firebase';
44
import {getUserFromFirestore, User} from '../services/userService';
55

66
interface AuthContextType {
77
user: User | null;
88
isLoading: boolean;
9+
loadUserState: () => Promise<void>;
910
login: (credential: string, provider?: 'google' | 'apple') => Promise<void>;
1011
loginWithApple: () => Promise<void>;
1112
logout: () => void;
@@ -21,36 +22,32 @@ interface AuthProviderProps {
2122

2223
export const AuthProvider: React.FC<AuthProviderProps> = ({children}) => {
2324
const [user, setUser] = useState<User | null>(null);
24-
const [isLoading, setIsLoading] = useState(true);
25-
26-
useEffect(() => {
27-
// Listen to Firebase auth state changes
28-
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
29-
if (firebaseUser) {
30-
// User is signed in, fetch their profile data
31-
try {
32-
const userData = await getUserFromFirestore(firebaseUser.uid);
33-
const appUser: User = {
34-
id: userData.id,
35-
displayName: userData.displayName,
36-
email: userData.email,
37-
plan: userData.plan,
38-
messagesRemaining: userData.messagesRemaining,
39-
};
40-
setUser(appUser);
41-
} catch (error) {
42-
console.error('Error fetching user data:', error);
43-
setUser(null);
44-
}
45-
} else {
46-
// User is signed out
25+
const [isLoading, setIsLoading] = useState(false);
26+
27+
// Function to manually load user state when needed
28+
const loadUserState = useCallback(async () => {
29+
setIsLoading(true);
30+
const firebaseUser = auth.currentUser;
31+
32+
if (firebaseUser) {
33+
try {
34+
const userData = await getUserFromFirestore(firebaseUser.uid);
35+
const appUser: User = {
36+
id: userData.id,
37+
displayName: userData.displayName,
38+
email: userData.email,
39+
plan: userData.plan,
40+
messagesRemaining: userData.messagesRemaining,
41+
};
42+
setUser(appUser);
43+
} catch (error) {
44+
console.error('Error fetching user data:', error);
4745
setUser(null);
4846
}
49-
setIsLoading(false);
50-
});
51-
52-
// Cleanup subscription on unmount
53-
return () => unsubscribe();
47+
} else {
48+
setUser(null);
49+
}
50+
setIsLoading(false);
5451
}, []);
5552

5653
const login = async (credential: string, provider: 'google' | 'apple' = 'google'): Promise<void> => {
@@ -154,6 +151,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({children}) => {
154151
const value: AuthContextType = {
155152
user,
156153
isLoading,
154+
loadUserState,
157155
login,
158156
loginWithApple,
159157
logout,

src/pages/Account/Account.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {getPlansFromFirestore, getRunnersFromFirestore, Plan, Runner} from '../.
77
import {ApiError, RunnerApiService} from '../../services/apiService';
88

99
const Account: React.FC = () => {
10-
const {user, logout, refreshUserData} = useAuth();
10+
const {user, logout, refreshUserData, loadUserState, isLoading} = useAuth();
11+
12+
useEffect(() => {
13+
// Load user state when Account page mounts
14+
loadUserState();
15+
}, [loadUserState]);
1116
const [plans, setPlans] = useState<Plan[]>([]);
1217
const [isLoadingPlans, setIsLoadingPlans] = useState(true);
1318
const [runners, setRunners] = useState<Runner[]>([]);
@@ -147,8 +152,31 @@ const Account: React.FC = () => {
147152
};
148153

149154

155+
// If no user after loading, redirect to auth
156+
if (!user && !isLoading) {
157+
window.location.hash = 'auth';
158+
return null;
159+
}
160+
161+
// Still loading user state
150162
if (!user) {
151-
return null; // This should not happen since App component handles redirects
163+
return (
164+
<div className="account-container">
165+
<Header isSubpage={true}/>
166+
<div className="account-content" style={{paddingTop: '6rem', textAlign: 'center'}}>
167+
<div style={{
168+
width: '32px',
169+
height: '32px',
170+
border: '3px solid #374151',
171+
borderTop: '3px solid #ff6b35',
172+
borderRadius: '50%',
173+
animation: 'spin 1s linear infinite',
174+
margin: '0 auto 16px'
175+
}}></div>
176+
<p style={{ color: '#d1d5db' }}>Loading your account...</p>
177+
</div>
178+
</div>
179+
);
152180
}
153181

154182
if (isLoadingPlans || isLoadingRunners) {

src/pages/Auth/Auth.tsx

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react';
22
import './Auth.css';
33
import {useAuth} from '../../contexts/AuthContext';
44
import {auth} from '../../config/firebase';
5+
import {User as FirebaseUser} from 'firebase/auth';
56

67
const CLIENT_ID = '87955960620-dv9h8pfv4a97mno598dcc3m1nlt0h6u4.apps.googleusercontent.com';
78

@@ -22,17 +23,38 @@ declare global {
2223

2324
const Auth: React.FC = () => {
2425
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
25-
const {user, login, loginWithApple} = useAuth();
26+
const {login, loginWithApple} = useAuth();
2627
const [isGenerating, setIsGenerating] = useState(false);
2728
const [isRedirecting, setIsRedirecting] = useState(false);
29+
const [firebaseUser, setFirebaseUser] = useState<FirebaseUser | null>(null);
30+
const [checkingAuth, setCheckingAuth] = useState(true);
2831

2932
// Get redirect URL from URL params (for CLI integration)
3033
// Parse URL parameters from the hash (e.g., #auth?callback=...)
3134
const hashParts = window.location.hash.split('?');
3235
const urlParams = new URLSearchParams(hashParts[1] || '');
3336
const redirectUrl = urlParams.get('redirect_url') || urlParams.get('callback');
3437

35-
const generateTokenAndRedirect = async () => {
38+
// Check Firebase auth state on mount
39+
useEffect(() => {
40+
const unsubscribe = auth.onAuthStateChanged((user) => {
41+
setFirebaseUser(user);
42+
setCheckingAuth(false);
43+
44+
// If user is signed in and we have a callback URL, generate token and redirect
45+
if (user && redirectUrl) {
46+
generateTokenAndRedirect(user);
47+
} else if (user && !redirectUrl) {
48+
// Normal web flow - redirect to account
49+
window.location.hash = 'account';
50+
}
51+
});
52+
53+
return () => unsubscribe();
54+
// eslint-disable-next-line react-hooks/exhaustive-deps
55+
}, [redirectUrl]);
56+
57+
const generateTokenAndRedirect = async (user: FirebaseUser) => {
3658
if (!redirectUrl || !user) return;
3759

3860
setIsGenerating(true);
@@ -72,29 +94,19 @@ const Auth: React.FC = () => {
7294
}
7395
};
7496

75-
useEffect(() => {
76-
// If user is signed in and we have a redirect URL, generate token and redirect
77-
if (user && redirectUrl && !isGenerating && !isRedirecting) {
78-
generateTokenAndRedirect();
79-
}
80-
// eslint-disable-next-line react-hooks/exhaustive-deps
81-
}, [user, redirectUrl, isGenerating, isRedirecting]);
97+
// This useEffect is no longer needed as we handle auth state in the onAuthStateChanged above
8298

8399
const handleCredentialResponse = useCallback(async (response: any) => {
84100
console.log('Encoded JWT ID token: ' + response.credential);
85101

86102
try {
87103
await login(response.credential);
88-
// If we have a redirect URL, the useEffect will handle the redirect
89-
if (!redirectUrl) {
90-
// Redirect to account page after successful login
91-
window.location.hash = 'account';
92-
}
104+
// Auth state change will handle the redirect automatically
93105
} catch (error) {
94106
console.error('Failed to process login:', error);
95107
alert(error instanceof Error ? error.message : 'Sign-in failed. Please try again.');
96108
}
97-
}, [login, redirectUrl]);
109+
}, [login]);
98110

99111
useEffect(() => {
100112
const initializeGoogle = () => {
@@ -131,19 +143,15 @@ const Auth: React.FC = () => {
131143
const handleAppleAuth = async () => {
132144
try {
133145
await loginWithApple();
134-
// If we have a redirect URL, the useEffect will handle the redirect
135-
if (!redirectUrl) {
136-
// Redirect to account page after successful login
137-
window.location.hash = 'account';
138-
}
146+
// Auth state change will handle the redirect automatically
139147
} catch (error) {
140148
console.error('Apple sign-in failed:', error);
141149
alert(error instanceof Error ? error.message : 'Apple sign-in failed. Please try again.');
142150
}
143151
};
144152

145-
// Show loading state if generating token or redirecting
146-
if (isGenerating || isRedirecting) {
153+
// Show loading state if checking auth, generating token, or redirecting
154+
if (checkingAuth || isGenerating || isRedirecting) {
147155
return (
148156
<div className="auth-container">
149157
<div className="auth-card">
@@ -164,7 +172,7 @@ const Auth: React.FC = () => {
164172
margin: '0 auto 16px'
165173
}}></div>
166174
<p style={{ color: '#d1d5db', fontSize: '14px' }}>
167-
{isRedirecting ? 'Redirecting to CLI...' : 'Generating authentication token...'}
175+
{isRedirecting ? 'Redirecting to CLI...' : isGenerating ? 'Generating authentication token...' : 'Loading...'}
168176
</p>
169177
{isRedirecting && (
170178
<p style={{ color: '#9ca3af', fontSize: '12px', marginTop: '16px', fontStyle: 'italic' }}>

src/pages/Welcome/Welcome.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import './Welcome.css';
33
import Footer from '../../components/Footer';
44
import Header from '../../components/Header';
5+
import { useAuth } from '../../contexts/AuthContext';
56

67
const Welcome: React.FC = () => {
8+
const { loadUserState } = useAuth();
9+
10+
useEffect(() => {
11+
// Load user state when Welcome page mounts
12+
loadUserState();
13+
}, [loadUserState]);
714
return (
815
<div className="welcome">
916
<Header/>

0 commit comments

Comments
 (0)