veza/apps/web/src/features/settings/components/account-settings/useAccountSettings.ts
senke ea7faeb703 refactor(settings): extract AccountSettings into account-settings module
- Add account-settings/ with useAccountSettings, AccountSettingsErrorBanner,
  AccountSettingsPasswordCard, AccountSettingsExportCard, AccountSettingsDeleteCard,
  AccountSettingsSkeleton
- Re-export from AccountSettings.tsx for backward compatibility
- Stories: Default, Loading (skeleton, min-h-layout-story); remove ToastProvider

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 23:04:16 +01:00

185 lines
5.5 KiB
TypeScript

import { useState, useRef, useCallback } from 'react';
import { useAuthStore } from '@/features/auth/store/authStore';
import { apiClient } from '@/services/api/client';
import { parseApiError } from '@/utils/apiErrorHandler';
import { useToast } from '@/hooks/useToast';
const MAX_RETRY = 3;
export function useAccountSettings() {
const { logout } = useAuthStore();
const toast = useToast();
const [isChangingPassword, setIsChangingPassword] = useState(false);
const [isDeletingAccount, setIsDeletingAccount] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [deletePassword, setDeletePassword] = useState('');
const [deleteReason, setDeleteReason] = useState('');
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [deleteValidationError, setDeleteValidationError] = useState<string | null>(null);
const [mutationError, setMutationError] = useState<Error | null>(null);
const [retryCount, setRetryCount] = useState(0);
const lastMutationRef = useRef<(() => Promise<void>) | null>(null);
const handleChangePassword = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
setPasswordError('');
if (newPassword !== confirmPassword) {
setPasswordError('New passwords do not match');
return;
}
if (newPassword.length < 12) {
setPasswordError('Password must be at least 12 characters long');
return;
}
const performMutation = async () => {
await apiClient.put('/users/me/password', {
current_password: currentPassword,
new_password: newPassword,
});
toast.success('Password changed successfully');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setMutationError(null);
setRetryCount(0);
lastMutationRef.current = null;
};
lastMutationRef.current = performMutation;
setIsChangingPassword(true);
try {
await performMutation();
} catch (error: unknown) {
const apiError = parseApiError(error);
setPasswordError(apiError.message);
setMutationError(new Error(apiError.message));
} finally {
setIsChangingPassword(false);
}
},
[
currentPassword,
newPassword,
confirmPassword,
toast,
],
);
const handleDeleteAccount = useCallback(async () => {
setDeleteValidationError(null);
setMutationError(null);
if (deleteConfirmText !== 'DELETE') {
setDeleteValidationError('Please type DELETE to confirm');
return;
}
try {
setIsDeletingAccount(true);
await apiClient.delete('/users/me', {
data: {
password: deletePassword,
reason: deleteReason,
confirm_text: deleteConfirmText,
},
});
toast.success('Account deletion requested. You will be logged out.');
setTimeout(() => {
logout();
window.location.href = '/login';
}, 2000);
} catch (error: unknown) {
const apiError = parseApiError(error);
setMutationError(new Error(apiError.message));
} finally {
setIsDeletingAccount(false);
setIsDeleteDialogOpen(false);
}
}, [deletePassword, deleteReason, deleteConfirmText, toast, logout]);
const handleExportData = useCallback(async () => {
const performMutation = async () => {
const response = await apiClient.get('/users/me/export', {
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute(
'download',
`veza-data-export-${new Date().toISOString()}.json`,
);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Data export started');
setMutationError(null);
setRetryCount(0);
lastMutationRef.current = null;
};
lastMutationRef.current = performMutation;
try {
await performMutation();
} catch (error: unknown) {
const apiError = parseApiError(error);
setMutationError(new Error(apiError.message));
}
}, [toast]);
const handleRetry = useCallback(async () => {
if (!lastMutationRef.current || retryCount >= MAX_RETRY) return;
setRetryCount((prev) => prev + 1);
try {
await lastMutationRef.current();
} catch {
// Error handled by mutation
}
}, [retryCount]);
const dismissError = useCallback(() => {
setMutationError(null);
setRetryCount(0);
lastMutationRef.current = null;
}, []);
return {
mutationError,
retryCount,
maxRetry: MAX_RETRY,
handleRetry,
dismissError,
isChangingPassword,
currentPassword,
setCurrentPassword,
newPassword,
setNewPassword,
confirmPassword,
setConfirmPassword,
passwordError,
handleChangePassword,
isDeleteDialogOpen,
setIsDeleteDialogOpen,
isDeletingAccount,
deletePassword,
setDeletePassword,
deleteReason,
setDeleteReason,
deleteConfirmText,
setDeleteConfirmText,
deleteValidationError,
setDeleteValidationError,
handleDeleteAccount,
handleExportData,
};
}