Merge branch 'dev' into test
This commit is contained in:
@@ -10,7 +10,6 @@ export * from "./chat-input-bar";
|
|||||||
export * from "./chat-input-text-field";
|
export * from "./chat-input-text-field";
|
||||||
export * from "./chat-quota-exhausted-banner";
|
export * from "./chat-quota-exhausted-banner";
|
||||||
export * from "./chat-screen";
|
export * from "./chat-screen";
|
||||||
|
|
||||||
export * from "./chat-send-button";
|
export * from "./chat-send-button";
|
||||||
export * from "./date-header";
|
export * from "./date-header";
|
||||||
export * from "./fullscreen-image-viewer";
|
export * from "./fullscreen-image-viewer";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||||
@@ -9,8 +9,6 @@ import { ROUTES } from "@/router/routes";
|
|||||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||||
|
|
||||||
import type { UserView } from "@/models/user/user";
|
|
||||||
|
|
||||||
import { BackBar } from "./back-bar";
|
import { BackBar } from "./back-bar";
|
||||||
import { UserHeader, type SidebarUserState } from "./user-header";
|
import { UserHeader, type SidebarUserState } from "./user-header";
|
||||||
import { VipBenefitsCard } from "./vip-benefits-card";
|
import { VipBenefitsCard } from "./vip-benefits-card";
|
||||||
@@ -41,16 +39,11 @@ export function SidebarScreen() {
|
|||||||
userDispatch({ type: "UserInit" });
|
userDispatch({ type: "UserInit" });
|
||||||
}, [userDispatch]);
|
}, [userDispatch]);
|
||||||
|
|
||||||
const prevUserRef = useRef<UserView | null>(user.currentUser);
|
const handleLogoutClick = () => {
|
||||||
useEffect(() => {
|
authDispatch({ type: "AuthReset" });
|
||||||
const wasLoggedIn = prevUserRef.current != null;
|
userDispatch({ type: "UserLogout" });
|
||||||
const isNowLoggedOut = user.currentUser == null;
|
router.replace(ROUTES.chat);
|
||||||
prevUserRef.current = user.currentUser;
|
};
|
||||||
if (wasLoggedIn && isNowLoggedOut) {
|
|
||||||
authDispatch({ type: "AuthReset" });
|
|
||||||
router.replace(ROUTES.chat);
|
|
||||||
}
|
|
||||||
}, [user.currentUser, authDispatch, router]);
|
|
||||||
|
|
||||||
// 状态派生:未登录 / 已登录未 VIP / 已登录 VIP
|
// 状态派生:未登录 / 已登录未 VIP / 已登录 VIP
|
||||||
const sidebarState: SidebarUserState =
|
const sidebarState: SidebarUserState =
|
||||||
@@ -111,7 +104,7 @@ export function SidebarScreen() {
|
|||||||
id: "logout",
|
id: "logout",
|
||||||
title: "Log out",
|
title: "Log out",
|
||||||
destructive: true,
|
destructive: true,
|
||||||
onClick: () => userDispatch({ type: "UserLogout" }),
|
onClick: handleLogoutClick,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
export * from "./subscription-back-link";
|
export * from "./subscription-back-link";
|
||||||
export * from "./subscription-banner";
|
export * from "./subscription-banner";
|
||||||
export * from "./subscription-benefits-card";
|
export * from "./subscription-benefits-card";
|
||||||
|
export * from "./subscription-checkout-button";
|
||||||
export * from "./subscription-cta-button";
|
export * from "./subscription-cta-button";
|
||||||
|
export * from "./subscription-manage-button";
|
||||||
export * from "./subscription-plan-card";
|
export * from "./subscription-plan-card";
|
||||||
export * from "./subscription-screen";
|
export * from "./subscription-screen";
|
||||||
export * from "./subscription-user-row";
|
export * from "./subscription-user-row";
|
||||||
|
|||||||
@@ -13,25 +13,25 @@
|
|||||||
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
|
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
|
||||||
*/
|
*/
|
||||||
import { AuthApi, authApi } from "@/data/services/api";
|
import { AuthApi, authApi } from "@/data/services/api";
|
||||||
import { AppleLoginRequest } from "@/data/dto/auth/apple_login_request";
|
import {
|
||||||
import { FacebookLoginRequest } from "@/data/dto/auth/facebook_login_request";
|
AppleLoginRequest,
|
||||||
import { FbIdLoginRequest } from "@/data/dto/auth/fb_id_login_request";
|
FacebookLoginRequest,
|
||||||
import { GoogleLoginRequest } from "@/data/dto/auth/google_login_request";
|
FbIdLoginRequest,
|
||||||
import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request";
|
GoogleLoginRequest,
|
||||||
import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
|
GuestLoginRequest,
|
||||||
import { LoginRequest } from "@/data/dto/auth/login_request";
|
GuestLoginResponse,
|
||||||
import { LoginResponse } from "@/data/dto/auth/login_response";
|
LoginRequest,
|
||||||
import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
|
LoginResponse,
|
||||||
import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
|
RefreshTokenRequest,
|
||||||
import { RegisterRequest } from "@/data/dto/auth/register_request";
|
RefreshTokenResponse,
|
||||||
import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
|
RegisterRequest,
|
||||||
import { User } from "@/data/dto/user/user";
|
SendCodeRequest,
|
||||||
|
} from "@/data/dto/auth";
|
||||||
|
import { User } from "@/data/dto/user";
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
import type { IAuthRepository } from "@/data/repositories/interfaces/iauth_repository";
|
import type { IAuthRepository } from "@/data/repositories/interfaces";
|
||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage, type IAuthStorage } from "@/data/storage/auth";
|
||||||
import type { IAuthStorage } from "@/data/storage/auth/iauth_storage";
|
import { UserStorage, type IUserStorage } from "@/data/storage/user";
|
||||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
|
||||||
import type { IUserStorage } from "@/data/storage/user/iuser_storage";
|
|
||||||
|
|
||||||
/** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`(Web 平台)。 */
|
/** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`(Web 平台)。 */
|
||||||
const WEB_PLATFORM = "web";
|
const WEB_PLATFORM = "web";
|
||||||
|
|||||||
@@ -16,14 +16,15 @@
|
|||||||
* 原始 Dart: lib/data/repositories/chat_repository_impl.dart
|
* 原始 Dart: lib/data/repositories/chat_repository_impl.dart
|
||||||
*/
|
*/
|
||||||
import { ChatApi, chatApi } from "@/data/services/api";
|
import { ChatApi, chatApi } from "@/data/services/api";
|
||||||
import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
|
import {
|
||||||
import { ChatMessage } from "@/data/dto/chat/chat_message";
|
ChatHistoryResponse,
|
||||||
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
|
ChatMessage,
|
||||||
import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
|
ChatSendResponse,
|
||||||
|
SendMessageRequest,
|
||||||
|
} from "@/data/dto/chat";
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
import type { IChatRepository } from "@/data/repositories/interfaces/ichat_repository";
|
import type { IChatRepository } from "@/data/repositories/interfaces";
|
||||||
import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage";
|
import { LocalChatStorage, LocalMessage } from "@/data/storage/chat";
|
||||||
import { LocalMessage } from "@/data/storage/chat/local_message";
|
|
||||||
|
|
||||||
export class ChatRepository implements IChatRepository {
|
export class ChatRepository implements IChatRepository {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Result } from "@/utils/result";
|
import type { Result } from "@/utils/result";
|
||||||
import type { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
|
import type {
|
||||||
import type { LoginResponse } from "@/data/dto/auth/login_response";
|
GuestLoginResponse,
|
||||||
import type { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
|
LoginResponse,
|
||||||
import type { User } from "@/data/dto/user/user";
|
RefreshTokenResponse,
|
||||||
|
} from "@/data/dto/auth";
|
||||||
|
import type { User } from "@/data/dto/user";
|
||||||
|
|
||||||
export interface IAuthRepository {
|
export interface IAuthRepository {
|
||||||
/** 用户注册。注册成功不会自动登录(与 Dart 行为一致)。 */
|
/** 用户注册。注册成功不会自动登录(与 Dart 行为一致)。 */
|
||||||
|
|||||||
@@ -12,9 +12,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Result } from "@/utils/result";
|
import type { Result } from "@/utils/result";
|
||||||
import type { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
|
import type {
|
||||||
import type { ChatMessage } from "@/data/dto/chat/chat_message";
|
ChatHistoryResponse,
|
||||||
import type { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
|
ChatMessage,
|
||||||
|
ChatSendResponse,
|
||||||
|
} from "@/data/dto/chat";
|
||||||
|
|
||||||
export interface IChatRepository {
|
export interface IChatRepository {
|
||||||
/** 发送一条消息。 */
|
/** 发送一条消息。 */
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Result } from "@/utils/result";
|
import type { Result } from "@/utils/result";
|
||||||
import type { CreditsData } from "@/data/dto/user/credits_data";
|
import type {
|
||||||
import type { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
|
CreditsData,
|
||||||
import type { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
|
CreditsHistoryData,
|
||||||
import type { User } from "@/data/dto/user/user";
|
UpdateProfileRequest,
|
||||||
import type { UserStatsResponse } from "@/data/dto/user/user_stats_response";
|
User,
|
||||||
|
UserStatsResponse,
|
||||||
|
} from "@/data/dto/user";
|
||||||
|
|
||||||
export interface IUserRepository {
|
export interface IUserRepository {
|
||||||
/** 获取用户统计信息。 */
|
/** 获取用户统计信息。 */
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
* 原始 Dart: lib/data/repositories/metrics_repository_impl.dart
|
* 原始 Dart: lib/data/repositories/metrics_repository_impl.dart
|
||||||
*/
|
*/
|
||||||
import { MetricsApi, metricsApi } from "@/data/services/api";
|
import { MetricsApi, metricsApi } from "@/data/services/api";
|
||||||
import { AppEvent } from "@/data/dto/metrics/app_event";
|
import { AppEvent, PwaEvent } from "@/data/dto/metrics";
|
||||||
import { PwaEvent } from "@/data/dto/metrics/pwa_event";
|
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
import type { IMetricsRepository } from "@/data/repositories/interfaces/imetrics_repository";
|
import type { IMetricsRepository } from "@/data/repositories/interfaces";
|
||||||
|
|
||||||
export class MetricsRepository implements IMetricsRepository {
|
export class MetricsRepository implements IMetricsRepository {
|
||||||
constructor(private readonly api: MetricsApi) {}
|
constructor(private readonly api: MetricsApi) {}
|
||||||
|
|||||||
@@ -9,13 +9,15 @@
|
|||||||
* 原始 Dart: lib/data/repositories/user_repository_impl.dart
|
* 原始 Dart: lib/data/repositories/user_repository_impl.dart
|
||||||
*/
|
*/
|
||||||
import { UserApi, userApi } from "@/data/services/api";
|
import { UserApi, userApi } from "@/data/services/api";
|
||||||
import { CreditsData } from "@/data/dto/user/credits_data";
|
import {
|
||||||
import { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
|
CreditsData,
|
||||||
import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
|
CreditsHistoryData,
|
||||||
import { User } from "@/data/dto/user/user";
|
UpdateProfileRequest,
|
||||||
import { UserStatsResponse } from "@/data/dto/user/user_stats_response";
|
User,
|
||||||
|
UserStatsResponse,
|
||||||
|
} from "@/data/dto/user";
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
import type { IUserRepository } from "@/data/repositories/interfaces/iuser_repository";
|
import type { IUserRepository } from "@/data/repositories/interfaces";
|
||||||
|
|
||||||
export class UserRepository implements IUserRepository {
|
export class UserRepository implements IUserRepository {
|
||||||
constructor(private readonly api: UserApi) {}
|
constructor(private readonly api: UserApi) {}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Zod 默认值辅助器(处理 `null | undefined` → 标量默认)
|
||||||
|
*
|
||||||
|
* 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`),
|
||||||
|
* Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。
|
||||||
|
* 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式:
|
||||||
|
* - input 接受 `string | null | undefined`(保持 optional)
|
||||||
|
* - output 永远是 `string`(User 类的 `email: string` 声明不会破)
|
||||||
|
*
|
||||||
|
* 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/** `string | null | undefined` → `string`(默认空串) */
|
||||||
|
export const stringOrEmpty = z
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.transform((v) => v ?? "")
|
||||||
|
.default("");
|
||||||
|
|
||||||
|
/** `number | null | undefined` → `number`(默认 0) */
|
||||||
|
export const numberOrZero = z
|
||||||
|
.number()
|
||||||
|
.nullable()
|
||||||
|
.transform((v) => v ?? 0)
|
||||||
|
.default(0);
|
||||||
|
|
||||||
|
/** `boolean | null | undefined` → `boolean`(默认 false) */
|
||||||
|
export const booleanOrFalse = z
|
||||||
|
.boolean()
|
||||||
|
.nullable()
|
||||||
|
.transform((v) => v ?? false)
|
||||||
|
.default(false);
|
||||||
@@ -4,19 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits";
|
import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits";
|
||||||
|
import {
|
||||||
/**
|
booleanOrFalse,
|
||||||
* 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`),
|
numberOrZero,
|
||||||
* Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。
|
stringOrEmpty,
|
||||||
* 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式:
|
} from "../nullable-defaults";
|
||||||
* - input 接受 `string | null | undefined`(保持 optional)
|
|
||||||
* - output 永远是 `string`(User 类的 `email: string` 声明不会破)
|
|
||||||
*
|
|
||||||
* 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。
|
|
||||||
*/
|
|
||||||
const stringOrEmpty = z.string().nullable().transform((v) => v ?? "").default("");
|
|
||||||
const numberOrZero = z.number().nullable().transform((v) => v ?? 0).default(0);
|
|
||||||
const booleanOrFalse = z.boolean().nullable().transform((v) => v ?? false).default(false);
|
|
||||||
|
|
||||||
export const UserSchema = z.object({
|
export const UserSchema = z.object({
|
||||||
id: stringOrEmpty, // 兜底:理论 id 不会 null,但保持防御
|
id: stringOrEmpty, // 兜底:理论 id 不会 null,但保持防御
|
||||||
|
|||||||
@@ -4,5 +4,7 @@
|
|||||||
|
|
||||||
export * from "./user-context";
|
export * from "./user-context";
|
||||||
export * from "./user-events";
|
export * from "./user-events";
|
||||||
|
export * from "./user-machine.actors";
|
||||||
|
export * from "./user-machine.helpers";
|
||||||
export * from "./user-machine";
|
export * from "./user-machine";
|
||||||
export * from "./user-state";
|
export * from "./user-state";
|
||||||
|
|||||||
Reference in New Issue
Block a user