Add proforma invoice step to subscription checkout

Insert a factor/invoice page between plan selection and payment showing
billing-period choice, line items, and totals before redirecting to the
gateway, moving payment-method selection to where the charge happens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-30 00:29:17 +03:30
parent 639573dfde
commit 09c55669ca
4 changed files with 297 additions and 68 deletions
@@ -2,13 +2,13 @@
import { useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useRouter } from "@/i18n/routing";
import { apiGet, apiPost } from "@/lib/api/client";
import { isCafeOwner } from "@/lib/auth-permissions";
import { useAuthStore } from "@/lib/stores/auth.store";
import { formatNumber } from "@/lib/format";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -32,27 +32,16 @@ type BillingStatus = {
isPlanExpired: boolean;
};
type SubscribeResponse = {
paymentId: string;
paymentUrl: string;
};
type PaymentMethod = {
id: string;
displayNameFa: string;
isDefault: boolean;
};
export function SubscriptionScreen() {
const t = useTranslations("subscription");
const tSettings = useTranslations("settings");
const searchParams = useSearchParams();
const router = useRouter();
const cafeId = useAuthStore((s) => s.user?.cafeId);
const role = useAuthStore((s) => s.user?.role);
const setAuth = useAuthStore((s) => s.setAuth);
const billingRefreshed = useRef(false);
const [billingBanner, setBillingBanner] = useState<"success" | "failed" | null>(null);
const [paymentMethod, setPaymentMethod] = useState("");
useEffect(() => {
const billing = searchParams.get("billing");
@@ -72,19 +61,6 @@ export function SubscriptionScreen() {
enabled: !!cafeId,
});
const { data: paymentMethods = [] } = useQuery({
queryKey: ["billing-payment-methods", cafeId],
queryFn: () => apiGet<PaymentMethod[]>("/api/billing/payment-methods"),
enabled: !!cafeId && isCafeOwner(role),
});
useEffect(() => {
if (!paymentMethod && paymentMethods.length > 0) {
const def = paymentMethods.find((m) => m.isDefault) ?? paymentMethods[0];
setPaymentMethod(def.id);
}
}, [paymentMethods, paymentMethod]);
useEffect(() => {
if (searchParams.get("billing") !== "success" || billingRefreshed.current) return;
const refresh = localStorage.getItem("meezi_refresh_token");
@@ -98,14 +74,6 @@ export function SubscriptionScreen() {
.catch(() => notify.warning(tSettings("profile.reloginHint")));
}, [searchParams, setAuth, refetch, tSettings]);
const subscribe = useMutation({
mutationFn: (body: { planTier: string; months: number; paymentMethod: string }) =>
apiPost<SubscribeResponse>("/api/billing/subscribe", body),
onSuccess: (data) => {
window.location.href = data.paymentUrl;
},
});
if (!cafeId) return null;
if (!isCafeOwner(role)) {
@@ -187,41 +155,11 @@ export function SubscriptionScreen() {
</CardContent>
</Card>
{paymentMethods.length > 0 ? (
<Card className="rounded-xl border border-border/80 shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base">{t("paymentMethod")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{paymentMethods.map((m) => (
<button
key={m.id}
type="button"
onClick={() => setPaymentMethod(m.id)}
className={cn(
"rounded-lg border px-3 py-2 text-sm transition active:scale-[0.98]",
paymentMethod === m.id
? "border-[#0F6E56] bg-[#E1F5EE] text-[#0F6E56]"
: "border-border/80 hover:border-[#0F6E56]/40"
)}
>
{m.displayNameFa}
</button>
))}
</CardContent>
</Card>
) : null}
<PlanComparison
currentPlan={status?.planTier ?? "Free"}
onSubscribe={(planTier) =>
subscribe.mutate({
planTier,
months: 1,
paymentMethod: paymentMethod || paymentMethods[0]?.id || "zarinpal",
})
router.push(`/subscription/checkout?plan=${planTier}`)
}
isSubscribing={subscribe.isPending}
/>
</div>
);