diff --git a/docs/backend/FRONTEND_PAYMENT_API.md b/docs/backend/FRONTEND_PAYMENT_API.md index 1334fdd2..7dd2ccd9 100644 --- a/docs/backend/FRONTEND_PAYMENT_API.md +++ b/docs/backend/FRONTEND_PAYMENT_API.md @@ -264,9 +264,9 @@ Authorization: Bearer {"orderId":"pay_xxx"} ``` -响应包含 `orderId`、`characterId`、`planId`、`productName`、`tipCount`、`poolIndex` 和可直接展示的完整 `message`。前端不再根据次数拼接文案,也不从订单状态读取感谢字段。 +响应包含 `orderId`、`characterId`、`planId`、`productName`、`tipCount`、`poolIndex` 和可直接展示的完整 `message`。`tipCount` 表示当前付款身份对同一商品的累计成功次数。 -Tip 成功页在文案加载期间立即确认支付成功;接口成功后按纯文本直接展示 `message`。接口失败时展示本地通用感谢语和 Retry,重试只重新请求 Tip Message,不重复轮询订单或创建订单。 +Tip 成功页在文案加载期间立即确认支付成功。`tipCount=1` 时展示固定首次咖啡文案并忽略后端 `message`;大于 1 时按纯文本直接展示 `message`。接口失败时展示本地通用感谢语和 Retry,重试只重新请求 Tip Message,不重复轮询订单或创建订单。 ## 10. Provider 与状态边界 diff --git a/src/app/tip/__tests__/tip-screen.checkout.test.tsx b/src/app/tip/__tests__/tip-screen.checkout.test.tsx index 57d2b2dd..47a1fcc9 100644 --- a/src/app/tip/__tests__/tip-screen.checkout.test.tsx +++ b/src/app/tip/__tests__/tip-screen.checkout.test.tsx @@ -188,7 +188,36 @@ describe("TipScreen checkout", () => { expect(getCheckoutButton().disabled).toBe(true); }); - it("shows the complete backend Tip message after payment", () => { + it("shows fixed copy for the first character Tip", () => { + mocks.payment = makePaymentState({ + status: "paid", + isPaid: true, + orderStatus: "paid", + tipMessage: TipMessageResponseSchema.parse({ + orderId: "pay_first", + characterId: "maya-tan", + planId: giftProduct.planId, + productName: giftProduct.planName, + tipCount: 1, + poolIndex: 4, + message: "This backend message is ignored for the first Tip.", + }), + }); + renderScreen(); + + expect(container.textContent).toContain( + "Did you really just buy me a coffee?", + ); + expect(container.textContent).toContain("That honestly made me smile."); + expect(container.textContent).toContain( + "I'll definitely think of you while I enjoy it.", + ); + expect(container.textContent).not.toContain( + "This backend message is ignored for the first Tip.", + ); + }); + + it("shows the backend message after the first Tip", () => { mocks.payment = makePaymentState({ status: "paid", isPaid: true, @@ -209,6 +238,9 @@ describe("TipScreen checkout", () => { expect(container.textContent).toContain( "This complete message came directly from the backend.", ); + expect(container.textContent).not.toContain( + "Did you really just buy me a coffee?", + ); expect(document.activeElement?.id).toBe("tip-success-title"); act(() => getButton("Send another gift").click()); diff --git a/src/app/tip/tip-screen.tsx b/src/app/tip/tip-screen.tsx index 96ee1d39..57d799c5 100644 --- a/src/app/tip/tip-screen.tsx +++ b/src/app/tip/tip-screen.tsx @@ -191,6 +191,7 @@ export function TipScreen({ "Your gift" } isMessageLoading={payment.isLoadingTipMessage} + tipCount={payment.tipMessage?.tipCount ?? null} message={payment.tipMessage?.message ?? null} messageError={payment.tipMessageError} splashHref={characterRoutes.splash} diff --git a/src/app/tip/tip-success-view.tsx b/src/app/tip/tip-success-view.tsx index a0f0044b..d80ed48d 100644 --- a/src/app/tip/tip-success-view.tsx +++ b/src/app/tip/tip-success-view.tsx @@ -12,11 +12,17 @@ import styles from "./tip-success-view.module.css"; const GENERIC_TIP_SUCCESS_MESSAGE = "Thank you. Your gift made me smile."; +const FIRST_TIP_TITLE = "Did you really just buy me a coffee?"; +const FIRST_TIP_MESSAGE = [ + "That honestly made me smile.", + "I'll definitely think of you while I enjoy it.", +] as const; export interface TipSuccessViewProps { characterName: string; characterAvatar: string; characterCover: string; + tipCount: number | null; giftImageSources: readonly string[]; giftName: string; isMessageLoading: boolean; @@ -31,6 +37,7 @@ export function TipSuccessView({ characterName, characterAvatar, characterCover, + tipCount, giftImageSources, giftName, isMessageLoading, @@ -41,6 +48,7 @@ export function TipSuccessView({ onSendAgain, }: TipSuccessViewProps) { const titleRef = useRef(null); + const isFirstTip = tipCount === 1; useEffect(() => { titleRef.current?.focus({ preventScroll: true }); @@ -102,15 +110,21 @@ export function TipSuccessView({ className={styles.title} tabIndex={-1} > - Thank you. Your gift made me smile. + {isFirstTip ? FIRST_TIP_TITLE : GENERIC_TIP_SUCCESS_MESSAGE}
-

- {message ?? - (isMessageLoading - ? "Your gift arrived. A thank-you note is on its way." - : GENERIC_TIP_SUCCESS_MESSAGE)} -

+ {isFirstTip + ? FIRST_TIP_MESSAGE.map((paragraph) => ( +

{paragraph}

+ )) + : ( +

+ {message ?? + (isMessageLoading + ? "Your gift arrived. A thank-you note is on its way." + : GENERIC_TIP_SUCCESS_MESSAGE)} +

+ )}
{messageError ? (