Skip to content

Commit

Permalink
Merge pull request #252 from djyde/subscription
Browse files Browse the repository at this point in the history
Subscription
  • Loading branch information
djyde authored Jul 13, 2023
2 parents 47a172e + c07f9e4 commit 67aed36
Show file tree
Hide file tree
Showing 21 changed files with 851 additions and 48 deletions.
147 changes: 136 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, Progress, 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,8 @@ 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"
import { usageLimitation } from "../config.common"

// From https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
function validateEmail(email) {
Expand Down Expand Up @@ -55,6 +57,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 +230,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,12 +266,43 @@ 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>
)
}, [])

const usageBoard = React.useMemo(() => {
return (
<>
<Text size="sm" weight={900}>
Usage (per month)
</Text>
<Stack spacing={4}>
<Group spacing={4}>
<Text weight={500} size="sm">Sites:</Text>
<Text size='sm'>
{`${props.usage.projectCount} / ${usageLimitation['create_site']}`}
</Text>
</Group>

<Group spacing={4}>
<Text weight={500} size="sm">Approve comments:</Text>
<Text size='sm'>
{`${props.usage.approveCommentUsage} / ${usageLimitation['approve_comment']}`}
</Text>
</Group>
<Group spacing={4}>
<Text weight={500} size="sm">Quick Approve:</Text>
<Text size='sm'>
{`${props.usage.quickApproveUsage} / ${usageLimitation['quick_approve']}`}
</Text>
</Group>
</Stack>
</>
)
}, [])

return (
<>
<Head title={`${props.project.title} - Cusdis`} />
Expand All @@ -273,7 +328,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 +353,81 @@ 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 && (
<>
{usageBoard}
<Stack spacing={8}>
<Text weight={900} 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>
)
}
11 changes: 11 additions & 0 deletions config.common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum UsageLabel {
ApproveComment = 'approve_comment',
QuickApprove = 'quick_approve',
CreateSite = 'create_site'
}

export const usageLimitation = {
[UsageLabel.ApproveComment]: 100,
[UsageLabel.QuickApprove]: 10,
[UsageLabel.CreateSite]: 1
}
18 changes: 18 additions & 0 deletions pages/api/comment/[commentId]/approve.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { AuthService } from '../../../../service/auth.service'
import { CommentService } from '../../../../service/comment.service'
import { SubscriptionService, usageLimitation } from '../../../../service/subscription.service'
import { UsageLabel, UsageService } from '../../../../service/usage.service'
import { getSession } from '../../../../utils.server'

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const commentService = new CommentService(req)
const authService = new AuthService(req, res)
const usageService = new UsageService(req)
const session = await getSession(req)

const subscriptionService = new SubscriptionService()

if (req.method === 'POST') {
const commentId = req.query.commentId as string
Expand All @@ -18,7 +25,18 @@ export default async function handler(
return
}

// check usage
if (!await subscriptionService.approveCommentValidate(session.uid)) {
res.status(402).json({
error:
`You have reached the maximum number of approving comments on free plan (${usageLimitation['approve_comment']}/month). Please upgrade to Pro plan to approve more comments.`,
})
return
}

await commentService.approve(commentId)
await usageService.incr(UsageLabel.ApproveComment)

res.json({
message: 'success',
})
Expand Down
16 changes: 16 additions & 0 deletions pages/api/open/approve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import { resolvedConfig } from '../../../utils.server'
import jwt from 'jsonwebtoken'
import { CommentService } from '../../../service/comment.service'
import { SecretKey, TokenBody, TokenService } from '../../../service/token.service'
import { UsageService } from '../../../service/usage.service'
import { SubscriptionService } from '../../../service/subscription.service'
import { UsageLabel, usageLimitation } from '../../../config.common'

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const commentService = new CommentService(req)
const usageService = new UsageService(req)
const subscriptionService = new SubscriptionService()

const tokenService = new TokenService()

if (req.method === 'GET') {
Expand Down Expand Up @@ -59,6 +65,14 @@ export default async function handler(
return
}

// check usage
if (!await subscriptionService.quickApproveValidate(tokenBody.owner.id)) {
res.status(402).json({
error: `You have reached the maximum number of Quick Approve on free plan (${usageLimitation.quick_approve}/month). Please upgrade to Pro plan to use Quick Approve more.`
})
return
}

// firstly, approve comment
await commentService.approve(tokenBody.commentId)

Expand All @@ -69,6 +83,8 @@ export default async function handler(
})
}

await usageService.incr(UsageLabel.QuickApprove)

res.json({
message: 'success'
})
Expand Down
21 changes: 21 additions & 0 deletions pages/api/projects.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import { NextApiRequest, NextApiResponse } from "next";
import { ProjectService } from "../../service/project.service";
import { SubscriptionService } from "../../service/subscription.service";
import { getSession, prisma } from "../../utils.server";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const projectService = new ProjectService(req)
const subscriptionService = new SubscriptionService()
const session = await getSession(req)

if (req.method === 'POST') {
if (!session) {
res.status(401).json({
error: 'Unauthorized'
})
return
}

// check subscription
if (!await subscriptionService.createProjectValidate(session.uid)) {
// if (true) {
res.status(402).json({
error: 'You have reached the maximum number of sites on free plan. Please upgrade to Pro plan to create more sites.'
})
return
}

const { title } = req.body as {
title: string
}

const created = await projectService.create(title)

res.json({
Expand Down
Loading

0 comments on commit 67aed36

Please sign in to comment.