chore(payments): remove EFT/bank-transfer method, keep Stripe only
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Removes the EFT (havale) payment surface across API, web, i18n, and docs.
Card payment via Stripe is now the only checkout path.
API (apps/api/src):
- Delete payments/bank-accounts/ module (controller + service + module)
- payments.service: drop EFT methods (createEftPayment, uploadEftReceipt,
approveEft, rejectEft, getActiveBankInfo, getPendingEftPayments) and the
BankAccountsService dependency
- payments.controller: drop /payments/eft, /payments/eft/:id/{receipt,approve,reject},
/payments/bank-info, /payments/pending; keep /payments/me
- payments.module: drop BankAccountsModule import
- admin.service: drop pending EFT counter from getDashboardStats; drop
getPendingPayments (admin EFT approval list)
- admin.controller: drop /admin/payments/pending endpoint
- admin.service.spec: drop the getPendingPayments + pendingPayments assertions
Web (apps/web/src):
- Delete components/payment/bank-transfer-card.tsx
- Delete routes/dashboard/admin/payments.tsx (EFT approval page); regenerate
routeTree.gen.ts
- subscription/index.tsx: drop EFT tab from PaymentMethodSection, drop the
paymentMethod state + handleEftCompleted, simplify ConfirmationCard to the
Stripe-only path, drop unused imports (Tabs, BankTransferCard, Building2)
- billing.tsx: drop "eft" from method filter chips; keep methodLabels.eft and
the legacy receipt download so historical EFT records still display
- dashboard.tsx + admin/index.tsx: drop /dashboard/admin/payments from the
sidebar + admin quick-link grid; trim unused icon imports; KEYS_6 → KEYS_5
for the now-5-card admin stats skeleton
- messages/{tr,en}.json: strip every EFT-only key under payment.* (eftTransfer,
eftConfirmationDescription, eftPaid, eftStatus, bank.*, uploadReceipt* etc.)
Schema (apps/api/src/database/schema):
- bankAccounts table + payments.bankAccountId column are kept as-is: legacy
EFT payments remain visible on the billing page and the schema preserves
historical records.
Docs:
- README.md: "Stripe (kart) + EFT" → "Stripe (kart)"
- CLAUDE.md: stack table + PaymentsModule row + payments table note + route
list updated; admin EFT approval route removed
Verification:
- pnpm typecheck: green (api + web + shared + config + ui)
- pnpm lint: green
- pnpm test: 20 web tests pass, 173 api tests pass
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
BookOpen,
|
||||
Copy,
|
||||
CreditCard,
|
||||
DollarSign,
|
||||
FlaskConical,
|
||||
History,
|
||||
LayoutDashboard,
|
||||
@@ -67,7 +66,6 @@ const supportItems = [
|
||||
const adminItems = [
|
||||
{ to: "/dashboard/admin", label: "Yönetim Paneli", icon: Shield },
|
||||
{ to: "/dashboard/admin/users", label: "Kullanıcılar", icon: Users },
|
||||
{ to: "/dashboard/admin/payments", label: "Ödemeler", icon: DollarSign },
|
||||
{ to: "/dashboard/admin/analytics", label: "Analitik", icon: BarChart3 },
|
||||
{ to: "/dashboard/admin/copy-logs", label: "OEM Kopyalama", icon: Copy },
|
||||
{ to: "/dashboard/admin/referrals", label: "Referanslar", icon: Share2 },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { KEYS_6 } from "@/lib/keys";
|
||||
import { KEYS_5 } from "@/lib/keys";
|
||||
import { formatTRY } from "@sase/shared";
|
||||
import { Card, CardContent } from "@sase/ui";
|
||||
import { Skeleton } from "@sase/ui";
|
||||
@@ -10,10 +10,8 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
Activity,
|
||||
Clock,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Receipt,
|
||||
Search,
|
||||
TrendingUp,
|
||||
UserCog,
|
||||
@@ -39,7 +37,6 @@ interface DashboardStats {
|
||||
totalRevenue: number;
|
||||
totalQueries: number;
|
||||
newUsersThisMonth: number;
|
||||
pendingPayments: number;
|
||||
}
|
||||
|
||||
interface DailyStat {
|
||||
@@ -77,7 +74,7 @@ function AdminDashboardPage() {
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{KEYS_6.map((__k) => (
|
||||
{KEYS_5.map((__k) => (
|
||||
<Skeleton key={__k} className="h-32" />
|
||||
))}
|
||||
</div>
|
||||
@@ -123,13 +120,6 @@ function AdminDashboardPage() {
|
||||
color: "text-orange-600",
|
||||
bg: "bg-orange-50",
|
||||
},
|
||||
{
|
||||
label: "Bekleyen Odeme",
|
||||
value: stats?.pendingPayments ?? 0,
|
||||
icon: Clock,
|
||||
color: "text-red-600",
|
||||
bg: "bg-red-50",
|
||||
},
|
||||
];
|
||||
|
||||
const quickLinks = [
|
||||
@@ -138,11 +128,6 @@ function AdminDashboardPage() {
|
||||
label: "Kullanici Yonetimi",
|
||||
icon: UserCog,
|
||||
},
|
||||
{
|
||||
to: "/dashboard/admin/payments",
|
||||
label: "Odeme Onaylari",
|
||||
icon: Receipt,
|
||||
},
|
||||
{
|
||||
to: "/dashboard/admin/analytics",
|
||||
label: "Sorgu Analizi",
|
||||
@@ -182,7 +167,7 @@ function AdminDashboardPage() {
|
||||
{/* Stat Cards */}
|
||||
{statsLoading ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{KEYS_6.map((__k) => (
|
||||
{KEYS_5.map((__k) => (
|
||||
<Skeleton key={__k} className="h-32" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { formatTRY } from "@sase/shared";
|
||||
import { Card, CardContent } from "@sase/ui";
|
||||
import { Button } from "@sase/ui";
|
||||
import { Badge } from "@sase/ui";
|
||||
import { Skeleton } from "@sase/ui";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { AlertTriangle, CheckCircle, ExternalLink, Receipt, XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { KEYS_5 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/admin/payments")({
|
||||
component: AdminPaymentsPage,
|
||||
});
|
||||
|
||||
interface PendingPayment {
|
||||
id: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
subscriptionId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
method: string;
|
||||
status: string;
|
||||
eftReceiptUrl: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function AdminPaymentsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { user, isLoading: authLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
id: string;
|
||||
type: "approve" | "reject";
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && user?.role !== "admin") {
|
||||
navigate({ to: "/dashboard/search" });
|
||||
}
|
||||
}, [authLoading, user, navigate]);
|
||||
|
||||
const { data: payments, isLoading } = useQuery({
|
||||
queryKey: ["admin", "payments", "pending"],
|
||||
queryFn: () => api.get<PendingPayment[]>("/admin/payments/pending"),
|
||||
enabled: user?.role === "admin",
|
||||
});
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: (paymentId: string) => api.patch(`/payments/eft/${paymentId}/approve`, {}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "payments"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "dashboard"] });
|
||||
setConfirmAction(null);
|
||||
},
|
||||
});
|
||||
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: (paymentId: string) => api.patch(`/payments/eft/${paymentId}/reject`, {}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "payments"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "dashboard"] });
|
||||
setConfirmAction(null);
|
||||
},
|
||||
});
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-96 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (user?.role !== "admin") return null;
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">EFT Odeme Onaylari</h2>
|
||||
<Badge variant="secondary">{payments?.length ?? 0} bekleyen</Badge>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{KEYS_5.map((__k) => (
|
||||
<Skeleton key={__k} className="h-32 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !payments || payments.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-3 py-16 text-center">
|
||||
<CheckCircle className="h-12 w-12 text-green-500" />
|
||||
<p className="text-lg font-medium">Bekleyen odeme bulunmuyor</p>
|
||||
<p className="text-sm text-muted-foreground">Tum EFT odemeleri islenmis durumda</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{payments.map((payment) => (
|
||||
<Card key={payment.id}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* User Info */}
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">{payment.userName}</p>
|
||||
<p className="text-sm text-muted-foreground">{payment.userEmail}</p>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{formatDate(payment.createdAt)}</span>
|
||||
<span>|</span>
|
||||
<span>ID: {payment.id.substring(0, 8)}...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount & Receipt */}
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<p className="text-xl font-bold">{formatTRY(payment.amount)}</p>
|
||||
{payment.eftReceiptUrl ? (
|
||||
<a
|
||||
href={payment.eftReceiptUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Dekont Goruntule
|
||||
</a>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<Receipt className="h-3 w-3" />
|
||||
Dekont yuklenmemis
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Dialog */}
|
||||
{confirmAction && confirmAction.id === payment.id ? (
|
||||
<div className="mt-4 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 text-amber-600" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">
|
||||
{confirmAction.type === "approve"
|
||||
? "Odemeyi onaylamak istediginize emin misiniz?"
|
||||
: "Odemeyi reddetmek istediginize emin misiniz?"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{confirmAction.type === "approve"
|
||||
? "Bu islem aboneligi aktif hale getirecektir."
|
||||
: "Bu islem odemeyi basarisiz olarak isaretleyecektir."}
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={confirmAction.type === "approve" ? "default" : "destructive"}
|
||||
disabled={approveMutation.isPending || rejectMutation.isPending}
|
||||
onClick={() => {
|
||||
if (confirmAction.type === "approve") {
|
||||
approveMutation.mutate(payment.id);
|
||||
} else {
|
||||
rejectMutation.mutate(payment.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{approveMutation.isPending || rejectMutation.isPending
|
||||
? "Isleniyor..."
|
||||
: "Evet, onayla"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setConfirmAction(null)}
|
||||
>
|
||||
Iptal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center gap-2 border-t pt-4">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
id: payment.id,
|
||||
type: "approve",
|
||||
})
|
||||
}
|
||||
>
|
||||
<CheckCircle className="mr-1 h-4 w-4" />
|
||||
Onayla
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
id: payment.id,
|
||||
type: "reject",
|
||||
})
|
||||
}
|
||||
>
|
||||
<XCircle className="mr-1 h-4 w-4" />
|
||||
Reddet
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ function BillingPage() {
|
||||
|
||||
{/* Method Filter */}
|
||||
<div className="flex gap-1">
|
||||
{["all", "stripe", "eft"].map((method) => (
|
||||
{["all", "stripe"].map((method) => (
|
||||
<Button
|
||||
key={method}
|
||||
variant={methodFilter === method ? "default" : "outline"}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BankTransferCard } from "@/components/payment/bank-transfer-card";
|
||||
import { StripeCheckoutButton } from "@/components/payment/stripe-checkout-button";
|
||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { api } from "@/lib/api-client";
|
||||
@@ -25,17 +24,12 @@ import {
|
||||
DialogTitle,
|
||||
Separator,
|
||||
Skeleton,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@sase/ui";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
Building2,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
@@ -189,7 +183,6 @@ export function SubscriptionPage() {
|
||||
const [selectedPlanKey, setSelectedPlanKey] = useState<string | null>(null);
|
||||
const [selectedBrandIds, setSelectedBrandIds] = useState<string[]>([]);
|
||||
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">("monthly");
|
||||
const [paymentMethod, setPaymentMethod] = useState<"stripe" | "eft">("stripe");
|
||||
|
||||
// ─── Page-level state ──────────────────────────────────────────────────────
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||||
@@ -343,7 +336,6 @@ export function SubscriptionPage() {
|
||||
if (search.stripe === "success" && !stripeResultRef.current) {
|
||||
stripeResultRef.current = true;
|
||||
setStep("confirmation");
|
||||
setPaymentMethod("stripe");
|
||||
capture("stripe_redirect_returned", { result: "success", session_id: search.session_id });
|
||||
} else if (search.stripe === "cancelled" && !stripeResultRef.current) {
|
||||
stripeResultRef.current = true;
|
||||
@@ -459,11 +451,6 @@ export function SubscriptionPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleEftCompleted() {
|
||||
setStep("confirmation");
|
||||
queryClient.invalidateQueries({ queryKey: ["subscription"] });
|
||||
}
|
||||
|
||||
function handleCancelClick() {
|
||||
if (downgradePlan) {
|
||||
const currentBrandIds = (subscription?.brands ?? []).map((b) => b.brandId);
|
||||
@@ -675,9 +662,7 @@ export function SubscriptionPage() {
|
||||
title={t("subscription.steps.payment.title")}
|
||||
isActive={step === "payment"}
|
||||
isComplete={step === "confirmation"}
|
||||
summary={
|
||||
paymentMethod === "stripe" ? t("payment.creditCard") : t("payment.eftTransfer")
|
||||
}
|
||||
summary={t("payment.creditCard")}
|
||||
onEdit={() => setStep("payment")}
|
||||
>
|
||||
<PaymentMethodSection
|
||||
@@ -685,18 +670,14 @@ export function SubscriptionPage() {
|
||||
period={billingPeriod}
|
||||
brandIds={selectedBrandIds}
|
||||
totalAmount={totalAmount}
|
||||
method={paymentMethod}
|
||||
onMethodChange={setPaymentMethod}
|
||||
onEftCompleted={handleEftCompleted}
|
||||
/>
|
||||
</StepCard>
|
||||
)}
|
||||
|
||||
{step === "confirmation" && (
|
||||
<ConfirmationCard
|
||||
method={paymentMethod}
|
||||
subscription={subscription ?? null}
|
||||
isStripeVerifying={paymentMethod === "stripe" && subscription?.status !== "active"}
|
||||
isStripeVerifying={subscription?.status !== "active"}
|
||||
navigate={navigate}
|
||||
/>
|
||||
)}
|
||||
@@ -1100,50 +1081,21 @@ function PaymentMethodSection({
|
||||
period,
|
||||
brandIds,
|
||||
totalAmount,
|
||||
method,
|
||||
onMethodChange,
|
||||
onEftCompleted,
|
||||
}: {
|
||||
planKey: string;
|
||||
period: "monthly" | "yearly";
|
||||
brandIds: string[];
|
||||
totalAmount: number;
|
||||
method: "stripe" | "eft";
|
||||
onMethodChange: (m: "stripe" | "eft") => void;
|
||||
onEftCompleted: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Tabs value={method} onValueChange={(v) => onMethodChange(v as "stripe" | "eft")}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="stripe" data-faro-user-action-name="payment-tab-card">
|
||||
<CreditCard className="mr-2 h-4 w-4" />
|
||||
{t("payment.creditCard")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="eft" data-faro-user-action-name="payment-tab-eft">
|
||||
<Building2 className="mr-2 h-4 w-4" />
|
||||
{t("payment.eftTransfer")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="stripe" className="pt-4">
|
||||
<StripeCheckoutButton
|
||||
planKey={planKey}
|
||||
period={period}
|
||||
brandIds={brandIds}
|
||||
totalAmount={totalAmount}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="eft" className="pt-4">
|
||||
<BankTransferCard
|
||||
planKey={planKey}
|
||||
period={period}
|
||||
brandIds={brandIds}
|
||||
totalAmount={totalAmount}
|
||||
onCompleted={onEftCompleted}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<StripeCheckoutButton
|
||||
planKey={planKey}
|
||||
period={period}
|
||||
brandIds={brandIds}
|
||||
totalAmount={totalAmount}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-xs text-foreground/70">
|
||||
<span aria-hidden="true">🔒</span>
|
||||
@@ -1160,18 +1112,16 @@ function PaymentMethodSection({
|
||||
}
|
||||
|
||||
function ConfirmationCard({
|
||||
method,
|
||||
subscription,
|
||||
isStripeVerifying,
|
||||
navigate,
|
||||
}: {
|
||||
method: "stripe" | "eft";
|
||||
subscription: Subscription | null;
|
||||
isStripeVerifying: boolean;
|
||||
navigate: ReturnType<typeof useNavigate>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (method === "stripe" && isStripeVerifying) {
|
||||
if (isStripeVerifying) {
|
||||
return (
|
||||
<Card className="border-primary/30 ring-1 ring-primary/10">
|
||||
<CardContent className="flex flex-col items-center gap-4 py-12 text-center">
|
||||
@@ -1192,13 +1142,9 @@ function ConfirmationCard({
|
||||
<CheckCircle2 className="h-16 w-16 text-emerald-500" />
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold">{t("payment.confirmation")}</h3>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{method === "stripe"
|
||||
? t("payment.confirmationDescription")
|
||||
: t("payment.eftConfirmationDescription")}
|
||||
</p>
|
||||
<p className="mt-2 text-muted-foreground">{t("payment.confirmationDescription")}</p>
|
||||
</div>
|
||||
{subscription?.endDate && method === "stripe" && (
|
||||
{subscription?.endDate && (
|
||||
<div className="rounded-lg border border-border bg-background/60 px-4 py-2 text-sm">
|
||||
<span className="text-muted-foreground">{t("subscription.endDate")}: </span>
|
||||
<span className="font-medium">
|
||||
|
||||
Reference in New Issue
Block a user