Files
cozsweet-frontend-nextjs/implementation_plan.md
T
Claude b362945195 feat(auth): migrate Flutter IAuthPlatform to Next.js 16 SDK service layer
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>
2026-06-09 11:13:14 +08:00

17 KiB
Raw Blame History

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,触发实际请求)

测试要点

  1. 环境变量加载process.env.NEXT_PUBLIC_API_BASE_URL 正确解析
  2. Token 注入getCurrentUser 自动携带 Authorization 头
  3. 401 刷新:模拟 401 响应,验证 token 刷新队列与重试
  4. Multipart 上传uploadImage 正确发送 FormData
  5. 超时控制30s/60s 配置生效

[Implementation Order]

按依赖关系,9 个步骤

  1. 安装 ofetchpnpm add ofetch(修改 package.json + pnpm-lock.yaml
  2. 创建 .env.example + .env.local:环境变量模板
  3. 批次 A:基础设施5 个文件)
    • api_path.ts(路径常量)
    • api_result.ts(响应包装)
    • config/api_config.ts(环境配置)
    • storage/auth_storage.tslocalStorage 包装)
    • storage/device_storage.ts(设备 ID
  4. 批次 B:拦截器 + http_client4 个文件)
    • interceptor/token_interceptor.ts
    • interceptor/auth_refresh_interceptor.ts
    • interceptor/logging_interceptor.ts
    • http_client.tsofetch 实例装配)
  5. 批次 CAPI clients4 个文件,独立)
    • auth_api.ts11 个方法)
    • chat_api.ts5 个方法)
    • user_api.ts5 个方法)
    • metrics_api.ts2 个方法)
  6. 批次 Dbarrel 导出1 个文件)
    • index.ts
  7. 最终验证
    • npx tsc --noEmit
    • pnpm lint
    • 端到端 smoke test

后续迁移:第三方登录平台(IAuthPlatform

[Overview]

将 Flutter IAuthPlatform/Users/chase/Documents/cozsweet/lib/data/services/auth/)迁移为 Next.js 16 项目的纯 TypeScript 服务层。SDK 选用 @react-oauth/googleGoogle Identity Services React 包装器)+ @greatsumini/react-facebook-loginreact-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
  • 跳过 AppleFlutter 端本来就是 stub
  • 平台只产 token,不调 authRepository / authApi
  • 保留 MockNEXT_PUBLIC_USE_MOCK_AUTH=true 切换,对齐 Dart kDebugMode

[Architecture: event-bridge 模式]

IAuthPlatform 必须是 plain TypeScript(不依赖 React),但 React 包装器 SDKuseGoogleOAuthFacebookLoginClient)只能在 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 回写 Promise
  • AuthPlatformBridge 监听 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_IDNEXT_PUBLIC_FACEBOOK_APP_IDNEXT_PUBLIC_FACEBOOK_API_VERSION=v25.0NEXT_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.5useGoogleLogin(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 }) —— 注册 callback
  • window.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]

  1. 安装pnpm add @react-oauth/google @greatsumini/react-facebook-login
  2. 追加 env.env.example
  3. 修整 barrelsby.json:删除 ./src/data/models 残留、新增 ./src/data/services./src/data/services/auth
  4. 删除 src/data/storage/index.ts(命名冲突)
  5. 新建 Zod schema + DTO 类 for FacebookUserData
  6. 生成 barrelpnpm generate-barrels
  7. 新建 global.d.ts:声明 window.google.accounts.id.* 类型
  8. 依次新建 auth_event_bridge.tsinitialize_*.tsauth_platform.tsweb_auth_platform.tsmock_auth_platform.tsauth_bridge_component.tsx
  9. 验证npx tsc --noEmitpnpm lintpnpm build