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:
@@ -123,10 +123,12 @@
|
|||||||
},
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"title": "Subscription",
|
"title": "Subscription",
|
||||||
"subtitle": "Pick a plan, choose your brands, complete payment — all on one page.",
|
"subtitle": "Find the right part in seconds — pick a plan and start searching.",
|
||||||
"step": "Step",
|
"step": "Step",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"tapToSelect": "Select",
|
"tapToSelect": "Select",
|
||||||
|
"perDay": "≈ {amount}/day",
|
||||||
|
"yearlyFreeMonths": "2 months free with yearly billing",
|
||||||
"selectExactBrands": "Please select exactly {count} brands.",
|
"selectExactBrands": "Please select exactly {count} brands.",
|
||||||
"steps": {
|
"steps": {
|
||||||
"plan": {
|
"plan": {
|
||||||
|
|||||||
@@ -123,10 +123,12 @@
|
|||||||
},
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"title": "Abonelik",
|
"title": "Abonelik",
|
||||||
"subtitle": "Planını seç, markalarını belirle, ödemeni yap. Hepsi tek sayfada.",
|
"subtitle": "Doğru parçayı saniyede bul — planını seç, hemen aramaya başla.",
|
||||||
"step": "Adım",
|
"step": "Adım",
|
||||||
"selected": "Seçildi",
|
"selected": "Seçildi",
|
||||||
"tapToSelect": "Seç",
|
"tapToSelect": "Seç",
|
||||||
|
"perDay": "≈ {amount}/gün",
|
||||||
|
"yearlyFreeMonths": "Yıllık ödemede 2 ay bedava",
|
||||||
"selectExactBrands": "Lütfen tam olarak {count} marka seçin.",
|
"selectExactBrands": "Lütfen tam olarak {count} marka seçin.",
|
||||||
"steps": {
|
"steps": {
|
||||||
"plan": {
|
"plan": {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -939,7 +939,7 @@ function StepCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BillingPeriodToggle({
|
export function BillingPeriodToggle({
|
||||||
period,
|
period,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
@@ -948,7 +948,7 @@ function BillingPeriodToggle({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
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">
|
<div className="inline-flex rounded-full border border-border bg-muted/50 p-1">
|
||||||
{(["monthly", "yearly"] as const).map((p) => (
|
{(["monthly", "yearly"] as const).map((p) => (
|
||||||
<button
|
<button
|
||||||
@@ -974,6 +974,11 @@ function BillingPeriodToggle({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{period === "yearly" && (
|
||||||
|
<p className="text-xs font-medium text-primary">
|
||||||
|
{t("subscription.yearlyFreeMonths")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1043,6 +1048,13 @@ export function PlanGrid({
|
|||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear")}
|
{billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear")}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
<ul className="space-y-1.5 text-xs text-foreground/85">
|
<ul className="space-y-1.5 text-xs text-foreground/85">
|
||||||
{plan.features.map((f) => (
|
{plan.features.map((f) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user