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
+132
View File
@@ -0,0 +1,132 @@
"use client";
import { useEffect, useRef, type CSSProperties } from "react";
import Image from "next/image";
import Link from "next/link";
import { Coffee, Heart, Sparkles } from "lucide-react";
import { CharacterAvatar } from "@/app/_components";
import { MobileShell } from "@/app/_components/core";
import type { TipCoffeeOption } from "@/lib/tip/tip_coffee";
import { resolveTipSuccessCopy } from "./tip-screen.helpers";
import styles from "./tip-success-view.module.css";
export interface TipSuccessViewProps {
characterName: string;
characterAvatar: string;
characterCover: string;
coffeeOption: TipCoffeeOption;
splashHref: string;
tipCount: number | null;
thankYouMessage: string | null;
onSendAgain: () => void;
}
export function TipSuccessView({
characterName,
characterAvatar,
characterCover,
coffeeOption,
splashHref,
tipCount,
thankYouMessage,
onSendAgain,
}: TipSuccessViewProps) {
const titleRef = useRef<HTMLHeadingElement>(null);
const copy = resolveTipSuccessCopy(tipCount, thankYouMessage);
useEffect(() => {
titleRef.current?.focus({ preventScroll: true });
}, []);
return (
<MobileShell background="#fff7f0">
<main
className={styles.shell}
style={
{
"--tip-success-cover": `url("${characterCover}")`,
} as CSSProperties
}
>
<div className={styles.coverWash} aria-hidden="true" />
<div className={styles.glowOne} aria-hidden="true" />
<div className={styles.glowTwo} aria-hidden="true" />
<section
className={styles.card}
aria-live="polite"
aria-labelledby="tip-success-title"
>
<div className={styles.visual}>
<span className={styles.sparkleOne} aria-hidden="true">
<Sparkles size={22} strokeWidth={1.8} />
</span>
<span className={styles.sparkleTwo} aria-hidden="true">
<Heart size={17} fill="currentColor" strokeWidth={1.8} />
</span>
<div className={styles.avatarFrame}>
<CharacterAvatar
src={characterAvatar}
alt={characterName}
size="100%"
imageSize={112}
priority
/>
</div>
<div className={styles.coffeeFrame}>
<Image
src={coffeeOption.image.src}
alt={`${coffeeOption.displayName} coffee`}
width={coffeeOption.image.width}
height={coffeeOption.image.height}
sizes="78px"
className={styles.coffeeImage}
priority
/>
</div>
</div>
<p className={styles.eyebrow}>
<Coffee size={15} aria-hidden="true" />
Coffee received
</p>
<h1
ref={titleRef}
id="tip-success-title"
className={styles.title}
tabIndex={-1}
>
{copy.title}
</h1>
{copy.body.length > 0 ? (
<div className={styles.copy}>
{copy.body.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
) : null}
<div className={styles.actions}>
<button
type="button"
className={styles.primaryAction}
data-analytics-key="tip.send_again"
onClick={onSendAgain}
>
Send another coffee
</button>
<Link
href={splashHref}
className={styles.secondaryAction}
data-analytics-key="tip.success_back_to_splash"
>
Back to {characterName}
</Link>
</div>
</section>
</main>
</MobileShell>
);
}