diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index 0f2a650..c21cfa9 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -60,8 +60,9 @@ export function AuthProvider({ children }) { return { success: true } } catch (err) { const message = err.response?.data?.message || 'Registration failed. Please try again.' + const errors = err.response?.data?.errors ?? null setAuthError(message) - return { success: false, message } + return { success: false, message, errors } } }, []) diff --git a/src/modules/auth/components/ForgotPasswordForm.jsx b/src/modules/auth/components/ForgotPasswordForm.jsx index ef0a8c1..6073581 100644 --- a/src/modules/auth/components/ForgotPasswordForm.jsx +++ b/src/modules/auth/components/ForgotPasswordForm.jsx @@ -81,6 +81,9 @@ export function ForgotPasswordForm({ className, ...props }) { const [errorMessage, setErrorMessage] = useState('') const [successDialogOpen, setSuccessDialogOpen] = useState(false) const [googleDialogOpen, setGoogleDialogOpen] = useState(false) + const [confirmDialogOpen, setConfirmDialogOpen] = useState(false) + const [pendingNewPassword, setPendingNewPassword] = useState('') + const [isConfirmingReset, setIsConfirmingReset] = useState(false) useEffect(() => { if (resendCooldown <= 0) return @@ -191,8 +194,19 @@ export function ForgotPasswordForm({ className, ...props }) { defaultValues: { new_password: '', confirm_password: '' }, }) + // Reset revokes every active session and trusted device on every one of the + // user's devices (see resetPassword in auth.controller.js) — surface that + // up front instead of silently signing the user out everywhere. const onPasswordSubmit = async ({ new_password }) => { - const result = await resetPassword({ email: pendingEmail, otp: pendingOtp, new_password }) + setPendingNewPassword(new_password) + setConfirmDialogOpen(true) + } + + const handleConfirmReset = async () => { + setIsConfirmingReset(true) + const result = await resetPassword({ email: pendingEmail, otp: pendingOtp, new_password: pendingNewPassword }) + setIsConfirmingReset(false) + setConfirmDialogOpen(false) if (!result.success) { setErrorMessage(result.message) @@ -460,6 +474,33 @@ export function ForgotPasswordForm({ className, ...props }) { + {/* Confirm Reset Dialog */} + !isConfirmingReset && setConfirmDialogOpen(open)}> + + + Sign out of all devices? + + Resetting your password will end every active session and sign you out on all + devices, including this one. You'll need to log in again with your new password + everywhere. + + + + Cancel + + {isConfirmingReset ? ( + + + Resetting... + + ) : ( + 'Reset and sign out everywhere' + )} + + + + + {/* Success Dialog */} diff --git a/src/modules/auth/components/RegisterForm.jsx b/src/modules/auth/components/RegisterForm.jsx index d152512..8f2a397 100644 --- a/src/modules/auth/components/RegisterForm.jsx +++ b/src/modules/auth/components/RegisterForm.jsx @@ -15,7 +15,7 @@ * May 23, 2026 lash0000 001 Initial creation - STAR Phase 1 Project * May 23, 2026 lash0000 002 birthday + occupation required; all calls via useAuth (register, verifyOTP, resendOTP) ***********************************************************************************************************************************************************************/ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useNavigate, Link, useSearchParams } from 'react-router-dom' import { useForm, Controller } from 'react-hook-form' import { useAuth } from '@/contexts/AuthContext' @@ -154,6 +154,27 @@ export function RegisterForm({ className, ...props }) { // Accumulated data across steps const [personalData, setPersonalData] = useState({}) + // ── Pending-verification resume (email already has an unverified account) ── + const [resumeDialogOpen, setResumeDialogOpen] = useState(false) + const [isResuming, setIsResuming] = useState(false) + const [pendingCreds, setPendingCreds] = useState(null) + + // ── Warn before the tab is closed/reloaded mid-registration ──────────────── + // Once the user reaches Credentials, submitting can already create a + // server-side account + send an OTP — losing the tab here (slow internet, + // accidental reload) is what leaves an orphaned unverified account behind. + useEffect(() => { + if (step === 0) return + + const handleBeforeUnload = (e) => { + e.preventDefault() + e.returnValue = '' + } + + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) + }, [step]) + // ── Step 1: Personal info ───────────────────────────────────────────────── const { register: regPersonal, @@ -180,45 +201,58 @@ export function RegisterForm({ className, ...props }) { defaultValues: { email: '', password: '', confirm_password: '', group_code: groupCode }, }) - const onCredentialsSubmit = async ({ email, password, group_code }) => { + const buildPersonalInfo = () => ({ + name: { + given_name: personalData.given_name, + middle_name: personalData.middle_name || '', + last_name: personalData.last_name, + extension_name: personalData.extension_name || '', + full_name: [ + `${personalData.last_name},`, + personalData.given_name, + personalData.middle_name || '', + personalData.extension_name || '', + ].filter(Boolean).join(' ').trim(), + }, + date_of_birth: personalData.date_of_birth, + occupation: personalData.occupation, + phone_number: personalData.phone + ? (() => { + const digits = personalData.phone.replace(/\D/g, '') + const number = digits.startsWith('63') + ? digits.slice(2) // strip country code if user typed +63... + : digits.replace(/^0/, '') // strip leading 0 if user typed 09... + return [{ + number, + country_code: '63', + full_number: `63${number}`, + phone_type: 'mobile', + }] + })() + : [], + addresses: [], + }) + + const submitRegistration = async ({ email, password, group_code }, { confirmResume = false } = {}) => { const result = await authRegister({ email, password, - personal_info: { - name: { - given_name: personalData.given_name, - middle_name: personalData.middle_name || '', - last_name: personalData.last_name, - extension_name: personalData.extension_name || '', - full_name: [ - `${personalData.last_name},`, - personalData.given_name, - personalData.middle_name || '', - personalData.extension_name || '', - ].filter(Boolean).join(' ').trim(), - }, - date_of_birth: personalData.date_of_birth, - occupation: personalData.occupation, - phone_number: personalData.phone - ? (() => { - const digits = personalData.phone.replace(/\D/g, '') - const number = digits.startsWith('63') - ? digits.slice(2) // strip country code if user typed +63... - : digits.replace(/^0/, '') // strip leading 0 if user typed 09... - return [{ - number, - country_code: '63', - full_number: `63${number}`, - phone_type: 'mobile', - }] - })() - : [], - addresses: [], - }, + personal_info: buildPersonalInfo(), ...(group_code ? { group_code } : {}), + ...(confirmResume ? { confirm_resume: true } : {}), }) if (!result.success) { + if (result.errors?.pendingVerification && !confirmResume) { + // Same email already has an unverified account sitting server-side — + // likely this same user retrying after a dropped connection. Ask + // before resending the OTP and reusing that account rather than + // dead-ending on a generic "already registered" error. + setPendingCreds({ email, password, group_code }) + setResumeDialogOpen(true) + return + } + setErrorMessage(result.message) setErrorDialogOpen(true) return @@ -228,6 +262,19 @@ export function RegisterForm({ className, ...props }) { setStep(2) } + const onCredentialsSubmit = (data) => submitRegistration(data) + + const handleConfirmResume = async () => { + if (!pendingCreds) return + setIsResuming(true) + try { + await submitRegistration(pendingCreds, { confirmResume: true }) + } finally { + setIsResuming(false) + setResumeDialogOpen(false) + } + } + // ───────────────────────────────────────────────────────────────────────── return ( <> @@ -576,6 +623,33 @@ export function RegisterForm({ className, ...props }) { + + !isResuming && setResumeDialogOpen(open)}> + + + Pending verification found + + This email already started registration but was never verified — likely from a dropped + connection. Resend the verification code and continue with the details you just entered? + + + + + + {isResuming ? ( + + + Sending... + + ) : ( + 'Resend code' + )} + + + + ) } \ No newline at end of file