42 | {page.title} 43 |
44 | {page.description && ( 45 |{page.description}
46 | )} 47 |49 |
{text}
} 17 |{page.description}
46 | )} 47 |32 | Please select your login method. 33 |
34 |37 | By clicking continue, you agree to our{" "} 38 | 42 | Privacy Policy 43 | 44 | . 45 |
46 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | ## Star 记录
79 |
80 | 
--------------------------------------------------------------------------------
/components/Icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertTriangle,
3 | ArrowRight,
4 | Check,
5 | ChevronLeft,
6 | ChevronRight,
7 | Command,
8 | CreditCard,
9 | File,
10 | FileText,
11 | HelpCircle,
12 | Image,
13 | Laptop,
14 | Loader2,
15 | LucideProps,
16 | Moon,
17 | MoreVertical,
18 | Pizza,
19 | Plus,
20 | Settings,
21 | SunMedium,
22 | Trash,
23 | Twitter,
24 | User,
25 | X,
26 | type LucideIcon,
27 | } from "lucide-react";
28 |
29 | export type Icon = LucideIcon;
30 |
31 | export const Icons = {
32 | logo: Command,
33 | close: X,
34 | spinner: Loader2,
35 | chevronLeft: ChevronLeft,
36 | chevronRight: ChevronRight,
37 | trash: Trash,
38 | post: FileText,
39 | page: File,
40 | media: Image,
41 | settings: Settings,
42 | billing: CreditCard,
43 | ellipsis: MoreVertical,
44 | add: Plus,
45 | warning: AlertTriangle,
46 | user: User,
47 | arrowRight: ArrowRight,
48 | help: HelpCircle,
49 | pizza: Pizza,
50 | sun: SunMedium,
51 | moon: Moon,
52 | laptop: Laptop,
53 | gitHub: ({ ...props }: LucideProps) => (
54 |
69 | ),
70 | google: (props: LucideProps) => (
71 |
77 | ),
78 | twitter: Twitter,
79 | check: Check,
80 | };
81 |
--------------------------------------------------------------------------------
/components/icons/GitHub.tsx:
--------------------------------------------------------------------------------
1 | export default function Github({ className }: { className?: string }) {
2 | return (
3 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/UserAccountHeader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { buttonVariants } from "@/components/ui/button";
3 | import { cn } from "@/lib/utils";
4 | import { signOut } from "next-auth/react";
5 | import Link from "next/link";
6 |
7 | import { UserAvatar } from "@/components/UserAvatar";
8 | import CrownIcon from "@/components/icons/CrownIcon";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuSeparator,
14 | DropdownMenuTrigger,
15 | } from "@/components/ui/dropdown-menu";
16 | import { UserInfo } from "@/types/user";
17 | import dayjs from "dayjs";
18 | import { useCallback } from "react";
19 |
20 | interface UserAccountNavProps extends React.HTMLAttributes69 | {user.email} 70 |
71 | )} 72 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | ## Star History
85 |
86 | 
87 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { ONE_DAY } from "@/lib/constants";
2 | import { getUserSubscriptionStatus } from "@/lib/lemonsqueezy/subscriptionFromStorage";
3 | import prisma from "@/lib/prisma";
4 | import { UserInfo } from "@/types/user";
5 | import { Account, NextAuthOptions, TokenSet } from "next-auth";
6 | import { JWT } from "next-auth/jwt";
7 | import GithubProvider from 'next-auth/providers/github';
8 | import GoogleProvider from 'next-auth/providers/google';
9 | import redis from "./redis";
10 |
11 | // Here you define the type for the token object that includes accessToken.
12 | interface ExtendedToken extends TokenSet {
13 | accessToken?: string;
14 | userId?: string;
15 | }
16 |
17 | export const authOptions: NextAuthOptions = {
18 | secret: process.env.NEXTAUTH_SECRET,
19 | session: {
20 | strategy: "jwt",
21 | },
22 | pages: {
23 | signIn: "/auth/login",
24 | signOut: '/auth/logout',
25 | },
26 | providers: [
27 | GithubProvider({
28 | clientId: `${process.env.GITHUB_ID}`,
29 | clientSecret: `${process.env.GITHUB_SECRET}`,
30 | httpOptions: {
31 | timeout: 50000,
32 | },
33 | }),
34 | GoogleProvider({
35 | clientId: `${process.env.GOOGLE_ID}`,
36 | clientSecret: `${process.env.GOOGLE_SECRET}`
37 | }),
38 | ],
39 | callbacks: {
40 | async jwt({ token, account }) {
41 | // 登录(account仅登录那一次有值)
42 | // Only on sign in (account only has a value at that time)
43 | if (account) {
44 | token.accessToken = account.access_token
45 |
46 | // 存储访问令牌
47 | // Store the access token
48 | await storeAccessToken(account.access_token || '', token.sub);
49 |
50 | // 用户信息存入数据库
51 | // Save user information in the database
52 | const userInfo = await upsertUserAndGetInfo(token, account);
53 | if (!userInfo || !userInfo.userId) {
54 | throw new Error('User information could not be saved or retrieved.');
55 | }
56 |
57 | const planRes = await getUserSubscriptionStatus({ userId: userInfo.userId, defaultUser: userInfo })
58 | const fullUserInfo = {
59 | userId: userInfo.userId,
60 | username: userInfo.username,
61 | avatar: userInfo.avatar,
62 | email: userInfo.email,
63 | platform: userInfo.platform,
64 | role: planRes.role,
65 | membershipExpire: planRes.membershipExpire,
66 | accessToken: account.access_token
67 | }
68 | return fullUserInfo
69 | }
70 | return token as any
71 | },
72 | async session({ session, token }) {
73 | // Append user information to the session
74 | if (token && token.userId) {
75 | session.user = await getSessionUser(token);
76 | }
77 | return session;
78 | }
79 | },
80 | }
81 | async function storeAccessToken(accessToken: string, sub?: string) {
82 | if (!accessToken || !sub) return;
83 | const expire = ONE_DAY * 30; // The number of seconds in 30 days
84 | await redis.set(accessToken, sub, { ex: expire });
85 | }
86 | async function upsertUserAndGetInfo(token: JWT, account: Account) {
87 | const user = await upsertUser(token, account.provider);
88 | if (!user || !user.userId) return null;
89 |
90 | const subscriptionStatus = await getUserSubscriptionStatus({ userId: user.userId, defaultUser: user });
91 |
92 | return {
93 | ...user,
94 | role: subscriptionStatus.role,
95 | membershipExpire: subscriptionStatus.membershipExpire,
96 | };
97 | }
98 | async function upsertUser(token: JWT, provider: string) {
99 | const userData = {
100 | userId: token.sub,
101 | username: token.name,
102 | avatar: token.picture,
103 | email: token.email,
104 | platform: provider,
105 | };
106 |
107 | const user = await prisma.user.upsert({
108 | where: { userId: token.sub },
109 | update: userData,
110 | create: { ...userData, role: 0 },
111 | });
112 |
113 | return user || null;
114 | }
115 | async function getSessionUser(token: ExtendedToken): Promise*]:text-muted-foreground", 90 | className 91 | )} 92 | {...props} 93 | /> 94 | ), 95 | img: ({ 96 | className, 97 | alt, 98 | ...props 99 | }: React.ImgHTMLAttributes) => ( 100 | // eslint-disable-next-line @next/next/no-img-element 101 | 102 | ), 103 | hr: ({ ...props }) =>
, 104 | table: ({ className, ...props }: React.HTMLAttributes) => ( 105 | 106 |108 | ), 109 | tr: ({ className, ...props }: React.HTMLAttributes107 |
) => ( 110 | 114 | ), 115 | th: ({ className, ...props }) => ( 116 | 123 | ), 124 | td: ({ className, ...props }) => ( 125 | 132 | ), 133 | pre: ({ className, ...props }) => ( 134 | 141 | ), 142 | code: ({ className, ...props }) => ( 143 | 150 | ), 151 | Image, 152 | Callout, 153 | Card: MdxCard, 154 | }; 155 | 156 | interface MdxProps { 157 | code: string; 158 | } 159 | 160 | export function Mdx({ code }: MdxProps) { 161 | const Component = useMDXComponent(code); 162 | 163 | return ( 164 |165 |167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef166 | , 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef , 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes36 | 44 | ) => ( 52 | 59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes ) => ( 66 | 73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef , 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef , 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef , 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef , 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /app/api/payment/subscribe/route.ts: -------------------------------------------------------------------------------- 1 | import { axios } from "@/lib/axios"; 2 | import { MEMBERSHIP_ROLE_VALUE, VARIANT_IDS_BY_TYPE } from "@/lib/constants"; 3 | import { getUserSubscriptionPlan } from "@/lib/lemonsqueezy/subscription"; 4 | import prisma from "@/lib/prisma"; 5 | import { unauthorizedResponse } from "@/lib/response/responseUtils"; 6 | import { verifyReferer, verifyToken } from "@/lib/verifyUtils/verifyUtils"; 7 | import { UpgradeType, } from "@/types/subscribe"; 8 | import type { CreateCheckoutResult } from "lemonsqueezy.ts/dist/types"; 9 | import { NextResponse } from "next/server"; 10 | 11 | export async function POST(request: Request) { 12 | try { 13 | // 判断referer 14 | // Check the referer 15 | if (!(await verifyReferer(request))) { 16 | return unauthorizedResponse("Invalid referer."); 17 | } 18 | // 判断token是否存在 19 | // Verify if token exists 20 | const redisUserId: string | false = await verifyToken(request); 21 | if (!redisUserId) { 22 | return unauthorizedResponse("Token validation failed. Please login again."); 23 | } 24 | 25 | const { userId, type }: { userId: string, type: UpgradeType } = await request.json() 26 | if (!userId) { 27 | return unauthorizedResponse("Your account was not found"); 28 | } 29 | const variantId = VARIANT_IDS_BY_TYPE[type] 30 | if (!type || !variantId) { 31 | return unauthorizedResponse("Your account was not found"); 32 | } 33 | 34 | const user = await prisma.user.findUnique({ 35 | where: { userId: userId.toString() }, 36 | select: { userId: true, email: true, username: true }, 37 | }); 38 | 39 | if (!user) return NextResponse.json({ message: "user not found" }, { status: 401 }); 40 | 41 | const checkout = (await axios.post( 42 | `${process.env.LEMON_SQUEEZY_HOST}/checkouts`, 43 | { 44 | data: { 45 | type: "checkouts", 46 | attributes: { checkout_data: { custom: { email: user.email, userId: user.userId, username: user.username, type } } }, 47 | relationships: { 48 | store: { data: { type: "stores", id: process.env.LEMON_SQUEEZY_STORE_ID } }, 49 | variant: { data: { type: "variants", id: variantId.toString() } }, 50 | }, 51 | }, 52 | }, 53 | { 54 | headers: { 55 | Authorization: `Bearer ${process.env.LEMON_SQUEEZY_API_KEY}`, 56 | Accept: 'application/vnd.api+json', 57 | 'Content-Type': 'application/vnd.api+json' 58 | } 59 | } 60 | )) as CreateCheckoutResult; 61 | 62 | return NextResponse.json({ checkoutURL: checkout.data.attributes.url }, { status: 200 }); 63 | } catch (error: any) { 64 | console.error('POST request failed:', error); 65 | return NextResponse.json({ 66 | error: "An unexpected error occurred. Please try again later." 67 | }, { status: 500 }); 68 | } 69 | } 70 | 71 | export async function DELETE(request: Request) { 72 | try { 73 | // 判断referer 74 | // Check the referer 75 | if (!(await verifyReferer(request))) { 76 | return unauthorizedResponse("Invalid referer."); 77 | } 78 | // 判断token是否存在 79 | // Verify if token exists 80 | const redisUserId: string | false = await verifyToken(request); 81 | if (!redisUserId) { 82 | return unauthorizedResponse("Token validation failed. Please login again."); 83 | } 84 | 85 | // 查询订阅信息 86 | // Query subscription information 87 | const subscriptionPlan = await getUserSubscriptionPlan({ userId: redisUserId }) 88 | // 校验角色 89 | // Validate roles 90 | if (!subscriptionPlan) { 91 | const errorText = `you're not a pro user.` 92 | return NextResponse.json({ message: errorText }, { status: 401 }); 93 | } 94 | if (subscriptionPlan.role !== MEMBERSHIP_ROLE_VALUE) { 95 | const errorText = `you're not a pro user.` 96 | return NextResponse.json({ message: errorText }, { status: 401 }); 97 | } 98 | // 校验订阅状态 99 | // Check the subscription status 100 | if (subscriptionPlan.isCanceled) { 101 | const errorText = `your subscription already canceled.` 102 | return NextResponse.json({ message: errorText }, { status: 401 }); 103 | } 104 | 105 | // 调用 lemon squeezy 取消订阅 106 | // Call lemon squeezy to cancel the subscription 107 | const unsubscribeRes = (await axios.delete( 108 | `${process.env.LEMON_SQUEEZY_HOST}/subscriptions/${subscriptionPlan.subscriptionId}`, 109 | { 110 | headers: { 111 | Authorization: `Bearer ${process.env.LEMON_SQUEEZY_API_KEY}`, 112 | Accept: 'application/vnd.api+json', 113 | 'Content-Type': 'application/vnd.api+json' 114 | } 115 | } 116 | )); 117 | // 根据返回处理,如果错误,提供错误提示,如果正确,提供已取消提示 118 | // console.log(unsubscribeRes, JSON.stringify(unsubscribeRes)); 119 | if (unsubscribeRes?.data?.attributes?.cancelled) { 120 | return NextResponse.json({ message: 'success' }, { status: 200 }); 121 | } 122 | return NextResponse.json({ message: 'fail' }, { status: 400 }); 123 | } catch (error) { 124 | console.error('DELETE request failed:', error); 125 | return NextResponse.json({ 126 | error: "An unexpected error occurred. Please try again later." 127 | }, { status: 500 }); 128 | } 129 | } -------------------------------------------------------------------------------- /components/icons/CrownIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CrownIcon({ className }: { className?: string }) { 2 | return ( 3 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /components/dashboard/billing-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { Icons } from "@/components/Icons"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card"; 15 | import toast, { Toaster } from "react-hot-toast"; 16 | 17 | import { 18 | AlertDialog, 19 | AlertDialogAction, 20 | AlertDialogCancel, 21 | AlertDialogContent, 22 | AlertDialogDescription, 23 | AlertDialogFooter, 24 | AlertDialogHeader, 25 | AlertDialogTitle, 26 | AlertDialogTrigger, 27 | } from "@/components/ui/alert-dialog"; 28 | import { axios } from "@/lib/axios"; 29 | import { cn } from "@/lib/utils"; 30 | import { Res } from "@/types/request"; 31 | import { UserSubscriptionPlan } from "@/types/subscribe"; 32 | import { UserInfo } from "@/types/user"; 33 | import dayjs from "dayjs"; 34 | import { useRouter } from "next/navigation"; 35 | 36 | interface BillingFormProps extends React.HTMLAttributes { 37 | subscriptionPlan: UserSubscriptionPlan; 38 | user: UserInfo; 39 | } 40 | 41 | export function BillingForm({ 42 | subscriptionPlan, 43 | user, 44 | className, 45 | }: BillingFormProps) { 46 | const router = useRouter(); 47 | const [isLoading, setIsLoading] = React.useState (false); 48 | 49 | async function updatePayment(event: React.SyntheticEvent) { 50 | event.preventDefault(); 51 | setIsLoading(!isLoading); 52 | if (subscriptionPlan.isPro && subscriptionPlan.updatePaymentMethodURL) { 53 | window.location.href = subscriptionPlan.updatePaymentMethodURL; 54 | } 55 | } 56 | async function upgrade(event: React.SyntheticEvent) { 57 | event.preventDefault(); 58 | setIsLoading(!isLoading); 59 | window.location.href = "/#subscription-card"; 60 | } 61 | async function cancelSubscription() { 62 | console.log("cancel subscription", subscriptionPlan); 63 | if (!subscriptionPlan) { 64 | toast.error("subscriptionId not found"); 65 | return; 66 | } 67 | if (!subscriptionPlan.subscriptionId) { 68 | toast.error("subscriptionId not found"); 69 | return; 70 | } 71 | if (!subscriptionPlan.isPro) { 72 | toast.error("you don't have a subscription"); 73 | return; 74 | } 75 | if (subscriptionPlan.isCanceled) { 76 | toast.error("subscription already canceled"); 77 | return; 78 | } 79 | try { 80 | const res = await axios.delete ("/api/payment/subscribe", { 81 | headers: { 82 | token: user.accessToken, 83 | }, 84 | }); 85 | if (res.code === 200) { 86 | router.replace(""); 87 | return; 88 | } 89 | toast.error("something wrong"); 90 | } catch (err) { 91 | console.log(err); 92 | } 93 | } 94 | 95 | return ( 96 | 97 |169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /lib/usage/usage.ts: -------------------------------------------------------------------------------- 1 | import { DATE_USAGE_KEY_EXPIRE, ROLES_LIMIT, getBoostPackKey, getUserDateUsageKey, getUserTotalUsageKey } from "@/lib/constants"; 2 | import { getUserSubscriptionStatus } from "@/lib/lemonsqueezy/subscriptionFromStorage"; 3 | import redis from "@/lib/redis"; 4 | import { DateRemaining } from "@/types/usage"; 5 | import { RemainingParams, Role, UserId } from "@/types/user"; 6 | import 'server-only'; 7 | 8 | interface IncrAfterChat { 9 | userId: string; 10 | remainingInfo: DateRemaining; 11 | } 12 | export const incrAfterChat = async ({ userId, remainingInfo }: IncrAfterChat) => { 13 | // 网站总使用用量自增 14 | // Increment website total usage after chat 15 | incrUsage() 16 | 17 | /** 18 | * 用户使用量自增 19 | * Increment user usage 20 | */ 21 | 22 | // 如果有默认次数,增加一次日使用次数 23 | // If there is a default number of times, add one to the daily usage count 24 | if (remainingInfo.userTodayRemaining > 0) { 25 | await incrementDailyUserUsage({ userId }) 26 | return 27 | } 28 | // 如果没有默认次数,有加油包,扣除加油包次数 29 | // If there is no default number but a boost pack is available, deduct from the boost pack count 30 | if (remainingInfo.boostPackRemaining > 0) { 31 | const boostPackKey = await getBoostPackKey({ userId }) 32 | await redis.decr(boostPackKey) 33 | return 34 | } 35 | // 如果没有默认次数,也没有加油包,则不处理 36 | // If there is no default number or boost pack, do nothing 37 | console.log('0 credit remaining today.'); 38 | } 39 | 40 | // 自增网站总使用次数 41 | // Increment total website usage count 42 | export const incrUsage = async () => { 43 | await redis.incr('usage'); 44 | } 45 | // 获取网站总使用次数 46 | // Get total website usage count 47 | export const getUsage = async () => { 48 | const usage = await redis.get('usage') || 0; 49 | return usage 50 | } 51 | 52 | // 自增用户使用次数,这个方法用于内部调用 53 | // Increment user's daily usage count, this method is for internal use 54 | export const incrementDailyUserUsage = async ({ userId }: UserId) => { 55 | const keyDate = getUserDateUsageKey({ userId }); 56 | const keyTotal = getUserTotalUsageKey({ userId }) 57 | 58 | // 使用 Redis pipeline 59 | // Use Redis pipeline 60 | const pipeline = redis.pipeline(); 61 | // 将所有命令加入 pipeline 62 | // Add all commands to the pipeline 63 | pipeline.incr(keyDate); // 增加日使用量 Increase daily usage 64 | pipeline.incr(keyTotal); // 增加总使用量 Increase total usage 65 | pipeline.expire(keyDate, DATE_USAGE_KEY_EXPIRE); // 设置日使用量的过期时间 // Set expiration time for daily usage 66 | 67 | try { 68 | await pipeline.exec(); 69 | } catch (error) { 70 | console.error('An error occurred while incrementing user usage data:', error); 71 | } 72 | } 73 | 74 | // 获取用户日使用次数 75 | // Get User's daily usage count 76 | export const getUserDateUsage = async ({ userId }: UserId) => { 77 | const keyDate = getUserDateUsageKey({ userId }); 78 | const userTodayUsageStr = await redis.get(keyDate) as string | null; 79 | const userTodayUsage = parseInt(userTodayUsageStr ?? '0', 10); 80 | return { userTodayUsage }; 81 | } 82 | // 获取用户总使用次数 83 | // Get user's total usage count 84 | export const getUserTotalUsage = async ({ userId }: UserId) => { 85 | const keyTotal = `USER_USAGE::uid:${userId}`; 86 | const userTotalUsageStr = await redis.get(keyTotal) as string | null; 87 | const userTotalUsage = parseInt(userTotalUsageStr ?? '0', 10); 88 | return { 89 | userTotalUsage 90 | } 91 | } 92 | 93 | // 计算当日可用次数:查询当日已用次数,计算剩余次数,再加上加油包剩余次数 94 | // Calculate the available number of times for the day: Query the number of times used that day, calculate the remaining number, and then add the remaining number of boost packs 95 | export const getUserDateRemaining = async ({ userId, role }: RemainingParams) => { 96 | const { userTodayUsage } = await getUserDateUsage({ userId }) 97 | 98 | let userRole: Role = 0 99 | if (role) { 100 | // 有传角色,复用订阅信息 101 | // If the role is passed, reuse subscription information 102 | userRole = role 103 | } else { 104 | // 没传角色,重新请求订阅信息 105 | // If no role is passed, request subscription information again 106 | const subscriptionRes = await getUserSubscriptionStatus({ 107 | userId, 108 | }) 109 | userRole = subscriptionRes.role 110 | } 111 | 112 | const userDateDefaultLimit: number = ROLES_LIMIT[userRole] 113 | 114 | const userTodayRemaining = userDateDefaultLimit - userTodayUsage <= 0 ? 0 : userDateDefaultLimit - userTodayUsage 115 | const boostPackKey = await getBoostPackKey({ userId }) 116 | const boostPackRemaining: number = await redis.get(boostPackKey) || 0 117 | // 查询次数是在请求openai前,自增次数是在请求后,这里把查询到的redis剩余次数返回,并传给自增方法,减少redis请求次数 118 | // The query for the number of times is before the request to openai, and the increment of the number of times is after the request, here the remaining redis times queried are returned and passed to the increment method, reducing the number of redis requests 119 | return { 120 | userTodayRemaining, 121 | boostPackRemaining, 122 | userDateRemaining: userTodayRemaining + boostPackRemaining 123 | } 124 | } 125 | // 计算当日剩余次数、会员到期时间、加油包剩余次数、加油包到期时间 126 | // Calculate today's remaining count, membership expiration time, remaining boost pack count, and boost pack expiration time 127 | export const checkStatus = async ({ userId }: UserId) => { 128 | // 获取用户订阅信息(角色、会员到期时间戳) 129 | // Get user subscription information 130 | const subscriptionRes = await getUserSubscriptionStatus({ 131 | userId, 132 | }); 133 | 134 | const pipeline = redis.pipeline(); 135 | const keyDate = getUserDateUsageKey({ userId }); 136 | pipeline.get(keyDate); 137 | const boostPackKey = getBoostPackKey({ userId }); 138 | pipeline.get(boostPackKey); 139 | 140 | const pipelineResults = await pipeline.exec(); 141 | const userTodayUsageStr = pipelineResults[0] as string; 142 | const boostPackRemainingStr = pipelineResults[1] as string; 143 | 144 | const userTodayUsage = parseInt(userTodayUsageStr ?? '0', 10); 145 | const boostPackRemaining = parseInt(boostPackRemainingStr ?? '0', 10); 146 | 147 | const userDateDefaultLimit: number = ROLES_LIMIT[subscriptionRes.role]; 148 | const userTodayRemaining = Math.max(userDateDefaultLimit - userTodayUsage, 0); 149 | 150 | // 获取加油包到期时间,如果有剩余次数 151 | // remaining boost pack count, and boost pack expiration time. 152 | let boostPackExpire = 0 153 | if (boostPackRemaining > 0) { 154 | boostPackExpire = await redis.ttl(boostPackKey) 155 | } 156 | 157 | return { 158 | role: subscriptionRes.role, 159 | todayRemaining: userTodayRemaining, 160 | membershipExpire: subscriptionRes.membershipExpire, 161 | boostPackRemaining, 162 | boostPackExpire, 163 | }; 164 | }; 165 | 166 | // 升级后清空当日已用次数 167 | // Clear the day's used count after upgrade. 168 | export const clearTodayUsage = async ({ userId }: UserId) => { 169 | const userDateUsageKey = getUserDateUsageKey({ userId }) 170 | await redis.setex(userDateUsageKey, DATE_USAGE_KEY_EXPIRE, 0) 171 | } -------------------------------------------------------------------------------- /app/api/payment/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { ONE_DAY, getSinglePayOrderKey } from "@/lib/constants"; 2 | import { client } from "@/lib/lemonsqueezy/lemons"; 3 | import prisma from "@/lib/prisma"; 4 | import redis from "@/lib/redis"; 5 | import { boostPack } from "@/lib/upgrade/upgrade"; 6 | import { clearTodayUsage } from "@/lib/usage/usage"; 7 | import { Buffer } from "buffer"; 8 | import crypto from "crypto"; 9 | import dayjs from "dayjs"; 10 | import { headers } from "next/headers"; 11 | import { NextResponse } from "next/server"; 12 | import rawBody from "raw-body"; 13 | import { Readable } from "stream"; 14 | 15 | export async function POST(request: Request) { 16 | console.log('webhook'); 17 | const body = await rawBody(Readable.from(Buffer.from(await request.text()))); 18 | const headersList = headers(); 19 | const payload = JSON.parse(body.toString()); 20 | 21 | const sigString = headersList.get("x-signature"); 22 | if (!sigString) { 23 | console.error(`Signature header not found`); 24 | return NextResponse.json({ message: "Signature header not found" }, { status: 401 }); 25 | } 26 | const secret = process.env.LEMONS_SQUEEZY_SIGNATURE_SECRET as string; 27 | const hmac = crypto.createHmac("sha256", secret); 28 | const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8"); 29 | const signature = Buffer.from( 30 | Array.isArray(sigString) ? sigString.join("") : sigString || "", 31 | "utf8" 32 | ); 33 | // validate signature 34 | if (!crypto.timingSafeEqual(digest, signature)) { 35 | return NextResponse.json({ message: "Invalid signature" }, { status: 403 }); 36 | } 37 | 38 | const userId = payload.meta.custom_data && payload.meta.custom_data.userId || ''; 39 | // Check if custom defined data i.e. the `userId` is there or not 40 | if (!userId) { 41 | return NextResponse.json({ message: "No userId provided" }, { status: 403 }); 42 | } 43 | const user = await prisma.user.findUnique({ 44 | where: { userId: userId.toString() }, 45 | select: { userId: true, email: true, username: true }, 46 | }); 47 | if (!user) return NextResponse.json({ message: "Your account was not found" }, { status: 401 }); 48 | 49 | const first_order_item = payload.data.attributes.first_order_item || null 50 | 51 | // is one-off 52 | if (first_order_item && parseInt(first_order_item.variant_id, 10) === parseInt(process.env.LEMON_SQUEEZY_MEMBERSHIP_SINGLE_TIME_VARIANT_ID as string, 10)) { 53 | return await singlePayDeal(first_order_item, payload, userId) 54 | } 55 | // is subscription 56 | if (!first_order_item && parseInt(payload.data.attributes.variant_id, 10) === parseInt(process.env.LEMON_SQUEEZY_MEMBERSHIP_MONTHLY_VARIANT_ID as string, 10)) { 57 | return await subscriptionDeal(payload, userId) 58 | } 59 | } 60 | 61 | const singlePayDeal = async (first_order_item: any, payload: any, userId: string) => { 62 | try { 63 | // Check if the webhook event was for this product or not 64 | if ( 65 | parseInt(first_order_item.product_id, 10) !== 66 | parseInt(process.env.LEMON_SQUEEZY_PRODUCT_ID as string, 10) 67 | ) { 68 | return NextResponse.json({ message: "Invalid product" }, { status: 403 }); 69 | } 70 | 71 | switch (payload.meta.event_name) { 72 | case "order_created": { 73 | const subscription = await client.retrieveOrder({ id: payload.data.id }); 74 | /** 75 | * Lemon Squeezy 可能推送多次,这里需要判断order是否已存在,相同order仅处理首次收到的推送 76 | * 检查redis里有没有存这个order_id,如果没有,则调用boostPack和redis保存,如果有,则不处理,直接返回200 77 | * 78 | * Lemon Squeezy might push multiple times; here we need to determine if the order already exists. The same order should only be processed on the first received push. 79 | * Check if this order_id is stored in Redis. If not, call boostPack and save it in Redis. If it is, do not process, return 200 directly. 80 | */ 81 | const key = await getSinglePayOrderKey({ identifier: payload.data.attributes.identifier }) 82 | const orderRedisRes = await redis.get(key) 83 | console.log('orderRedisRes', orderRedisRes); 84 | if (!orderRedisRes) { 85 | await redis.setex(key, ONE_DAY, first_order_item.created_at) 86 | await boostPack({ userId }) 87 | } 88 | return NextResponse.json({ status: 200 }); 89 | } 90 | 91 | default: { 92 | return NextResponse.json({ message: 'event_name not support' }, { status: 400 }); 93 | } 94 | } 95 | } catch (e) { 96 | console.log('single pay deal', e); 97 | return NextResponse.json({ message: 'single pay something wrong' }, { status: 500 }); 98 | } 99 | } 100 | const subscriptionDeal = async (payload: any, userId: string) => { 101 | try { 102 | const attributes = payload.data.attributes 103 | // Check if the webhook event was for this product or not 104 | if ( 105 | parseInt(attributes.product_id, 10) !== 106 | parseInt(process.env.LEMON_SQUEEZY_PRODUCT_ID as string, 10) 107 | ) { 108 | return NextResponse.json({ message: "Invalid product" }, { status: 403 }); 109 | } 110 | 111 | switch (payload.meta.event_name) { 112 | case "subscription_created": { 113 | const subscription = await client.retrieveSubscription({ id: payload.data.id }); 114 | // 订阅 subscription 115 | await prisma.user.update({ 116 | where: { userId }, 117 | data: { 118 | subscriptionId: `${subscription.data.id}`, 119 | customerId: `${payload.data.attributes.customer_id}`, 120 | variantId: subscription.data.attributes.variant_id, 121 | currentPeriodEnd: dayjs(subscription.data.attributes.renews_at).unix(), 122 | }, 123 | }); 124 | // 重置今天的积分 125 | // Reset today's points 126 | clearTodayUsage({ userId }) 127 | return NextResponse.json({ status: 200 }); 128 | } 129 | 130 | case "subscription_updated": { 131 | const subscription = await client.retrieveSubscription({ id: payload.data.id }); 132 | // 订阅 subscription 133 | const user = await prisma.user.findUnique({ 134 | where: { userId, subscriptionId: `${subscription.data.id}` }, 135 | select: { subscriptionId: true }, 136 | }); 137 | if (!user || !user.subscriptionId) return NextResponse.json({ message: 'userId or subscriptionId not found' }, { status: 400 });; 138 | 139 | await prisma.user.update({ 140 | where: { userId, subscriptionId: user.subscriptionId }, 141 | data: { 142 | variantId: subscription.data.attributes.variant_id, 143 | currentPeriodEnd: dayjs(subscription.data.attributes.renews_at).unix(), 144 | }, 145 | }); 146 | // 重置今天的积分 147 | // Reset today's points 148 | clearTodayUsage({ userId }) 149 | return NextResponse.json({ status: 200 }); 150 | } 151 | 152 | default: { 153 | return NextResponse.json({ message: 'event_name not support' }, { status: 400 }); 154 | } 155 | } 156 | } catch (e) { 157 | console.log('subscription deal', e); 158 | return NextResponse.json({ message: 'subscription something wrong' }, { status: 500 }); 159 | } 160 | } -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef98 | 163 |99 | 105 |Subscription Plan 100 |101 | You are currently on the {subscriptionPlan.name}{" "} 102 | plan. 103 | 104 |{subscriptionPlan.description} 106 |107 | 162 |108 | {subscriptionPlan.isPro ? ( 109 | 115 | ) : ( 116 | 122 | )} 123 | {subscriptionPlan.isCanceled ? ( 124 | <>> 125 | ) : ( 126 |150 | {subscriptionPlan.isPro ? ( 151 |127 | 148 | )} 149 |128 | 129 | 130 |131 | 147 |132 | 140 |133 | Are you absolutely sure? 134 | 135 |136 | After unsubscribing, you will lose your current privileges 137 | once your current subscription expires. 138 | 139 |141 | 146 |Close Dialog 142 |143 | Unsubscribe 144 | 145 |152 | {subscriptionPlan.isCanceled 153 | ? "Your plan will be canceled on " 154 | : "Your plan renews on "} 155 | {dayjs(subscriptionPlan.membershipExpire).format( 156 | "YYYY-MM-DD HH:mm" 157 | )} 158 | . 159 |
160 | ) : null} 161 |168 | , 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef38 | , 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef , 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef73 | , 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef , 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef110 | 112 | 113 | {children} 114 |111 | , 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef133 | 135 | 136 | {children} 137 |134 | , 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef , 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes ) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /app/(home)/homePage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import DropDown, { LanguageType } from "@/components/DropDown"; 4 | import Github from "@/components/icons/GitHub"; 5 | import Twitter from "@/components/icons/Twitter"; 6 | import Subscribe from "@/components/subscribe/Subscribe"; 7 | import { siteConfig } from "@/config/site"; 8 | import { formatNumber } from "@/lib/data"; 9 | import { UserInfo } from "@/types/user"; 10 | import { useCompletion } from "ai/react"; 11 | import dayjs from "dayjs"; 12 | import Image from "next/image"; 13 | import Link from "next/link"; 14 | import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; 15 | import { Toaster, toast } from "react-hot-toast"; 16 | 17 | interface HomePageProps { 18 | usage: number; 19 | user: UserInfo | null; 20 | remaining: number; 21 | boostPackRemaining: number; 22 | membershipExpire: number; 23 | boostPackExpire: number; 24 | } 25 | 26 | export default function HomePage({ 27 | usage, 28 | user, 29 | remaining, 30 | boostPackRemaining, 31 | membershipExpire, 32 | boostPackExpire, 33 | }: HomePageProps) { 34 | const [currentUses, setCurrentUses] = useState(0); 35 | const [remainingCredits, setRemainingCredits] = useState(0); 36 | const [boostPackRemainingCredits, setBoostPackRemainingCredits] = useState(0); 37 | const [content, setContent] = useState(""); 38 | const [language, setLanguage] = useState ("English"); 39 | const answerRef = useRef (null); 40 | 41 | const scrollToAnswer = () => { 42 | if (answerRef.current !== null) { 43 | answerRef.current.scrollIntoView({ behavior: "smooth" }); 44 | } 45 | }; 46 | 47 | const { complete, completion, isLoading, handleSubmit } = useCompletion({ 48 | api: "/api/completion", 49 | body: { 50 | language, 51 | prompt: content, 52 | }, 53 | headers: { 54 | token: user?.accessToken || "", 55 | }, 56 | onResponse: (res) => { 57 | if (res.status === 429) { 58 | toast.error("You are being rate limited. Please try again later."); 59 | return; 60 | } 61 | setCurrentUses((pre) => pre + 1); 62 | scrollToAnswer(); 63 | }, 64 | }); 65 | 66 | const handleInputChange = useCallback( 67 | (e: React.ChangeEvent ) => setContent(e.target.value), 68 | [] 69 | ); 70 | 71 | const onSubmit = (e: FormEvent ) => { 72 | complete(content); 73 | handleSubmit(e); 74 | }; 75 | 76 | const answer = completion; 77 | 78 | useEffect(() => { 79 | if (currentUses <= remaining) { 80 | setRemainingCredits(remaining - currentUses); 81 | setBoostPackRemainingCredits(boostPackRemaining); 82 | } else { 83 | setBoostPackRemainingCredits( 84 | boostPackRemaining - (currentUses - remaining) 85 | ); 86 | } 87 | }, [remaining, boostPackRemaining, currentUses]); 88 | 89 | return ( 90 | <> 91 | 95 | 101 |114 |102 | Follow Me
103 | 104 | 110 |111 | Star on GitHub
112 | 113 |115 | {siteConfig.description} 116 |
117 | 118 |119 | {formatNumber({ value: Number(usage) + currentUses })} Excel formulas 120 | generated so far. 121 |
122 | 223 |228 |
229 | 256 | 257 | {/* subscribe */} 258 |259 | > 260 | ); 261 | } 262 | -------------------------------------------------------------------------------- /components/social-icons/icons.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | 3 | // Icons taken from: https://simpleicons.org/ 4 | // To add a new icon, add a new function here and add it to components in social-icons/index.tsx 5 | 6 | export function Facebook(svgProps: SVGProps ) { 7 | return ( 8 | 11 | ) 12 | } 13 | 14 | export function Github(svgProps: SVGProps ) { 15 | return ( 16 | 19 | ) 20 | } 21 | 22 | export function Linkedin(svgProps: SVGProps ) { 23 | return ( 24 | 27 | ) 28 | } 29 | 30 | export function Mail(svgProps: SVGProps ) { 31 | return ( 32 | 36 | ) 37 | } 38 | 39 | export function Twitter(svgProps: SVGProps ) { 40 | return ( 41 | 44 | ) 45 | } 46 | 47 | export function TwitterX(svgProps: SVGProps ) { 48 | return ( 49 | 55 | ) 56 | } 57 | 58 | export function Youtube(svgProps: SVGProps ) { 59 | return ( 60 | 63 | ) 64 | } 65 | 66 | export function Mastodon(svgProps: SVGProps ) { 67 | return ( 68 | 71 | ) 72 | } 73 | 74 | export function Threads(svgProps: SVGProps ) { 75 | return ( 76 | 79 | ) 80 | } 81 | 82 | export function Instagram(svgProps: SVGProps ) { 83 | return ( 84 | 87 | ) 88 | } 89 | 90 | export function WeChat(svgProps: SVGProps ) { 91 | return ( 92 | 102 | ) 103 | } 104 | 105 | export function JueJin(svgProps: SVGProps ) { 106 | return ( 107 | 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /content/about/privacy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Privacy 3 | description: The Privacy Policy for Taxonomy App. 4 | --- 5 | 6 | Last updated: November 03, 2023 7 | 8 | This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You. 9 | 10 | We use Your Personal data to provide and improve the Service. By using the Servic, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the [Privacy Policy Generator](https://www.privacypolicies.com/privacy-policy-generator/). 11 | 12 | ## Interpretation and Definitions 13 | 14 | ### Interpretation 15 | 16 | The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural. 17 | 18 | ### Definitions 19 | 20 | For the purposes of this Privacy Policy: 21 | 22 | - **Account** means a unique account created for You to access our Service or parts of our Service. 23 | - **Affiliate** means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority. 24 | 25 | - **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Smart Excel. 26 | 27 | - **Cookies** are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses. 28 | 29 | - **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet. 30 | 31 | - **Personal Data** is any information that relates to an identified or identifiable individual. 32 | 33 | - **Service** refers to the Website. 34 | 35 | - **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used. 36 | 37 | - **Third-party Social Media Service** refers to any website or any social network website through which a User can log in or create an account to use the Service. 38 | 39 | - **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit). 40 | 41 | - **Website** refers to Smart Excel, accessible from [https://smartexcel.cc](https://smartexcel.cc) 42 | 43 | - **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable. 44 | 45 | ## Collecting and Using Your Personal Data 46 | 47 | ### Types of Data Collected 48 | 49 | #### Personal Data 50 | 51 | While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to: 52 | 53 | - Email address 54 | 55 | - Usage Data 56 | 57 | #### Usage Data 58 | 59 | Usage Data is collected automatically when using the Service. 60 | 61 | Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data. 62 | 63 | When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data. 64 | 65 | We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device. 66 | 67 | #### Information from Third-Party Social Media Services 68 | 69 | The Company allows You to create an account and log in to use the Service through the following Third-party Social Media Services: 70 | 71 | - Google 72 | - Github 73 | 74 | If You decide to register through or otherwise grant us access to a Third-Party Social Media Service, We may collect Personal data that is already associated with Your Third-Party Social Media Service's account, such as Your name, Your email address, Your activities or Your contact list associated with that account. 75 | 76 | You may also have the option of sharing additional information with the Company through Your Third-Party Social Media Service's account. If You choose to provide such information and Personal Data, during registration or otherwise, You are giving the Company permission to use, share, and store it in a manner consistent with this Privacy Policy. 77 | 78 | #### Tracking Technologies and Cookies 79 | 80 | We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include: 81 | 82 | - **Cookies or Browser Cookies.** A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies. 83 | - **Web Beacons.** Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity). 84 | 85 | Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser. Learn more about cookies on the [Privacy Policies website](https://www.privacypolicies.com/blog/privacy-policy-template/#Use_Of_Cookies_Log_Files_And_Tracking) article. 86 | 87 | We use both Session and Persistent Cookies for the purposes set out below: 88 | 89 | - **Necessary / Essential Cookies** 90 | 91 | Type: Session Cookies 92 | 93 | Administered by: Us 94 | 95 | Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services. 96 | 97 | - **Cookies Policy / Notice Acceptance Cookies** 98 | 99 | Type: Persistent Cookies 100 | 101 | Administered by: Us 102 | 103 | Purpose: These Cookies identify if users have accepted the use of cookies on the Website. 104 | 105 | - **Functionality Cookies** 106 | 107 | Type: Persistent Cookies 108 | 109 | Administered by: Us 110 | 111 | Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website. 112 | 113 | For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy. 114 | 115 | ### Use of Your Personal Data 116 | 117 | The Company may use Personal Data for the following purposes: 118 | 119 | - **To provide and maintain our Service**, including to monitor the usage of our Service. 120 | - **To manage Your Account:** to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user. 121 | - **For the performance of a contract:** the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service. 122 | - **To contact You:** To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation. 123 | - **To provide You** with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information. 124 | - **To manage Your requests:** To attend and manage Your requests to Us. 125 | 126 | - **For business transfers:** We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred. 127 | - **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience. 128 | 129 | We may share Your personal information in the following situations: 130 | 131 | - **With Service Providers:** We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You. 132 | - **For business transfers:** We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company. 133 | - **With Affiliates:** We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us. 134 | - **With business partners:** We may share Your information with Our business partners to offer You certain products, services or promotions. 135 | - **With other users:** when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see Your name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile. 136 | - **With Your consent**: We may disclose Your personal information for any other purpose with Your consent. 137 | 138 | ### Retention of Your Personal Data 139 | 140 | The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies. 141 | 142 | The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods. 143 | 144 | ### Transfer of Your Personal Data 145 | 146 | Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction. 147 | 148 | Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer. 149 | 150 | The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information. 151 | 152 | ### Delete Your Personal Data 153 | 154 | You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You. 155 | 156 | Our Service may give You the ability to delete certain information about You from within the Service. 157 | 158 | You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us. 159 | 160 | Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so. 161 | 162 | ### Disclosure of Your Personal Data 163 | 164 | #### Business Transactions 165 | 166 | If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy. 167 | 168 | #### Law enforcement 169 | 170 | Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency). 171 | 172 | #### Other legal requirements 173 | 174 | The Company may disclose Your Personal Data in the good faith belief that such action is necessary to: 175 | 176 | - Comply with a legal obligation 177 | - Protect and defend the rights or property of the Company 178 | - Prevent or investigate possible wrongdoing in connection with the Service 179 | - Protect the personal safety of Users of the Service or the public 180 | - Protect against legal liability 181 | 182 | ### Security of Your Personal Data 183 | 184 | The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security. 185 | 186 | ## Children's Privacy 187 | 188 | Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers. 189 | 190 | If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information. 191 | 192 | ## Links to Other Websites 193 | 194 | Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit. 195 | 196 | We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. 197 | 198 | ## Changes to this Privacy Policy 199 | 200 | We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page. 201 | 202 | We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy. 203 | 204 | You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. 205 | 206 | ## Contact Us 207 | 208 | If you have any questions about this Privacy Policy, You can contact us: 209 | 210 | - By email: weijunext@gmail.com 211 | --------------------------------------------------------------------------------