feat(tip): add personalized payment success experience

This commit is contained in:
2026-07-20 19:06:42 +08:00
parent c187f0b817
commit edf50e9cc4
16 changed files with 922 additions and 74 deletions
@@ -145,7 +145,6 @@ describe("TipScreen checkout", () => {
["the selected plan is missing", { plans: [], selectedPlanId: "" }],
["an order is being created", { isCreatingOrder: true }],
["an order is being polled", { isPollingOrder: true }],
["the order is paid", { isPaid: true }],
] as const)("disables checkout when %s", (_label, overrides) => {
mocks.payment = makePaymentState(overrides);
renderScreen();
@@ -153,6 +152,75 @@ describe("TipScreen checkout", () => {
expect(getCheckoutButton().disabled).toBe(true);
});
it("replaces checkout with the first Tip success experience", () => {
mocks.payment = makePaymentState({
status: "paid",
isPaid: true,
orderStatus: "paid",
tipCount: 1,
thankYouMessage: "A backend message that is not used for first Tip.",
});
renderScreen();
expect(container.querySelector('[data-testid="tip-checkout"]')).toBeNull();
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(
"Thank you. I'll definitely think of you while I enjoy it.",
);
expect(document.activeElement?.id).toBe("tip-success-title");
const sendAgain = getButton("Send another coffee");
act(() => sendAgain.click());
expect(mocks.paymentDispatch).toHaveBeenCalledWith({
type: "PaymentReset",
});
expect(
container
.querySelector<HTMLAnchorElement>(
'[data-analytics-key="tip.success_back_to_splash"]',
)
?.getAttribute("href"),
).toBe("/characters/maya/splash");
});
it("shows the repeat Tip count and backend thank-you message", () => {
mocks.payment = makePaymentState({
status: "paid",
isPaid: true,
orderStatus: "paid",
tipCount: 22,
thankYouMessage: "You always make my day sweeter.",
});
renderScreen();
expect(container.textContent).toContain(
"This is the 22nd coffee you've treated me to.",
);
expect(container.textContent).toContain(
"You always make my day sweeter.",
);
});
it("uses generic success copy when Tip metadata is incomplete", () => {
mocks.payment = makePaymentState({
status: "paid",
isPaid: true,
orderStatus: "paid",
tipCount: 2,
thankYouMessage: null,
});
renderScreen();
expect(container.textContent).toContain(
"Thank you. Your coffee made me smile.",
);
expect(container.textContent).not.toContain("2nd coffee");
});
function renderScreen(): void {
act(() => root.render(<TipScreen />));
}
@@ -164,6 +232,14 @@ describe("TipScreen checkout", () => {
if (!button) throw new Error("Missing Tip checkout button");
return button;
}
function getButton(label: string): HTMLButtonElement {
const button = Array.from(
container.querySelectorAll<HTMLButtonElement>("button"),
).find((item) => item.textContent?.trim() === label);
if (!button) throw new Error(`Missing button: ${label}`);
return button;
}
});
function makePaymentState(
@@ -180,6 +256,8 @@ function makePaymentState(
currentOrderId: null,
payParams: null,
orderStatus: null,
tipCount: null,
thankYouMessage: null,
errorMessage: null,
launchNonce: 0,
isLoadingPlans: false,