fix: Bug - Add logout in mobile version of settings, allow clearing data on android
All checks were successful
Queue Release Build / prepare (push) Successful in 19s
Deploy Web Apps / deploy (push) Successful in 7m55s
Queue Release Build / build-windows (push) Successful in 28m37s
Queue Release Build / build-linux (push) Successful in 47m3s
Queue Release Build / build-android (push) Successful in 20m33s
Queue Release Build / finalize (push) Successful in 3m48s

Expose settings logout on mobile where the title bar is hidden, and enable
Capacitor data settings with storage visibility and local erase/sign-out.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 22:31:40 +02:00
parent cb59af6b6c
commit 07e91a0d09
20 changed files with 553 additions and 39 deletions

View File

@@ -0,0 +1,104 @@
import '@angular/compiler';
import { Injector, runInInjectionContext } from '@angular/core';
import {
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import { PlatformService } from '../../core/platform';
import { AuthTokenStoreService } from '../../domains/authentication/application/services/auth-token-store.service';
import { UserLogoutService } from '../../domains/authentication/application/services/user-logout.service';
import { DatabaseService } from './database.service';
import { LocalUserDataService } from './local-user-data.service';
const filesystemMocks = vi.hoisted(() => ({
rmdir: vi.fn(() => Promise.resolve())
}));
vi.mock('../../domains/attachment/infrastructure/services/capacitor-attachment-filesystem.adapter', () => ({
loadCapacitorAttachmentFilesystem: vi.fn(async () => ({
filesystem: {
rmdir: filesystemMocks.rmdir
},
directory: 'DATA',
convertFileSrc: (url: string) => url
}))
}));
describe('LocalUserDataService', () => {
let database: { clearAllData: ReturnType<typeof vi.fn> };
let authTokenStore: { clearAllTokens: ReturnType<typeof vi.fn> };
let userLogout: { logout: ReturnType<typeof vi.fn> };
let service: LocalUserDataService;
beforeEach(() => {
const storage = new Map<string, string>();
vi.stubGlobal('localStorage', {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => { storage.set(key, value); },
removeItem: (key: string) => { storage.delete(key); },
clear: () => { storage.clear(); },
key: (index: number) => Array.from(storage.keys())[index] ?? null,
get length() {
return storage.size;
}
});
localStorage.setItem('metoyou_currentUserId', 'user-1');
localStorage.setItem('metoyou_voice_settings', '{}');
localStorage.setItem('metoyou.authTokens', '{"http://localhost:3001":{"token":"abc","expiresAt":9999999999999}}');
database = { clearAllData: vi.fn(() => Promise.resolve()) };
authTokenStore = { clearAllTokens: vi.fn() };
userLogout = { logout: vi.fn() };
filesystemMocks.rmdir.mockClear();
const injector = Injector.create({
providers: [
LocalUserDataService,
{ provide: PlatformService, useValue: { isCapacitor: true, isElectron: false, isBrowser: false } },
{ provide: DatabaseService, useValue: database },
{ provide: AuthTokenStoreService, useValue: authTokenStore },
{ provide: UserLogoutService, useValue: userLogout }
]
});
service = runInInjectionContext(injector, () => injector.get(LocalUserDataService));
});
it('clears native storage, auth tokens, and logs the user out', async () => {
const result = await service.eraseLocalUserData();
expect(database.clearAllData).toHaveBeenCalledOnce();
expect(filesystemMocks.rmdir).toHaveBeenCalledWith({
path: 'metoyou',
directory: 'DATA',
recursive: true
});
expect(authTokenStore.clearAllTokens).toHaveBeenCalledOnce();
expect(localStorage.getItem('metoyou_currentUserId')).toBeNull();
expect(localStorage.getItem('metoyou_voice_settings')).toBeNull();
expect(userLogout.logout).toHaveBeenCalledOnce();
expect(result).toEqual({ restartRequired: false });
});
it('rejects erase on non-native shells', async () => {
const injector = Injector.create({
providers: [
LocalUserDataService,
{ provide: PlatformService, useValue: { isCapacitor: false, isElectron: false, isBrowser: true } },
{ provide: DatabaseService, useValue: database },
{ provide: AuthTokenStoreService, useValue: authTokenStore },
{ provide: UserLogoutService, useValue: userLogout }
]
});
const browserService = runInInjectionContext(injector, () => injector.get(LocalUserDataService));
await expect(browserService.eraseLocalUserData()).rejects.toThrow(/native mobile/i);
});
});

View File

@@ -0,0 +1,54 @@
import { Injectable, inject } from '@angular/core';
import { PlatformService } from '../../core/platform';
import { clearStoredLocalAppData } from '../../core/storage/current-user-storage';
import { DatabaseService } from './database.service';
import { UserLogoutService } from '../../domains/authentication/application/services/user-logout.service';
import { AuthTokenStoreService } from '../../domains/authentication/application/services/auth-token-store.service';
import { loadCapacitorAttachmentFilesystem } from '../../domains/attachment/infrastructure/services/capacitor-attachment-filesystem.adapter';
const CAPACITOR_APP_DATA_ROOT = 'metoyou';
export interface EraseLocalUserDataResult {
restartRequired: boolean;
}
@Injectable({ providedIn: 'root' })
export class LocalUserDataService {
private readonly platform = inject(PlatformService);
private readonly database = inject(DatabaseService);
private readonly userLogout = inject(UserLogoutService);
private readonly authTokenStore = inject(AuthTokenStoreService);
async eraseLocalUserData(): Promise<EraseLocalUserDataResult> {
if (!this.platform.isCapacitor) {
throw new Error('Local user data erase is only supported on native mobile shells.');
}
await this.database.clearAllData();
await this.deleteCapacitorAttachmentTree();
this.authTokenStore.clearAllTokens();
clearStoredLocalAppData();
this.userLogout.logout();
return { restartRequired: false };
}
private async deleteCapacitorAttachmentTree(): Promise<void> {
const filesystem = await loadCapacitorAttachmentFilesystem();
if (!filesystem) {
return;
}
try {
await filesystem.filesystem.rmdir({
path: CAPACITOR_APP_DATA_ROOT,
directory: filesystem.directory,
recursive: true
});
} catch {
// Missing directory is fine during erase.
}
}
}