274 lines
7.8 KiB
TypeScript
274 lines
7.8 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
|
import {
|
|
IonButton,
|
|
IonCard,
|
|
IonCardContent,
|
|
IonContent,
|
|
IonIcon,
|
|
IonInput,
|
|
IonItem,
|
|
IonLabel,
|
|
IonPage,
|
|
IonSegment,
|
|
IonSegmentButton,
|
|
IonText,
|
|
useIonViewWillEnter,
|
|
} from '@ionic/react';
|
|
import { logoGoogle } from 'ionicons/icons';
|
|
import { FirebaseAuthentication } from '@capacitor-firebase/authentication';
|
|
import { Redirect, useHistory } from 'react-router-dom';
|
|
import {
|
|
Style,
|
|
setStatusBarBackground,
|
|
setStatusBarStyle,
|
|
} from '../utils/statusBar';
|
|
import { supabase } from '../supabase';
|
|
|
|
const Auth: React.FC = () => {
|
|
const history = useHistory();
|
|
const [mode, setMode] = useState<'login' | 'signup'>('login');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [googleLoading, setGoogleLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [info, setInfo] = useState('');
|
|
const [verificationMessage, setVerificationMessage] = useState('');
|
|
const [sessionChecked, setSessionChecked] = useState(false);
|
|
const [hasSession, setHasSession] = useState(false);
|
|
|
|
useIonViewWillEnter(() => {
|
|
setStatusBarStyle(Style.Dark);
|
|
setStatusBarBackground('#f6f7fb');
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
let mounted = true;
|
|
|
|
supabase.auth.getSession().then(({ data }) => {
|
|
if (!mounted) return;
|
|
setHasSession(Boolean(data.session));
|
|
setSessionChecked(true);
|
|
});
|
|
|
|
const {
|
|
data: { subscription },
|
|
} = supabase.auth.onAuthStateChange((_event, session) => {
|
|
if (!mounted) return;
|
|
setHasSession(Boolean(session));
|
|
setSessionChecked(true);
|
|
});
|
|
|
|
return () => {
|
|
mounted = false;
|
|
subscription.unsubscribe();
|
|
};
|
|
}, []);
|
|
|
|
const formValid = useMemo(() => {
|
|
return email.trim().length > 3 && password.length >= 6;
|
|
}, [email, password]);
|
|
|
|
const handleSubmit = async () => {
|
|
setError('');
|
|
setInfo('');
|
|
setVerificationMessage('');
|
|
setLoading(true);
|
|
|
|
try {
|
|
if (mode === 'login') {
|
|
const { error: signInError } = await supabase.auth.signInWithPassword({
|
|
email: email.trim(),
|
|
password,
|
|
});
|
|
|
|
if (signInError) {
|
|
setError(signInError.message);
|
|
return;
|
|
}
|
|
|
|
history.replace('/home');
|
|
return;
|
|
}
|
|
|
|
const { data, error: signUpError } = await supabase.auth.signUp({
|
|
email: email.trim(),
|
|
password,
|
|
});
|
|
|
|
if (signUpError) {
|
|
setError(signUpError.message);
|
|
return;
|
|
}
|
|
|
|
if (data.session) {
|
|
history.replace('/home');
|
|
return;
|
|
}
|
|
|
|
setVerificationMessage(
|
|
'Account created! Check your email for a verification link.'
|
|
);
|
|
setInfo('Once verified, come back and sign in.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleGoogleLogin = async () => {
|
|
setError('');
|
|
setInfo('');
|
|
setVerificationMessage('');
|
|
setGoogleLoading(true);
|
|
|
|
try {
|
|
const result = await FirebaseAuthentication.signInWithGoogle();
|
|
const idToken = result.credential?.idToken;
|
|
|
|
if (!idToken) {
|
|
setError('Google sign-in did not return an ID token.');
|
|
return;
|
|
}
|
|
|
|
const { error: googleAuthError } = await supabase.auth.signInWithIdToken({
|
|
provider: 'google',
|
|
token: idToken,
|
|
});
|
|
|
|
if (googleAuthError) {
|
|
setError(googleAuthError.message);
|
|
return;
|
|
}
|
|
|
|
history.replace('/home');
|
|
} catch (err) {
|
|
const message =
|
|
err instanceof Error ? err.message : 'Google sign-in failed.';
|
|
setError(message);
|
|
} finally {
|
|
setGoogleLoading(false);
|
|
}
|
|
};
|
|
|
|
if (sessionChecked && hasSession) {
|
|
return <Redirect to="/home" />;
|
|
}
|
|
|
|
return (
|
|
<IonPage>
|
|
<IonContent fullscreen style={{ '--background': '#f6f7fb' }}>
|
|
<div className="auth-shell">
|
|
<div className="auth-hero">
|
|
<span className="eyebrow">Protect your streaks</span>
|
|
<h1>Build habits that actually stick.</h1>
|
|
<p>
|
|
Track daily wins, recover faster from misses, and see which habits
|
|
are getting real momentum.
|
|
</p>
|
|
</div>
|
|
|
|
<IonCard className="auth-card">
|
|
<IonCardContent>
|
|
<IonSegment
|
|
value={mode}
|
|
onIonChange={(e) =>
|
|
setMode(e.detail.value as 'login' | 'signup')
|
|
}
|
|
>
|
|
<IonSegmentButton value="login">
|
|
<IonLabel>Login</IonLabel>
|
|
</IonSegmentButton>
|
|
<IonSegmentButton value="signup">
|
|
<IonLabel>Sign Up</IonLabel>
|
|
</IonSegmentButton>
|
|
</IonSegment>
|
|
|
|
{verificationMessage ? (
|
|
<div className="auth-message-block">
|
|
<h2>Check your inbox</h2>
|
|
<p>{verificationMessage}</p>
|
|
<IonText color="medium">
|
|
<p>{info}</p>
|
|
</IonText>
|
|
<IonButton fill="clear" onClick={() => setMode('login')}>
|
|
Back to Login
|
|
</IonButton>
|
|
</div>
|
|
) : (
|
|
<div className="auth-form">
|
|
<IonItem lines="none" className="auth-input-item">
|
|
<IonInput
|
|
type="email"
|
|
label="Email"
|
|
labelPlacement="stacked"
|
|
value={email}
|
|
placeholder="you@example.com"
|
|
onIonInput={(e) => setEmail(e.detail.value ?? '')}
|
|
/>
|
|
</IonItem>
|
|
|
|
<IonItem lines="none" className="auth-input-item">
|
|
<IonInput
|
|
type="password"
|
|
label="Password"
|
|
labelPlacement="stacked"
|
|
value={password}
|
|
placeholder="At least 6 characters"
|
|
onIonInput={(e) => setPassword(e.detail.value ?? '')}
|
|
/>
|
|
</IonItem>
|
|
|
|
{error ? (
|
|
<IonText color="danger">
|
|
<p className="auth-feedback">{error}</p>
|
|
</IonText>
|
|
) : null}
|
|
|
|
{info ? (
|
|
<IonText color="medium">
|
|
<p className="auth-feedback">{info}</p>
|
|
</IonText>
|
|
) : null}
|
|
|
|
<IonButton
|
|
expand="block"
|
|
disabled={!formValid || loading || googleLoading}
|
|
onClick={handleSubmit}
|
|
>
|
|
{loading
|
|
? 'Please wait…'
|
|
: mode === 'login'
|
|
? 'Log In'
|
|
: 'Create Account'}
|
|
</IonButton>
|
|
|
|
{mode === 'login' ? (
|
|
<>
|
|
<div className="auth-divider">
|
|
<span>or</span>
|
|
</div>
|
|
|
|
<IonButton
|
|
expand="block"
|
|
fill="outline"
|
|
className="google-auth-button"
|
|
disabled={loading || googleLoading}
|
|
onClick={handleGoogleLogin}
|
|
>
|
|
<IonIcon icon={logoGoogle} slot="start" />
|
|
{googleLoading ? 'Connecting…' : 'Continue with Google'}
|
|
</IonButton>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</IonCardContent>
|
|
</IonCard>
|
|
</div>
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default Auth;
|