From 3d622c7a5393b0e74defa699c9de7e58499d2e03 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 20 Mar 2025 01:19:39 -0600 Subject: [PATCH] feat: integrate Stripe for self-hosted license purchasing and update pricing component --- apps/website/components/pricing.tsx | 266 ++++++++++++++++++++++++--- apps/website/components/ui/input.tsx | 6 +- apps/website/locales/en.json | 1 + apps/website/locales/fr.json | 14 ++ apps/website/locales/zh-Hans.json | 5 + apps/website/package.json | 9 +- pnpm-lock.yaml | 9 + 7 files changed, 278 insertions(+), 32 deletions(-) diff --git a/apps/website/components/pricing.tsx b/apps/website/components/pricing.tsx index a904b47..9d2587d 100644 --- a/apps/website/components/pricing.tsx +++ b/apps/website/components/pricing.tsx @@ -2,6 +2,7 @@ import clsx from "clsx"; import { cn } from "@/lib/utils"; +import { loadStripe } from "@stripe/stripe-js"; import { IconInfoCircle } from "@tabler/icons-react"; import { ArrowRight, @@ -14,7 +15,7 @@ import { import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Container } from "./Container"; import { Badge } from "./ui/badge"; import { Button, buttonVariants } from "./ui/button"; @@ -28,6 +29,10 @@ import { TooltipTrigger, } from "./ui/tooltip"; +const stripePromise = loadStripe( + process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, +); + function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) { return ( ) { ); } +const SERVER_LICENSE_URL = + process.env.NODE_ENV === "development" + ? "http://localhost:4002/api" + : "https://licenses.dokploy.com"; + function CheckIcon({ className, ...props @@ -74,6 +84,7 @@ function CheckIcon({ ); } + export const calculatePrice = (count: number, isAnnual = false) => { if (isAnnual) { if (count <= 1) return 45.9; @@ -83,14 +94,96 @@ export const calculatePrice = (count: number, isAnnual = false) => { return count * 3.5; }; +const calculateLicensePrice = ( + type: "basic" | "professional" | "enterprise", + count: number, + isAnnual: boolean, +): number => { + const prices = { + basic: { + monthly: 9.99, + annual: 89.99, + }, + professional: { + monthly: 19.99, + annual: 199.99, + }, + enterprise: { + monthly: 29.99, + annual: 299.99, + }, + }; + + const price = isAnnual ? prices[type].annual : prices[type].monthly; + return price * count; +}; + export function Pricing() { const router = useRouter(); const t = useTranslations("Pricing"); const [isAnnual, setIsAnnual] = useState(false); const [isSelfHostedAnnual, setIsSelfHostedAnnual] = useState(false); const [serverQuantity, setServerQuantity] = useState(1); + const [basicServers, setBasicServers] = useState(1); + const [professionalServers, setProfessionalServers] = useState(1); + const [enterpriseServers, setEnterpriseServers] = useState(1); + const [isLoading, setIsLoading] = useState(false); const featured = true; + const handleBuyNow = async (plan: "basic" | "premium" | "business") => { + try { + setIsLoading(true); + + // Get server quantity based on plan + const servers = { + basic: basicServers, + premium: professionalServers, + business: enterpriseServers, + }[plan]; + + // Get Stripe.js instance + const stripe = await stripePromise; + if (!stripe) throw new Error("Stripe failed to initialize"); + + // Create checkout session + const response = await fetch( + `${SERVER_LICENSE_URL!}/create-checkout-session`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: plan, + isAnnual: isSelfHostedAnnual, + serverQuantity: servers, + }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create checkout session"); + } + + const { sessionId } = await response.json(); + + console.log(sessionId); + + // Redirect to Stripe checkout + const { error } = await stripe.redirectToCheckout({ + sessionId, + }); + + if (error) { + throw error; + } + } catch (error) { + console.error("Error creating checkout session:", error); + } finally { + setIsLoading(false); + } + }; + const [openVideo, setOpenVideo] = useState(false); return (
- + @@ -233,12 +326,12 @@ export function Pricing() { {isAnnual ? (
-

+

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

| -

+

${" "} {(calculatePrice(serverQuantity, isAnnual) / 12).toFixed( 2, @@ -247,7 +340,7 @@ export function Pricing() {

) : ( -

+

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

)} @@ -407,9 +500,12 @@ export function Pricing() {

- {isSelfHostedAnnual - ? t("plan.selfHosted.basic.priceAnnual") - : t("plan.selfHosted.basic.priceMonthly")}{" "} + $ + {calculateLicensePrice( + "basic", + basicServers, + isSelfHostedAnnual, + ).toFixed(2)}{" "} USD

@@ -434,11 +530,47 @@ export function Pricing() { ), )} +
+
+ + {t("plan.selfHosted.servers", { + serverQuantity: basicServers, + })} + +
+
+ + { + setBasicServers(e.target.value as unknown as number); + }} + /> + +
+
-
@@ -447,9 +579,12 @@ export function Pricing() {

- {isSelfHostedAnnual - ? t("plan.selfHosted.professional.priceAnnual") - : t("plan.selfHosted.professional.priceMonthly")}{" "} + $ + {calculateLicensePrice( + "professional", + professionalServers, + isSelfHostedAnnual, + ).toFixed(2)}{" "} USD

@@ -474,11 +609,49 @@ export function Pricing() { ))} +
+
+ + {t("plan.selfHosted.servers", { + serverQuantity: professionalServers, + })} + +
+
+ + { + setProfessionalServers( + e.target.value as unknown as number, + ); + }} + /> + +
+
-
@@ -487,9 +660,12 @@ export function Pricing() {

- {isSelfHostedAnnual - ? t("plan.selfHosted.enterprise.priceAnnual") - : t("plan.selfHosted.enterprise.priceMonthly")}{" "} + $ + {calculateLicensePrice( + "enterprise", + enterpriseServers, + isSelfHostedAnnual, + ).toFixed(2)}{" "} USD

@@ -514,11 +690,49 @@ export function Pricing() { ))} +
+
+ + {t("plan.selfHosted.servers", { + serverQuantity: enterpriseServers, + })} + +
+
+ + { + setEnterpriseServers( + e.target.value as unknown as number, + ); + }} + /> + +
+
-
diff --git a/apps/website/components/ui/input.tsx b/apps/website/components/ui/input.tsx index 8fe7ab2..6693f0a 100644 --- a/apps/website/components/ui/input.tsx +++ b/apps/website/components/ui/input.tsx @@ -36,7 +36,7 @@ const NumberInput = React.forwardRef( return ( ( if (value === "") { props.onChange?.(e); } else { - const number = Number.parseInt(value, 10); + // Remove currency symbol and convert to number + const numericValue = value.replace(/[^0-9.]/g, ""); + const number = Number.parseFloat(numericValue); if (!Number.isNaN(number)) { const syntheticEvent = { ...e, diff --git a/apps/website/locales/en.json b/apps/website/locales/en.json index 2bb6284..7163326 100644 --- a/apps/website/locales/en.json +++ b/apps/website/locales/en.json @@ -181,6 +181,7 @@ "selfHosted": { "title": "Self-Hosted License", "description": "Deploy and manage Dokploy on your own infrastructure with enterprise-grade features and support", + "servers": "{serverQuantity} Servers", "basic": { "title": "Basic License", "description": "Perfect for small teams and startups", diff --git a/apps/website/locales/fr.json b/apps/website/locales/fr.json index 57bd6cb..2271bc3 100644 --- a/apps/website/locales/fr.json +++ b/apps/website/locales/fr.json @@ -176,6 +176,20 @@ "f7": "Nouvelles mises à jour" }, "go": "S'abonner" + }, + "selfHosted": { + "title": "Licence Auto-hébergée", + "description": "Déployez et gérez Dokploy sur votre propre infrastructure avec des fonctionnalités et un support de niveau entreprise", + "servers": "{serverQuantity} Serveurs", + "features": { + "f1": "Complete Flexibility: Install Dokploy UI on your own infrastructure", + "f2": "Infrastructure Auto-Hébergée", + "f3": "Support de la communauté", + "f4": "Accès à toutes les fonctionnalités", + "f5": "Accès à toutes les mises à jour", + "f9": "Serveurs Illimitées" + }, + "go": "Installation" } }, "faq": { diff --git a/apps/website/locales/zh-Hans.json b/apps/website/locales/zh-Hans.json index e727384..97a68d4 100644 --- a/apps/website/locales/zh-Hans.json +++ b/apps/website/locales/zh-Hans.json @@ -122,6 +122,11 @@ }, "footer": { "copyright": "版权属于 © {year} Dokploy, 保留所有权利" + }, + "selfHosted": { + "title": "自托管许可证", + "description": "在您自己的基础设施上部署和管理 Dokploy,享受企业级功能和支持", + "servers": "{serverQuantity} 台服务器" } }, "404": { diff --git a/apps/website/package.json b/apps/website/package.json index 257fd12..7429a80 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -12,9 +12,9 @@ }, "browserslist": "defaults, not ie <= 11", "dependencies": { - "hast-util-to-jsx-runtime": "2.3.5", "@headlessui/react": "^2.2.0", "@headlessui/tailwindcss": "^0.2.0", + "@prettier/plugin-xml": "^3.4.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.0", @@ -23,6 +23,7 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "1.1.1", "@radix-ui/react-tooltip": "^1.1.3", + "@stripe/stripe-js": "^6.1.0", "@tabler/icons-react": "3.21.0", "@tryghost/content-api": "^1.11.21", "@types/node": "20.4.6", @@ -32,9 +33,11 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "framer-motion": "^11.3.19", + "hast-util-to-jsx-runtime": "2.3.5", "lucide-react": "0.364.0", "next": "15.2.0", "next-intl": "^3.26.5", + "prettier": "^3.3.3", "react": "18.2.0", "react-dom": "18.2.0", "react-ga4": "^2.1.0", @@ -52,9 +55,7 @@ "tailwindcss-animate": "^1.0.7", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", - "typescript": "5.1.6", - "prettier": "^3.3.3", - "@prettier/plugin-xml": "^3.4.1" + "typescript": "5.1.6" }, "devDependencies": { "@babel/core": "^7.26.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b73d4e..a2c267e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@stripe/stripe-js': + specifier: ^6.1.0 + version: 6.1.0 '@tabler/icons-react': specifier: 3.21.0 version: 3.21.0(react@18.2.0) @@ -1649,6 +1652,10 @@ packages: engines: {node: '>= 8.0.0'} hasBin: true + '@stripe/stripe-js@6.1.0': + resolution: {integrity: sha512-/5zxRol+MU4I7fjZXPxP2M6E1nuHOxAzoc0tOEC/TLnC31Gzc+5EE93mIjoAnu28O1Sqpl7/BkceDHwnGmn75A==} + engines: {node: '>=12.16'} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -5388,6 +5395,8 @@ snapshots: fflate: 0.7.4 string.prototype.codepointat: 0.2.1 + '@stripe/stripe-js@6.1.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.13':