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