From e3aac03e2835bdeedb2400f038ccff1287bdefb7 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 23:48:12 +0800 Subject: [PATCH] feat: add 1 month, 3 month purchase --- process.d.ts | 6 +- src/components/AccountView.tsx | 6 +- src/components/CreateConnectionModal.tsx | 10 ++++ src/components/PricingView.tsx | 72 ++++++++++++++++++------ src/locales/en.json | 6 +- src/locales/es.json | 6 +- src/locales/zh.json | 6 +- src/pages/api/checkout_sessions/index.ts | 5 +- src/pages/api/webhook/index.ts | 16 +++++- src/utils/index.ts | 1 + src/utils/plan.ts | 22 ++++++++ 11 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 src/utils/plan.ts diff --git a/process.d.ts b/process.d.ts index d45bbba..c0a5366 100644 --- a/process.d.ts +++ b/process.d.ts @@ -30,7 +30,11 @@ declare namespace NodeJS { STRIPE_API_KEY: string; // Optional. Stripe webhook secret. STRIPE_WEBHOOK_SECRET: string; + // Optional. Stripe price id for Pro plan 1 month subscription. + NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_1_MONTH_SUBSCRIPTION: string; + // Optional. Stripe price id for Pro plan 3 month subscription. + NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_3_MONTH_SUBSCRIPTION: string; // Optional. Stripe price id for Pro plan 1 year subscription. - STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION: string; + NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION: string; } } diff --git a/src/components/AccountView.tsx b/src/components/AccountView.tsx index dca1abd..e65946e 100644 --- a/src/components/AccountView.tsx +++ b/src/components/AccountView.tsx @@ -48,9 +48,9 @@ const AccountView = () => { {t(`setting.plan.${session.user.subscription.plan.toLowerCase()}`)} diff --git a/src/components/CreateConnectionModal.tsx b/src/components/CreateConnectionModal.tsx index 5ccb73e..d8e8288 100644 --- a/src/components/CreateConnectionModal.tsx +++ b/src/components/CreateConnectionModal.tsx @@ -59,6 +59,16 @@ const engines = [ name: "TiDB Serverless", defaultPort: "4000", }, + { + type: Engine.Snowflake, + name: "Snowflake", + defaultPort: "443", + }, + { + type: Engine.Hive, + name: "Hive", + defaultPort: "4000", + }, ]; const defaultConnection: Connection = { diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index f41b1ea..c63ba4f 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -4,9 +4,11 @@ import { useTranslation } from "react-i18next"; import getStripe from "../utils/get-stripejs"; import { fetchPostJSON } from "../utils/api-helpers"; -const checkout = async () => { +const checkout = async (priceId: string) => { // Create a Checkout Session. - const response = await fetchPostJSON("/api/checkout_sessions", {}); + const response = await fetchPostJSON("/api/checkout_sessions", { + price: priceId, + }); if (response.statusCode === 500) { console.error(response.message); @@ -31,24 +33,62 @@ const PricingView = () => { const { t } = useTranslation(); const { data: session, status } = useSession(); + const tiers = [ + { + name: t("setting.plan.1-month"), + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_1_MONTH_SUBSCRIPTION, + priceMonthly: "$5", + buyButton: t("setting.plan.purhcase-1-month"), + }, + { + name: t("setting.plan.n-months", { + count: 3, + }), + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_3_MONTH_SUBSCRIPTION, + priceMonthly: "$15", + buyButton: t("setting.plan.purhcase-n-months", { + count: 3, + }), + }, + { + name: t("setting.plan.n-months", { + count: 12, + }), + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION, + priceMonthly: "$50", + buyButton: t("setting.plan.purhcase-n-months", { + count: 12, + }), + }, + ]; + return ( -
- - {"🎈 "} {t(`setting.plan.pro`)} - -
-

+
+
+ + {t(`setting.plan.pro-early-bird`)} + +

{t("setting.plan.n-question-per-month", { count: 1000, })} -

-
- +

+
+
+
+ {tiers.map((tier) => ( +
+

+ {tier.name} - {tier.priceMonthly} +

+ +
+ ))}
diff --git a/src/locales/en.json b/src/locales/en.json index e9e397a..3251d7f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -64,12 +64,16 @@ "guest": "Guest", "free": "Free", "pro": "Pro", + "pro-early-bird": "🐤 Early Bird - Pro 50% OFF", "signup-for-more": "Sign up for more", "upgrade": "Upgrade", "renew": "Renew", "expired": "Expired", "n-question-per-month": "{{count}} quota / month", - "early-bird-checkout": "Early bird discount, 50% off for 1 year" + "1-month": "1 month", + "n-months": "{{count}} months", + "purhcase-1-month": "Purchase 1 month", + "purhcase-n-months": "Purchase {{count}} months" }, "subscription": { "self": "Subscription", diff --git a/src/locales/es.json b/src/locales/es.json index 7e1970e..2754f30 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -62,12 +62,16 @@ "guest": "Invitada", "free": "Gratis", "pro": "Pro", + "pro-early-bird": "🐤 Early Bird - Pro 50% de descuento", "signup-for-more": "Regístrese para obtener más", "upgrade": "Mejora", "renew": "Renovar", "expired": "Expirado", "n-question-per-month": "{{count}} Cuota / mes", - "early-bird-checkout": "Descuento por reserva anticipada, 50 % de descuento durante 1 año" + "1-month": "1 mes", + "n-months": "{{count}} meses", + "purhcase-1-month": "Comprar 1 mes", + "purhcase-n-months": "Comprar {{count}} meses" }, "subscription": { "self": "Suscripción", diff --git a/src/locales/zh.json b/src/locales/zh.json index 033c85c..d1c1b3d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -64,12 +64,16 @@ "guest": "访客", "free": "免费版", "pro": "专业版", + "pro-early-bird": "🐤 早鸟优惠 - 专业版 5 折", "signup-for-more": "注册获得额度", "upgrade": "升级", "renew": "续费", "expired": "已过期", "n-question-per-month": "{{count}} 点额度 / 月", - "early-bird-checkout": "早鸟优惠,5 折购买 1 年" + "1-month": "1 个月", + "n-months": "{{count}} 个月", + "purhcase-1-month": "购买 1 个月", + "purhcase-n-months": "购买 {{count}} 个月" }, "subscription": { "self": "订阅", diff --git a/src/pages/api/checkout_sessions/index.ts b/src/pages/api/checkout_sessions/index.ts index c4920b3..6b0fa75 100644 --- a/src/pages/api/checkout_sessions/index.ts +++ b/src/pages/api/checkout_sessions/index.ts @@ -20,7 +20,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } if (req.method === "POST") { - const session = await getServerSession(req, res, authOptions); const email = session?.user?.email!; const user = await prisma.user.findUniqueOrThrow({ where: { email: email }, @@ -33,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) payment_method_types: ["affirm", "alipay", "card", "cashapp", "klarna", "link", "wechat_pay"], line_items: [ { - price: process.env.STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION, + price: req.body.price, quantity: 1, }, ], @@ -48,7 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) metadata: { email: session?.user?.email!, plan: "PRO", - description: "Pro 1 Year (Early Bird)", + price: req.body.price, }, }, // Link customer if present otherwise pass email and let Stripe create a new customer. diff --git a/src/pages/api/webhook/index.ts b/src/pages/api/webhook/index.ts index 8190c5a..dbaf7aa 100644 --- a/src/pages/api/webhook/index.ts +++ b/src/pages/api/webhook/index.ts @@ -4,6 +4,7 @@ import Cors from "micro-cors"; import { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; import { getSubscriptionByEmail } from "../utils/subscription"; +import { getPlanFromPriceId } from "@/utils"; const stripe = new Stripe(process.env.STRIPE_API_KEY, { // https://github.com/stripe/stripe-node#configuration @@ -47,6 +48,15 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { const paymentIntent = event.data.object as Stripe.PaymentIntent; const charge = await stripe.charges.retrieve(paymentIntent.latest_charge as string); + let plan; + try { + plan = getPlanFromPriceId(paymentIntent.metadata.price); + } catch (err) { + console.log(err); + res.status(400).send(`Invalid price id: ${paymentIntent.metadata.price}`); + return; + } + const customerId = paymentIntent.customer as string; if (customerId) { // Save the stripe customer id so that we can relate this customer to future payments. @@ -70,7 +80,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { createdAt: new Date(paymentIntent.created * 1000), paymentId: paymentIntent.id, customerId: customerId || "", - description: paymentIntent.metadata.description, + description: plan.description, amount: paymentIntent.amount, currency: paymentIntent.currency, receipt: charge.receipt_url as string, @@ -82,7 +92,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { if (currentSubscription.plan === "FREE" || currentSubscription.canceledAt || currentSubscription.expireAt < new Date().getTime()) { const today = new Date(new Date().setHours(0, 0, 0, 0)); // Subtract 1 second from the year from now to make it 23:59:59 - const yearFromNow = new Date(new Date(new Date().setHours(0, 0, 0, 0)).setFullYear(today.getFullYear() + 1) - 1000); + const yearFromNow = new Date(new Date(new Date().setHours(0, 0, 0, 0)).setMonth(today.getMonth() + plan.month) - 1000); const subscription: Prisma.SubscriptionUncheckedCreateInput = { userId: user.id, email: paymentIntent.metadata.email, @@ -95,7 +105,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { } else { // Extend the current subscription if there is an active paid subscription. const expireAt = new Date(Math.max(currentSubscription.expireAt, new Date().getTime())); - expireAt.setFullYear(expireAt.getFullYear() + 1); + expireAt.setMonth(expireAt.getMonth() + plan.month); await prisma.subscription.update({ where: { id: currentSubscription.id }, data: { diff --git a/src/utils/index.ts b/src/utils/index.ts index c2ea814..5886fcb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,3 +6,4 @@ export * from "./model"; export * from "./feature"; export * from "./tidb"; export * from "./misc"; +export * from "./plan"; diff --git a/src/utils/plan.ts b/src/utils/plan.ts new file mode 100644 index 0000000..8353677 --- /dev/null +++ b/src/utils/plan.ts @@ -0,0 +1,22 @@ +export const getPlanFromPriceId = (priceId: string) => { + switch (priceId) { + case process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_1_MONTH_SUBSCRIPTION: + return { + month: 1, + description: "Pro 1 Month (Early Bird)", + }; + case process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_3_MONTH_SUBSCRIPTION: + return { + month: 1, + description: "Pro 3 Months (Early Bird)", + }; + case process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION: + return { + month: 12, + description: "Pro 1 Year (Early Bird)", + }; + default: { + throw Error(`Invalid price ID ${priceId}`); + } + } +};