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:
2026-06-27 16:37:21 +03:00
parent 3de7fa4989
commit a4e53f3cfd
11 changed files with 105 additions and 69 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,8 +1,8 @@
/**
* Tests for TrialProgressBanner — the calm, persistent trial-status strip
* shown for days 830 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 47 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);
});

View File

@@ -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;