From bf9d265d9321bc2e2f921c3162aff9a55e9ed7d8 Mon Sep 17 00:00:00 2001 From: directorfloo Date: Thu, 27 Aug 2026 21:45:32 +0100 Subject: [PATCH] feat: add account deletion functionality with confirmation modal --- src/app/dashboard/settings/page.tsx | 24 ++- src/components/auth/DeleteAccountModal.tsx | 206 +++++++++++++++++++++ src/lib/api/services.ts | 3 + src/lib/hooks/use-auth-store.ts | 12 ++ 4 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/components/auth/DeleteAccountModal.tsx diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 1ff8fea..3d62408 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1,8 +1,9 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Bell, Shield, Save, Loader2 } from 'lucide-react'; +import { Bell, Shield, Save, Loader2, Trash2 } from 'lucide-react'; import { Breadcrumb } from '@/components/ui'; +import DeleteAccountModal from '@/components/auth/DeleteAccountModal'; export default function SettingsPage() { const [saving, setSaving] = useState(false); @@ -12,6 +13,7 @@ export default function SettingsPage() { const [emailUpdates, setEmailUpdates] = useState(true); const [courseUpdates, setCourseUpdates] = useState(true); const [securityAlerts, setSecurityAlerts] = useState(true); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); useEffect(() => { // No dedicated settings endpoints found in current client services. @@ -134,6 +136,26 @@ export default function SettingsPage() { Preferences will be updated once backend endpoints are available. + +
+
+
+
+
+

+ Permanently delete your account, progress, certificates, and course access. +

+
+ +
+
+ + setDeleteModalOpen(false)} /> ); } diff --git a/src/components/auth/DeleteAccountModal.tsx b/src/components/auth/DeleteAccountModal.tsx new file mode 100644 index 0000000..324b765 --- /dev/null +++ b/src/components/auth/DeleteAccountModal.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { AlertTriangle, Check, Loader2, X } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { useAuthStore } from '@/lib/hooks/use-auth-store'; + +type DeleteAccountFormValues = { + understood: boolean; + password: string; +}; + +type DeleteAccountModalProps = { + open: boolean; + onClose: () => void; +}; + +const consequences = [ + 'All course progress and enrolled course access will be permanently removed.', + 'Your certificates and learning history will be deleted.', + 'Any pending refunds or account-related claims may no longer be available.', +]; + +export default function DeleteAccountModal({ open, onClose }: DeleteAccountModalProps) { + const router = useRouter(); + const deleteAccount = useAuthStore((state) => state.deleteAccount); + const [step, setStep] = useState<1 | 2 | 3>(1); + const [formError, setFormError] = useState(null); + const [secondsRemaining, setSecondsRemaining] = useState(5); + + const { + register, + handleSubmit, + reset, + trigger, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { understood: false, password: '' }, + mode: 'onSubmit', + }); + + useEffect(() => { + if (open) { + setStep(1); + setFormError(null); + setSecondsRemaining(5); + reset({ understood: false, password: '' }); + } + }, [open, reset]); + + useEffect(() => { + if (!open || step !== 3) return; + + const countdown = window.setInterval(() => { + setSecondsRemaining((value) => Math.max(value - 1, 0)); + }, 1000); + const redirect = window.setTimeout(() => router.replace('/'), 5000); + + return () => { + window.clearInterval(countdown); + window.clearTimeout(redirect); + }; + }, [open, router, step]); + + useEffect(() => { + if (!open) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && step !== 3) onClose(); + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onClose, open, step]); + + if (!open) return null; + + const goToPasswordStep = async () => { + setFormError(null); + if (await trigger('understood')) setStep(2); + }; + + const submitPassword = async ({ password }: DeleteAccountFormValues) => { + setFormError(null); + + try { + await deleteAccount(password); + setStep(3); + } catch (error: any) { + const message = error?.response?.data?.message ?? error?.response?.data?.error; + setFormError(typeof message === 'string' ? message : 'We could not delete your account. Check your password and try again.'); + } + }; + + return ( +
+ + )} + + {step === 3 ? ( +
+
+
+

+ Your account has been deleted +

+

+ Your enrolled course access has been revoked. You will be redirected to the homepage in {secondsRemaining} seconds. +

+
+ ) : ( +
+
+
+
+
+

Step {step} of 2

+

+ {step === 1 ? 'Delete your account?' : 'Confirm your identity'} +

+
+
+ + {step === 1 ? ( + <> +

+ This action is permanent. Once your account is deleted, it cannot be restored. +

+
    + {consequences.map((consequence) => ( +
  • + + {consequence} +
  • + ))} +
+ + {errors.understood &&

{errors.understood.message}

} +
+ + +
+ + ) : ( + <> +

+ Enter your current password to verify your identity before we permanently delete your account. +

+ + + {errors.password &&

{errors.password.message}

} + {formError &&

{formError}

} +
+ + +
+ + )} +
+ )} +
+ + ); +} diff --git a/src/lib/api/services.ts b/src/lib/api/services.ts index 06a6709..90bbea3 100644 --- a/src/lib/api/services.ts +++ b/src/lib/api/services.ts @@ -376,6 +376,9 @@ export const usersApi = { const { data } = await apiClient.patch>('/users/me', payload); return data.data; }, + deleteAccount: async (password: string): Promise => { + await apiClient.delete('/users/me', { data: { password } }); + }, getInstructorStats: async () => { const { data } = await apiClient.get('/users/me/instructor-stats'); return data.data; diff --git a/src/lib/hooks/use-auth-store.ts b/src/lib/hooks/use-auth-store.ts index 6a4885d..e5a89bb 100644 --- a/src/lib/hooks/use-auth-store.ts +++ b/src/lib/hooks/use-auth-store.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import type { User } from '@/types'; +import { usersApi } from '@/lib/api/services'; interface AuthState { address: string | null; @@ -9,6 +10,7 @@ interface AuthState { setAuth: (address: string, token: string, user: User, rememberMe?: boolean) => void; setAddress: (address: string) => void; updateUser: (patch: Partial) => void; + deleteAccount: (password: string) => Promise; logout: () => void; rehydrate: () => void; } @@ -31,6 +33,16 @@ export const useAuthStore = create((set) => ({ updateUser: (patch) => set((state) => ({ user: state.user ? { ...state.user, ...patch } : state.user })), + deleteAccount: async (password) => { + await usersApi.deleteAccount(password); + if (typeof window !== 'undefined') { + localStorage.removeItem('hamplard_token'); + localStorage.removeItem('hamplard_address'); + document.cookie = 'hamplard_token=; path=/; max-age=0'; + } + set({ address: null, token: null, user: null, isConnected: false }); + }, + logout: () => { if (typeof window !== 'undefined') { localStorage.removeItem('hamplard_token');