feat(FN-203): implement P0-1 through P0-6 subscription page fixes
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- feat(FN-203): complete Step 1 — implement P0-1 through P0-6 subscription page fixes

Files changed:
apps/web/src/messages/en.json                      |   7 +
 apps/web/src/messages/tr.json                      |   7 +
 .../src/routes/dashboard/subscription/index.tsx    | 188 +++++++++++++++------
 apps/web/vitest.config.ts                          |   3 +
 4 files changed, 153 insertions(+), 52 deletions(-)

Fusion-Task-Id: FN-203
This commit is contained in:
Fusion
2026-05-12 16:07:44 +00:00
parent c79b23860d
commit 9c8054d6be
4 changed files with 153 additions and 52 deletions

View File

@@ -177,6 +177,13 @@
"expired": "Expired"
},
"popular": "Popular",
"yearlyDiscount": "17% off",
"proceed": "Continue",
"orderSummary": "Order Summary",
"orderSummaryPlan": "Plan",
"orderSummaryPeriod": "Period",
"orderSummaryBrands": "Brands",
"orderSummaryPrice": "Price",
"trialTitle": "30-Day Full Package Trial",
"trialDescription": "Free access to all brands for 30 days. No credit card required.",
"startTrial": "Start Free Trial",

View File

@@ -177,6 +177,13 @@
"expired": "Süresi Doldu"
},
"popular": "Popüler",
"yearlyDiscount": "17% indirim",
"proceed": "Devam Et",
"orderSummary": "Sipariş Özeti",
"orderSummaryPlan": "Plan",
"orderSummaryPeriod": "Dönem",
"orderSummaryBrands": "Markalar",
"orderSummaryPrice": "Tutar",
"trialTitle": "30 Gün Full Paket Denemesi",
"trialDescription": "Tüm markalara 30 gün boyunca ücretsiz erişim. Kredi kartı gerekmez.",
"startTrial": "Ücretsiz Denemeyi Başlat",

View File

@@ -552,43 +552,42 @@ function SubscriptionPage() {
)}
{/* Trial CTA Card */}
{eligibleForTrial &&
(!subscription || subscription.status === "expired" || subscription.status === "trial") && (
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
<div className="pointer-events-none absolute -right-24 top-0 h-[300px] w-[300px] rounded-full bg-brand/15 blur-[100px]" />
<CardHeader className="relative">
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-brand" />
<CardTitle className="text-foreground">{t("subscription.trialTitle")}</CardTitle>
</div>
<CardDescription className="text-muted-foreground">
{t("subscription.trialDescription")}
</CardDescription>
</CardHeader>
<CardContent className="relative space-y-4">
<ul className="space-y-2 text-sm">
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
<li key={f} className="flex items-center gap-2 text-foreground/85">
<Check className="h-4 w-4 text-brand" />
{t(`subscription.features.${f}`)}
</li>
))}
</ul>
<Button
className="bg-brand text-brand-foreground hover:bg-brand/90"
onClick={() => {
startAction("trial-start");
capture("trial_started");
trialMutation.mutate();
}}
disabled={trialMutation.isPending}
>
<ShieldCheck className="mr-2 h-4 w-4" />
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
</Button>
</CardContent>
</Card>
)}
{eligibleForTrial && (!subscription || subscription.status === "expired") && (
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
<div className="pointer-events-none absolute -right-24 top-0 h-[300px] w-[300px] rounded-full bg-brand/15 blur-[100px]" />
<CardHeader className="relative">
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-brand" />
<CardTitle className="text-foreground">{t("subscription.trialTitle")}</CardTitle>
</div>
<CardDescription className="text-muted-foreground">
{t("subscription.trialDescription")}
</CardDescription>
</CardHeader>
<CardContent className="relative space-y-4">
<ul className="space-y-2 text-sm">
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
<li key={f} className="flex items-center gap-2 text-foreground/85">
<Check className="h-4 w-4 text-brand" />
{t(`subscription.features.${f}`)}
</li>
))}
</ul>
<Button
className="bg-brand text-brand-foreground hover:bg-brand/90"
onClick={() => {
startAction("trial-start");
capture("trial_started");
trialMutation.mutate();
}}
disabled={trialMutation.isPending}
>
<ShieldCheck className="mr-2 h-4 w-4" />
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
</Button>
</CardContent>
</Card>
)}
{/* No Subscription Banner */}
{!eligibleForTrial && !subscription && (
@@ -640,12 +639,16 @@ function SubscriptionPage() {
<Card
key={plan.key}
data-faro-user-action-name={`select-plan-${plan.key}`}
className={`relative cursor-pointer transition-all hover:shadow-md ${
isSelected ? "border-primary ring-2 ring-primary/20" : ""
className={`relative transition-all hover:shadow-md ${
isCurrentPlan ? "" : "cursor-pointer"
} ${
isSelected && !isCurrentPlan ? "border-primary ring-2 ring-primary/20" : ""
} ${isCurrentPlan ? "border-green-500/50 bg-green-50/50 dark:bg-green-950/10" : ""} ${
plan.popular ? "border-primary shadow-lg" : ""
plan.popular && !isCurrentPlan
? "border-primary ring-2 ring-primary/30 shadow-xl dark:shadow-primary/10 bg-primary/5"
: ""
}`}
onClick={() => handleSelectPlan(plan.key)}
onClick={isCurrentPlan ? undefined : () => handleSelectPlan(plan.key)}
>
{plan.popular && (
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2">
@@ -674,6 +677,11 @@ function SubscriptionPage() {
<span className="text-muted-foreground">
{billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear")}
</span>
{billingPeriod === "yearly" && (
<Badge variant="secondary" className="ml-2 align-middle">
{t("subscription.yearlyDiscount")}
</Badge>
)}
</div>
<ul className="space-y-2 text-sm">
{plan.features.map((f) => (
@@ -685,16 +693,23 @@ function SubscriptionPage() {
</ul>
</CardContent>
<CardFooter>
<Button
className="w-full"
variant={isSelected ? "default" : plan.popular ? "default" : "outline"}
onClick={(e) => {
e.stopPropagation();
handleSelectPlan(plan.key);
}}
>
{isSelected ? t("subscription.choosePlan") : t("subscription.subscribe")}
</Button>
{isCurrentPlan ? (
<div className="flex w-full items-center justify-center rounded-md bg-muted py-2 text-sm font-medium text-muted-foreground">
<Check className="mr-1.5 h-4 w-4" />
{t("subscription.currentPlan")}
</div>
) : (
<Button
className="w-full"
variant={isSelected ? "default" : plan.popular ? "default" : "outline"}
onClick={(e) => {
e.stopPropagation();
handleSelectPlan(plan.key);
}}
>
{isSelected ? t("subscription.proceed") : t("subscription.choosePlan")}
</Button>
)}
</CardFooter>
</Card>
);
@@ -702,6 +717,66 @@ function SubscriptionPage() {
</div>
</div>
{/* Order Summary (shown when a plan is selected) */}
{selectedPlanKey &&
(() => {
const selectedPlan = plans.find((p) => p.key === selectedPlanKey);
const summaryPrice =
billingPeriod === "monthly"
? (selectedPlan?.priceMonthly ?? 0)
: (selectedPlan?.priceYearly ?? 0);
const periodLabel =
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
return (
<>
<Separator />
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">{t("subscription.orderSummary")}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{t("subscription.orderSummaryPlan")}
</span>
<span className="font-medium">
{t(`subscription.plans.${selectedPlanKey}.name`)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{t("subscription.orderSummaryPeriod")}
</span>
<span className="font-medium">
{billingPeriod === "monthly" ? t("common.monthly") : t("common.yearly")}
</span>
</div>
{selectedBrandIds.length > 0 && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{t("subscription.orderSummaryBrands")}
</span>
<span className="font-medium">
{selectedBrandIds.length} {t("subscription.brandsSelected")}
</span>
</div>
)}
<Separator />
<div className="flex items-center justify-between font-semibold">
<span>{t("subscription.orderSummaryPrice")}</span>
<span>
{formatTRY(summaryPrice)}
<span className="text-xs font-normal text-muted-foreground">
{periodLabel}
</span>
</span>
</div>
</CardContent>
</Card>
</>
);
})()}
{/* Brand Selector (shown when a plan is selected) */}
{selectedPlanKey && (
<>
@@ -721,7 +796,16 @@ function SubscriptionPage() {
</Suspense>
<div className="flex justify-end">
<Button size="lg" onClick={handleProceedToPayment}>
{t("subscription.subscribe")} &rarr;
{(() => {
const proceedPlan = plans.find((p) => p.key === selectedPlanKey);
const proceedPrice =
billingPeriod === "monthly"
? (proceedPlan?.priceMonthly ?? 0)
: (proceedPlan?.priceYearly ?? 0);
const proceedPeriodLabel =
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
return `${formatTRY(proceedPrice)}${proceedPeriodLabel} ile ${t("subscription.proceed")}`;
})()}
</Button>
</div>
</div>

View File

@@ -10,6 +10,9 @@ export default defineConfig({
test: {
globals: true,
environment: "jsdom",
env: {
NODE_ENV: "development",
},
include: ["src/**/*.{test,spec}.{ts,tsx}"],
setupFiles: ["./src/test-setup.ts"],
coverage: {