feat(web): add order summary to subscription payment step

At the payment step the user previously saw only the pay button with no
recap of what they were buying — a trust + friction gap right before
checkout. Added an OrderSummary block (plan, selected brands, billing
period, total) above the Stripe button, wired through the already-defined
but unused orderSummary* i18n keys. Brand names resolve from the cached
/brands query; the full plan shows "Tümü seçildi".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:19:37 +03:00
parent b32c777951
commit 02ea505af7
2 changed files with 134 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
/**
* Tests for the OrderSummary block shown above the Stripe pay button.
*
* Previously the payment step rendered only the pay button — the user had no
* "what am I buying" recap (plan / brands / period / total) at the moment of
* payment, a trust + friction gap. OrderSummary closes it using the
* pre-existing (but unused) orderSummary* i18n keys.
*/
import { render } from "@testing-library/react";
import { vi } from "vitest";
vi.mock("@/lib/i18n", () => ({
useTranslation: () => ({
t: (key: string, vars?: Record<string, unknown>) =>
vars ? `${key}:${JSON.stringify(vars)}` : key,
locale: "tr",
setLocale: vi.fn(),
}),
}));
vi.mock("@sase/shared", async (orig) => {
const actual = await orig<typeof import("@sase/shared")>();
return { ...actual, formatTRY: (n: number) => `TRY${n}` };
});
import { OrderSummary } from "@/routes/dashboard/subscription/index";
describe("OrderSummary", () => {
test("renders plan, brands, period and total rows", () => {
const { container } = render(
<OrderSummary
planKey="brand2"
period="monthly"
totalAmount={35000}
brandsValue="BMW, Audi"
/>,
);
const text = container.textContent ?? "";
expect(text).toContain("subscription.orderSummary");
expect(text).toContain("subscription.plans.brand2.name");
expect(text).toContain("BMW, Audi");
expect(text).toContain("common.monthly");
expect(text).toContain("TRY35000");
expect(text).toContain("common.perMonth");
});
test("yearly period shows yearly label and /year suffix", () => {
const { container } = render(
<OrderSummary planKey="full" period="yearly" totalAmount={999000} brandsValue="Tümü" />,
);
const text = container.textContent ?? "";
expect(text).toContain("common.yearly");
expect(text).toContain("common.perYear");
expect(text).toContain("TRY999000");
expect(text).not.toContain("common.perMonth");
});
test("uses a description list for accessible label/value pairing", () => {
const { container } = render(
<OrderSummary planKey="brand1" period="monthly" totalAmount={20000} brandsValue="BMW" />,
);
expect(container.querySelector("dl")).not.toBeNull();
expect(container.querySelectorAll("dt").length).toBe(4);
expect(container.querySelectorAll("dd").length).toBe(4);
});
});

View File

@@ -1083,6 +1083,50 @@ export function PlanGrid({
);
}
export function OrderSummary({
planKey,
period,
totalAmount,
brandsValue,
}: {
planKey: string;
period: "monthly" | "yearly";
totalAmount: number;
brandsValue: string;
}) {
const { t } = useTranslation();
const periodSuffix = period === "yearly" ? t("common.perYear") : t("common.perMonth");
return (
<div className="rounded-2xl border border-border bg-muted/30 p-4">
<p className="mb-3 text-sm font-semibold">{t("subscription.orderSummary")}</p>
<dl className="space-y-2 text-sm">
<div className="flex items-start justify-between gap-4">
<dt className="text-muted-foreground">{t("subscription.orderSummaryPlan")}</dt>
<dd className="text-right font-medium">{t(`subscription.plans.${planKey}.name`)}</dd>
</div>
<div className="flex items-start justify-between gap-4">
<dt className="shrink-0 text-muted-foreground">{t("subscription.orderSummaryBrands")}</dt>
<dd className="text-right font-medium">{brandsValue}</dd>
</div>
<div className="flex items-start justify-between gap-4">
<dt className="text-muted-foreground">{t("subscription.orderSummaryPeriod")}</dt>
<dd className="text-right font-medium">
{period === "yearly" ? t("common.yearly") : t("common.monthly")}
</dd>
</div>
<Separator className="my-1" />
<div className="flex items-baseline justify-between gap-4">
<dt className="font-medium">{t("subscription.orderSummaryPrice")}</dt>
<dd className="text-right text-lg font-bold">
{formatTRY(totalAmount)}
<span className="text-xs font-normal text-muted-foreground">{periodSuffix}</span>
</dd>
</div>
</dl>
</div>
);
}
function PaymentMethodSection({
planKey,
period,
@@ -1095,8 +1139,31 @@ function PaymentMethodSection({
totalAmount: number;
}) {
const { t } = useTranslation();
const isFullPlan = planKey === "full";
const { data: brands } = useQuery({
queryKey: ["brands"],
queryFn: () => api.get<Array<{ id: string; name: string }>>("/brands"),
staleTime: 5 * 60 * 1000,
enabled: !isFullPlan && brandIds.length > 0,
});
const selectedBrandNames = (brands ?? [])
.filter((b) => brandIds.includes(b.id))
.map((b) => b.name);
const brandsValue = isFullPlan
? t("subscription.allBrandsSelected")
: selectedBrandNames.length > 0
? selectedBrandNames.join(", ")
: `${brandIds.length} ${t("subscription.brandsSelected")}`;
return (
<div className="space-y-4">
<OrderSummary
planKey={planKey}
period={period}
totalAmount={totalAmount}
brandsValue={brandsValue}
/>
<StripeCheckoutButton
planKey={planKey}
period={period}