Skip to content

Commit

Permalink
Merge pull request #115 from codelitdev/issue-113
Browse files Browse the repository at this point in the history
Billing screen and webhook
  • Loading branch information
rajat1saxena authored Mar 21, 2024
2 parents 45e01dd + 3ba2a42 commit cc0b536
Show file tree
Hide file tree
Showing 17 changed files with 896 additions and 5 deletions.
1 change: 1 addition & 0 deletions .migrations/00003-initialize-subscriptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
db.users.updateMany({}, { $set: { subscriptionStatus: "not-subscribed" } });
126 changes: 126 additions & 0 deletions apps/web/app/account/billing/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use server";

import { Session } from "next-auth";
import connectToDatabase from "@/lib/connect-db";
import UserModel from "@/models/user";
import { LEMONSQUEEZY_API_KEY } from "@/lib/constants";
import { error } from "@/utils/logger";
import { auth } from "@/auth";
import { User } from "@medialit/models";

export async function cancelSubscription(
prevState: Record<string, unknown>,
formData: FormData
): Promise<{
success: boolean;
error?: string;
}> {
try {
if (!LEMONSQUEEZY_API_KEY) {
throw new Error("Lemon API key not found");
}

const session: Session | null = await auth();
if (!session || !session.user) {
throw new Error("Unauthorized");
}

await connectToDatabase();

const user: User | null = await UserModel.findOne({
email: session.user.email,
});

if (!user) {
throw new Error("Unauthorized");
}

const response = await fetch(
`https://api.lemonsqueezy.com/v1/subscriptions/${user.subscriptionId}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
Authorization: `Bearer ${LEMONSQUEEZY_API_KEY}`,
},
}
);
if (response.ok) {
const resp = await response.json();
return { success: true };
}

throw new Error("Some error occurred");
} catch (error: any) {
return { success: false, error: error.message };
}
}

export async function resumeSubscription(
prevState: Record<string, unknown>,
formData: FormData
): Promise<{
success: boolean;
error?: string;
}> {
try {
if (!LEMONSQUEEZY_API_KEY) {
throw new Error("Lemon API key not found");
}

const session: Session | null = await auth();
if (!session || !session.user) {
throw new Error("Unauthorized");
}

await connectToDatabase();

const user: User | null = await UserModel.findOne({
email: session.user.email,
});

if (!user) {
throw new Error("Unauthorized");
}

const response = await fetch(
`https://api.lemonsqueezy.com/v1/subscriptions/${user.subscriptionId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
Authorization: `Bearer ${LEMONSQUEEZY_API_KEY}`,
},
body: JSON.stringify({
data: {
type: "subscriptions",
id: user.subscriptionId,
attributes: {
cancelled: false,
},
},
}),
}
);
const resp = await response.json();

if (response.ok) {
return { success: true };
}

error(`Error in resuming subscription`, {
userId: user.subscriptionId,
apiResponse: resp,
statusCode: response.status,
});
return {
success: false,
error: "Some error occurred while resuming subscription. Try again in a while.",
};
} catch (err: any) {
error(`Error in resuming subscription`, err.stack);
return { success: false, error: err.message };
}
}
136 changes: 136 additions & 0 deletions apps/web/app/account/billing/cancel-subscription-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use client";

import { Button } from "@/components/ui/button";
import { useFormState, useFormStatus } from "react-dom";
import { cancelSubscription } from "./action";
import { useEffect, useState } from "react";
import { useToast } from "@/components/ui/use-toast";
import { useRouter } from "next/navigation";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog";

export default function CancelSubscriptionButton({
subscriptionStatus,
currentPlan,
}: {
subscriptionStatus: string;
currentPlan: string;
}) {
const [formState, formAction] = useFormState(cancelSubscription, {
success: false,
});
const { toast } = useToast();
const router = useRouter();

const [open, setOpen] = useState(false);

useEffect(() => {
if (formState.success) {
setOpen(false);
toast({
title: "We are sorry to see you go",
description: "Your subscription has been cancelled",
});
router.refresh();
}
if (formState.error) {
toast({
title: "Uh oh!",
description: formState.error,
});
}
}, [formState]);

return (
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{currentPlan === "Basic" &&
subscriptionStatus === "subscribed" ? (
<Button className="bg-primary hover:bg-[#333333]">
Downgrade to free
</Button>
) : (
<Button className="bg-red-600 hover:bg-red-700">
Cancel subscription
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
{currentPlan === "Basic" &&
subscriptionStatus === "subscribed" ? (
<>
<DialogHeader>
<DialogTitle>Downgrade to free</DialogTitle>
</DialogHeader>
Are you sure, you want to cancel your current
subscription?
</>
) : (
<>
<DialogHeader>
<DialogTitle>Cancel subscription</DialogTitle>
</DialogHeader>
Are you sure, you want to cancel subscription?
</>
)}

<form action={formAction}>
<DialogFooter>
<DialogClose asChild>
<Button>Nevermind</Button>
</DialogClose>
<DialogClose asChild>
<Submit
currentPlan={currentPlan}
subscriptionStatus={subscriptionStatus}
>
Yes! Cancel
</Submit>
</DialogClose>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}

function Submit({
children,
currentPlan,
subscriptionStatus,
}: {
children: React.ReactNode;
currentPlan: string;
subscriptionStatus: string;
}) {
const status = useFormStatus();

let buttonText = children;
let className;

if (currentPlan === "Basic" && subscriptionStatus === "subscribed") {
buttonText = "Yes! Cancel";
className = "bg-red-500 hover:bg-red-700";
}

return (
<Button
className={`bg-red-500 hover:bg-red-700 text-white ${className}`}
type="submit"
variant="secondary"
disabled={status.pending}
>
{buttonText}
</Button>
);
}
36 changes: 36 additions & 0 deletions apps/web/app/account/billing/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Link from "next/link";
import { ReactNode } from "react";
import { redirect } from "next/navigation";
import Script from "next/script";
import { auth } from "@/auth";

export default async function SchoolDetailsLayout({
params,
children,
}: {
params: { name: string };
children: ReactNode;
}) {
const session = await auth();

if (!session) {
return redirect("/login?from=/account/billing");
}

return (
<div>
<div className="text-primary font-semibold py-2">
<Link href="/" className="hover:border-b-2 border-primary">
All apps{" "}
</Link>
/ <span className="text-muted-foreground">Billing</span>
</div>
<Script
src="https://app.lemonsqueezy.com/js/lemon.js"
strategy="beforeInteractive"
id="lemonsqueezy"
/>
{children}
</div>
);
}
Loading

0 comments on commit cc0b536

Please sign in to comment.