From d894b2a3bf3dde05a57b29132f1b68b0c7a8d1c9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 18:17:19 -0600 Subject: [PATCH 1/6] feat(stripe): add customer_email to payment metadata --- apps/dokploy/server/api/routers/stripe.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index d2a000324..2d1556a27 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -81,6 +81,7 @@ export const stripeRouter = createTRPCRouter({ metadata: { adminId: owner.id, }, + customer_email: owner.email, allow_promotion_codes: true, success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`, cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`, From 4001f1d067c7da62d343b794acc554ecc77cf127 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 18:27:19 -0600 Subject: [PATCH 2/6] feat(billing): implement invoice display and retrieval functionality - Added `ShowInvoices` component to display user invoices with status and actions. - Integrated Stripe API to fetch invoices for the authenticated user. - Updated `ShowBilling` component to conditionally render invoices if the user has a Stripe customer ID. --- .../settings/billing/show-billing.tsx | 7 +- .../settings/billing/show-invoices.tsx | 159 ++++++++++++++++++ apps/dokploy/server/api/routers/stripe.ts | 35 ++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index ac211a1c5..faa5bcdcc 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -24,6 +24,7 @@ import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; +import { ShowInvoices } from "./show-invoices"; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, @@ -75,8 +76,8 @@ export const ShowBilling = () => { const safePercentage = Math.min(percentage, 100); return ( -
- +
+
@@ -319,6 +320,8 @@ export const ShowBilling = () => {
+ + {admin?.user.stripeCustomerId && }
); }; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx new file mode 100644 index 000000000..513bb3dc8 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx @@ -0,0 +1,159 @@ +import { Download, ExternalLink, FileText, Loader2 } from "lucide-react"; +import type Stripe from "stripe"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { api } from "@/utils/api"; + +const formatDate = (timestamp: number | null) => { + if (!timestamp) return "-"; + return new Date(timestamp * 1000).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +}; + +const formatAmount = (amount: number, currency: string) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency.toUpperCase(), + }).format(amount / 100); +}; + +const getStatusBadge = (status: Stripe.Invoice.Status | null) => { + const statusConfig: Record< + Stripe.Invoice.Status, + { label: string; variant: "default" | "secondary" | "destructive" } + > = { + paid: { label: "Paid", variant: "default" }, + open: { label: "Open", variant: "secondary" }, + draft: { label: "Draft", variant: "secondary" }, + void: { label: "Void", variant: "destructive" }, + uncollectible: { label: "Uncollectible", variant: "destructive" }, + }; + + if (!status) { + return Unknown; + } + + const config = statusConfig[status] || { + label: status, + variant: "secondary" as const, + }; + + return {config.label}; +}; + +export const ShowInvoices = () => { + const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery(); + + return ( + +
+ + + + Invoices + + + View and download your billing invoices + + + + {isLoading ? ( +
+ + Loading invoices... + + +
+ ) : invoices && invoices.length > 0 ? ( +
+ + + + Invoice + Date + Due Date + Amount + Status + Actions + + + + {invoices.map((invoice) => ( + + + {invoice.number || invoice.id.slice(0, 12)} + + {formatDate(invoice.created)} + {formatDate(invoice.dueDate)} + + {formatAmount(invoice.amountDue, invoice.currency)} + + {getStatusBadge(invoice.status)} + +
+ {invoice.hostedInvoiceUrl && ( + + )} + {invoice.invoicePdf && ( + + )} +
+
+
+ ))} +
+
+
+ ) : ( +
+ +

+ No invoices found +

+

+ Your invoices will appear here once you have a subscription +

+
+ )} +
+
+
+ ); +}; diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 2d1556a27..3354c3311 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -129,4 +129,39 @@ export const stripeRouter = createTRPCRouter({ return servers.length < user.serversQuantity; }), + + getInvoices: adminProcedure.query(async ({ ctx }) => { + const user = await findUserById(ctx.user.ownerId); + const stripeCustomerId = user.stripeCustomerId; + + if (!stripeCustomerId) { + return []; + } + + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: "2024-09-30.acacia", + }); + + try { + const invoices = await stripe.invoices.list({ + customer: stripeCustomerId, + limit: 100, + }); + + return invoices.data.map((invoice) => ({ + id: invoice.id, + number: invoice.number, + status: invoice.status, + amountDue: invoice.amount_due, + amountPaid: invoice.amount_paid, + currency: invoice.currency, + created: invoice.created, + dueDate: invoice.due_date, + hostedInvoiceUrl: invoice.hosted_invoice_url, + invoicePdf: invoice.invoice_pdf, + })); + } catch (_) { + return []; + } + }), }); From 4e0cb2a9c7ca4051a019d534c84e4252d3704c6f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 18:34:14 -0600 Subject: [PATCH 3/6] feat(billing): add billing invoices page and update billing components - Introduced `ShowBillingInvoices` component to manage and display billing invoices. - Updated `ShowBilling` component to include navigation for invoices and enhanced subscription management. - Refactored `ShowInvoices` component for improved loading and display logic. - Created a new invoices page with server-side validation and layout integration. --- .../billing/show-billing-invoices.tsx | 74 +++ .../settings/billing/show-billing.tsx | 500 ++++++++++-------- .../settings/billing/show-invoices.tsx | 176 +++--- .../pages/dashboard/settings/invoices.tsx | 63 +++ 4 files changed, 483 insertions(+), 330 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx create mode 100644 apps/dokploy/pages/dashboard/settings/invoices.tsx diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx new file mode 100644 index 000000000..67c15ee63 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx @@ -0,0 +1,74 @@ +import { CreditCard, FileText } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { ShowInvoices } from "./show-invoices"; + +const navigationItems = [ + { + name: "Subscription", + href: "/dashboard/settings/billing", + icon: CreditCard, + }, + { + name: "Invoices", + href: "/dashboard/settings/invoices", + icon: FileText, + }, +]; + +export const ShowBillingInvoices = () => { + const router = useRouter(); + + return ( +
+ +
+ + + + Billing + + + Manage your subscription and invoices + + + + + +
+ +
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index faa5bcdcc..1d79903cf 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -4,11 +4,13 @@ import { AlertTriangle, CheckIcon, CreditCard, + FileText, Loader2, MinusIcon, PlusIcon, } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -24,7 +26,6 @@ import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { ShowInvoices } from "./show-invoices"; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, @@ -38,7 +39,22 @@ export const calculatePrice = (count: number, isAnnual = false) => { if (count <= 1) return 4.5; return count * 3.5; }; + +const navigationItems = [ + { + name: "Subscription", + href: "/dashboard/settings/billing", + icon: CreditCard, + }, + { + name: "Invoices", + href: "/dashboard/settings/invoices", + icon: FileText, + }, +]; + export const ShowBilling = () => { + const router = useRouter(); const { data: servers } = api.server.count.useQuery(); const { data: admin } = api.user.get.useQuery(); const { data, isLoading } = api.stripe.getProducts.useQuery(); @@ -76,252 +92,274 @@ export const ShowBilling = () => { const safePercentage = Math.min(percentage, 100); return ( -
- -
- +
+ +
+ Billing - Manage your subscription + + Manage your subscription and invoices + - -
- setIsAnnual(e === "annual")} - > - - Monthly - Annual - - - {admin?.user.stripeSubscriptionId && ( -
-

Servers Plan

-

- You have {servers} server on your plan of{" "} - {admin?.user.serversQuantity} servers -

-
- -
- {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( -
- - - You have reached the maximum number of servers you can - create, please upgrade your plan to add more servers. - + + + +
+ setIsAnnual(e === "annual")} + > + + Monthly + Annual + + + {admin?.user.stripeSubscriptionId && ( +
+

Servers Plan

+

+ You have {servers} server on your plan of{" "} + {admin?.user.serversQuantity} servers +

+
+ +
+ {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( +
+ + + You have reached the maximum number of servers you can + create, please upgrade your plan to add more servers. + +
+ )}
)} -
- )} -
- - Need Help? We are here to help you. - - - Join to our Discord server and we will help you. - - -
- {isLoading ? ( - - Loading... - - - ) : ( - <> - {products?.map((product) => { - const featured = true; - return ( -
-
+ + Need Help? We are here to help you. + + + Join to our Discord server and we will help you. + + - { - setServerQuantity( - e.target.value as unknown as number, - ); - }} - /> - - -
-
0 - ? "justify-between" - : "justify-end", - "flex flex-row items-center gap-2 mt-4", + + + Join Discord + + +
+ {isLoading ? ( + + Loading... + + + ) : ( + <> + {products?.map((product) => { + const featured = true; + return ( +
+
- {admin?.user.stripeCustomerId && ( - - )} - - {data?.subscriptions?.length === 0 && ( -
- + {isAnnual && ( +
+ Recommended 🚀
)} -
+ {isAnnual ? ( +
+

+ ${" "} + {calculatePrice( + serverQuantity, + isAnnual, + ).toFixed(2)}{" "} + USD +

+ | +

+ ${" "} + {( + calculatePrice(serverQuantity, isAnnual) / 12 + ).toFixed(2)}{" "} + / Month USD +

+
+ ) : ( +

+ ${" "} + {calculatePrice(serverQuantity, isAnnual).toFixed( + 2, + )}{" "} + USD +

+ )} +

+ {product.name} +

+

+ {product.description} +

+ +
    + {[ + "All the features of Dokploy", + "Unlimited deployments", + "Self-hosted on your own infrastructure", + "Full access to all deployment features", + "Dokploy integration", + "Backups", + "All Incoming features", + ].map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ + {serverQuantity} Servers + +
+ +
+ + { + setServerQuantity( + e.target.value as unknown as number, + ); + }} + /> + + +
+
0 + ? "justify-between" + : "justify-end", + "flex flex-row items-center gap-2 mt-4", + )} + > + {admin?.user.stripeCustomerId && ( + + )} + + {data?.subscriptions?.length === 0 && ( +
+ +
+ )} +
+
+
- -
- ); - })} - - )} -
+ ); + })} + + )} +
- - {admin?.user.stripeCustomerId && }
); }; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx index 513bb3dc8..73cc82efc 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx @@ -2,13 +2,6 @@ import { Download, ExternalLink, FileText, Loader2 } from "lucide-react"; import type Stripe from "stripe"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Table, TableBody, @@ -63,97 +56,82 @@ export const ShowInvoices = () => { const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery(); return ( - -
- - - - Invoices - - - View and download your billing invoices - - - - {isLoading ? ( -
- - Loading invoices... - - -
- ) : invoices && invoices.length > 0 ? ( -
- - - - Invoice - Date - Due Date - Amount - Status - Actions - - - - {invoices.map((invoice) => ( - - - {invoice.number || invoice.id.slice(0, 12)} - - {formatDate(invoice.created)} - {formatDate(invoice.dueDate)} - - {formatAmount(invoice.amountDue, invoice.currency)} - - {getStatusBadge(invoice.status)} - -
- {invoice.hostedInvoiceUrl && ( - - )} - {invoice.invoicePdf && ( - - )} -
-
-
- ))} -
-
-
- ) : ( -
- -

- No invoices found -

-

- Your invoices will appear here once you have a subscription -

-
- )} -
-
-
+
+ {isLoading ? ( +
+ + Loading invoices... + + +
+ ) : invoices && invoices.length > 0 ? ( +
+ + + + Invoice + Date + Due Date + Amount + Status + Actions + + + + {invoices.map((invoice) => ( + + + {invoice.number || invoice.id.slice(0, 12)} + + {formatDate(invoice.created)} + {formatDate(invoice.dueDate)} + + {formatAmount(invoice.amountDue, invoice.currency)} + + {getStatusBadge(invoice.status)} + +
+ {invoice.hostedInvoiceUrl && ( + + )} + {invoice.invoicePdf && ( + + )} +
+
+
+ ))} +
+
+
+ ) : ( +
+ +

No invoices found

+

+ Your invoices will appear here once you have a subscription +

+
+ )} +
); }; diff --git a/apps/dokploy/pages/dashboard/settings/invoices.tsx b/apps/dokploy/pages/dashboard/settings/invoices.tsx new file mode 100644 index 000000000..a37c3607c --- /dev/null +++ b/apps/dokploy/pages/dashboard/settings/invoices.tsx @@ -0,0 +1,63 @@ +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import type { GetServerSidePropsContext } from "next"; +import type { ReactElement } from "react"; +import superjson from "superjson"; +import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { appRouter } from "@/server/api/root"; + +const Page = () => { + return ; +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return {page}; +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }>, +) { + if (!IS_CLOUD) { + return { + redirect: { + permanent: true, + destination: "/dashboard/projects", + }, + }; + } + const { req, res } = ctx; + const { user, session } = await validateRequest(req); + if (!user || user.role !== "owner") { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session as any, + user: user as any, + }, + transformer: superjson, + }); + + await helpers.user.get.prefetch(); + + await helpers.settings.isCloud.prefetch(); + + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; +} From edc8efe81657e938aae75be1ae8c11b5a10db107 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 19:21:29 -0600 Subject: [PATCH 4/6] refactor(servers): replace DropdownMenuItem with Button for Setup Server action - Updated the SetupServer component to use a Button instead of DropdownMenuItem for better accessibility and user experience. - Enhanced the ShowServers component by adding tooltips for the Setup Server action, providing users with additional context on server configuration. --- .../settings/servers/setup-server.tsx | 11 +++-- .../settings/servers/show-servers.tsx | 45 +++++++++++-------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index d88e9a3e4..13ff2d6e4 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -22,7 +22,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; @@ -89,15 +88,15 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => { ) : ( - { - e.preventDefault(); + size="sm" + onClick={() => { setIsOpen(true); }} > - Setup Server - + Setup Server + )} diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index 85e7f3ee7..92d6fc5c3 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -6,9 +6,7 @@ import { Loader2, MoreHorizontal, Network, - Pencil, ServerIcon, - Settings, Terminal, Trash2, User, @@ -31,9 +29,7 @@ import { import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -285,7 +281,32 @@ export const ShowServers = () => { {/* Compact Actions */} {isActive && ( -
+
+
+ + + + + +
+

+ Setup Server +

+

+ Configure and initialize your + server with Docker, Traefik, and + other essential services +

+
+
+
+
+ {server.sshKeyId && ( @@ -311,20 +332,6 @@ export const ShowServers = () => { )} - - -
- -
-
- -

Setup Server

-
-
-
From f3039623191ae6f66e810b2a74bd6e560265e238 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 20:21:41 -0600 Subject: [PATCH 5/6] fix(database): update container name query to use exact match - Modified the SQL queries in GetLastNContainerMetrics and GetAllMetricsContainer functions to use an exact match for container names instead of a LIKE clause, improving query accuracy and performance. --- apps/monitoring/database/containers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/monitoring/database/containers.go b/apps/monitoring/database/containers.go index 568ad12e5..4e41f5fae 100644 --- a/apps/monitoring/database/containers.go +++ b/apps/monitoring/database/containers.go @@ -58,7 +58,7 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta WITH recent_metrics AS ( SELECT metrics_json FROM container_metrics - WHERE container_name LIKE ? || '%' + WHERE container_name = ? ORDER BY timestamp DESC LIMIT ? ) @@ -98,7 +98,7 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e WITH recent_metrics AS ( SELECT metrics_json FROM container_metrics - WHERE container_name LIKE ? || '%' + WHERE container_name = ? ORDER BY timestamp DESC ) SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC From 2acaaede3733fe0fa83bbef33e4d43040f12cf90 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:22:33 +0000 Subject: [PATCH 6/6] [autofix.ci] apply automated fixes --- .../settings/billing/show-billing.tsx | 438 +++++++++--------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index 1d79903cf..1460244c1 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -128,235 +128,235 @@ export const ShowBilling = () => {
- setIsAnnual(e === "annual")} - > - - Monthly - Annual - - - {admin?.user.stripeSubscriptionId && ( -
-

Servers Plan

-

- You have {servers} server on your plan of{" "} - {admin?.user.serversQuantity} servers -

-
- -
- {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( -
- - - You have reached the maximum number of servers you can - create, please upgrade your plan to add more servers. - -
- )} + setIsAnnual(e === "annual")} + > + + Monthly + Annual + + + {admin?.user.stripeSubscriptionId && ( +
+

Servers Plan

+

+ You have {servers} server on your plan of{" "} + {admin?.user.serversQuantity} servers +

+
+ +
+ {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( +
+ + + You have reached the maximum number of servers you can + create, please upgrade your plan to add more servers. +
)} -
- - Need Help? We are here to help you. - - - Join to our Discord server and we will help you. - - +
+ {isLoading ? ( + + Loading... + + + ) : ( + <> + {products?.map((product) => { + const featured = true; + return ( +
+
- - - Join Discord - - -
- {isLoading ? ( - - Loading... - - - ) : ( - <> - {products?.map((product) => { - const featured = true; - return ( -
-
+ Recommended 🚀 +
+ )} + {isAnnual ? ( +
+

+ ${" "} + {calculatePrice( + serverQuantity, + isAnnual, + ).toFixed(2)}{" "} + USD +

+ | +

+ ${" "} + {( + calculatePrice(serverQuantity, isAnnual) / 12 + ).toFixed(2)}{" "} + / Month USD +

+
+ ) : ( +

+ ${" "} + {calculatePrice(serverQuantity, isAnnual).toFixed( + 2, + )}{" "} + USD +

+ )} +

+ {product.name} +

+

+ {product.description} +

+ +
    + {[ + "All the features of Dokploy", + "Unlimited deployments", + "Self-hosted on your own infrastructure", + "Full access to all deployment features", + "Dokploy integration", + "Backups", + "All Incoming features", + ].map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ + {serverQuantity} Servers + +
+ +
+ + { + setServerQuantity( + e.target.value as unknown as number, + ); + }} + /> + + +
+
0 + ? "justify-between" + : "justify-end", + "flex flex-row items-center gap-2 mt-4", )} > - {isAnnual && ( -
- Recommended 🚀 -
- )} - {isAnnual ? ( -
-

- ${" "} - {calculatePrice( - serverQuantity, - isAnnual, - ).toFixed(2)}{" "} - USD -

- | -

- ${" "} - {( - calculatePrice(serverQuantity, isAnnual) / 12 - ).toFixed(2)}{" "} - / Month USD -

-
- ) : ( -

- ${" "} - {calculatePrice(serverQuantity, isAnnual).toFixed( - 2, - )}{" "} - USD -

- )} -

- {product.name} -

-

- {product.description} -

+ {admin?.user.stripeCustomerId && ( + - { - setServerQuantity( - e.target.value as unknown as number, - ); - }} - /> - - -
-
0 - ? "justify-between" - : "justify-end", - "flex flex-row items-center gap-2 mt-4", - )} + window.open(session.url); + }} > - {admin?.user.stripeCustomerId && ( - + )} - window.open(session.url); - }} - > - Manage Subscription - - )} - - {data?.subscriptions?.length === 0 && ( -
- -
- )} + {data?.subscriptions?.length === 0 && ( +
+
-
- + )} +
- ); - })} - - )} -
+ +
+ ); + })} + + )} +