Skip to content

Commit

Permalink
subscription db
Browse files Browse the repository at this point in the history
  • Loading branch information
djyde committed Jul 13, 2023
1 parent 47a172e commit c82cd09
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 19 deletions.
112 changes: 101 additions & 11 deletions components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useMutation, useQuery } from "react-query"
import { useRouter } from "next/router"
import { AiOutlineLogout, AiOutlineSetting, AiOutlineFileText, AiOutlineAlert, AiOutlinePlus, AiOutlineComment, AiOutlineCode, AiOutlineRight, AiOutlineDown, AiOutlineFile, AiOutlineQuestion, AiOutlineQuestionCircle } from 'react-icons/ai'
import { signout, signOut } from "next-auth/client"
import { Anchor, AppShell, Avatar, Badge, Box, Button, Code, Group, Header, Menu, Modal, Navbar, NavLink, ScrollArea, Select, Space, Stack, Switch, Text, TextInput, Title } from "@mantine/core"
import { Anchor, AppShell, Avatar, Badge, Box, Button, Code, Grid, Group, Header, List, Menu, Modal, Navbar, NavLink, Paper, ScrollArea, Select, Space, Stack, Switch, Text, TextInput, Title } from "@mantine/core"
import Link from "next/link"
import type { ProjectServerSideProps } from "../pages/dashboard/project/[projectId]/settings"
import { modals } from "@mantine/modals"
Expand All @@ -13,6 +13,7 @@ import { apiClient } from "../utils.client"
import { useForm } from "react-hook-form"
import { MainLayoutData } from "../service/viewData.service"
import { Head } from "./Head"
import dayjs from "dayjs"

// From https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
function validateEmail(email) {
Expand Down Expand Up @@ -55,6 +56,25 @@ export function MainLayout(props: {
},
})

const downgradePlanMutation = useMutation(async () => {
await apiClient.delete('/subscription')
}, {
onSuccess() {
notifications.show({
title: 'Success',
message: 'Downgrade success',
color: 'green'
})
},
onError() {
notifications.show({
title: 'Error',
message: 'Something went wrong, please contact [email protected]',
color: 'red'
})
}
})

const updateNewCommentNotification = useMutation(updateUserSettings, {
onSuccess() {
notifications.show({
Expand Down Expand Up @@ -209,11 +229,14 @@ export function MainLayout(props: {
}, [])

const badge = React.useMemo(() => {
if (!props.config.isHosted) {
return <Badge color="green" size="xs">OSS</Badge>
if (props.subscription.isActived) {
return <Badge color="green" size="xs">PRO</Badge>
}

return <Badge color="green" size="xs">PRO</Badge>
if (props.config.isHosted) {
return <Badge color="gray" size="xs">OSS</Badge>
}
return <Badge color="green" size="xs">FREE</Badge>
}, [])

const header = React.useMemo(() => {
Expand Down Expand Up @@ -242,7 +265,7 @@ export function MainLayout(props: {
<Group spacing={4}>
<Button onClick={_ => {
openUserModal()
}} size="xs" rightIcon={<AiOutlineRight />} variant='subtle'>{props.session.user.name}</Button>
}} size="xs" rightIcon={<AiOutlineRight />} variant='subtle'>{props.session.user.name} {badge}</Button>
</Group>
</Group>
)
Expand Down Expand Up @@ -273,7 +296,7 @@ export function MainLayout(props: {
}
}}
>
<Modal opened={isUserPannelOpen} onClose={closeUserModal}
<Modal opened={isUserPannelOpen} size="lg" onClose={closeUserModal}
title="User Settings"
>
<Stack>
Expand All @@ -298,11 +321,78 @@ export function MainLayout(props: {
<Text weight={500} size="sm">Display name</Text>
<TextInput placeholder={props.userInfo.name} {...userSettingsForm.register("displayName")} size="sm" />
</Stack>
{/* <Stack spacing={8}>
<Text weight={500} size="sm">Subscription </Text>
<Text size="sm">Current plan: {badge}</Text>
<Anchor size="sm">Manage subscription</Anchor>
</Stack> */}
{props.config.checkout.enabled && (
<Stack spacing={8}>
<Text weight={500} size="sm">Subscription </Text>
<Grid>
<Grid.Col span={6}>
<Paper sx={theme => ({
border: '1px solid #eaeaea',
padding: theme.spacing.md
})}>
<Stack>
<Title order={4}>
Free
</Title>
<List size='sm' sx={{
}}>
<List.Item>
Up to 1 site
</List.Item>
<List.Item>
10 Quick Approve / month
</List.Item>
<List.Item>
100 approved comments / month
</List.Item>
</List>
{!props.subscription.isActived || props.subscription.status === 'cancelled' ? (
<Button disabled size="xs">Current plan</Button>
) : (
<Button size="xs" variant={'outline'} loading={downgradePlanMutation.isLoading} onClick={_ => {
if (window.confirm('Are you sure to downgrade?')) {
downgradePlanMutation.mutate()
}
}}>Downgrade</Button>
)}
</Stack>
</Paper>
</Grid.Col>
<Grid.Col span={6}>
<Paper sx={theme => ({
border: '1px solid #eaeaea',
padding: theme.spacing.md
})}>
<Stack>
<Title order={4}>
Pro
</Title>
<List size='sm' sx={{
}}>
<List.Item>
Unlimited sites
</List.Item>
<List.Item>
Unlimited Quick Approve
</List.Item>
<List.Item>
Unlimited approved comments
</List.Item>
</List>
{props.subscription.isActived ? (
<>
<Button size="xs" component="a" href={props.subscription.updatePaymentMethodUrl}>Manage payment method</Button>
{props.subscription.status === 'cancelled' && (<Text size='xs' align='center'>Expire on {dayjs(props.subscription.endAt).format('YYYY/MM/DD')}</Text>)}
</>
) : (
<Button size='xs' component="a" href={`${props.config.checkout.url}?checkout=[custom][user_id]=${props.session.uid}`}>Upgrade $5/month</Button>
)}
</Stack>
</Paper>
</Grid.Col>
</Grid>
</Stack>
)}
<Button loading={updateUserSettingsMutation.isLoading} onClick={onClickSaveUserSettings}>Save</Button>
<Button onClick={_ => signOut()} variant={'outline'} color='red'>
Logout
Expand Down
40 changes: 40 additions & 0 deletions components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
Box,
Container,
Flex,
Link,
Menu,
MenuButton,
MenuItem,
MenuList,
Spacer,
} from "@chakra-ui/react"
import React from "react"
import { signOut } from "next-auth/client"
import { UserSession } from "../service"

export function Navbar(props: { session: UserSession }) {
return (
<Box py={4}>
<Container maxWidth={"5xl"}>
<Flex>
<Box>
<Link fontWeight="bold" fontSize="xl" href="/dashboard">
Cusdis
</Link>
</Box>
<Spacer />
<Box>
<Menu>
<MenuButton>{props.session.user.name}</MenuButton>
<MenuList>
<MenuItem><Link width="100%" href="/user">Settings</Link></MenuItem>
<MenuItem onClick={() => signOut()}>Logout</MenuItem>
</MenuList>
</Menu>
</Box>
</Flex>
</Container>
</Box>
)
}
88 changes: 88 additions & 0 deletions pages/api/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import axios from 'axios';
import * as crypto from 'crypto'
import { NextApiRequest, NextApiResponse } from 'next';
import { Readable } from 'stream';
import { SubscriptionService } from '../../service/subscription.service';
import { getSession, prisma, resolvedConfig } from '../../utils.server';

export const config = {
api: {
bodyParser: false,
},
};

// Get raw body as string
async function getRawBody(readable: Readable): Promise<Buffer> {
const chunks = [];
for await (const chunk of readable) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
}
return Buffer.concat(chunks);
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {

if (req.method === 'POST') {
const rawBody = await getRawBody(req);
const secret = resolvedConfig.checkout.lemonSecret;
console.log(req.headers)
const hmac = crypto.createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8');
const signature = Buffer.from(req.headers['x-signature'] as string, 'utf8');

if (!crypto.timingSafeEqual(digest, signature)) {
res.status(401).send('Invalid signature');
return
}

const data = JSON.parse(Buffer.from(rawBody).toString('utf8'));
const subscriptionService = new SubscriptionService()
const eventName = req.headers['x-event-name'] as string

switch (eventName) {
case 'subscription_created':
case 'subscription_updated': {
await subscriptionService.update(data)
break
}
}

res.json({

})
} else if (req.method === 'DELETE') {
const session = await getSession(req)

if (!session) {
res.status(401).send('Unauthorized')
return
}

const subscription = await prisma.subscription.findUnique({
where: {
userId: session.uid
},
})

if (!subscription) {
res.status(404).send('Subscription not found')
return
}

try {
const response = await axios.delete(`https://api.lemonsqueezy.com/v1/subscriptions/${subscription.lemonSubscriptionId}`, {
headers: {
'Authorization': `Bearer ${resolvedConfig.checkout.lemonApiKey}`,
'Content-Type': 'application/vnd.api+json',
'Accept': "application/vnd.api+json"
}
})
} catch (e) {
console.log(e.response.data, e.request)
}

res.json({
message: 'success'
})
}
}
17 changes: 17 additions & 0 deletions prisma/sqlite/migrations/20230713042856_subscription/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Subscription" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"order_id" TEXT NOT NULL,
"product_id" TEXT,
"variant_id" TEXT,
"customer_id" TEXT,
"status" TEXT NOT NULL,
"endsAt" DATETIME,
"update_payment_method_url" TEXT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Subscription" ADD COLUMN "lemon_subscription_id" TEXT;
35 changes: 27 additions & 8 deletions prisma/sqlite/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ model Account {
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@index([providerAccountId], name: "providerAccountId")
@@index([providerId], name: "providerId")
@@index([userId], name: "userId")
Expand All @@ -47,7 +46,7 @@ model Session {
model User {
id String @id @default(uuid())
name String?
displayName String?
displayName String?
email String? @unique
emailVerified DateTime? @map(name: "email_verified")
image String?
Expand All @@ -58,12 +57,33 @@ model User {
Comment Comment[]
subscription Subscription?
enableNewCommentNotification Boolean? @default(true) @map(name: "enable_new_comment_notification")
notificationEmail String? @map(name: "notification_email")
notificationEmail String? @map(name: "notification_email")
@@map(name: "users")
}

model Subscription {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
lemonSubscriptionId String? @map(name: "lemon_subscription_id")
orderId String @map(name: "order_id")
productId String? @map(name: "product_id")
variantId String? @map(name: "variant_id")
customerId String? @map(name: "customer_id")
status String
endsAt DateTime? @map(name: "endsAt")
updatePaymentMethodUrl String? @map(name: "update_payment_method_url")
createdAt DateTime @default(now()) @map(name: "created_at")
}

model VerificationRequest {
id Int @id @default(autoincrement())
identifier String
Expand All @@ -78,10 +98,10 @@ model VerificationRequest {
// next-auth END

model Project {
id String @id @default(uuid())
id String @id @default(uuid())
title String
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
deletedAt DateTime? @map(name: "deleted_at")
ownerId String
Expand All @@ -94,7 +114,7 @@ model Project {
enableNotification Boolean? @default(true) @map(name: "enable_notification")
webhook String?
webhook String?
enableWebhook Boolean?
@@map(name: "projects")
Expand All @@ -115,7 +135,6 @@ model Page {
comments Comment[]
@@index([projectId], name: "projectId")
@@map("pages")
}
Expand Down
Loading

0 comments on commit c82cd09

Please sign in to comment.