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