bases de la billing page
This commit is contained in:
parent
0f2c3daf77
commit
d1b88078fb
18
app/settings/components/paddle-link.tsx
Normal file
18
app/settings/components/paddle-link.tsx
Normal 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;
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
44
app/settings/hooks/use-paddle.ts
Normal file
44
app/settings/hooks/use-paddle.ts
Normal 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 };
|
||||||
|
}
|
96
app/settings/hooks/use-subscription.ts
Normal file
96
app/settings/hooks/use-subscription.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
@ -1,135 +1,69 @@
|
|||||||
/* 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>
|
<SettingsSection title="Payment method">
|
||||||
<div className="flex flex-col space-y-6 p-6">
|
<PaddleLink
|
||||||
{subscription ? (
|
onClick={() => updatePaymentMethod({ updateUrl: subscription.updateUrl })}
|
||||||
<>
|
text="Update payment method on Paddle"
|
||||||
<SettingsSection title="Payment method">
|
/>
|
||||||
<PaddleLink
|
</SettingsSection>
|
||||||
onClick={() =>
|
|
||||||
updatePaymentMethod({
|
|
||||||
updateUrl: subscription.updateUrl,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
text="Update payment method on Paddle"
|
|
||||||
/>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<Divider />
|
<Divider />
|
||||||
</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">
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsSection title="Cancel subscription">
|
<SettingsSection title="Cancel subscription">
|
||||||
<PaddleLink
|
<PaddleLink
|
||||||
onClick={() =>
|
onClick={() => cancelSubscription({ cancelUrl: subscription.cancelUrl })}
|
||||||
cancelSubscription({
|
text="Cancel subscription on Paddle"
|
||||||
cancelUrl: subscription.cancelUrl,
|
/>
|
||||||
})
|
</SettingsSection>
|
||||||
}
|
</>
|
||||||
text="Cancel subscription on Paddle"
|
|
||||||
/>
|
|
||||||
</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;
|
||||||
|
9
app/settings/queries/get-subscription.ts
Normal file
9
app/settings/queries/get-subscription.ts
Normal 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 } });
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Ctx } from "blitz";
|
import type { Ctx } from "blitz";
|
||||||
|
|
||||||
import db from "db";
|
import db from "db";
|
||||||
|
|
||||||
|
@ -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 }) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user