feat(web): show persistent trial-progress banner for days 8-30
Trial users previously got no reminder of remaining days until the urgent (<=7d) amber banner appeared, leaving most of the 30-day window with no conversion nudge. Added a calm, brand-toned TrialProgressBanner shown when more than 7 days remain: days left, a progress bar (elapsed share of the 30-day trial), and a "Planını seç" CTA that scrolls to checkout and fires trial_progress_cta_clicked. The urgent banner still owns the final 7 days. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,11 @@
|
||||
"description": "{days} days left. Upgrade now to continue accessing all features.",
|
||||
"cta": "Upgrade Plan"
|
||||
},
|
||||
"trialProgress": {
|
||||
"title": "{days} days left in your Full Package trial",
|
||||
"description": "Pick a plan now to keep your access without interruption.",
|
||||
"cta": "Choose a plan"
|
||||
},
|
||||
"pendingBanner": {
|
||||
"title": "You have a pending payment",
|
||||
"description": "Checkout for {plan} ({brands}) was left incomplete. Resume it or cancel to pick a different plan.",
|
||||
|
||||
@@ -270,6 +270,11 @@
|
||||
"description": "{days} gününüz kaldı. Tüm özelliklere erişmeye devam etmek için hemen yükseltin.",
|
||||
"cta": "Plana Yükselt"
|
||||
},
|
||||
"trialProgress": {
|
||||
"title": "Full Paket denemende {days} gün kaldı",
|
||||
"description": "Kesintisiz devam etmek için planını şimdi seç.",
|
||||
"cta": "Planını seç"
|
||||
},
|
||||
"pendingBanner": {
|
||||
"title": "Bekleyen ödemen var",
|
||||
"description": "{plan} planı, {brands} için ödeme yarıda kaldı. Devam edebilir veya farklı plan seçmek için vazgeçebilirsin.",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Tests for TrialProgressBanner — the calm, persistent trial-status strip
|
||||
* shown for days 8–30 of a trial. Previously trial users saw no reminder of
|
||||
* remaining days until the urgent (<=7d) banner appeared, leaving the bulk of
|
||||
* the trial window with no conversion nudge.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, vars?: Record<string, unknown>) =>
|
||||
vars ? `${key}:${JSON.stringify(vars)}` : key,
|
||||
locale: "tr",
|
||||
setLocale: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { TrialProgressBanner } from "@/routes/dashboard/subscription/index";
|
||||
|
||||
describe("TrialProgressBanner", () => {
|
||||
test("renders remaining days, description and CTA", () => {
|
||||
const { container } = render(<TrialProgressBanner daysRemaining={20} onCta={vi.fn()} />);
|
||||
const text = container.textContent ?? "";
|
||||
expect(text).toContain("subscription.trialProgress.title");
|
||||
expect(text).toContain('"days":20');
|
||||
expect(text).toContain("subscription.trialProgress.description");
|
||||
expect(text).toContain("subscription.trialProgress.cta");
|
||||
});
|
||||
|
||||
test("progress bar width reflects elapsed share of a 30-day trial", () => {
|
||||
// 20 days left of 30 => 10 elapsed => 33%
|
||||
const { container } = render(<TrialProgressBanner daysRemaining={20} onCta={vi.fn()} />);
|
||||
const bar = container.querySelector('[style*="width"]') as HTMLElement | null;
|
||||
expect(bar).not.toBeNull();
|
||||
expect(bar!.style.width).toBe("33%");
|
||||
});
|
||||
|
||||
test("clamps progress for out-of-range day counts", () => {
|
||||
const { container } = render(<TrialProgressBanner daysRemaining={40} onCta={vi.fn()} />);
|
||||
const bar = container.querySelector('[style*="width"]') as HTMLElement;
|
||||
expect(bar.style.width).toBe("0%");
|
||||
});
|
||||
|
||||
test("CTA click invokes onCta", () => {
|
||||
const onCta = vi.fn();
|
||||
const { getByRole } = render(<TrialProgressBanner daysRemaining={15} onCta={onCta} />);
|
||||
fireEvent.click(getByRole("button"));
|
||||
expect(onCta).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -589,6 +589,16 @@ export function SubscriptionPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{trialDaysRemaining !== null && trialDaysRemaining > 7 && (
|
||||
<TrialProgressBanner
|
||||
daysRemaining={trialDaysRemaining}
|
||||
onCta={() => {
|
||||
capture("trial_progress_cta_clicked", { days_remaining: trialDaysRemaining });
|
||||
document.getElementById("checkout-flow")?.scrollIntoView({ behavior: "smooth" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPending && subscription && (
|
||||
<PendingPaymentBanner
|
||||
planName={subscription.plan?.name ?? ""}
|
||||
@@ -1611,6 +1621,50 @@ function TrialUrgencyBanner({
|
||||
);
|
||||
}
|
||||
|
||||
export function TrialProgressBanner({
|
||||
daysRemaining,
|
||||
totalDays = 30,
|
||||
onCta,
|
||||
}: {
|
||||
daysRemaining: number;
|
||||
totalDays?: number;
|
||||
onCta: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const elapsed = Math.min(totalDays, Math.max(0, totalDays - daysRemaining));
|
||||
const pct = Math.round((elapsed / totalDays) * 100);
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className="animate-fade-in-up flex flex-col gap-3 rounded-2xl border border-brand/25 bg-brand/5 px-5 py-4 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div className="flex flex-1 items-start gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-brand/15 text-brand">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold leading-5 text-foreground">
|
||||
{t("subscription.trialProgress.title", { days: daysRemaining })}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{t("subscription.trialProgress.description")}
|
||||
</p>
|
||||
<div className="mt-2 h-1.5 w-full max-w-xs overflow-hidden rounded-full bg-brand/15">
|
||||
<div
|
||||
className="h-full rounded-full bg-brand transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" className="shrink-0" onClick={onCta}>
|
||||
{t("subscription.trialProgress.cta")}
|
||||
<ArrowRight className="ml-1.5 h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TrialMutation {
|
||||
isPending: boolean;
|
||||
isError: boolean;
|
||||
|
||||
Reference in New Issue
Block a user