mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 08:17:57 -04:00
Add shared logout flow with dirty check to all logout buttons (#1473)
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
interface ILogoutConfirmModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A modal component for logout confirmation that checks for unsynced changes.
|
||||
* Shows a warning if the vault has unsynced changes that would be lost.
|
||||
*/
|
||||
const LogoutConfirmModal: React.FC<ILogoutConfirmModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDirty, setIsDirty] = useState<boolean | null>(null);
|
||||
|
||||
/**
|
||||
* Check if the vault has unsynced changes when the modal opens.
|
||||
*/
|
||||
const checkSyncState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const syncState = await sendMessage('GET_SYNC_STATE', {}, 'background') as {
|
||||
isDirty: boolean;
|
||||
mutationSequence: number;
|
||||
serverRevision: number;
|
||||
};
|
||||
setIsDirty(syncState.isDirty);
|
||||
} catch (error) {
|
||||
console.error('Failed to check sync state:', error);
|
||||
// Default to showing the simple logout confirmation on error
|
||||
setIsDirty(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check sync state when modal opens.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Reset state when modal opens
|
||||
setIsDirty(null);
|
||||
checkSyncState();
|
||||
}
|
||||
}, [isOpen, checkSyncState]);
|
||||
|
||||
// Don't render anything if not open or still loading
|
||||
if (!isOpen || isDirty === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render dirty logout warning modal
|
||||
if (isDirty) {
|
||||
return (
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
maxWidth="max-w-sm"
|
||||
showHeaderBorder={false}
|
||||
showCloseButton={false}
|
||||
bodyClassName="p-6"
|
||||
>
|
||||
<div className="flex items-start mb-4">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-6 w-6 text-amber-500" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t('logout.unsyncedChangesTitle')}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('logout.unsyncedChangesWarning')}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('logout.logoutAnyway')}
|
||||
</button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Render normal logout confirmation modal
|
||||
return (
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
maxWidth="max-w-sm"
|
||||
showHeaderBorder={false}
|
||||
showCloseButton={false}
|
||||
bodyClassName="p-6"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
{t('common.logout')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('auth.logoutConfirm')}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoutConfirmModal;
|
||||
@@ -5,6 +5,7 @@ import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import AlertMessage from '@/entrypoints/popup/components/AlertMessage';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import LogoutConfirmModal from '@/entrypoints/popup/components/Dialogs/LogoutConfirmModal';
|
||||
import MobileUnlockModal from '@/entrypoints/popup/components/Dialogs/MobileUnlockModal';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -75,6 +76,9 @@ const Unlock: React.FC = () => {
|
||||
// Mobile unlock state
|
||||
const [showMobileUnlockModal, setShowMobileUnlockModal] = useState(false);
|
||||
|
||||
// Logout confirmation state
|
||||
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
||||
|
||||
/**
|
||||
* Make status call to API which acts as health check.
|
||||
* Updates dbContext.isOffline state and returns the result.
|
||||
@@ -418,9 +422,17 @@ const Unlock: React.FC = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
* Handle logout click - opens the logout confirmation modal.
|
||||
*/
|
||||
const handleLogoutClick = () : void => {
|
||||
setShowLogoutConfirm(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout (after confirmation).
|
||||
*/
|
||||
const handleLogout = () : void => {
|
||||
setShowLogoutConfirm(false);
|
||||
app.logout();
|
||||
};
|
||||
|
||||
@@ -670,7 +682,7 @@ const Unlock: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
|
||||
{t('auth.switchAccounts')} <button type="button" onClick={handleLogout} className="text-primary-600 hover:text-primary-700 dark:text-primary-500 dark:hover:text-primary-400 hover:underline font-medium">{t('common.logout')}</button>
|
||||
{t('auth.switchAccounts')} <button type="button" onClick={handleLogoutClick} className="text-primary-600 hover:text-primary-700 dark:text-primary-500 dark:hover:text-primary-400 hover:underline font-medium">{t('common.logout')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -689,6 +701,13 @@ const Unlock: React.FC = () => {
|
||||
webApi={webApi}
|
||||
mode="unlock"
|
||||
/>
|
||||
|
||||
{/* Logout Confirmation Modal */}
|
||||
<LogoutConfirmModal
|
||||
isOpen={showLogoutConfirm}
|
||||
onClose={() => setShowLogoutConfirm(false)}
|
||||
onConfirm={handleLogout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import LogoutConfirmModal from '@/entrypoints/popup/components/Dialogs/LogoutConfirmModal';
|
||||
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -34,6 +35,7 @@ const Upgrade: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSelfHostedWarning, setShowSelfHostedWarning] = useState(false);
|
||||
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
||||
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
@@ -190,9 +192,17 @@ const Upgrade: React.FC = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the logout.
|
||||
* Handle logout click - opens the logout confirmation modal.
|
||||
*/
|
||||
const handleLogout = async (): Promise<void> => {
|
||||
const handleLogoutClick = (): void => {
|
||||
setShowLogoutConfirm(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the logout (after confirmation).
|
||||
*/
|
||||
const handleLogout = (): void => {
|
||||
setShowLogoutConfirm(false);
|
||||
logout();
|
||||
};
|
||||
|
||||
@@ -306,7 +316,7 @@ const Upgrade: React.FC = () => {
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
onClick={handleLogoutClick}
|
||||
className="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium py-2"
|
||||
disabled={isLoading}
|
||||
>
|
||||
@@ -314,6 +324,13 @@ const Upgrade: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Logout Confirmation Modal */}
|
||||
<LogoutConfirmModal
|
||||
isOpen={showLogoutConfirm}
|
||||
onClose={() => setShowLogoutConfirm(false)}
|
||||
onConfirm={handleLogout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import LogoutConfirmModal from '@/entrypoints/popup/components/Dialogs/LogoutConfirmModal';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useApp } from '@/entrypoints/popup/context/AppContext';
|
||||
@@ -31,8 +32,7 @@ const Settings: React.FC = () => {
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false);
|
||||
const [showDirtyLogoutWarning, setShowDirtyLogoutWarning] = React.useState(false);
|
||||
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
@@ -113,23 +113,10 @@ const Settings: React.FC = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout click - checks if vault has unsynced changes first.
|
||||
* Handle logout click - opens the logout confirmation modal.
|
||||
*/
|
||||
const handleLogoutClick = async () : Promise<void> => {
|
||||
// Check if vault has unsynced changes
|
||||
const syncState = await sendMessage('GET_SYNC_STATE', {}, 'background') as {
|
||||
isDirty: boolean;
|
||||
mutationSequence: number;
|
||||
serverRevision: number;
|
||||
};
|
||||
|
||||
if (syncState.isDirty) {
|
||||
// Show warning about unsynced changes
|
||||
setShowDirtyLogoutWarning(true);
|
||||
} else {
|
||||
// No unsynced changes, show normal logout confirmation
|
||||
setShowLogoutConfirm(true);
|
||||
}
|
||||
const handleLogoutClick = () : void => {
|
||||
setShowLogoutConfirm(true);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -137,7 +124,6 @@ const Settings: React.FC = () => {
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
setShowLogoutConfirm(false);
|
||||
setShowDirtyLogoutWarning(false);
|
||||
|
||||
try {
|
||||
await webApi.revokeTokens();
|
||||
@@ -208,70 +194,13 @@ const Settings: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Logout Confirmation Modal (No Unsynced Changes) */}
|
||||
{showLogoutConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
{t('common.logout')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('auth.logoutConfirm')}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowLogoutConfirm(false)}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Logout Confirmation Modal */}
|
||||
<LogoutConfirmModal
|
||||
isOpen={showLogoutConfirm}
|
||||
onClose={() => setShowLogoutConfirm(false)}
|
||||
onConfirm={handleLogout}
|
||||
/>
|
||||
|
||||
{/* Dirty Logout Warning Modal (Unsynced Changes) */}
|
||||
{showDirtyLogoutWarning && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full mx-4">
|
||||
<div className="flex items-start mb-4">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-6 w-6 text-amber-500" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t('logout.unsyncedChangesTitle')}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('logout.unsyncedChangesWarning')}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowDirtyLogoutWarning(false)}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('logout.logoutAnyway')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('common.settings')}</h2>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { VaultVersion } from '@/utils/dist/core/vault';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/core/vault';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useLogout } from '@/hooks/useLogout';
|
||||
import { useVaultMutate } from '@/hooks/useVaultMutate';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
@@ -26,7 +27,8 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
* Upgrade screen.
|
||||
*/
|
||||
export default function UpgradeScreen() : React.ReactNode {
|
||||
const { username, logout } = useApp();
|
||||
const { username } = useApp();
|
||||
const { logoutUserInitiated } = useLogout();
|
||||
const webApi = useWebApi();
|
||||
const dbContext = useDb();
|
||||
const { sqliteClient } = dbContext;
|
||||
@@ -252,15 +254,11 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the logout.
|
||||
* Handle the logout - uses the shared useLogout hook which
|
||||
* checks for unsynced changes and shows appropriate confirmation dialog.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
/*
|
||||
* Clear any stored tokens or session data
|
||||
* This will be handled by the auth context
|
||||
*/
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
await logoutUserInitiated();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user