diff --git a/app/settings/components/paddle-link.tsx b/app/settings/components/paddle-link.tsx new file mode 100644 index 0000000..5bde0f2 --- /dev/null +++ b/app/settings/components/paddle-link.tsx @@ -0,0 +1,18 @@ +import type { FunctionComponent, MouseEventHandler } from "react"; +import { HiExternalLink } from "react-icons/hi"; + +type Props = { + onClick: MouseEventHandler; + text: string; +}; + +const PaddleLink: FunctionComponent = ({ onClick, text }) => ( + +); + +export default PaddleLink; diff --git a/app/settings/components/settings-layout.tsx b/app/settings/components/settings-layout.tsx index d70e7ac..58d503b 100644 --- a/app/settings/components/settings-layout.tsx +++ b/app/settings/components/settings-layout.tsx @@ -19,7 +19,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => { -
{children}
+
{children}
); }; diff --git a/app/settings/hooks/use-paddle.ts b/app/settings/hooks/use-paddle.ts new file mode 100644 index 0000000..bcf0de8 --- /dev/null +++ b/app/settings/hooks/use-paddle.ts @@ -0,0 +1,44 @@ +import { useEffect } from "react"; +import { useRouter, getConfig } from "blitz"; + +declare global { + interface Window { + Paddle: any; + } +} + +const { publicRuntimeConfig } = getConfig(); + +const vendor = parseInt(publicRuntimeConfig.paddle.vendorId, 10); + +export default function usePaddle({ eventCallback }: { eventCallback: (data: any) => void }) { + const router = useRouter(); + + useEffect(() => { + if (!window.Paddle) { + const script = document.createElement("script"); + script.onload = () => { + window.Paddle.Setup({ + vendor, + eventCallback(data: any) { + eventCallback(data); + + if (data.event === "Checkout.Complete") { + setTimeout(() => router.reload(), 1000); + } + }, + }); + }; + script.src = "https://cdn.paddle.com/paddle/paddle.js"; + + document.head.appendChild(script); + return; + } + }, []); + + if (typeof window === "undefined") { + return { Paddle: null }; + } + + return { Paddle: window.Paddle }; +} diff --git a/app/settings/hooks/use-subscription.ts b/app/settings/hooks/use-subscription.ts new file mode 100644 index 0000000..1719aa4 --- /dev/null +++ b/app/settings/hooks/use-subscription.ts @@ -0,0 +1,96 @@ +import { useEffect, useRef } from "react"; +import { useQuery, useMutation, useRouter, useSession } from "blitz"; + +import getSubscription from "../queries/get-subscription"; +import usePaddle from "./use-paddle"; +import useCurrentUser from "../../core/hooks/use-current-user"; +import updateSubscription from "../mutations/update-subscription"; + +export default function useSubscription() { + const session = useSession(); + const { user } = useCurrentUser(); + const [subscription] = useQuery(getSubscription, null, { enabled: Boolean(session.orgId) }); + const [updateSubscriptionMutation] = useMutation(updateSubscription); + + const router = useRouter(); + const resolve = useRef<() => void>(); + const promise = useRef>(); + + const { Paddle } = usePaddle({ + eventCallback(data) { + if (["Checkout.Close", "Checkout.Complete"].includes(data.event)) { + resolve.current!(); + promise.current = new Promise((r) => (resolve.current = r)); + } + }, + }); + + useEffect(() => { + promise.current = new Promise((r) => (resolve.current = r)); + }, []); + + type BuyParams = { + planId: string; + coupon?: string; + }; + + async function subscribe(params: BuyParams) { + if (!user || !session.orgId) { + return; + } + + const { planId, coupon } = params; + const checkoutOpenParams = { + email: user.email, + product: planId, + allowQuantity: false, + passthrough: JSON.stringify({ orgId: session.orgId }), + coupon: "", + }; + + if (coupon) { + checkoutOpenParams.coupon = coupon; + } + + Paddle.Checkout.open(checkoutOpenParams); + + return promise.current; + } + + async function updatePaymentMethod({ updateUrl }: { updateUrl: string }) { + const checkoutOpenParams = { override: updateUrl }; + + Paddle.Checkout.open(checkoutOpenParams); + + return promise.current; + } + + async function cancelSubscription({ cancelUrl }: { cancelUrl: string }) { + const checkoutOpenParams = { override: cancelUrl }; + + Paddle.Checkout.open(checkoutOpenParams); + + return promise.current; + } + + type ChangePlanParams = { + planId: string; + }; + + async function changePlan({ planId }: ChangePlanParams) { + try { + await updateSubscriptionMutation({ planId }); + router.reload(); + } catch (error) { + console.log("error", error); + } + } + + return { + subscription, + subscribe, + updatePaymentMethod, + cancelSubscription, + changePlan, + }; +} diff --git a/app/settings/pages/settings/billing.tsx b/app/settings/pages/settings/billing.tsx index cdf242e..55718b7 100644 --- a/app/settings/pages/settings/billing.tsx +++ b/app/settings/pages/settings/billing.tsx @@ -1,135 +1,69 @@ -/* TODO -import type { FunctionComponent, MouseEventHandler } from "react"; import type { BlitzPage } from "blitz"; - -import SettingsLayout from "../../components/settings/settings-layout"; -import SettingsSection from "../../components/settings/settings-section"; -import BillingPlans from "../../components/billing/billing-plans"; -import Divider from "../../components/divider"; +import { GetServerSideProps, Routes } from "blitz"; import useSubscription from "../../hooks/use-subscription"; - -import { withPageOnboardingRequired } from "../../../lib/session-helpers"; -import type { Subscription } from "../../database/subscriptions"; -import { findUserSubscription } from "../../database/subscriptions"; - -import appLogger from "../../../lib/logger"; -import ConnectedLayout from "../../components/connected-layout"; +import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; +import SettingsLayout from "../../components/settings-layout"; +import appLogger from "../../../../integrations/logger"; +import PaddleLink from "../../components/paddle-link"; +import SettingsSection from "../../components/settings-section"; +import Divider from "../../components/divider"; const logger = appLogger.child({ page: "/account/settings/billing" }); -type Props = { - subscription: Subscription | null; -}; - -const Billing: BlitzPage = ({ subscription }) => { - /!* +const Billing: BlitzPage = () => { + /* TODO: I want to be able to - - renew subscription (after pause/cancel for example) (message like "your subscription expired, would you like to renew ?") - - know when is the last time I paid and for how much - - know when is the next time I will pay and for how much - *!/ - const { cancelSubscription, updatePaymentMethod } = useSubscription(); + - renew subscription (after pause/cancel for example) (message like "your subscription expired, would you like to renew ?") + - know when is the last time I paid and for how much + - know when is the next time I will pay and for how much + */ + + useRequireOnboarding(); + const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription(); + console.log("subscription", subscription); + + if (!subscription) { + return {/**/}; + } return ( - - -
- {subscription ? ( - <> - - - updatePaymentMethod({ - updateUrl: subscription.updateUrl, - }) - } - text="Update payment method on Paddle" - /> - + <> + + updatePaymentMethod({ updateUrl: subscription.updateUrl })} + text="Update payment method on Paddle" + /> + -
- -
+
+ +
- - - + + {/**/} + -
- -
+
+ +
- - - cancelSubscription({ - cancelUrl: subscription.cancelUrl, - }) - } - text="Cancel subscription on Paddle" - /> - - - ) : ( - - - - )} -
-
-
+ + cancelSubscription({ cancelUrl: subscription.cancelUrl })} + text="Cancel subscription on Paddle" + /> + + ); }; -export default Billing; - -type PaddleLinkProps = { - onClick: MouseEventHandler; - text: string; -}; - -const PaddleLink: FunctionComponent = ({ onClick, text }) => ( - -); - -export const getServerSideProps = withPageOnboardingRequired( - async (context, user) => { - // const subscription = await findUserSubscription({ userId: user.id }); - - return { - props: { subscription: null }, - }; - }, -); -*/ - -import type { BlitzPage } from "blitz"; -import { Routes } from "blitz"; -import { useRouter } from "blitz"; -import { useEffect } from "react"; - -import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; -import SettingsLayout from "../../components/settings-layout"; - -const Billing: BlitzPage = () => { - useRequireOnboarding(); - const router = useRouter(); - - useEffect(() => { - router.push("/messages"); - }); - - return null; -}; - Billing.getLayout = (page) => {page}; Billing.authenticate = { redirectTo: Routes.SignIn() }; +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + return { props: {} }; +}; + export default Billing; diff --git a/app/settings/queries/get-subscription.ts b/app/settings/queries/get-subscription.ts new file mode 100644 index 0000000..39e2e3e --- /dev/null +++ b/app/settings/queries/get-subscription.ts @@ -0,0 +1,9 @@ +import type { Ctx } from "blitz"; + +import db from "db"; + +export default async function getCurrentUser(_ = null, { session }: Ctx) { + if (!session.orgId) return null; + + return db.subscription.findFirst({ where: { organizationId: session.orgId } }); +} diff --git a/app/users/queries/get-current-user.ts b/app/users/queries/get-current-user.ts index 45ed51c..c09f135 100644 --- a/app/users/queries/get-current-user.ts +++ b/app/users/queries/get-current-user.ts @@ -1,4 +1,4 @@ -import { Ctx } from "blitz"; +import type { Ctx } from "blitz"; import db from "db"; diff --git a/blitz.config.ts b/blitz.config.ts index afdb6c0..78db045 100644 --- a/blitz.config.ts +++ b/blitz.config.ts @@ -83,6 +83,9 @@ const { SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN, NODE_ENV, GIT panelBear: { siteId: process.env.PANELBEAR_SITE_ID, }, + paddle: { + vendorId: process.env.PADDLE_VENDOR_ID, + }, }, // @ts-ignore webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {