Skip to content

Commit

Permalink
feat(cloud): add billing wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Siumauricio committed Oct 20, 2024
1 parent 319584d commit fe0a662
Show file tree
Hide file tree
Showing 11 changed files with 4,933 additions and 37 deletions.
141 changes: 141 additions & 0 deletions apps/dokploy/components/dashboard/settings/billing/review-payment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { ArrowRightIcon } from "lucide-react";
import { useState } from "react";
import { calculatePrice } from "./show-billing";

interface Props {
isAnnual: boolean;
serverQuantity: number;
}

export const ReviewPayment = ({ isAnnual, serverQuantity }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: billingSubscription } =
api.stripe.getBillingSubscription.useQuery();

const { data: calculateUpgradeCost } =
api.stripe.calculateUpgradeCost.useQuery(
{
serverQuantity,
isAnnual,
},
{
enabled: !!serverQuantity && isOpen,
},
);

// const { data: calculateNewMonthlyCost } =
// api.stripe.calculateNewMonthlyCost.useQuery(
// {
// serverQuantity,
// isAnnual,
// },
// {
// enabled: !!serverQuantity && isOpen,
// },
// );

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Review Payment</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Upgrade Plan</DialogTitle>
<DialogDescription>
You are about to upgrade your plan to a{" "}
{isAnnual ? "annual" : "monthly"} plan. This will automatically
renew your subscription.
</DialogDescription>
</DialogHeader>
<div className="flex flex-row w-full gap-4 items-center">
<div className="flex flex-col border gap-4 p-4 rounded-lg w-full">
<Label className="text-base font-semibold border-b border-b-divider pb-2">
Current Plan
</Label>
<div className="grid flex-1 gap-2">
<Label>Amount</Label>
<span className="text-sm text-muted-foreground">
${billingSubscription?.monthlyAmount}
</span>
</div>
<div className="grid flex-1 gap-2">
<Label>Servers</Label>

<span className="text-sm text-muted-foreground">
{billingSubscription?.totalServers}
</span>
</div>
<div className="grid flex-1 gap-2">
<Label>Next Payment</Label>
<span className="text-sm text-muted-foreground">
{billingSubscription?.nextPaymentDate
? format(billingSubscription?.nextPaymentDate, "MMM d, yyyy")
: "-"}
{/* {format(billingSubscription?.nextPaymentDate, "MMM d, yyyy")} */}
</span>
</div>
</div>
<div className="size-10">
<ArrowRightIcon className="size-6" />
</div>
<div className="flex flex-col border gap-4 p-4 rounded-lg w-full">
<Label className="text-base font-semibold border-b border-b-divider pb-2">
New Plan
</Label>
<div className="grid flex-1 gap-2">
<Label>Amount</Label>
<span className="text-sm text-muted-foreground">
${calculatePrice(serverQuantity).toFixed(2)}
</span>
</div>
<div className="grid flex-1 gap-2">
<Label>Servers</Label>
<span className="text-sm text-muted-foreground">
{serverQuantity}
</span>
</div>
<div className="grid flex-1 gap-2">
<Label>Difference</Label>
<span className="text-sm text-muted-foreground">
{Number(billingSubscription?.totalServers) === serverQuantity
? "-"
: `$${calculateUpgradeCost} USD`}{" "}
</span>
</div>
{/* <div className="grid flex-1 gap-2">
<Label>New {isAnnual ? "annual" : "monthly"} cost</Label>
<span className="text-sm text-muted-foreground">
{Number(billingSubscription?.totalServers) === serverQuantity
? "-"
: `${calculateNewMonthlyCost} USD`}{" "}
</span>
</div> */}
</div>
</div>

<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
Pay
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
228 changes: 228 additions & 0 deletions apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { Button } from "@/components/ui/button";
import { NumberInput } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { loadStripe } from "@stripe/stripe-js";
import clsx from "clsx";
import { CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
import React, { useState } from "react";
import { toast } from "sonner";
import { ReviewPayment } from "./review-payment";

const stripePromise = loadStripe(
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
);

export const calculatePrice = (count: number, isAnnual = false) => {
if (isAnnual) {
if (count === 1) return 40.8;
if (count <= 3) return 81.5;
return 81.5 + (count - 3) * 35.7;
}
if (count === 1) return 4.0;
if (count <= 3) return 7.99;
return 7.99 + (count - 3) * 3.5;
};

export const ShowBilling = () => {
const { data: admin } = api.admin.one.useQuery();
const { data, refetch } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();

const [serverQuantity, setServerQuantity] = useState(3);

const { mutateAsync: upgradeSubscription } =
api.stripe.upgradeSubscription.useMutation();
const [isAnnual, setIsAnnual] = useState(false);

const handleCheckout = async (productId: string) => {
const stripe = await stripePromise;

if (data && admin?.stripeSubscriptionId && data.subscriptions.length > 0) {
upgradeSubscription({
subscriptionId: admin?.stripeSubscriptionId,
serverQuantity,
isAnnual,
})
.then(async (subscription) => {
toast.success("Subscription upgraded successfully");
await refetch();
})
.catch((error) => {
toast.error("Error to upgrade the subscription");
console.error(error);
});
} else {
createCheckoutSession({
productId,
serverQuantity: serverQuantity,
isAnnual,
}).then(async (session) => {
await stripe?.redirectToCheckout({
sessionId: session.sessionId,
});
});
}
};

return (
<div className="flex flex-col gap-4 w-full justify-center">
<Tabs
defaultValue="monthly"
className="w-full"
onValueChange={(e) => {
console.log(e);
setIsAnnual(e === "annual");
}}
>
<TabsList>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="annual">Annual</TabsTrigger>
</TabsList>
</Tabs>
{data?.products?.map((product) => {
const featured = true;

return (
<div key={product.id}>
<section
className={clsx(
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
featured
? "order-first bg-black border py-8 lg:order-none"
: "lg:py-8",
)}
>
<h3 className="mt-5 font-medium text-lg text-white">
{product.name}
</h3>
<p
className={clsx(
"text-sm",
featured ? "text-white" : "text-slate-400",
)}
>
{product.description}
</p>
<p className="order-first text-3xl font-semibold tracking-tight text-primary">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>

<ul
role="list"
className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200",
)}
>
{[
"All the features of Dokploy",
"Unlimited deployments",
"Self-hosted on your own infrastructure",
"Full access to all deployment features",
"Dokploy integration",
"Free",
].map((feature) => (
<li key={feature} className="flex text-muted-foreground">
<CheckIcon />
<span className="ml-4">{feature}</span>
</li>
))}
</ul>
<div className="flex flex-col gap-2 mt-4">
<div className="flex items-center gap-2 justify-center">
<span className="text-sm text-muted-foreground">
{serverQuantity} Servers
</span>
</div>

<div className="flex items-center space-x-2">
<Button
disabled={serverQuantity <= 1}
variant="outline"
onClick={() => {
if (serverQuantity <= 1) return;

if (serverQuantity === 3) {
setServerQuantity(serverQuantity - 2);
return;
}
setServerQuantity(serverQuantity - 1);
}}
>
<MinusIcon className="h-4 w-4" />
</Button>
<NumberInput
value={serverQuantity}
onChange={(e) => {
if (Number(e.target.value) === 2) {
setServerQuantity(3);
return;
}
setServerQuantity(e.target.value);
}}
/>

<Button
variant="outline"
onClick={() => {
if (serverQuantity === 1) {
setServerQuantity(3);
return;
}

setServerQuantity(serverQuantity + 1);
}}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
<div
className={cn(
data.subscriptions.length > 0
? "justify-between"
: "justify-end",
"flex flex-row items-center gap-2 mt-4",
)}
>
{data.subscriptions.length > 0 && (
<ReviewPayment
isAnnual={isAnnual}
serverQuantity={serverQuantity}
/>
)}

<div className="justify-end">
<Button
onClick={async () => {
handleCheckout(product.id);
}}
disabled={serverQuantity < 1}
>
Subscribe
</Button>
</div>
</div>
</div>
</section>
</div>
);
})}

{/* <Button
variant="destructive"
onClick={async () => {
// Crear una sesión del portal del cliente
const session = await createCustomerPortalSession();
// Redirigir al portal del cliente en Stripe
window.location.href = session.url;
}}
>
Manage Subscription
</Button> */}
</div>
);
};
3 changes: 3 additions & 0 deletions apps/dokploy/drizzle/0041_small_aaron_stack.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE "admin" ADD COLUMN "stripeCustomerId" text;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "stripeSubscriptionId" text;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "totalServers" integer DEFAULT 0 NOT NULL;
Loading

0 comments on commit fe0a662

Please sign in to comment.