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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user