From 6195fee994a6a562ae9479cbbabe5a216f8b32b1 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Tue, 21 May 2024 19:15:44 +0530 Subject: [PATCH] feat: billing page --- apps/web/package.json | 1 + .../app/[orgShortCode]/settings/layout.tsx | 2 +- .../setup/billing/_components/plans-table.tsx | 120 ++++++++++++++++++ .../settings/org/setup/billing/page.tsx | 94 ++++++++++++++ pnpm-lock.yaml | 28 ++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/[orgShortCode]/settings/org/setup/billing/_components/plans-table.tsx create mode 100644 apps/web/src/app/[orgShortCode]/settings/org/setup/billing/page.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 653c6e6f..57779b0c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,6 +10,7 @@ "check": "tsc --noEmit && next lint" }, "dependencies": { + "@calcom/embed-react": "^1.5.0", "@phosphor-icons/react": "^2.1.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.6", diff --git a/apps/web/src/app/[orgShortCode]/settings/layout.tsx b/apps/web/src/app/[orgShortCode]/settings/layout.tsx index ef03f330..e3e879ff 100644 --- a/apps/web/src/app/[orgShortCode]/settings/layout.tsx +++ b/apps/web/src/app/[orgShortCode]/settings/layout.tsx @@ -6,7 +6,7 @@ export default function Layout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - + {children} diff --git a/apps/web/src/app/[orgShortCode]/settings/org/setup/billing/_components/plans-table.tsx b/apps/web/src/app/[orgShortCode]/settings/org/setup/billing/_components/plans-table.tsx new file mode 100644 index 00000000..a809a1ce --- /dev/null +++ b/apps/web/src/app/[orgShortCode]/settings/org/setup/billing/_components/plans-table.tsx @@ -0,0 +1,120 @@ +'use client'; +import { Button } from '@/src/components/shadcn-ui/button'; +import { api } from '@/src/lib/trpc'; +import { useGlobalStore } from '@/src/providers/global-store-provider'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { AlertDialog as Dialog } from '@radix-ui/themes'; +import useAwaitableModal, { + type ModalComponent +} from '@/src/hooks/use-awaitable-modal'; + +export function PlansTable() { + const [period /*setPeriod*/] = useState<'monthly' | 'yearly'>('monthly'); + + const [StripeWatcherRoot, openPaymentModal] = useAwaitableModal( + StripeWatcher, + { + period + } + ); + + return ( +
+
+
Free
+
Free Plan Perks Here
+ +
+
+
Pro
+
Pro Plan Perks Here
+ {/* Monthly only for now, setup the period selector */} + +
+ +
+ ); +} + +function StripeWatcher({ + open, + period, + onClose, + onResolve +}: ModalComponent<{ period: 'monthly' | 'yearly' }>) { + const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); + const { + data: paymentLinkInfo, + error: linkError, + isLoading: paymentLinkLoading + } = api.org.setup.billing.getOrgSubscriptionPaymentLink.useQuery( + { + orgShortCode, + period, + plan: 'pro' + }, + { enabled: open } + ); + const paymentLinkCache = + api.useUtils().org.setup.billing.getOrgSubscriptionPaymentLink; + + const overviewApi = api.useUtils().org.setup.billing.getOrgBillingOverview; + const timeout = useRef(null); + const checkPayment = useCallback(async () => { + const overview = await overviewApi.fetch({ orgShortCode }); + if (overview.currentPlan === 'pro') { + await overviewApi.invalidate({ orgShortCode }); + if (timeout.current) { + clearTimeout(timeout.current); + } + onResolve(null); + return; + } else { + timeout.current = setTimeout(() => { + void checkPayment(); + }, 7500); + } + }, [onResolve, orgShortCode, overviewApi]); + + useEffect(() => { + if (!open || !paymentLinkInfo) return; + window.open(paymentLinkInfo.subLink, '_blank'); + timeout.current = setTimeout(() => { + void checkPayment(); + }, 10000); + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + void paymentLinkCache.reset(); + }; + }, [open, paymentLinkInfo, paymentLinkCache, checkPayment]); + + return ( + + + Upgrade to Pro + + {paymentLinkLoading + ? 'Generating Payment Link' + : 'Waiting For you to complete your Payment (This may take a few seconds)'} + +
+ We are waiting for you to complete your payment, If you have already + done the payment, please wait for a few seconds for the payment to + reflect. If this modal is not detecting your payment, please close + this modal and try refreshing. If the issue persists, please contact + support. +
+
+ {linkError?.message} +
+
+ +
+
+
+ ); +} diff --git a/apps/web/src/app/[orgShortCode]/settings/org/setup/billing/page.tsx b/apps/web/src/app/[orgShortCode]/settings/org/setup/billing/page.tsx new file mode 100644 index 00000000..5f8236fa --- /dev/null +++ b/apps/web/src/app/[orgShortCode]/settings/org/setup/billing/page.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { Button } from '@/src/components/shadcn-ui/button'; +import { api } from '@/src/lib/trpc'; +import { useGlobalStore } from '@/src/providers/global-store-provider'; +import { useState } from 'react'; +import { PlansTable } from './_components/plans-table'; +import CalEmbed from '@calcom/embed-react'; +import Link from 'next/link'; +import { cn } from '@/src/lib/utils'; + +export default function Page() { + const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); + const { data, isLoading } = + api.org.setup.billing.getOrgBillingOverview.useQuery({ + orgShortCode + }); + + const { data: portalLink } = + api.org.setup.billing.getOrgStripePortalLink.useQuery( + { orgShortCode }, + { + enabled: data?.currentPlan === 'pro' + } + ); + + const [showPlan, setShowPlans] = useState(false); + + return ( +
+

Billing

+
Manage your organization's subscription
+ {isLoading &&
Loading...
} + {data && ( + <> +
+
+
Current Plan
+
+ {data.currentPlan === 'pro' ? 'Pro' : 'Free'} +
+
+ {data.totalUsers && ( +
+
Users
+
+ {data.totalUsers} +
+
+ )} + {data.currentPlan === 'pro' && ( +
+
Billing Period
+
+ {data.currentPeriod === 'monthly' ? 'Monthly' : 'Yearly'} +
+
+ )} +
+ {data.currentPlan !== 'pro' && !showPlan && ( + + )} + {showPlan && } + {data.currentPlan === 'pro' && ( + + )} + {data.currentPlan === 'pro' && ( +
+
+ Jump on a Free Onboarding Call +
+ +
+ )} + + )} +
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c20e280f..f09a31e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,6 +260,9 @@ importers: apps/web: dependencies: + '@calcom/embed-react': + specifier: ^1.5.0 + version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@phosphor-icons/react': specifier: ^2.1.5 version: 2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1217,6 +1220,18 @@ packages: resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} engines: {node: '>=6.9.0'} + '@calcom/embed-core@1.5.0': + resolution: {integrity: sha512-2R+u9tpsKl87AfNkZa2q+bZy4qM4wVNwU4xmlAXg2x9/vI8uhm7MmzctV1SdyhzR5jlwa5iD62+pzVQTgoj6ZA==} + + '@calcom/embed-react@1.5.0': + resolution: {integrity: sha512-HREjDsEu9cEuhbB0aP1nqzw+aW3J1LOutCap/i5EGGfAoxUQDvWekDViE8zFk9lsQ0HKZRBaSc73AsG6bc0B3Q==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + + '@calcom/embed-snippet@1.3.0': + resolution: {integrity: sha512-g/JnC02VpDHIDLdRWbrh3/L2xLTTph46zojeH6y7O2GXAqQIDtCDOqakmng2zhvoLHt+cs7KuiiN4a/r+CswKg==} + '@cloudflare/kv-asset-handler@0.3.1': resolution: {integrity: sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==} @@ -9709,6 +9724,19 @@ snapshots: '@babel/helper-validator-identifier': 7.24.5 to-fast-properties: 2.0.0 + '@calcom/embed-core@1.5.0': {} + + '@calcom/embed-react@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@calcom/embed-core': 1.5.0 + '@calcom/embed-snippet': 1.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@calcom/embed-snippet@1.3.0': + dependencies: + '@calcom/embed-core': 1.5.0 + '@cloudflare/kv-asset-handler@0.3.1': dependencies: mime: 3.0.0