build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonInput,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { chevronBackOutline, lockClosedOutline } from 'ionicons/icons';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { supabase } from '../supabase';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import OtpInputSlots from '../components/OtpInputSlots';
|
||||
import '../styles/auth.css';
|
||||
|
||||
interface LocationState {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
const VerifyResetPage: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation<LocationState>();
|
||||
const { user, refreshProfile, profileStatus } = useAuth();
|
||||
const email = location.state?.email;
|
||||
|
||||
const [code, setCode] = useState<string[]>(['', '', '', '', '', '']);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const [passwordUpdated, setPasswordUpdated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!email) {
|
||||
history.replace('/forgot-password');
|
||||
}
|
||||
}, [email, history]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cooldown <= 0) return;
|
||||
const timer = setTimeout(() => setCooldown((current) => current - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [cooldown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && passwordUpdated && profileStatus !== 'loading') {
|
||||
loadProfileAndNavigate();
|
||||
}
|
||||
}, [user, passwordUpdated, profileStatus]);
|
||||
|
||||
const showMessage = (message: string, kind: 'error' | 'status') => {
|
||||
if (kind === 'error') {
|
||||
setError(message);
|
||||
setStatus(null);
|
||||
setTimeout(() => setError(null), 4000);
|
||||
} else {
|
||||
setStatus(message);
|
||||
setError(null);
|
||||
setTimeout(() => setStatus(null), 4000);
|
||||
}
|
||||
};
|
||||
|
||||
const loadProfileAndNavigate = async () => {
|
||||
if (!user) return;
|
||||
|
||||
const data = await refreshProfile();
|
||||
|
||||
if (
|
||||
!data ||
|
||||
!data.first_name ||
|
||||
!data.last_name ||
|
||||
!data.phone ||
|
||||
!data.country_of_residence
|
||||
) {
|
||||
history.replace('/setup-profile');
|
||||
} else {
|
||||
history.replace('/home');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDigitChange = (index: number, value: string) => {
|
||||
const nextCode = [...code];
|
||||
nextCode[index] = value.replace(/\D/g, '').slice(-1);
|
||||
setCode(nextCode);
|
||||
};
|
||||
|
||||
const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
const digits = event.clipboardData
|
||||
.getData('Text')
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, 6);
|
||||
if (!digits) return;
|
||||
|
||||
const nextCode = ['', '', '', '', '', ''];
|
||||
digits.split('').forEach((digit, index) => {
|
||||
nextCode[index] = digit;
|
||||
});
|
||||
setCode(nextCode);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const token = code.join('');
|
||||
if (token.length !== 6) {
|
||||
showMessage('Please enter the 6-digit reset code', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
showMessage('Password must be at least 6 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setStatus(null);
|
||||
|
||||
const { error: verifyError } = await supabase.auth.verifyOtp({
|
||||
email: email!,
|
||||
token,
|
||||
type: 'recovery',
|
||||
});
|
||||
|
||||
if (verifyError) {
|
||||
showMessage(verifyError.message, 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
if (updateError) {
|
||||
showMessage(updateError.message, 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage('Password updated', 'status');
|
||||
setPasswordUpdated(true);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
if (cooldown > 0 || !email) return;
|
||||
|
||||
const { error: resendError } =
|
||||
await supabase.auth.resetPasswordForEmail(email);
|
||||
if (resendError) {
|
||||
showMessage(resendError.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage('A new reset code has been sent', 'status');
|
||||
setCooldown(60);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage style={{ backgroundColor: '#fafafa' }}>
|
||||
<IonHeader className="ion-no-border">
|
||||
<IonToolbar
|
||||
style={
|
||||
{
|
||||
'--background': 'transparent',
|
||||
'--border-width': '0px',
|
||||
'--color': '#111827',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<IonButtons slot="start">
|
||||
<IonButton
|
||||
fill="clear"
|
||||
onClick={() => history.goBack()}
|
||||
style={
|
||||
{
|
||||
'--color': '#111827',
|
||||
'--border-radius': '12px',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
aria-label="Go back"
|
||||
>
|
||||
<IonIcon icon={chevronBackOutline} slot="icon-only" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle style={{ fontSize: '18px', fontWeight: 700 }}>
|
||||
New password
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent
|
||||
className="auth-content"
|
||||
style={
|
||||
{
|
||||
'--background': 'linear-gradient(180deg, #fafafa 0%, #f4f0ff 100%)',
|
||||
'--padding-start': '0px',
|
||||
'--padding-end': '0px',
|
||||
'--padding-top': '0px',
|
||||
'--padding-bottom': '0px',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="auth-shell">
|
||||
<div
|
||||
className="auth-intro-block"
|
||||
style={{ alignItems: 'flex-start', textAlign: 'left' }}
|
||||
>
|
||||
<div className="auth-icon-container">
|
||||
<IonIcon icon={lockClosedOutline} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="auth-intro-heading">Enter your reset code</h1>
|
||||
<p className="auth-intro-body" style={{ marginTop: '8px' }}>
|
||||
Use the 6-digit code sent to <strong>{email}</strong>, then
|
||||
choose a new password.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="auth-form-card" style={{ marginBottom: '12px' }}>
|
||||
<OtpInputSlots
|
||||
value={code}
|
||||
onChange={handleDigitChange}
|
||||
onPaste={handlePaste}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResend}
|
||||
disabled={cooldown > 0 || loading}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
padding: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 700,
|
||||
color: cooldown > 0 ? '#9ca3af' : '#6d28d9',
|
||||
}}
|
||||
>
|
||||
{cooldown > 0 ? `Resend code in ${cooldown}s` : 'Resend code'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="auth-form-card">
|
||||
<IonInput
|
||||
type="password"
|
||||
label="New password"
|
||||
labelPlacement="floating"
|
||||
value={newPassword}
|
||||
onIonInput={(event) => setNewPassword(event.detail.value ?? '')}
|
||||
disabled={loading}
|
||||
placeholder="Enter a new password"
|
||||
style={
|
||||
{
|
||||
'--background': '#fafafa',
|
||||
'--border-radius': '12px',
|
||||
'--padding-start': '14px',
|
||||
'--padding-end': '14px',
|
||||
'--highlight-color-focused': '#6d28d9',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="auth-status-text" style={{ color: '#dc2626' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{status && (
|
||||
<p className="auth-status-text" style={{ color: '#16a34a' }}>
|
||||
{status}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<IonButton
|
||||
type="submit"
|
||||
expand="block"
|
||||
disabled={
|
||||
loading ||
|
||||
code.join('').length !== 6 ||
|
||||
newPassword.length < 6
|
||||
}
|
||||
style={
|
||||
{
|
||||
'--background': '#6d28d9',
|
||||
'--background-activated': '#5b21b6',
|
||||
'--border-radius': '999px',
|
||||
'--box-shadow': 'none',
|
||||
'--color': '#ffffff',
|
||||
height: '52px',
|
||||
fontSize: '15px',
|
||||
fontWeight: 700,
|
||||
marginTop: '4px',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{loading ? 'Updating password...' : 'Update password'}
|
||||
</IonButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyResetPage;
|
||||
Reference in New Issue
Block a user