feat(FN-300): add trial urgency banner to subscription page
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - feat(FN-300): complete Step 1 — add trial urgency banner to subscription page Files changed: .../src/routes/dashboard/subscription/index.tsx | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) Fusion-Task-Id: FN-300
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
Minus,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Suspense, lazy, useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -163,6 +165,10 @@ export function SubscriptionPage() {
|
||||
const [downgradeOfferOpen, setDowngradeOfferOpen] = useState(false);
|
||||
const [downgradeSelectedBrandIds, setDowngradeSelectedBrandIds] = useState<string[]>([]);
|
||||
|
||||
// Trial urgency banner state
|
||||
const [trialBannerDismissed, setTrialBannerDismissed] = useState(false);
|
||||
const trialBannerViewedRef = useRef(false);
|
||||
|
||||
// Onboarding state
|
||||
const [onboardingPhase, setOnboardingPhase] = useState<"provisioning" | "completed" | null>(null);
|
||||
const [animationEnded, setAnimationEnded] = useState(false);
|
||||
@@ -392,6 +398,47 @@ export function SubscriptionPage() {
|
||||
return () => observer.disconnect();
|
||||
}, [selectedPlanKey]);
|
||||
|
||||
// ─── Trial urgency banner: compute days remaining ────────────────────────
|
||||
const trialDaysRemaining = (() => {
|
||||
if (!subscription || subscription.status !== "trial" || !subscription.endDate) return null;
|
||||
return Math.max(
|
||||
0,
|
||||
Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)),
|
||||
);
|
||||
})();
|
||||
|
||||
// Check localStorage dismissal
|
||||
useEffect(() => {
|
||||
if (!subscription?.endDate) return;
|
||||
const key = `trialBannerDismissed-${subscription.endDate}`;
|
||||
if (localStorage.getItem(key) === "true") {
|
||||
setTrialBannerDismissed(true);
|
||||
}
|
||||
}, [subscription?.endDate]);
|
||||
|
||||
// Fire view event once when banner becomes visible
|
||||
useEffect(() => {
|
||||
if (
|
||||
trialDaysRemaining === null ||
|
||||
trialDaysRemaining > 7 ||
|
||||
trialBannerDismissed ||
|
||||
trialBannerViewedRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
trialBannerViewedRef.current = true;
|
||||
capture("trial_urgency_banner_viewed", {
|
||||
days_remaining: trialDaysRemaining,
|
||||
plan_name: subscription?.plan?.name,
|
||||
billing_period: subscription?.billingPeriod,
|
||||
});
|
||||
}, [
|
||||
trialDaysRemaining,
|
||||
trialBannerDismissed,
|
||||
subscription?.plan?.name,
|
||||
subscription?.billingPeriod,
|
||||
]);
|
||||
|
||||
// ─── Social proof impression tracking ────────────────────────────────────
|
||||
const trustBadgesRef = useRef<HTMLDivElement>(null);
|
||||
const paymentTrustRef = useRef<HTMLDivElement>(null);
|
||||
@@ -440,6 +487,26 @@ export function SubscriptionPage() {
|
||||
capture("social_proof_engaged", { page: "subscription", section: "payment_trust" });
|
||||
}
|
||||
|
||||
// ─── Trial urgency banner handlers ────────────────────────────────────────
|
||||
function handleTrialBannerCTAClick() {
|
||||
capture("trial_urgency_banner_cta_clicked", {
|
||||
days_remaining: trialDaysRemaining,
|
||||
plan_name: subscription?.plan?.name,
|
||||
billing_period: subscription?.billingPeriod,
|
||||
});
|
||||
planCardsRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
function handleTrialBannerDismiss() {
|
||||
capture("trial_urgency_banner_dismissed", {
|
||||
days_remaining: trialDaysRemaining,
|
||||
plan_name: subscription?.plan?.name,
|
||||
billing_period: subscription?.billingPeriod,
|
||||
});
|
||||
localStorage.setItem(`trialBannerDismissed-${subscription?.endDate}`, "true");
|
||||
setTrialBannerDismissed(true);
|
||||
}
|
||||
|
||||
// ─── Loading state ─────────────────────────────────────────────────────────
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -921,6 +988,46 @@ export function SubscriptionPage() {
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Trial Urgency Banner */}
|
||||
{trialDaysRemaining !== null && trialDaysRemaining <= 7 && !trialBannerDismissed && (
|
||||
<div
|
||||
role="alert"
|
||||
className="animate-fade-in-up flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-800 dark:bg-amber-950 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<AlertTriangle className="size-5 shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
|
||||
<div className="flex flex-1 flex-col gap-1 sm:flex-row sm:items-center sm:gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
{t("subscription.trialBanner.title")}
|
||||
</p>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||||
{t("subscription.trialBanner.description", {
|
||||
days: trialDaysRemaining,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="shrink-0 bg-amber-600 text-white hover:bg-amber-700 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400"
|
||||
onClick={handleTrialBannerCTAClick}
|
||||
>
|
||||
{t("subscription.trialBanner.cta")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTrialBannerDismiss}
|
||||
className="flex size-8 shrink-0 items-center justify-center rounded-md text-amber-600 transition-colors hover:bg-amber-100 dark:text-amber-400 dark:hover:bg-amber-900"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Billing Period Toggle */}
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user