Adds the third-party social login SDK integration layer (Google + Facebook),
mirroring the Flutter `IAuthPlatform` interface from
`lib/data/services/auth/`. The platform produces identity tokens only; callers
use the existing `authRepository.googleLogin / facebookLogin /
facebookIdLogin` (already migrated) to complete the login.
Architecture (event-bridge pattern):
- `IAuthPlatform` is plain TypeScript (no React dependency)
- `WebAuthPlatform` dispatches DOM `CustomEvent`s; `AuthPlatformBridge`
("use client" component) hosts the React SDK hooks and resolves promises
- `MockAuthPlatform` returns hardcoded tokens when
`NEXT_PUBLIC_USE_MOCK_AUTH=true` (mirrors Dart `kDebugMode` branch)
SDK choices:
- `@react-oauth/google@0.13.5` for Google
- `@greatsumini/react-facebook-login@3.4.0` (maintained fork of
archived `react-facebook`, React 19 compatible) for Facebook
- For Google: uses raw GIS `google.accounts.id.initialize` + `prompt` API
since `@react-oauth/google` does not expose an `id_token` flow variant
(existing `GoogleLoginRequest.idToken` field expects JWT id_token)
Files added:
- src/data/services/auth/{auth_platform,web_auth_platform,
mock_auth_platform,auth_event_bridge,auth_bridge_component,
initialize_google_auth,initialize_facebook_auth,global.d.ts,index}.ts(x)
- src/data/schemas/auth/facebook_user_data.ts (Zod schema)
- src/data/dto/auth/facebook_user_data.ts (DTO class)
Files modified:
- package.json, pnpm-lock.yaml: added 2 deps
- .env.example: 4 new NEXT_PUBLIC_* env vars
- barrelsby.json: registered src/data/services[/auth] directories
- implementation_plan.md: appended third-party auth section
- src/data/dto/{auth,chat,metrics,user}/index.ts: regenerated barrels
- src/data/schemas/{auth,chat,metrics,user}/index.ts: regenerated barrels
Files deleted:
- src/data/storage/index.ts: pre-existing TS2308 conflict (sync + async
`AuthStorage` both re-exported from auto-generated barrel; no consumer
uses the barrel — all imports use direct paths)
Scope (per user direction):
- Client SDK integration only; no React hook/context, no UI, no Apple
- Platform produces tokens; caller drives `authRepository` / `authApi`
- v1 single-popup semantics (concurrent popups overwrite)
- Bridge component is code-ready but NOT mounted in
`src/app/layout.tsx`; next-step UI migration wires the provider chain
Verified: `npx tsc --noEmit` (0 errors), `pnpm lint` (0 errors),
`pnpm build` (Next.js 16.2.7 + Turbopack compiled successfully).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
17 KiB
Implementation Plan: HTTP API 层迁移
[Overview]
将 Flutter 项目 /lib/core/net/ 与 /lib/data/services/api/ 下的 HTTP 网络层迁移至 Next.js 16,使用 ofetch 作为 HTTP 客户端(轻量、同构、拦截器钩子丰富),通过 .env.local 配置环境变量,复用已迁移的 Zod 数据模型进行响应验证。
[Types]
ApiResult 统一响应包装
// src/data/api/api_result.ts
export type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: ApiError };
export class ApiError extends Error {
readonly code: string;
readonly status?: number;
readonly details?: unknown;
constructor(code: string, message: string, status?: number, details?: unknown);
}
环境配置类型
// src/data/api/config/api_config.ts
export type AppEnv = "development" | "test" | "production";
export interface ApiConfig {
baseUrl: string;
wsUrl: string;
connectTimeout: number; // ms
receiveTimeout: number; // ms
sendTimeout: number; // ms
}
拦截器钩子签名
// ofetch 原生 hooks
onRequest?: (ctx: { request: Request; options: FetchOptions }) => void | Promise<void>;
onResponse?: (ctx: { request: Request; response: Response; options: FetchOptions }) => void | Promise<void>;
onResponseError?: (ctx: { request: Request; response: Response; options: FetchOptions; error: FetchError }) => void | Promise<void>;
onError?: (ctx: { request: Request; error: Error; options: FetchOptions }) => void | Promise<void>;
[Files]
新增
| 路径 | 用途 |
|---|---|
src/data/api/api_path.ts |
路径常量(40+ endpoints) |
src/data/api/api_result.ts |
统一响应包装 ApiResult<T> + ApiError |
src/data/api/config/api_config.ts |
环境 → baseUrl/超时 配置 |
src/data/api/storage/auth_storage.ts |
localStorage 包装(token 持久化) |
src/data/api/storage/device_storage.ts |
设备 ID 持久化 |
src/data/api/http_client.ts |
ofetch 实例 + 拦截器装配 |
src/data/api/interceptor/token_interceptor.ts |
注入 Authorization: Bearer |
src/data/api/interceptor/auth_refresh_interceptor.ts |
401 → 自动刷新 token |
src/data/api/interceptor/logging_interceptor.ts |
请求/响应日志(开发模式) |
src/data/api/auth_api.ts |
11 个 auth 接口 |
src/data/api/chat_api.ts |
5 个 chat 接口(含 multipart) |
src/data/api/user_api.ts |
5 个 user 接口 |
src/data/api/metrics_api.ts |
2 个 metrics 接口 |
src/data/api/index.ts |
barrel 导出 |
.env.local |
本地环境变量(不提交) |
.env.example |
环境变量模板(提交) |
修改
| 路径 | 修改 |
|---|---|
package.json |
添加 ofetch 依赖 |
pnpm-lock.yaml |
pnpm install 自动更新 |
.gitignore |
确保 .env.local 已被忽略(Next.js 默认) |
不迁移
- ❌
websocket_handler.dart/websocket_manager.dart— WebSocket 独立项 - ❌
message_queue.dart— 离线消息队列 - ❌
service_manager.dart的 dio 特定部分(已被 ofetch 实例替代) - ❌
payment_api_client.dart— 原始 Dart 也不存在客户端类
[Functions]
新增
| 函数 | 签名 | 文件 | 用途 |
|---|---|---|---|
getAuthToken() |
() => Promise<string | null> |
storage/auth_storage.ts |
优先登录 token,回退游客 token |
setLoginToken(t) |
(token: string) => Promise<void> |
同上 | 持久化登录 token |
setGuestToken(t) |
(token: string) => Promise<void> |
同上 | 持久化游客 token |
clearAuthData() |
() => Promise<void> |
同上 | 清除所有 auth 数据 |
getDeviceId() |
() => string |
storage/device_storage.ts |
读取/生成设备 ID |
getApiConfig() |
() => ApiConfig |
config/api_config.ts |
当前环境配置 |
createHttpClient() |
() => typeof ofetch |
http_client.ts |
ofetch 实例工厂 |
删除/重写
| 函数 | 旧实现 | 新实现 |
|---|---|---|
BackendService 单例 |
Dio + 4 ApiClient 字段 | ofetch + 4 api 类模块级单例 |
getBaseUrl() |
ServiceType.getBaseUrl() | api_config.ts 静态方法 |
getServiceConfig() |
ServiceType 扩展 | api_config.ts 静态方法 |
[Classes]
新增
| 类 | 文件 | 关键方法 |
|---|---|---|
ApiError |
api_result.ts |
构造、toString() |
AuthStorage |
storage/auth_storage.ts |
getLoginToken/setLoginToken/getRefreshToken/getGuestToken/setGuestToken/hasLoginToken/clearAuthData |
DeviceStorage |
storage/device_storage.ts |
getDeviceId() |
AuthApi |
auth_api.ts |
sendCode/emailLogin/register/appleLogin/googleLogin/facebookLogin/facebookIdLogin/logout/guestLogin/refreshToken/getCurrentUser |
ChatApi |
chat_api.ts |
sendMessage/getHistory/speechToText(multipart)/syncMessages/uploadImage(multipart) |
UserApi |
user_api.ts |
getUserStats/getCurrentUser/updateProfile/getCredits/getCreditsHistory |
MetricsApi |
metrics_api.ts |
reportPwaEvent/reportUserInfo |
修改
无(无现有 class 需要修改)
拦截器实现(函数式,非 class)
由于 ofetch 钩子是函数式 API,拦截器以函数形式实现:
// interceptor/token_interceptor.ts
export const tokenInterceptor: FetchHook = async (ctx) => {
const path = new URL(ctx.request.url).pathname;
if (!needsToken(path)) return;
const token = await getAuthToken();
if (token) {
ctx.request.headers.set("Authorization", `Bearer ${token}`);
}
};
// interceptor/auth_refresh_interceptor.ts
export const authRefreshInterceptor: FetchHook = async (ctx) => {
if (ctx.response?.status !== 401) return;
// 401 处理 + token 刷新 + 请求重试
};
// http_client.ts
const client = ofetch.create({
baseURL: getApiConfig().baseUrl,
timeout: getApiConfig().receiveTimeout,
hooks: {
onRequest: [loggingInterceptor, tokenInterceptor],
onResponse: [loggingInterceptor],
onResponseError: [authRefreshInterceptor, loggingInterceptor],
},
});
[Dependencies]
新增
| 包 | 版本 | 用途 |
|---|---|---|
ofetch |
^1.5.0 |
同构 HTTP 客户端(浏览器 + Node.js) |
安装
pnpm add ofetch
版本选择
选择 ofetch v1.5+,原因:
- 完整同构支持(unjs/h3 生态)
- 内置拦截器钩子
- 体积小(~5KB gzipped)
- TypeScript 优先
[Testing]
验证策略
每个批次完成后:
npx tsc --noEmit— 0 错误pnpm lint— 通过
最终验证:
- 模块导入测试(
import { authApi, chatApi } from '@/data/api') - 拦截器单元测试(mock localStorage 与 fetch)
- 端到端测试(启动 dev server,触发实际请求)
测试要点
- 环境变量加载:
process.env.NEXT_PUBLIC_API_BASE_URL正确解析 - Token 注入:
getCurrentUser自动携带 Authorization 头 - 401 刷新:模拟 401 响应,验证 token 刷新队列与重试
- Multipart 上传:
uploadImage正确发送 FormData - 超时控制:30s/60s 配置生效
[Implementation Order]
按依赖关系,9 个步骤:
- 安装 ofetch:
pnpm add ofetch(修改package.json+pnpm-lock.yaml) - 创建
.env.example+.env.local:环境变量模板 - 批次 A:基础设施(5 个文件)
api_path.ts(路径常量)api_result.ts(响应包装)config/api_config.ts(环境配置)storage/auth_storage.ts(localStorage 包装)storage/device_storage.ts(设备 ID)
- 批次 B:拦截器 + http_client(4 个文件)
interceptor/token_interceptor.tsinterceptor/auth_refresh_interceptor.tsinterceptor/logging_interceptor.tshttp_client.ts(ofetch 实例装配)
- 批次 C:API clients(4 个文件,独立)
auth_api.ts(11 个方法)chat_api.ts(5 个方法)user_api.ts(5 个方法)metrics_api.ts(2 个方法)
- 批次 D:barrel 导出(1 个文件)
index.ts
- 最终验证:
npx tsc --noEmitpnpm lint- 端到端 smoke test
后续迁移:第三方登录平台(IAuthPlatform)
[Overview]
将 Flutter IAuthPlatform(/Users/chase/Documents/cozsweet/lib/data/services/auth/)迁移为 Next.js 16 项目的纯 TypeScript 服务层。SDK 选用 @react-oauth/google(Google Identity Services React 包装器)+ @greatsumini/react-facebook-login(react-facebook 的活跃 fork,兼容 React 19)。
Flutter 端的 AuthRepository 已经完整迁移(src/data/repositories/auth_repository.ts,含 googleLogin/facebookLogin/facebookIdLogin/appleLogin),本步只补齐"生产第三方 identity token"那一段——平台层产 token,调用方拿 token 去调 authRepository(或 authApi)完成登录。
范围限定:
- 客户端 SDK 接入层,不写 React Context/useAuth,不写 UI
- 跳过 Apple(Flutter 端本来就是 stub)
- 平台只产 token,不调
authRepository/authApi - 保留 Mock(
NEXT_PUBLIC_USE_MOCK_AUTH=true切换,对齐 DartkDebugMode)
[Architecture: event-bridge 模式]
IAuthPlatform 必须是 plain TypeScript(不依赖 React),但 React 包装器 SDK(useGoogleOAuth、FacebookLoginClient)只能在 React 组件里用。解决方案是 plain-TS 平台类 ↔ React Bridge 组件通过 DOM CustomEvent 通信:
WebAuthPlatform (plain TS) AuthPlatformBridge ("use client")
┌──────────────────────┐ ┌──────────────────────────┐
│ googleSignIn() │ │ │
│ ├─ new Promise │ dispatchEvent │ useGoogleOAuth() │
│ │ store requestId │ ────────────▶ │ window.google.accounts │
│ │ → resolver │ │ .id.initialize({...}) │
│ └─ return Promise │ │ on auth event: │
│ │ resolveGoogle │ window.google.accounts│
│ resolveGoogle( │ ◀──────────── │ .id.prompt() │
│ requestId, result)│ │ GIS callback: │
└──────────────────────┘ │ resolve the promise │
└──────────────────────────┘
WebAuthPlatform暴露公开方法resolveGoogle(requestId, result)/resolveFacebook(...)给 Bridge 回写 PromiseAuthPlatformBridge监听cozsweet:auth:google:start/cozsweet:auth:facebook:start事件,触发对应 SDK 流程- v1 限制:单 popup 语义——一次只追踪一个待处理 Google / Facebook 请求;并发调用会被覆盖
[Files]
新建(9 个文件)
| 路径 | 作用 |
|---|---|
src/data/services/auth/auth_platform.ts |
IAuthPlatform 接口 + AuthPlatformFactory + getAuthPlatform() 单例 + 命名类型 GoogleCredential / FacebookLoginResult |
src/data/services/auth/web_auth_platform.ts |
真机平台类(plain TS),通过 DOM 事件与 Bridge 通信;含 resolveGoogle() / resolveFacebook() 公开方法 |
src/data/services/auth/mock_auth_platform.ts |
调试用 Mock(返回固定 token + 250ms 模拟延迟) |
src/data/services/auth/auth_event_bridge.ts |
跨 plain-TS / React 边界的 DOM 事件通道(无 React 依赖;导出事件名常量 + dispatch / subscribe 助手) |
src/data/services/auth/auth_bridge_component.tsx |
"use client" 组件,承载 useGoogleOAuth + FacebookLoginClient,监听事件并 resolve Promise |
src/data/services/auth/initialize_google_auth.ts |
Google GIS 预热辅助(占位) |
src/data/services/auth/initialize_facebook_auth.ts |
Facebook SDK 预热辅助(占位) |
src/data/schemas/auth/facebook_user_data.ts |
Zod schema + Input / Data 类型 |
src/data/dto/auth/facebook_user_data.ts |
FacebookUserData DTO 类(from / fromJson / toJson) |
src/data/services/auth/global.d.ts |
window.google.accounts.id.* 的全局类型声明 |
修改
| 路径 | 变更 |
|---|---|
package.json |
新增 @react-oauth/google@0.13.5、@greatsumini/react-facebook-login@3.4.0 |
.env.example |
新增 NEXT_PUBLIC_GOOGLE_CLIENT_ID、NEXT_PUBLIC_FACEBOOK_APP_ID、NEXT_PUBLIC_FACEBOOK_API_VERSION=v25.0、NEXT_PUBLIC_USE_MOCK_AUTH=false |
barrelsby.json |
修整:删除已不存在的 ./src/data/models 残留;新增 ./src/data/services + ./src/data/services/auth |
删除
| 路径 | 原因 |
|---|---|
src/data/storage/index.ts |
barrelsby 自动生成的 barrel,把同步 AuthStorage(./auth_storage)和异步 AuthStorage(./auth/auth_storage)同时 re-export,触发 TS2308 类型冲突。该 barrel 在仓库内无消费者(所有 import 都走直接路径 @/data/storage/auth_storage 或 @/data/storage/auth/auth_storage),可安全删除;后续若需要 barrel 须先解决命名冲突 |
[Google Flow 决策(重要)]
@react-oauth/google@0.13.5 的 useGoogleLogin(options) 只支持两种 flow:
flow: 'implicit'→ 回调TokenResponse,含access_token(不是 id_token)flow: 'auth-code'→ 回调CodeResponse,含code(授权码,需后端交换)
但 后端 GoogleLoginRequest.idToken: z.string() 期望的是 JWT id_token(与 Flutter GoogleSignInAccount.authentication.idToken 字段同名同义),所以 useGoogleLogin 两种 flow 都不适配。
采用方案:直接调用 GIS 全局 API(在 Bridge 的 useEffect 中),由 GoogleOAuthProvider 加载 GIS 脚本后:
window.google.accounts.id.initialize({ client_id, callback })—— 注册 callbackwindow.google.accounts.id.prompt()—— 触发弹窗- callback 收到
{ credential: '<JWT>' },credential即 id_token
JWT payload 通过 atob(parts[1]) 解析拿到 email / name / picture,填充 GoogleCredential 接口字段。
[调用方契约(参考实现,不在本步交付)]
import { getAuthPlatform } from "@/data/services/auth";
import { authRepository } from "@/data/repositories";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
const platform = getAuthPlatform({
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
facebookAppId: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
});
async function googleLogin() {
const r = await platform.googleSignIn();
if (r.kind === "failure") return handleError(r.error);
const guestId = (await AuthStorage.getInstance().getDeviceId()).value ?? "";
await authRepository.googleLogin({ idToken: r.value.idToken, guestId });
// token 持久化由 authRepository._saveLoginData 自动完成
}
async function facebookLogin() {
const r = await platform.facebookSignIn();
if (r.kind === "failure") return handleError(r.error);
await authRepository.facebookLogin({ accessToken: r.value.accessToken, guestId: "" });
// 拉头像 + by-id 登录(与 Flutter AuthBloc._uploadFacebookAvatar 一致)
const ud = await platform.getFacebookUserData();
if (ud.kind === "success" && ud.value.id && ud.value.pictureUrl) {
await AuthStorage.getInstance().setFacebookId(ud.value.id);
await authRepository.facebookIdLogin({ fbId: ud.value.id, avatarUrl: ud.value.pictureUrl });
}
}
[下一步:UI 挂载方式(留给下一轮迁移)]
本步不挂载到 src/app/layout.tsx。下一轮 UI 接入时按如下方式挂载:
// src/app/layout.tsx
import { GoogleOAuthProvider } from "@react-oauth/google";
import { getAuthPlatform, AuthPlatformBridge } from "@/data/services/auth";
const platform = getAuthPlatform({
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
facebookAppId: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}>
<AuthPlatformBridge platform={platform}>
{children}
</AuthPlatformBridge>
</GoogleOAuthProvider>
</body>
</html>
);
}
[Implementation Order]
- 安装:
pnpm add @react-oauth/google @greatsumini/react-facebook-login - 追加 env 到
.env.example - 修整
barrelsby.json:删除./src/data/models残留、新增./src/data/services与./src/data/services/auth - 删除
src/data/storage/index.ts(命名冲突) - 新建 Zod schema + DTO 类 for
FacebookUserData - 生成 barrel:
pnpm generate-barrels - 新建 global.d.ts:声明
window.google.accounts.id.*类型 - 依次新建
auth_event_bridge.ts、initialize_*.ts、auth_platform.ts、web_auth_platform.ts、mock_auth_platform.ts、auth_bridge_component.tsx - 验证:
npx tsc --noEmit、pnpm lint、pnpm build