feat(assets): add chat typing indicator lottie animation

This commit is contained in:
2026-06-08 17:26:09 +08:00
parent f4e1c30051
commit 90fe892995
22 changed files with 988 additions and 3 deletions
+115
View File
@@ -0,0 +1,115 @@
"use client";
/**
* AuthStorage 完整实现(示范 1KV / localStorage
*
* SECURITY: 当前与 Dart 端行为一致,refresh_token 以明文存于 localStorage。
* 后续轮次将升级到 HttpOnly Cookie 方案,参见 `clearAuthData` 处的 TODO 标记。
*
* 设计说明:
* - 组合而非继承 `LocalStorage`(避免多重继承层级)
* - 单例挂在 class 静态字段,规避 HMR 复用污染
* - 构造函数接受可选 `LocalStorage` 注入,便于测试时使用 jsdom 的 storage
* - `hasXxx` = `token != null && token.length > 0`,对齐 Dart 端 `isNotEmpty` 语义
* - `clearAuthData` 顺序清理:loginToken → guestToken → refreshToken,任一失败立即返回
*/
import { LocalStorage } from "../local_storage";
import { Result, type Result as ResultT } from "../result";
import { StorageKeys } from "../storage_keys";
import type { IAuthStorage } from "./iauth_storage";
export class AuthStorage implements IAuthStorage {
private readonly ls: LocalStorage;
private static _instance: AuthStorage | null = null;
constructor(localStorage?: LocalStorage) {
this.ls = localStorage ?? LocalStorage.getInstance();
}
static getInstance(): AuthStorage {
if (!AuthStorage._instance) AuthStorage._instance = new AuthStorage();
return AuthStorage._instance;
}
/** @internal */
static _resetForTests(): void {
AuthStorage._instance = null;
}
// ---- login token ----
getLoginToken(): Promise<ResultT<string | null>> {
return this.ls.getString(StorageKeys.loginToken);
}
setLoginToken(token: string): Promise<ResultT<void>> {
return this.ls.setString(StorageKeys.loginToken, token);
}
async hasLoginToken(): Promise<ResultT<boolean>> {
const r = await this.getLoginToken();
if (r.kind === "failure") return Result.failure(r.error);
return Result.success(r.value !== null && r.value.length > 0);
}
clearLoginToken(): Promise<ResultT<void>> {
return this.ls.remove(StorageKeys.loginToken);
}
// ---- guest token ----
getGuestToken(): Promise<ResultT<string | null>> {
return this.ls.getString(StorageKeys.guestToken);
}
setGuestToken(token: string): Promise<ResultT<void>> {
return this.ls.setString(StorageKeys.guestToken, token);
}
async hasGuestToken(): Promise<ResultT<boolean>> {
const r = await this.getGuestToken();
if (r.kind === "failure") return Result.failure(r.error);
return Result.success(r.value !== null && r.value.length > 0);
}
clearGuestToken(): Promise<ResultT<void>> {
return this.ls.remove(StorageKeys.guestToken);
}
// ---- device id ----
getDeviceId(): Promise<ResultT<string | null>> {
return this.ls.getString(StorageKeys.deviceId);
}
setDeviceId(deviceId: string): Promise<ResultT<void>> {
return this.ls.setString(StorageKeys.deviceId, deviceId);
}
// ---- refresh token ----
// TODO(security): 升级到 HttpOnly Cookie 方案
// SECURITY: 明文存储,仅供本轮与 Dart 行为对齐
getRefreshToken(): Promise<ResultT<string | null>> {
return this.ls.getString(StorageKeys.refreshToken);
}
setRefreshToken(token: string): Promise<ResultT<void>> {
return this.ls.setString(StorageKeys.refreshToken, token);
}
clearRefreshToken(): Promise<ResultT<void>> {
return this.ls.remove(StorageKeys.refreshToken);
}
// ---- facebook id ----
getFacebookId(): Promise<ResultT<string | null>> {
return this.ls.getString(StorageKeys.facebookId);
}
setFacebookId(id: string): Promise<ResultT<void>> {
return this.ls.setString(StorageKeys.facebookId, id);
}
// ---- bulk clear ----
async clearAuthData(): Promise<ResultT<void>> {
const r1 = await this.clearLoginToken();
if (r1.kind === "failure") return r1;
const r2 = await this.clearGuestToken();
if (r2.kind === "failure") return r2;
return this.clearRefreshToken();
}
}