build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17

This commit is contained in:
AppCakes
2026-07-04 11:56:40 +00:00
commit 98554b0086
149 changed files with 26648 additions and 0 deletions
+317
View File
@@ -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;