diff --git a/env-example/.env.local.example b/env-example/.env.local.example index 9a7f6e48..dccf21c6 100644 --- a/env-example/.env.local.example +++ b/env-example/.env.local.example @@ -3,8 +3,8 @@ NEXT_PUBLIC_APP_ENV=test # NextAuth v4 —— OAuth callback **公**网 base URL(**必**须与**公**网域名**一**致) NEXTAUTH_URL=https://frontend-test.banlv-ai.com NEXTAUTH_URL_INTERNAL=http://localhost:3000 -NEXT_PUBLIC_API_BASE_URL=https://testapi.banlv-ai.com -NEXT_PUBLIC_WS_BASE_URL=wss://testapi.banlv-ai.com/ws +NEXT_PUBLIC_API_BASE_URL=https://proapi.banlv-ai.com +NEXT_PUBLIC_WS_BASE_URL=wss://proapi.banlv-ai.com/ws NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51Te8ybEJDtUGxQHo0UMySJ2hW0vVryynzllmIUI3UQdQCFgxNLci0Jmm2lQkRsTaIP7C7IQlFzfFyBJ5rOhr1ML800G8P8DF5R NEXT_PUBLIC_API_CONNECT_TIMEOUT=30000 diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index 703a31bd..7ae5b62b 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -43,10 +43,15 @@ export function ChatScreen() { // isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段) const isGuest = deriveIsGuest(authState.loginStatus); - // 消息数量限制由后端统一返回 blocked/daily_limit,前端不再处理本地额度。 - const showDailyLimitBanner = + // 消息数量限制由后端统一返回 blocked,前端不再处理本地额度。 + const showMessageLimitBanner = state.paywallTriggered && - state.paywallReason === "daily_limit"; + (state.paywallReason === "daily_limit" || + state.paywallReason === "total_limit"); + const messageLimitTitle = + state.paywallReason === "total_limit" + ? "The limit for free chat times\nhas been reached" + : undefined; const showPhotoPaywallBanner = state.paywallTriggered && state.paywallReason === "photo_paywall"; const showPrivatePaywallBanner = @@ -168,8 +173,9 @@ export function ChatScreen() { /> ) : null} - {showDailyLimitBanner ? ( + {showMessageLimitBanner ? ( ) : ( diff --git a/src/app/subscription/components/subscription-back-link.tsx b/src/app/subscription/components/subscription-back-link.tsx index 42bf3f6c..ba2186ce 100644 --- a/src/app/subscription/components/subscription-back-link.tsx +++ b/src/app/subscription/components/subscription-back-link.tsx @@ -3,10 +3,11 @@ * 订阅页返回箭头 * * 视觉规格(与设计稿对齐): - * - 24×24 SVG chevron-left + * - 24×24 chevron-left 图标 * - 黑色,2px 描边 * - 无 "Back" 文本 */ +import { ChevronLeft } from "lucide-react"; import Link from "next/link"; import { ROUTES } from "@/router/routes"; @@ -24,22 +25,12 @@ export function SubscriptionBackLink({ className }: SubscriptionBackLinkProps) { aria-label="Back" className={[styles.backLink, className].filter(Boolean).join(" ")} > - + /> ); } diff --git a/src/app/subscription/components/subscription-screen.module.css b/src/app/subscription/components/subscription-screen.module.css index ef666517..ce5e7862 100644 --- a/src/app/subscription/components/subscription-screen.module.css +++ b/src/app/subscription/components/subscription-screen.module.css @@ -3,14 +3,13 @@ flex-direction: column; min-height: 100dvh; background: var(--color-page-background); - padding: 30px 30px 10px; + padding: 18 30px 10px; } .header { display: flex; align-items: center; height: 40px; - padding-top: var(--spacing-sm); } .backSlot { @@ -73,7 +72,7 @@ } .agreementLabel { - font-size: 17px; + font-size: 12px; } .agreementLink { diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index 67bdf790..ba2179f4 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -78,7 +78,8 @@ function currencySymbol(currency: string): string { return `${currency} `; } -function formatAmount(amountCents: number): string { +function formatAmount(amountCents: number | null): string | null { + if (amountCents === null) return null; return (amountCents / 100).toFixed(2).replace(/\.00$/, ""); } @@ -91,9 +92,15 @@ function planCaption( ? "Voice package" : `${plan.dolAmount} voice messages`; } + if (plan.dailyPriceCents !== null) { + const dailyPrice = formatAmount(plan.dailyPriceCents); + return dailyPrice === null + ? "Lifetime" + : `${currencySymbol(plan.currency)}${dailyPrice}/day`; + } if (plan.vipDays === null) return "Lifetime"; - const perDay = plan.amountCents / 100 / Math.max(plan.vipDays, 1); - return `${currencySymbol(plan.currency)}${perDay.toFixed(2)}/day`; + const fallbackDailyPrice = plan.amountCents / Math.max(plan.vipDays, 1); + return `${currencySymbol(plan.currency)}${formatAmount(fallbackDailyPrice)}/day`; } function toPlanView( @@ -103,8 +110,8 @@ function toPlanView( return { id: plan.planId, name: plan.planName, - price: formatAmount(plan.amountCents), - originalPrice: null, + price: formatAmount(plan.amountCents) ?? "", + originalPrice: formatAmount(plan.originalAmountCents), perDay: planCaption(plan, subscriptionType), currencySymbol: currencySymbol(plan.currency), }; diff --git a/src/data/dto/payment/payment_plan.ts b/src/data/dto/payment/payment_plan.ts index d5f933c1..76a62641 100644 --- a/src/data/dto/payment/payment_plan.ts +++ b/src/data/dto/payment/payment_plan.ts @@ -12,6 +12,8 @@ export class PaymentPlan { declare readonly planName: string; declare readonly orderType: string; declare readonly amountCents: number; + declare readonly originalAmountCents: number | null; + declare readonly dailyPriceCents: number | null; declare readonly currency: string; declare readonly vipDays: number | null; declare readonly dolAmount: number | null; diff --git a/src/data/mock/chat/responses/send-message-total-limit-blocked.json b/src/data/mock/chat/responses/send-message-total-limit-blocked.json new file mode 100644 index 00000000..348f8e6d --- /dev/null +++ b/src/data/mock/chat/responses/send-message-total-limit-blocked.json @@ -0,0 +1,30 @@ +{ + "code": 200, + "message": "success", + "success": true, + "data": { + "mode": "http", + "type": "text", + "reply": "", + "voiceUrl": "", + "audioUrl": "", + "intimacyChange": 0, + "newIntimacy": 0, + "relationshipStage": "密友", + "currentMood": "happy", + "messageId": "", + "isGuest": true, + "timestamp": 1782197944984, + "blocked": true, + "blockReason": "total_limit", + "blockDetail": { + "type": "guest_total_msg_limit", + "usedTotal": 12, + "limit": 5 + }, + "paywallTriggered": false, + "showUpgrade": false, + "imageType": null, + "imageUrl": null + } +} diff --git a/src/data/schemas/chat/chat_send_response.ts b/src/data/schemas/chat/chat_send_response.ts index 26d489e7..add4757b 100644 --- a/src/data/schemas/chat/chat_send_response.ts +++ b/src/data/schemas/chat/chat_send_response.ts @@ -6,7 +6,8 @@ import { z } from "zod"; export const ChatBlockDetailSchema = z.object({ type: z.string(), - usedToday: z.number(), + usedToday: z.number().optional(), + usedTotal: z.number().optional(), limit: z.number(), }); diff --git a/src/data/schemas/payment/payment_plan.ts b/src/data/schemas/payment/payment_plan.ts index 249e51f3..6bbd2c72 100644 --- a/src/data/schemas/payment/payment_plan.ts +++ b/src/data/schemas/payment/payment_plan.ts @@ -8,6 +8,8 @@ const PaymentPlanWireSchema = z.object({ plan_name: z.string(), order_type: z.string(), amount_cents: z.number(), + original_amount_cents: z.number().nullable().default(null), + daily_price_cents: z.number().nullable().default(null), currency: z.string(), vip_days: z.number().nullable(), dol_amount: z.number().nullable(), @@ -23,6 +25,8 @@ export const PaymentPlanSchema = z plan_name: data.planName, order_type: data.orderType, amount_cents: data.amountCents, + original_amount_cents: data.originalAmountCents, + daily_price_cents: data.dailyPriceCents, currency: data.currency, vip_days: data.vipDays, dol_amount: data.dolAmount, @@ -35,6 +39,8 @@ export const PaymentPlanSchema = z planName: data.plan_name, orderType: data.order_type, amountCents: data.amount_cents, + originalAmountCents: data.original_amount_cents, + dailyPriceCents: data.daily_price_cents, currency: data.currency, vipDays: data.vip_days, dolAmount: data.dol_amount, diff --git a/src/stores/chat/chat-machine.helpers.ts b/src/stores/chat/chat-machine.helpers.ts index 1f31b054..e0fa41a2 100644 --- a/src/stores/chat/chat-machine.helpers.ts +++ b/src/stores/chat/chat-machine.helpers.ts @@ -103,7 +103,12 @@ export function applyHttpSendOutput( ): Partial { const { response, reply } = output; - if (response.blocked === true && response.blockReason === "daily_limit") { + const isMessageLimitBlocked = + response.blocked === true && + (response.blockReason === "daily_limit" || + response.blockReason === "total_limit"); + + if (isMessageLimitBlocked) { const detail = response.blockDetail; const lastMessage = context.messages[context.messages.length - 1]; const messages = @@ -115,9 +120,19 @@ export function applyHttpSendOutput( messages, isReplyingAI: false, paywallTriggered: true, - paywallReason: "daily_limit", + paywallReason: + response.blockReason === "total_limit" ? "total_limit" : "daily_limit", paywallDetail: detail - ? { usedToday: detail.usedToday, limit: detail.limit } + ? { + type: detail.type, + ...(detail.usedToday != null + ? { usedToday: detail.usedToday } + : {}), + ...(detail.usedTotal != null + ? { usedTotal: detail.usedTotal } + : {}), + limit: detail.limit, + } : null, }; } diff --git a/src/stores/chat/chat-state.ts b/src/stores/chat/chat-state.ts index 12e2df89..69c3d2d0 100644 --- a/src/stores/chat/chat-state.ts +++ b/src/stores/chat/chat-state.ts @@ -19,8 +19,20 @@ export interface ChatState { */ wsConnected: boolean; paywallTriggered: boolean; - paywallReason: "daily_limit" | "photo_paywall" | "private_paywall" | null; - paywallDetail: { usedToday: number; limit: number } | null; + paywallReason: + | "daily_limit" + | "total_limit" + | "photo_paywall" + | "private_paywall" + | null; + paywallDetail: + | { + type?: string; + usedToday?: number; + usedTotal?: number; + limit: number; + } + | null; unlockingPrivateMessageId: string | null; isLoadingMore: boolean; hasMore: boolean;