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;