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
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user