From d1d5263486f96db578aad918a59007045c3c077f Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 13 Jul 2025 09:28:24 +0000 Subject: feat: Add stripe based subscriptions --- .../components/settings/SubscriptionSettings.tsx | 233 +++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 apps/web/components/settings/SubscriptionSettings.tsx (limited to 'apps/web/components/settings') diff --git a/apps/web/components/settings/SubscriptionSettings.tsx b/apps/web/components/settings/SubscriptionSettings.tsx new file mode 100644 index 00000000..53f1caf4 --- /dev/null +++ b/apps/web/components/settings/SubscriptionSettings.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useEffect } from "react"; +import { useTranslation } from "@/lib/i18n/client"; +import { api } from "@/lib/trpc"; +import { CreditCard, Loader2 } from "lucide-react"; + +import { Alert, AlertDescription } from "../ui/alert"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../ui/card"; +import { Skeleton } from "../ui/skeleton"; +import { toast } from "../ui/use-toast"; + +export default function SubscriptionSettings() { + const { t } = useTranslation(); + const { + data: subscriptionStatus, + refetch, + isLoading: isQueryLoading, + } = api.subscriptions.getSubscriptionStatus.useQuery(); + + const { data: subscriptionPrice } = + api.subscriptions.getSubscriptionPrice.useQuery(); + + const { mutate: syncStripeState } = + api.subscriptions.syncWithStripe.useMutation({ + onSuccess: () => { + refetch(); + }, + }); + const createCheckoutSession = + api.subscriptions.createCheckoutSession.useMutation({ + onSuccess: (resp) => { + if (resp.url) { + window.location.href = resp.url; + } + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + }, + }); + const createPortalSession = api.subscriptions.createPortalSession.useMutation( + { + onSuccess: (resp) => { + if (resp.url) { + window.location.href = resp.url; + } + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + }, + }, + ); + + const isLoading = + createCheckoutSession.isPending || createPortalSession.isPending; + + useEffect(() => { + syncStripeState(); + }, []); + + const formatDate = (date: Date | null) => { + if (!date) return "N/A"; + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }).format(date); + }; + + const getStatusBadge = (status: "free" | "paid") => { + switch (status) { + case "paid": + return ( + + {t("settings.subscription.paid")} + + ); + case "free": + return ( + {t("settings.subscription.free")} + ); + } + }; + + return ( + + + + + {t("settings.subscription.subscription")} + + + {t("settings.subscription.manage_subscription")} + + + + {isQueryLoading ? ( +
+
+
+ +
+ + +
+
+
+ + +
+
+
+
+
+
+ + + +
+ +
+
+
+
+ ) : ( + <> +
+
+ +
+ {subscriptionStatus?.tier && + getStatusBadge(subscriptionStatus.tier)} +
+
+ + {subscriptionStatus?.hasActiveSubscription && ( + <> +
+ +
+ {formatDate(subscriptionStatus.startDate)} -{" "} + {formatDate(subscriptionStatus.endDate)} +
+
+ + )} +
+ +
+ {!subscriptionStatus?.hasActiveSubscription ? ( +
+
+
+
+

+ {t("settings.subscription.paid_plan")} +

+

+ {t("settings.subscription.unlock_bigger_quota")} +

+ {subscriptionPrice && subscriptionPrice.amount ? ( +

+ {subscriptionPrice.amount / 100}{" "} + {subscriptionPrice.currency} +

+ ) : ( + + )} +
+ +
+
+
+ ) : ( +
+
+ +
+ + {subscriptionStatus.cancelAtPeriodEnd && ( + + + {t("settings.subscription.subscription_canceled", { + date: formatDate(subscriptionStatus.endDate), + })} + + + )} +
+ )} +
+ + )} +
+
+ ); +} -- cgit v1.2.3-70-g09d2