From b77260cc671ad5cffdcf70fef5ec5ecc1f9d5513 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 3 Sep 2025 14:10:44 -0600 Subject: [PATCH] Better login handling --- .gitignore | 1 + .../components/features/auth/LoginForm.tsx | 95 +++++++++++++------ .../components/features/auth/useLastUsed.tsx | 34 +++++++ .../Supabase/SupabaseContextProvider.tsx | 8 +- .../src/context/Supabase/supabase-hooks.ts | 5 + apps/web-tss/src/routes/auth.login.tsx | 3 +- 6 files changed, 115 insertions(+), 31 deletions(-) create mode 100644 apps/web-tss/src/components/features/auth/useLastUsed.tsx diff --git a/.gitignore b/.gitignore index c99174f27..11c95ef89 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ drizzle/meta/ *.tsbuildinfo /packages/aTest/.mastra **/*.private.* +apps/web-tss/.env.prod diff --git a/apps/web-tss/src/components/features/auth/LoginForm.tsx b/apps/web-tss/src/components/features/auth/LoginForm.tsx index 4252110de..8ad5f51b2 100644 --- a/apps/web-tss/src/components/features/auth/LoginForm.tsx +++ b/apps/web-tss/src/components/features/auth/LoginForm.tsx @@ -8,6 +8,7 @@ import Github from '@/components/ui/icons/customIcons/Github'; import Google from '@/components/ui/icons/customIcons/Google'; import Microsoft from '@/components/ui/icons/customIcons/Microsoft'; import { Input } from '@/components/ui/inputs'; +import { AppTooltip } from '@/components/ui/tooltip'; import { Text, Title } from '@/components/ui/typography'; import { env } from '@/env'; import { useMemoizedFn } from '@/hooks/useMemoizedFn'; @@ -22,12 +23,20 @@ import { cn } from '@/lib/classMerge'; import { isValidEmail } from '@/lib/email'; import { inputHasText } from '@/lib/text'; import { PolicyCheck } from './PolicyCheck'; +import { type LastUsedReturnType, useLastUsed } from './useLastUsed'; -export const LoginForm: React.FC<{ redirectTo: string | null | undefined }> = ({ redirectTo }) => { +export const LoginForm: React.FC<{ + redirectTo: string | null | undefined; + isAnonymousUser?: boolean; +}> = ({ redirectTo }) => { const navigate = useNavigate(); + const lastUsedProps = useLastUsed(); + const [loading, setLoading] = useState<'google' | 'github' | 'azure' | 'email' | null>(null); const [errorMessages, setErrorMessages] = useState([]); - const [signUpFlow, setSignUpFlow] = useState(true); + const [signUpFlow, setSignUpFlow] = useState( + lastUsedProps.isAnonymousUser && !env.VITE_PUBLIC_USER + ); const [signUpSuccess, setSignUpSuccess] = useState(false); const onSignInWithUsernameAndPassword = useMemoizedFn( @@ -162,6 +171,7 @@ export const LoginForm: React.FC<{ redirectTo: string | null | undefined }> = ({ onSignInWithGoogle={onSignInWithGoogle} onSignInWithGithub={onSignInWithGithub} onSignInWithAzure={onSignInWithAzure} + lastUsedProps={lastUsedProps} /> )} @@ -179,6 +189,7 @@ const LoginOptions: React.FC<{ loading: 'google' | 'github' | 'azure' | 'email' | null; setErrorMessages: (value: string[]) => void; signUpFlow: boolean; + lastUsedProps: LastUsedReturnType; }> = ({ onSubmitClick, onSignInWithGoogle, @@ -189,11 +200,15 @@ const LoginOptions: React.FC<{ loading, setErrorMessages, signUpFlow, + lastUsedProps, }) => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [email, setEmail] = useState( + lastUsedProps.supabaseUser?.email || env.VITE_PUBLIC_USER || '' + ); + const [password, setPassword] = useState(env.VITE_PUBLIC_USER_PASSWORD || ''); const [password2, setPassword2] = useState(''); const [passwordCheck, setPasswordCheck] = useState(false); + const navigate = useNavigate(); const disableSubmitButton = !inputHasText(password) || !inputHasText(password2) || password !== password2 || !passwordCheck; @@ -209,13 +224,13 @@ const LoginOptions: React.FC<{ useHotkeys( 'meta+shift+b', - () => { + (e) => { + e.preventDefault(); setSignUpFlow(false); const DEFAULT_CREDENTIALS = { email: env.VITE_PUBLIC_USER || '', password: env.VITE_PUBLIC_USER_PASSWORD || '', }; - console.log('DEFAULT_CREDENTIALS', DEFAULT_CREDENTIALS); onSubmitClick({ email: DEFAULT_CREDENTIALS.email, @@ -228,7 +243,7 @@ const LoginOptions: React.FC<{ return ( <>
- +
@@ -358,6 +373,14 @@ const LoginOptions: React.FC<{ > {!signUpFlow ? 'Sign in' : 'Sign up'} + + {lastUsedProps.isCurrentlySignedIn && !signUpFlow && ( + + + + )}
@@ -366,6 +389,7 @@ const LoginOptions: React.FC<{ setPassword2={setPassword2} setSignUpFlow={setSignUpFlow} signUpFlow={signUpFlow} + lastUsedProps={lastUsedProps} /> {!signUpFlow && } @@ -401,13 +425,21 @@ const SignUpSuccess: React.FC<{ const WelcomeText: React.FC<{ signUpFlow: boolean; -}> = ({ signUpFlow }) => { + lastUsedProps: LastUsedReturnType; +}> = ({ signUpFlow, lastUsedProps }) => { const text = !signUpFlow ? 'Sign in' : 'Sign up for free'; + const { isCurrentlySignedIn, supabaseUser } = lastUsedProps; return ( - - {text} - +
+ + + {text} + + +
); }; @@ -426,25 +458,30 @@ const AlreadyHaveAccount: React.FC<{ setPassword2: (value: string) => void; setSignUpFlow: (value: boolean) => void; signUpFlow: boolean; -}> = React.memo(({ setErrorMessages, setPassword2, setSignUpFlow, signUpFlow }) => { - return ( -
- - {signUpFlow ? 'Already have an account? ' : "Don't already have an account?"} - + lastUsedProps: LastUsedReturnType; +}> = React.memo(({ setErrorMessages, setPassword2, setSignUpFlow, signUpFlow, lastUsedProps }) => { + const { isCurrentlySignedIn, supabaseUser } = lastUsedProps; - { - setErrorMessages([]); - setPassword2(''); - setSignUpFlow(!signUpFlow); - }} - > - {!signUpFlow ? 'Sign up' : 'Sign in'} - + return ( +
+
+ + {signUpFlow ? 'Already have an account? ' : "Don't already have an account?"} + + + { + setErrorMessages([]); + setPassword2(''); + setSignUpFlow(!signUpFlow); + }} + > + {!signUpFlow ? 'Sign up' : 'Sign in'} + +
); }); diff --git a/apps/web-tss/src/components/features/auth/useLastUsed.tsx b/apps/web-tss/src/components/features/auth/useLastUsed.tsx new file mode 100644 index 000000000..cc144d5ff --- /dev/null +++ b/apps/web-tss/src/components/features/auth/useLastUsed.tsx @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { useGetSupabaseUser } from '@/context/Supabase'; + +export type SignInType = 'google' | 'github' | 'azure' | 'email' | null; + +export const useLastUsed = () => { + const supabaseUser = useGetSupabaseUser(); + + const provider = supabaseUser?.app_metadata?.provider; + const isAnonymousUser = !!supabaseUser?.is_anonymous; + const isCurrentlySignedIn = !isAnonymousUser && !!supabaseUser?.id; + + const lastUsed: SignInType = useMemo(() => { + if (provider === 'google') { + return 'google'; + } else if (provider === 'github') { + return 'github'; + } else if (provider === 'azure') { + return 'azure'; + } else if (provider === 'email' && !isAnonymousUser) { + return 'email'; + } + return null; + }, [provider]); + + return { + lastUsed, + isAnonymousUser, + isCurrentlySignedIn, + supabaseUser, + }; +}; + +export type LastUsedReturnType = ReturnType; diff --git a/apps/web-tss/src/context/Supabase/SupabaseContextProvider.tsx b/apps/web-tss/src/context/Supabase/SupabaseContextProvider.tsx index 36174f4cd..e02406138 100644 --- a/apps/web-tss/src/context/Supabase/SupabaseContextProvider.tsx +++ b/apps/web-tss/src/context/Supabase/SupabaseContextProvider.tsx @@ -8,6 +8,9 @@ export type SupabaseContextType = { user: { id: string; is_anonymous: boolean; + email: string; + identities: User['identities']; + app_metadata: User['app_metadata']; }; accessToken: string; }; @@ -20,7 +23,10 @@ const useSupabaseContextInternal = ({ accessToken: accessTokenProp, }: SupabaseContextType) => { const refreshTimerRef = useRef | undefined>(undefined); - const [supabaseUser, setSupabaseUser] = useState | null>(user); + const [supabaseUser, setSupabaseUser] = useState | null>(user); const [accessToken, setAccessToken] = useState(accessTokenProp); const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true; diff --git a/apps/web-tss/src/context/Supabase/supabase-hooks.ts b/apps/web-tss/src/context/Supabase/supabase-hooks.ts index b7ee83e7f..6b293a0dd 100644 --- a/apps/web-tss/src/context/Supabase/supabase-hooks.ts +++ b/apps/web-tss/src/context/Supabase/supabase-hooks.ts @@ -10,3 +10,8 @@ const stableIsAnonymousUserSelector = (state: SupabaseContextReturnType) => stat export const useIsAnonymousSupabaseUser = () => { return useContextSelector(SupabaseContext, stableIsAnonymousUserSelector); }; + +const stableSupabaseUserSelector = (state: SupabaseContextReturnType) => state.supabaseUser; +export const useGetSupabaseUser = () => { + return useContextSelector(SupabaseContext, stableSupabaseUserSelector); +}; diff --git a/apps/web-tss/src/routes/auth.login.tsx b/apps/web-tss/src/routes/auth.login.tsx index 70e1d9b4b..5383d975f 100644 --- a/apps/web-tss/src/routes/auth.login.tsx +++ b/apps/web-tss/src/routes/auth.login.tsx @@ -1,4 +1,4 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { ClientOnly, createFileRoute } from '@tanstack/react-router'; import { z } from 'zod'; import { LoginForm } from '@/components/features/auth/LoginForm'; @@ -21,6 +21,7 @@ export const Route = createFileRoute('/auth/login')({ function LoginComp() { const { next } = Route.useSearch(); + const cxt = Route.useRouteContext(); return ; }