feat(web): reinforce value on subscription plan selection

Three CRO additions at the point of decision:
- Outcome-focused page subtitle ("Doğru parçayı saniyede bul …") replacing
  the purely procedural copy.
- Per-day price anchor on every plan card (≈ ₺X/gün), reframing the cost as
  a small daily number. Computed from the displayed monthly/yearly price.
- "2 ay bedava" note under the billing toggle when yearly is selected,
  making the otherwise-abstract -17% concrete (yearly = 10× monthly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:23:21 +03:00
parent 02ea505af7
commit ef4ebec025
4 changed files with 87 additions and 4 deletions

View File

@@ -0,0 +1,67 @@
/**
* Tests for the value-reinforcement additions:
* - per-day price anchor on every plan card
* - "2 months free" note shown only for yearly billing
*
* These reframe the price at the moment of decision (small daily number) and
* make the yearly discount concrete, per the CRO audit.
*/
import { fireEvent, render } from "@testing-library/react";
import { vi } from "vitest";
vi.mock("@/lib/i18n", () => ({
useTranslation: () => ({
t: (key: string) => key,
locale: "tr",
setLocale: vi.fn(),
}),
}));
vi.mock("@/lib/posthog", () => ({
capture: vi.fn(),
setPeopleProperties: vi.fn(),
}));
import {
BillingPeriodToggle,
PlanGrid,
plans,
} from "@/routes/dashboard/subscription/index";
describe("per-day price anchor (PlanGrid)", () => {
test("renders a per-day line on every plan card", () => {
const { container } = render(
<PlanGrid
billingPeriod="monthly"
selectedPlanKey={null}
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
const perDayNodes = Array.from(container.querySelectorAll("p")).filter((p) =>
p.textContent?.includes("subscription.perDay"),
);
expect(perDayNodes).toHaveLength(plans.length);
});
});
describe("yearly free-months note (BillingPeriodToggle)", () => {
test("shows the free-months note when yearly is selected", () => {
const { container } = render(<BillingPeriodToggle period="yearly" onChange={vi.fn()} />);
expect(container.textContent).toContain("subscription.yearlyFreeMonths");
});
test("hides the free-months note for monthly billing", () => {
const { container } = render(<BillingPeriodToggle period="monthly" onChange={vi.fn()} />);
expect(container.textContent).not.toContain("subscription.yearlyFreeMonths");
});
test("toggling fires onChange with the chosen period", () => {
const onChange = vi.fn();
const { getByText } = render(<BillingPeriodToggle period="monthly" onChange={onChange} />);
fireEvent.click(getByText("common.yearly"));
expect(onChange).toHaveBeenCalledWith("yearly");
});
});

View File

@@ -939,7 +939,7 @@ function StepCard({
);
}
function BillingPeriodToggle({
export function BillingPeriodToggle({
period,
onChange,
}: {
@@ -948,7 +948,7 @@ function BillingPeriodToggle({
}) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center gap-2">
<div className="flex flex-col items-center gap-2">
<div className="inline-flex rounded-full border border-border bg-muted/50 p-1">
{(["monthly", "yearly"] as const).map((p) => (
<button
@@ -974,6 +974,11 @@ function BillingPeriodToggle({
</button>
))}
</div>
{period === "yearly" && (
<p className="text-xs font-medium text-primary">
{t("subscription.yearlyFreeMonths")}
</p>
)}
</div>
);
}
@@ -1043,6 +1048,13 @@ export function PlanGrid({
<span className="text-xs text-muted-foreground">
{billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear")}
</span>
<p className="mt-0.5 text-xs text-muted-foreground">
{t("subscription.perDay", {
amount: formatTRY(
Math.round(billingPeriod === "monthly" ? price / 30 : price / 365),
),
})}
</p>
</div>
<ul className="space-y-1.5 text-xs text-foreground/85">
{plan.features.map((f) => (