forked from Dokploy/dokploy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
319584d
commit fe0a662
Showing
11 changed files
with
4,933 additions
and
37 deletions.
There are no files selected for viewing
141 changes: 141 additions & 0 deletions
141
apps/dokploy/components/dashboard/settings/billing/review-payment.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
228
apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.