bases de la billing page

This commit is contained in:
m5r 2021-09-27 06:44:26 +08:00
parent 0f2c3daf77
commit d1b88078fb
8 changed files with 220 additions and 116 deletions

View File

@ -0,0 +1,18 @@
import type { FunctionComponent, MouseEventHandler } from "react";
import { HiExternalLink } from "react-icons/hi";
type Props = {
onClick: MouseEventHandler<HTMLButtonElement>;
text: string;
};
const PaddleLink: FunctionComponent<Props> = ({ onClick, text }) => (
<button className="flex space-x-2 items-center text-left" onClick={onClick}>
<HiExternalLink className="w-6 h-6 flex-shrink-0" />
<span className="transition-colors duration-150 border-b border-transparent hover:border-primary-500">
{text}
</span>
</button>
);
export default PaddleLink;

View File

@ -19,7 +19,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
</header> </header>
</header> </header>
<main>{children}</main> <main className="flex flex-col flex-grow">{children}</main>
</Layout> </Layout>
); );
}; };

View File

@ -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 };
}

View File

@ -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<Promise<void>>();
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,
};
}

View File

@ -1,49 +1,37 @@
/* TODO
import type { FunctionComponent, MouseEventHandler } from "react";
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { GetServerSideProps, Routes } 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 useSubscription from "../../hooks/use-subscription"; import useSubscription from "../../hooks/use-subscription";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
import { withPageOnboardingRequired } from "../../../lib/session-helpers"; import SettingsLayout from "../../components/settings-layout";
import type { Subscription } from "../../database/subscriptions"; import appLogger from "../../../../integrations/logger";
import { findUserSubscription } from "../../database/subscriptions"; import PaddleLink from "../../components/paddle-link";
import SettingsSection from "../../components/settings-section";
import appLogger from "../../../lib/logger"; import Divider from "../../components/divider";
import ConnectedLayout from "../../components/connected-layout";
const logger = appLogger.child({ page: "/account/settings/billing" }); const logger = appLogger.child({ page: "/account/settings/billing" });
type Props = { const Billing: BlitzPage = () => {
subscription: Subscription | null; /*
};
const Billing: BlitzPage<Props> = ({ subscription }) => {
/!*
TODO: I want to be able to TODO: I want to be able to
- renew subscription (after pause/cancel for example) (message like "your subscription expired, would you like to renew ?") - 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 last time I paid and for how much
- know when is the next time I will pay and for how much - know when is the next time I will pay and for how much
*!/ */
const { cancelSubscription, updatePaymentMethod } = useSubscription();
useRequireOnboarding();
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription();
console.log("subscription", subscription);
if (!subscription) {
return <SettingsSection title="Plan">{/*<BillingPlans />*/}</SettingsSection>;
}
return ( return (
<ConnectedLayout>
<SettingsLayout>
<div className="flex flex-col space-y-6 p-6">
{subscription ? (
<> <>
<SettingsSection title="Payment method"> <SettingsSection title="Payment method">
<PaddleLink <PaddleLink
onClick={() => onClick={() => updatePaymentMethod({ updateUrl: subscription.updateUrl })}
updatePaymentMethod({
updateUrl: subscription.updateUrl,
})
}
text="Update payment method on Paddle" text="Update payment method on Paddle"
/> />
</SettingsSection> </SettingsSection>
@ -53,7 +41,7 @@ const Billing: BlitzPage<Props> = ({ subscription }) => {
</div> </div>
<SettingsSection title="Plan"> <SettingsSection title="Plan">
<BillingPlans activePlanId={subscription?.planId} /> {/*<BillingPlans activePlanId={subscription.paddlePlanId} />*/}
</SettingsSection> </SettingsSection>
<div className="hidden lg:block"> <div className="hidden lg:block">
@ -62,74 +50,20 @@ const Billing: BlitzPage<Props> = ({ subscription }) => {
<SettingsSection title="Cancel subscription"> <SettingsSection title="Cancel subscription">
<PaddleLink <PaddleLink
onClick={() => onClick={() => cancelSubscription({ cancelUrl: subscription.cancelUrl })}
cancelSubscription({
cancelUrl: subscription.cancelUrl,
})
}
text="Cancel subscription on Paddle" text="Cancel subscription on Paddle"
/> />
</SettingsSection> </SettingsSection>
</> </>
) : (
<SettingsSection title="Plan">
<BillingPlans />
</SettingsSection>
)}
</div>
</SettingsLayout>
</ConnectedLayout>
); );
}; };
export default Billing;
type PaddleLinkProps = {
onClick: MouseEventHandler<HTMLButtonElement>;
text: string;
};
const PaddleLink: FunctionComponent<PaddleLinkProps> = ({ onClick, text }) => (
<button className="flex space-x-2 items-center text-left" onClick={onClick}>
<ExternalLinkIcon className="w-6 h-6 flex-shrink-0" />
<span className="transition-colors duration-150 border-b border-transparent hover:border-primary-500">
{text}
</span>
</button>
);
export const getServerSideProps = withPageOnboardingRequired<Props>(
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) => <SettingsLayout>{page}</SettingsLayout>; Billing.getLayout = (page) => <SettingsLayout>{page}</SettingsLayout>;
Billing.authenticate = { redirectTo: Routes.SignIn() }; Billing.authenticate = { redirectTo: Routes.SignIn() };
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
return { props: {} };
};
export default Billing; export default Billing;

View File

@ -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 } });
}

View File

@ -1,4 +1,4 @@
import { Ctx } from "blitz"; import type { Ctx } from "blitz";
import db from "db"; import db from "db";

View File

@ -83,6 +83,9 @@ const { SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN, NODE_ENV, GIT
panelBear: { panelBear: {
siteId: process.env.PANELBEAR_SITE_ID, siteId: process.env.PANELBEAR_SITE_ID,
}, },
paddle: {
vendorId: process.env.PADDLE_VENDOR_ID,
},
}, },
// @ts-ignore // @ts-ignore
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {