feat(web): add plan decision helper + trial value hook

The brand-count plan model leaves users unsure whether they need 1/2/3
brands. Added a PlanHelperHint above the grid ("Hangi planı seçmeliyim? …
4+ marka için Full Paket marka başına daha avantajlı"). Also added an
outcome-focused hook line to the trial card ("Şase numarasını gir, doğru
parçayı saniyede bul …") above its feature list, selling the result rather
than only listing features.

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

View File

@@ -175,6 +175,7 @@
"featureMatrix": "Feature Comparison",
"matrixBrandCount": "Brand count",
"brandCountAll": "All",
"planHelper": "Which plan should I pick? Choose the number of brands you work with. For 4+ brands, the Full Package is cheaper per brand.",
"selectBrands": "Select Brands",
"selectBrandsDescription": "Choose the brands you want to include in your plan.",
"brandsSelected": "brands selected",
@@ -224,6 +225,7 @@
"orderSummaryPrice": "Price",
"trialTitle": "30-Day Full Package Trial",
"trialDescription": "Free access to all brands for 30 days. No credit card required.",
"trialHook": "Enter the VIN, find the right part in seconds. All catalogs unlocked for 30 days.",
"startTrial": "Start Free Trial",
"trialStarted": "Your 30-day Full Package trial has started!",
"onboarding": {

View File

@@ -175,6 +175,7 @@
"featureMatrix": "Özellik Karşılaştırması",
"matrixBrandCount": "Marka sayısı",
"brandCountAll": "Tümü",
"planHelper": "Hangi planı seçmeliyim? İşine giren marka sayısını seç. 4+ marka için Full Paket marka başına daha avantajlı.",
"selectBrands": "Marka Seçin",
"selectBrandsDescription": "Planınıza dahil etmek istediğiniz markaları seçin.",
"brandsSelected": "marka seçildi",
@@ -224,6 +225,7 @@
"orderSummaryPrice": "Tutar",
"trialTitle": "30 Gün Full Paket Denemesi",
"trialDescription": "Tüm markalara 30 gün boyunca ücretsiz erişim. Kredi kartı gerekmez.",
"trialHook": "Şase numarasını gir, doğru parçayı saniyede bul. 30 gün boyunca tüm kataloglar açık.",
"startTrial": "Ücretsiz Denemeyi Başlat",
"trialStarted": "30 günlük Full Paket denemeniz başlatıldı!",
"onboarding": {

View File

@@ -0,0 +1,52 @@
/**
* Tests for the plan decision helper and the trial value hook.
*
* The brand-count plan model is ambiguous ("do I need 1, 2 or 3?"), so a
* helper hint sits above the grid. The trial card gained an outcome-focused
* hook line above its feature list. Both are exported pure components.
*/
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(),
}),
}));
import { PlanHelperHint, TrialCtaCard } from "@/routes/dashboard/subscription/index";
describe("PlanHelperHint", () => {
test("renders the plan-helper guidance text", () => {
const { container } = render(<PlanHelperHint />);
expect(container.textContent).toContain("subscription.planHelper");
});
});
describe("TrialCtaCard value hook", () => {
test("renders the outcome hook line alongside title and CTA", () => {
const { container } = render(<TrialCtaCard onStartTrial={vi.fn()} pending={false} />);
const text = container.textContent ?? "";
expect(text).toContain("subscription.trialHook");
expect(text).toContain("subscription.trialTitle");
expect(text).toContain("subscription.startTrial");
});
test("clicking the CTA invokes onStartTrial", () => {
const onStartTrial = vi.fn();
const { getByText } = render(<TrialCtaCard onStartTrial={onStartTrial} pending={false} />);
fireEvent.click(getByText("subscription.startTrial"));
expect(onStartTrial).toHaveBeenCalledTimes(1);
});
test("pending state disables the CTA and shows loading label", () => {
const { getByText, container } = render(
<TrialCtaCard onStartTrial={vi.fn()} pending={true} />,
);
expect(getByText("common.loading")).toBeTruthy();
expect(container.querySelector("button")?.hasAttribute("disabled")).toBe(true);
});
});

View File

@@ -35,6 +35,7 @@ import {
Clock,
CreditCard,
Crown,
Info,
Loader2,
Minus,
Pencil,
@@ -624,6 +625,7 @@ export function SubscriptionPage() {
}
onEdit={() => setStep("plan")}
>
<PlanHelperHint />
<BillingPeriodToggle period={billingPeriod} onChange={setBillingPeriod} />
<PlanGrid
billingPeriod={billingPeriod}
@@ -939,6 +941,16 @@ function StepCard({
);
}
export function PlanHelperHint() {
const { t } = useTranslation();
return (
<div className="flex items-start gap-2 rounded-lg border border-border bg-muted/30 px-3 py-2 text-xs leading-5 text-muted-foreground">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" />
<span>{t("subscription.planHelper")}</span>
</div>
);
}
export function BillingPeriodToggle({
period,
onChange,
@@ -1411,7 +1423,10 @@ function ActiveSubscriptionCard({
);
}
function TrialCtaCard({ onStartTrial, pending }: { onStartTrial: () => void; pending: boolean }) {
export function TrialCtaCard({
onStartTrial,
pending,
}: { onStartTrial: () => void; pending: boolean }) {
const { t } = useTranslation();
return (
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
@@ -1428,6 +1443,9 @@ function TrialCtaCard({ onStartTrial, pending }: { onStartTrial: () => void; pen
</CardDescription>
</CardHeader>
<CardContent className="relative space-y-4">
<p className="text-sm font-medium leading-6 text-foreground">
{t("subscription.trialHook")}
</p>
<ul className="space-y-2 text-sm leading-relaxed">
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
<li key={f} className="flex items-center gap-2 text-foreground/85">