feat(trial): shorten free trial from 30 to 7 days
Backend source of truth (subscriptions.service.startFullTrial) now grants a
7-day trial via a named TRIAL_DAYS constant. Existing trials keep their stored
endDate — only new sign-ups are affected, so no migration is needed.
In-app conversion UX rescaled to the 7-day window, preserving the two-banner
no-overlap design:
- urgency banner: visible the last 3 days (soft d3 / urgent d2 / critical d1)
- value-upsell: owns days > 3 (the opening days), still proven-value gated
- progress bars now derive the total span from real start->end dates, so both
7-day and any remaining legacy 30-day trials render an accurate bar
Subscription-page thresholds (<=3 / >3) and the analytics gate updated to match.
Copy: 17 trial strings each in tr.json/en.json plus hardcoded CTAs in
site-header, _auth, register and demo-footer-cta -> "7 gun / 7 Gun / 7-Day".
Lifecycle emails need no change: trial-ending fires 3-4 days before endDate
and win-back after expiry (both already relative to endDate), and templates
use the dynamic {{daysLeft}} variable (no hardcoded 30).
Test: trial-progress-banner spec updated to 7-day scenarios.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -160,8 +160,8 @@ function AuthLayout() {
|
||||
|
||||
{/* Bottom trial badge */}
|
||||
<div className="relative flex items-center gap-2 pt-6 text-sm text-muted-foreground">
|
||||
<ShieldCheck className="size-4 text-brand" />
|
||||
30 gün Full Paket ücretsiz deneyin — kredi kartı gerekmez
|
||||
<ShieldCheck className="size-4 text-brand" />7 gün Full Paket ücretsiz deneyin — kredi
|
||||
kartı gerekmez
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -259,7 +259,7 @@ function RegisterPage() {
|
||||
{/* Trust strip — kartsız + iptal + KVKK/SSL rozetleri tek satır */}
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-brand/20 bg-brand/10 px-3 py-2 text-sm text-foreground">
|
||||
<ShieldCheck className="size-4 shrink-0 text-brand" />
|
||||
Kart bilgisi gerekmez · 30 gün ücretsiz · istediğin an iptal
|
||||
Kart bilgisi gerekmez · 7 gün ücretsiz · istediğin an iptal
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* 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.
|
||||
* shown during the early part of the trial (e.g. days 4–7 of a 7-day trial),
|
||||
* before the urgent (<=3d) banner takes over. Without it the opening days of
|
||||
* the trial window would have no conversion nudge.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
@@ -21,31 +21,35 @@ 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 { container } = render(<TrialProgressBanner daysRemaining={5} onCta={vi.fn()} />);
|
||||
const text = container.textContent ?? "";
|
||||
expect(text).toContain("subscription.trialProgress.title");
|
||||
expect(text).toContain('"days":20');
|
||||
expect(text).toContain('"days":5');
|
||||
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()} />);
|
||||
test("progress bar width reflects elapsed share of the trial", () => {
|
||||
// 4 days left of a 7-day trial => 3 elapsed => 43%
|
||||
const { container } = render(
|
||||
<TrialProgressBanner daysRemaining={4} totalDays={7} onCta={vi.fn()} />,
|
||||
);
|
||||
const bar = container.querySelector('[style*="width"]') as HTMLElement | null;
|
||||
expect(bar).not.toBeNull();
|
||||
expect(bar!.style.width).toBe("33%");
|
||||
expect(bar!.style.width).toBe("43%");
|
||||
});
|
||||
|
||||
test("clamps progress for out-of-range day counts", () => {
|
||||
const { container } = render(<TrialProgressBanner daysRemaining={40} onCta={vi.fn()} />);
|
||||
const { container } = render(
|
||||
<TrialProgressBanner daysRemaining={40} totalDays={7} 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} />);
|
||||
const { getByRole } = render(<TrialProgressBanner daysRemaining={5} onCta={onCta} />);
|
||||
fireEvent.click(getByRole("button"));
|
||||
expect(onCta).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -426,6 +426,19 @@ export function SubscriptionPage() {
|
||||
);
|
||||
})();
|
||||
|
||||
// Real trial span (days) from start→end so the progress bar is accurate for
|
||||
// both 7-day trials and any remaining legacy 30-day trials.
|
||||
const trialTotalDays = (() => {
|
||||
if (!subscription?.startDate || !subscription.endDate) return 7;
|
||||
return Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
(new Date(subscription.endDate).getTime() - new Date(subscription.startDate).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
),
|
||||
);
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscription?.endDate) return;
|
||||
const key = trialBannerKey(userId, subscription.endDate);
|
||||
@@ -435,7 +448,7 @@ export function SubscriptionPage() {
|
||||
useEffect(() => {
|
||||
if (
|
||||
trialDaysRemaining === null ||
|
||||
trialDaysRemaining > 7 ||
|
||||
trialDaysRemaining > 3 ||
|
||||
trialBannerDismissed ||
|
||||
trialBannerViewedRef.current
|
||||
) {
|
||||
@@ -658,7 +671,7 @@ export function SubscriptionPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{trialDaysRemaining !== null && trialDaysRemaining <= 7 && !trialBannerDismissed && (
|
||||
{trialDaysRemaining !== null && trialDaysRemaining <= 3 && !trialBannerDismissed && (
|
||||
<TrialUrgencyBanner
|
||||
daysRemaining={trialDaysRemaining}
|
||||
onCta={handleTrialBannerCTAClick}
|
||||
@@ -666,9 +679,10 @@ export function SubscriptionPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{trialDaysRemaining !== null && trialDaysRemaining > 7 && (
|
||||
{trialDaysRemaining !== null && trialDaysRemaining > 3 && (
|
||||
<TrialProgressBanner
|
||||
daysRemaining={trialDaysRemaining}
|
||||
totalDays={trialTotalDays}
|
||||
onCta={() => {
|
||||
capture("trial_progress_cta_clicked", { days_remaining: trialDaysRemaining });
|
||||
document.getElementById("checkout-flow")?.scrollIntoView({ behavior: "smooth" });
|
||||
@@ -1709,7 +1723,7 @@ function TrialUrgencyBanner({
|
||||
|
||||
export function TrialProgressBanner({
|
||||
daysRemaining,
|
||||
totalDays = 30,
|
||||
totalDays = 7,
|
||||
onCta,
|
||||
}: {
|
||||
daysRemaining: number;
|
||||
|
||||
Reference in New Issue
Block a user