feat(chat): render role-bound payment guidance
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ArrowRight, CircleDollarSign, X } from "lucide-react";
|
||||
|
||||
import type { PaymentGuidance } from "@/data/schemas/chat";
|
||||
import { recordChatActionEventById } from "@/lib/chat/chat_action_events";
|
||||
import { useActiveCharacter } from "@/providers/character-provider";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import { useUserSelector } from "@/stores/user/user-context";
|
||||
|
||||
import styles from "./chat-area.module.css";
|
||||
|
||||
export interface PaymentGuidanceCardProps {
|
||||
guidance: PaymentGuidance;
|
||||
}
|
||||
|
||||
export function PaymentGuidanceCard({ guidance }: PaymentGuidanceCardProps) {
|
||||
const character = useActiveCharacter();
|
||||
const navigator = useAppNavigator();
|
||||
const isVip = useUserSelector((state) => state.context.isVip);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const viewedIdRef = useRef<string | null>(null);
|
||||
const belongsToCurrentCharacter = guidance.characterId === character.id;
|
||||
const hasPurchaseAction =
|
||||
guidance.mode === "guide" &&
|
||||
guidance.target === "subscription" &&
|
||||
guidance.ctaLabel !== null &&
|
||||
guidance.purchaseOptions.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!belongsToCurrentCharacter) return;
|
||||
if (viewedIdRef.current === guidance.guidanceId) return;
|
||||
viewedIdRef.current = guidance.guidanceId;
|
||||
void recordChatActionEventById(
|
||||
guidance.guidanceId,
|
||||
character.id,
|
||||
"viewed",
|
||||
).catch(() => undefined);
|
||||
}, [belongsToCurrentCharacter, character.id, guidance.guidanceId]);
|
||||
|
||||
if (!belongsToCurrentCharacter || dismissed) return null;
|
||||
|
||||
const subscriptionType =
|
||||
isVip || !guidance.purchaseOptions.includes("vip") ? "topup" : "vip";
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={styles.commercialAction}
|
||||
aria-label={guidance.title ?? guidance.characterName}
|
||||
data-payment-guidance-id={guidance.guidanceId}
|
||||
data-payment-guidance-mode={guidance.mode}
|
||||
data-payment-guidance-scene={guidance.scene}
|
||||
>
|
||||
<div className={styles.commercialActionHeader}>
|
||||
<CircleDollarSign size={18} aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
className={styles.commercialActionDismiss}
|
||||
title="Dismiss"
|
||||
aria-label="Dismiss"
|
||||
onClick={() => {
|
||||
setDismissed(true);
|
||||
void recordChatActionEventById(
|
||||
guidance.guidanceId,
|
||||
character.id,
|
||||
"dismissed",
|
||||
).catch(() => undefined);
|
||||
}}
|
||||
>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{guidance.title ? (
|
||||
<strong>{guidance.title}</strong>
|
||||
) : null}
|
||||
<p className={styles.commercialActionCopy}>{guidance.copy}</p>
|
||||
{hasPurchaseAction ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.commercialActionButton}
|
||||
onClick={() => {
|
||||
void recordChatActionEventById(
|
||||
guidance.guidanceId,
|
||||
character.id,
|
||||
"opened",
|
||||
).catch(() => undefined);
|
||||
navigator.openSubscription({
|
||||
type: subscriptionType,
|
||||
returnTo: "chat",
|
||||
chatActionId: guidance.guidanceId,
|
||||
analytics: {
|
||||
entryPoint: "chat_input",
|
||||
triggerReason: "insufficient_credits",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>{guidance.ctaLabel}</span>
|
||||
<ArrowRight size={17} aria-hidden="true" />
|
||||
</button>
|
||||
) : null}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user