feat(payment): add chat support discounts and gratitude
Docker Image / Build and Push Docker Image (push) Successful in 2m14s

(cherry picked from commit ef9b79bc83)
This commit is contained in:
Codex
2026-07-28 17:42:53 +08:00
parent 59e4eac736
commit 74b7eae18b
35 changed files with 768 additions and 56 deletions
+45 -9
View File
@@ -9,10 +9,10 @@ import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics
import type { PayChannel } from "@/data/schemas/payment";
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
import {
DEFAULT_CHARACTER,
DEFAULT_CHARACTER_SLUG,
getCharacterBySlug,
} from "@/data/constants/character";
import { useCharacterCatalog } from "@/providers/character-catalog-provider";
import {
behaviorAnalytics,
getDefaultPaymentAnalyticsContext,
@@ -51,7 +51,7 @@ export interface SubscriptionScreenProps {
returnTo?: SubscriptionReturnTo;
initialPayChannel?: PayChannel | null;
analyticsContext?: PaymentAnalyticsContext;
sourceCharacterSlug?: string;
sourceCharacterSlug?: string | null;
initialPlanId?: string | null;
initialAutoRenew?: boolean | null;
commercialOfferId?: string | null;
@@ -80,9 +80,15 @@ export function SubscriptionScreen({
const [paymentIssueNotice, setPaymentIssueNotice] = useState<string | null>(
null,
);
const characterCatalog = useCharacterCatalog();
const [selectedSupportCharacterSlug, setSelectedSupportCharacterSlug] =
useState<string | null>(() =>
getCharacterBySlug(sourceCharacterSlug)?.slug ?? null,
);
const userState = useUserState();
const sourceCharacter =
getCharacterBySlug(sourceCharacterSlug) ?? DEFAULT_CHARACTER;
const sourceCharacter = characterCatalog.getBySlug(
selectedSupportCharacterSlug,
);
const hasHydrated = useHasHydrated();
const countryCode = userState.currentUser?.countryCode;
const paymentMethodConfig = getPaymentMethodConfig({
@@ -102,7 +108,7 @@ export function SubscriptionScreen({
subscriptionType,
shouldResumePendingOrder,
returnTo,
sourceCharacterSlug,
sourceCharacterSlug: sourceCharacter?.slug ?? null,
initialPayChannel: paymentMethodConfig.initialPayChannel,
initialPlanId,
initialAutoRenew,
@@ -163,6 +169,7 @@ export function SubscriptionScreen({
(plan) => plan.id === payment.selectedPlanId,
);
const canActivate =
sourceCharacter !== null &&
selectedPlan !== null &&
canCheckoutSubscriptionPlan({
selectedPlanId: payment.selectedPlanId,
@@ -271,7 +278,36 @@ export function SubscriptionScreen({
</p>
) : null}
{chatActionId ? (
<section className={styles.supportCharacterSelector}>
<h2>
{sourceCharacter
? `Supporting ${sourceCharacter.shortName}`
: "Choose who you want to support"}
</h2>
<p>
Your VIP or credit purchase supports the character you choose, and
they will thank you after the payment and benefits are confirmed.
</p>
<div className={styles.supportCharacterOptions}>
{characterCatalog.characters.map((character) => (
<button
key={character.id}
type="button"
className={
sourceCharacter?.id === character.id
? styles.supportCharacterOptionActive
: styles.supportCharacterOption
}
aria-pressed={sourceCharacter?.id === character.id}
onClick={() => setSelectedSupportCharacterSlug(character.slug)}
>
{character.shortName}
</button>
))}
</div>
</section>
{chatActionId && sourceCharacter ? (
<section
className={styles.characterSupportBanner}
aria-label={`Support ${sourceCharacter.displayName}`}
@@ -287,7 +323,7 @@ export function SubscriptionScreen({
</section>
) : null}
{payment.commercialOffer && !hasFirstRechargeOffer ? (
{payment.commercialOffer && !hasFirstRechargeOffer && sourceCharacter ? (
<section
className={styles.firstRechargeBanner}
aria-label={`${sourceCharacter.shortName} private offer`}
@@ -339,7 +375,7 @@ export function SubscriptionScreen({
disabled={!canActivate}
subscriptionType={subscriptionType}
returnTo={returnTo}
sourceCharacterSlug={sourceCharacterSlug}
sourceCharacterSlug={sourceCharacter?.slug ?? DEFAULT_CHARACTER_SLUG}
/>
</div>
@@ -364,7 +400,7 @@ export function SubscriptionScreen({
orderId={payment.currentOrderId}
payChannel={payment.payChannel}
countryCode={countryCode}
characterId={sourceCharacterSlug}
characterId={sourceCharacter?.id ?? ""}
onClose={() => setShowPaymentIssueDialog(false)}
onSubmitted={setPaymentIssueNotice}
/>