From aa5399fd74164d45de529240a2ae024d7f13d4f7 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 27 May 2024 14:47:50 +0800 Subject: [PATCH 001/134] Fix File list query --- hooks/file/useFetchFileList.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/file/useFetchFileList.ts b/hooks/file/useFetchFileList.ts index 46dba61af4..5355e8ebcd 100644 --- a/hooks/file/useFetchFileList.ts +++ b/hooks/file/useFetchFileList.ts @@ -9,7 +9,7 @@ import HomeContext from '@/components/home/home.context'; export const useFetchFileList = () => { const supabase = useSupabaseClient(); const { - state: { user, featureFlags }, + state: { user, isUltraUser }, } = useContext(HomeContext); const fetchFileList = async () => { if (!user) { @@ -32,7 +32,7 @@ export const useFetchFileList = () => { return useQuery(['gcp-files', user?.id], fetchFileList, { keepPreviousData: true, staleTime: 1000 * 60 * 5, // 5 minutes - enabled: !!user && !!featureFlags['enable-chat-with-doc'], + enabled: !!user && isUltraUser, onError: (error) => { console.error('There was a problem with your fetch operation:', error); }, From 4c04384705f3acc86c15983bc88fbed751887d49 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 27 May 2024 16:18:04 +0800 Subject: [PATCH 002/134] chore: Update subscription plan payment links and add new plans --- utils/app/const.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/utils/app/const.ts b/utils/app/const.ts index 8231b195cb..fe8acc6d41 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -1,4 +1,5 @@ import { OpenAIModels, fallbackModelID } from '@/types/openai'; +import { SubscriptionPlan } from '@/types/user'; import dayjs from 'dayjs'; import { v4 as uuidv4 } from 'uuid'; @@ -100,3 +101,21 @@ export const newDefaultConversation = { folderId: null, lastUpdateAtUTC: dayjs().valueOf(), }; + +export const OrderedSubscriptionPlans: SubscriptionPlan[] = [ + 'free', + 'pro', + 'ultra', + 'edu', +]; + +export const ProPlanPaymentLink = + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' + : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; + +// TODO: Update the link +export const UltraPlanPaymentLink = + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' + : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; From 62343895b9e4dff5c2a7495de3f10d10fd6f04a9 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 27 May 2024 16:18:21 +0800 Subject: [PATCH 003/134] chore: Enable chat with document feature --- utils/app/ui.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/app/ui.tsx b/utils/app/ui.tsx index 07ae4254ef..84b0260dda 100644 --- a/utils/app/ui.tsx +++ b/utils/app/ui.tsx @@ -39,7 +39,7 @@ export const PlanDetail = { 'LINE connection', 'Unlimited GPT-4', 'Unlimited MidJourney generation', - 'Chat with document (coming soon)', + 'Chat with document', ], }, combinedSimplify: [ From a2734e829b5e77f7091637655c66a8573eefb23f Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 27 May 2024 16:18:34 +0800 Subject: [PATCH 004/134] chore: Update SettingsModel dialog panel width for better responsiveness --- components/User/Settings/SettingsModel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/User/Settings/SettingsModel.tsx b/components/User/Settings/SettingsModel.tsx index 8de6af3b57..80972dc450 100644 --- a/components/User/Settings/SettingsModel.tsx +++ b/components/User/Settings/SettingsModel.tsx @@ -67,7 +67,7 @@ export default function SettingsModel({ onClose }: Props) { leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - + Date: Mon, 27 May 2024 17:40:11 +0800 Subject: [PATCH 005/134] chore: Add PlanComparison component to Settings_Account --- components/User/Settings/PlanComparison.tsx | 189 ++++++++++++++++++ components/User/Settings/Settings_Account.tsx | 107 +--------- 2 files changed, 191 insertions(+), 105 deletions(-) create mode 100644 components/User/Settings/PlanComparison.tsx diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx new file mode 100644 index 0000000000..97a5408cb8 --- /dev/null +++ b/components/User/Settings/PlanComparison.tsx @@ -0,0 +1,189 @@ +import { useMemo } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; + +import { + OrderedSubscriptionPlans, + ProPlanPaymentLink, + UltraPlanPaymentLink, +} from '@/utils/app/const'; +import { trackEvent } from '@/utils/app/eventTracking'; +import { FeatureItem, PlanDetail } from '@/utils/app/ui'; + +import { SubscriptionPlan, User } from '@/types/user'; + +import dayjs from 'dayjs'; + +const PlanComparison = ({ + user, + isPaidUser, +}: { + user: User | null; + isPaidUser: boolean; +}) => { + const { t } = useTranslation('model'); + + const upgradeLinkOnClick = (upgradePlan: 'pro' | 'ultra') => { + let paymentLink = ''; + if (upgradePlan === 'pro') { + paymentLink = ProPlanPaymentLink; + } else if (upgradePlan === 'ultra') { + paymentLink = UltraPlanPaymentLink; + } + + const userEmail = user?.email; + const userId = user?.id; + + trackEvent('Upgrade button clicked'); + + if (!user) { + toast.error('Please sign-up before upgrading to pro plan'); + } else { + window.open( + `${paymentLink}?prefilled_email=${userEmail}&client_reference_id=${userId}`, + '_blank', + ); + } + }; + + const showUpgradeToPro = useMemo(() => { + if (!user) return true; + const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); + const proPlanIndex = OrderedSubscriptionPlans.indexOf('pro'); + return userPlanIndex < proPlanIndex; + }, [user]); + + const showUpgradeToUltra = useMemo(() => { + if (!user) return true; + const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); + const ultraPlanIndex = OrderedSubscriptionPlans.indexOf('ultra'); + return userPlanIndex < ultraPlanIndex; + }, [user]); + + return ( +
+ {/* Free Plan */} +
+ Free +
+ {PlanDetail.free.features.map((feature, index) => ( + + ))} +
+
+ + {/* Pro Plan */} +
+ + + {/* Upgrade button */} + {showUpgradeToPro && ( + + )} + + {(user?.plan === 'pro' || user?.plan === 'ultra') && + user.proPlanExpirationDate && ( + + )} +
+ + {/* Ultra Plan */} +
+ + + {/* Upgrade button */} + {showUpgradeToUltra && ( + + )} + + {(user?.plan === 'pro' || user?.plan === 'ultra') && + user.proPlanExpirationDate && ( + + )} +
+
+ ); +}; + +export default PlanComparison; + +const PlanExpirationDate: React.FC<{ expirationDate: string }> = ({ + expirationDate, +}) => { + console.log({ + expirationDate, + }); + const { t } = useTranslation('model'); + return ( +
+ {`${t('Expires on')}: ${dayjs(expirationDate).format('ll')}`} +
+ ); +}; + +const ProPlanContent = () => { + const { t } = useTranslation('model'); + return ( + <> + Pro + {t('USD$9.99 / month')} +
+ + + {PlanDetail.pro.features.map((feature, index) => ( + + ))} +
+ + ); +}; + +const UltraPlanContent = () => { + const { t } = useTranslation('model'); + return ( + <> + + Ultra + +
+ + + {PlanDetail.ultra.features.map((feature, index) => ( + + ))} +
+ + ); +}; diff --git a/components/User/Settings/Settings_Account.tsx b/components/User/Settings/Settings_Account.tsx index 35cbc91642..439f0fe85b 100644 --- a/components/User/Settings/Settings_Account.tsx +++ b/components/User/Settings/Settings_Account.tsx @@ -10,6 +10,7 @@ import HomeContext from '@/components/home/home.context'; import { ReferralCodeEnter } from '../ReferralCodeEnter'; import { LineConnectionButton } from './LineConnectionButton'; +import PlanComparison from './PlanComparison'; import dayjs from 'dayjs'; import LocalizedFormat from 'dayjs/plugin/localizedFormat'; @@ -30,26 +31,6 @@ export default function Settings_Account() { dispatch({ field: 'showLoginSignUpModel', value: true }); }; - const upgradeLinkOnClick = () => { - const paymentLink = - process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' - : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; - const userEmail = user?.email; - const userId = user?.id; - - trackEvent('Upgrade button clicked'); - - if (!user) { - toast.error('Please sign-up before upgrading to pro plan'); - } else { - window.open( - `${paymentLink}?prefilled_email=${userEmail}&client_reference_id=${userId}`, - '_blank', - ); - } - }; - const subscriptionManagementLink = () => process.env.NEXT_PUBLIC_ENV === 'production' ? 'https://billing.stripe.com/p/login/5kAbMj0wt5VF6AwaEE' @@ -81,49 +62,8 @@ export default function Settings_Account() {

)} -
-
- Free -
- {PlanDetail.free.features.map((feature, index) => ( - - ))} -
-
-
- {user?.plan === 'ultra' ? ( - - ) : ( - - )} + {} - {(!user || !isPaidUser) && ( - - )} - - {(user?.plan === 'pro' || user?.plan === 'ultra') && - user.proPlanExpirationDate && ( -
- {`${t('Expires on')}: - ${dayjs(user.proPlanExpirationDate).format( - 'll', - )}`}{' '} -
- )} -
-
{displayReferralCodeEnterer && }
{isPaidUser && !user?.isInReferralTrial && ( @@ -196,46 +136,3 @@ export default function Settings_Account() {
); } - -const ProPlanContent = () => { - const { t } = useTranslation('model'); - return ( - <> - Pro - {t('USD$9.99 / month')} -
- - - {PlanDetail.pro.features.map((feature, index) => ( - - ))} -
- - ); -}; - -const UltraPlanContent = () => { - const { t } = useTranslation('model'); - return ( - <> - - Ultra - -
- - - {PlanDetail.ultra.features.map((feature, index) => ( - - ))} -
- - ); -}; From 717e7eb6d236ee3eafb5c7c817c2af01fb7acc71 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 27 May 2024 17:40:44 +0800 Subject: [PATCH 006/134] chore: Move payment links to const file --- components/User/UsageCreditModel.tsx | 18 ++++++------------ components/v2Chat/payment-dialog.tsx | 6 ++---- utils/app/const.ts | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/components/User/UsageCreditModel.tsx b/components/User/UsageCreditModel.tsx index 7f10cc7e7d..c2ab7b9d63 100644 --- a/components/User/UsageCreditModel.tsx +++ b/components/User/UsageCreditModel.tsx @@ -3,6 +3,10 @@ import { FC, Fragment, useContext, useEffect } from 'react'; import { useTranslation } from 'next-i18next'; +import { + AiImageCreditPurchaseLinks, + Gpt4CreditPurchaseLinks, +} from '@/utils/app/const'; import { DefaultMonthlyCredits } from '@/utils/config'; import { PluginID } from '@/types/plugin'; @@ -13,16 +17,6 @@ type Props = { onClose: () => void; }; -const gpt4CreditPurchaseLinks = { - '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', - '150': 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW', - '300': 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn', -}; -const aiImageCreditPurchaseLinks = { - '100': 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT', - '500': 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ', -}; - export const UsageCreditModel: FC = ({ onClose }) => { const { t } = useTranslation('model'); @@ -107,7 +101,7 @@ export const UsageCreditModel: FC = ({ onClose }) => { : gpt4Credit} - {Object.entries(gpt4CreditPurchaseLinks).map( + {Object.entries(Gpt4CreditPurchaseLinks).map( ([key, value]) => ( = ({ onClose }) => { : aiImageCredit} - {Object.entries(aiImageCreditPurchaseLinks).map( + {Object.entries(AiImageCreditPurchaseLinks).map( ([key, value]) => ( { - const paymentLink = - process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' - : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU'; + const paymentLink = V2ChatUpgradeLink; trackEvent('v2 Payment link clicked'); diff --git a/utils/app/const.ts b/utils/app/const.ts index fe8acc6d41..b54e48d471 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -119,3 +119,18 @@ export const UltraPlanPaymentLink = process.env.NEXT_PUBLIC_ENV === 'production' ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; + +export const Gpt4CreditPurchaseLinks = { + '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', + '150': 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW', + '300': 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn', +}; +export const AiImageCreditPurchaseLinks = { + '100': 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT', + '500': 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ', +}; + +export const V2ChatUpgradeLink = + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' + : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU'; From b67415eb2f6619bb6cb9748791f5f76979b9c513 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 12:11:08 +0800 Subject: [PATCH 007/134] Update the style of the Tier badge --- components/User/Settings/PlanComparison.tsx | 24 +++--- components/User/Settings/Settings_Account.tsx | 6 +- components/User/UserAccountBadge.tsx | 12 +-- styles/globals.css | 10 ++- tailwind.config.js | 73 ++++++++++++------- 5 files changed, 71 insertions(+), 54 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 97a5408cb8..ab37861a0f 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -10,17 +10,11 @@ import { import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; -import { SubscriptionPlan, User } from '@/types/user'; +import { User } from '@/types/user'; import dayjs from 'dayjs'; -const PlanComparison = ({ - user, - isPaidUser, -}: { - user: User | null; - isPaidUser: boolean; -}) => { +const PlanComparison = ({ user }: { user: User | null }) => { const { t } = useTranslation('model'); const upgradeLinkOnClick = (upgradePlan: 'pro' | 'ultra') => { @@ -149,7 +143,17 @@ const ProPlanContent = () => { const { t } = useTranslation('model'); return ( <> - Pro + + Pro + {t('USD$9.99 / month')}
@@ -167,7 +171,7 @@ const UltraPlanContent = () => { return ( <>
)} - {} + {} {displayReferralCodeEnterer && }
diff --git a/components/User/UserAccountBadge.tsx b/components/User/UserAccountBadge.tsx index fd7b6caacb..71156ab885 100644 --- a/components/User/UserAccountBadge.tsx +++ b/components/User/UserAccountBadge.tsx @@ -9,15 +9,7 @@ export default function UserAccountBadge() { if (user) { if (user.plan === 'ultra') { return ( - + Ultra ); @@ -25,7 +17,7 @@ export default function UserAccountBadge() { if (user.plan === 'pro') { return ( - + Pro ); diff --git a/styles/globals.css b/styles/globals.css index d329539cf3..01b5abca3f 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -24,7 +24,8 @@ height: 6px; } -html, body { +html, +body { overscroll-behavior-y: none; } @@ -160,3 +161,10 @@ body { } } } + +.text-clip-transparent { + color: transparent; + -webkit-background-clip: text; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: transparent; +} diff --git a/tailwind.config.js b/tailwind.config.js index c89d33fe2e..1b817f3b95 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -15,15 +15,13 @@ module.exports = { ...defaultTheme.screens, }, extend: { - screens:{ - mobile: - { - 'raw': '(max-width: 640px)', + screens: { + mobile: { + raw: '(max-width: 640px)', + }, + tablet: { + raw: '(max-width: 768px)', }, - tablet: - { - 'raw': '(max-width: 768px)', - } }, colors: { border: 'hsl(var(--border))', @@ -33,63 +31,71 @@ module.exports = { foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' + foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' + foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' + foreground: 'hsl(var(--destructive-foreground))', }, muted: { DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' + foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' + foreground: 'hsl(var(--accent-foreground))', }, popover: { DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' + foreground: 'hsl(var(--popover-foreground))', }, card: { DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - } + foreground: 'hsl(var(--card-foreground))', + }, }, borderRadius: { lg: `var(--radius)`, md: `calc(var(--radius) - 2px)`, - sm: 'calc(var(--radius) - 4px)' + sm: 'calc(var(--radius) - 4px)', }, keyframes: { 'accordion-down': { from: { height: 0 }, - to: { height: 'var(--radix-accordion-content-height)' } + to: { height: 'var(--radix-accordion-content-height)' }, }, 'accordion-up': { from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: 0 } + to: { height: 0 }, }, 'slide-from-left': { '0%': { - transform: 'translateX(-100%)' + transform: 'translateX(-100%)', }, '100%': { - transform: 'translateX(0)' - } + transform: 'translateX(0)', + }, }, 'slide-to-left': { '0%': { - transform: 'translateX(0)' + transform: 'translateX(0)', }, '100%': { - transform: 'translateX(-100%)' - } - } + transform: 'translateX(-100%)', + }, + }, + 'background-gradient-slide': { + '0%': { + backgroundPosition: '-20% 50%', + }, + '100%': { + backgroundPosition: '100% 50%', + }, + }, }, animation: { 'slide-from-left': @@ -97,8 +103,19 @@ module.exports = { 'slide-to-left': 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)', 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' - } + 'accordion-up': 'accordion-up 0.2s ease-out', + 'background-gradient-slide': + 'background-gradient-slide 6s linear infinite', + }, + backgroundSize: { + '500%': '500% 100%', + }, + backgroundImage: { + 'gradient-ultra': + 'linear-gradient(to right, #fd68a6, #6c62f7, #fd68a6)', + 'gradient-pro': + 'linear-gradient(to right, rgb(99 102 241), rgb(129 140 248), rgb(99 102 241))', + }, }, }, variants: { From bc3cbf55e79df30143f77d66986c182cf1495d23 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 14:38:51 +0800 Subject: [PATCH 008/134] refactor: Update PlanComparison component to display current plan and add CurrentTag --- components/User/Settings/PlanComparison.tsx | 93 +++++++++++++-------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index ab37861a0f..06c2b040e5 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -57,18 +57,13 @@ const PlanComparison = ({ user }: { user: User | null }) => { return (
{/* Free Plan */} -
- Free -
- {PlanDetail.free.features.map((feature, index) => ( - - ))} -
+
+
{/* Pro Plan */}
- + {/* Upgrade button */} {showUpgradeToPro && ( @@ -95,7 +90,7 @@ const PlanComparison = ({ user }: { user: User | null }) => { {/* Ultra Plan */}
- + {/* Upgrade button */} {showUpgradeToUltra && ( @@ -139,21 +134,40 @@ const PlanExpirationDate: React.FC<{ expirationDate: string }> = ({ ); }; -const ProPlanContent = () => { +const FreePlanContent = ({ user }: { user: User | null }) => { const { t } = useTranslation('model'); return ( <> - - Pro - +
+ Free + {(user?.plan === 'free' || !user) && } +
+
+ {PlanDetail.free.features.map((feature, index) => ( + + ))} +
+ + ); +}; +const ProPlanContent = ({ user }: { user: User | null }) => { + const { t } = useTranslation('model'); + return ( + <> +
+ + Pro + + {user?.plan === 'pro' && } +
{t('USD$9.99 / month')}
@@ -166,21 +180,26 @@ const ProPlanContent = () => { ); }; -const UltraPlanContent = () => { +const UltraPlanContent = ({ user }: { user: User | null }) => { const { t } = useTranslation('model'); return ( <> - - Ultra - +
+ + Ultra + + {user?.plan === 'ultra' && } +
+ {t('USD$19.99 / month')} +
@@ -191,3 +210,11 @@ const UltraPlanContent = () => { ); }; + +const CurrentTag = () => { + return ( + + CURRENT PLAN + + ); +}; From 032248818bddb9481ae22be457c0088110931aa3 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 15:20:38 +0800 Subject: [PATCH 009/134] chore: Update payment links and use CONST variable instead of env variable for Stripe plan code --- utils/app/const.ts | 13 +++++++--- .../stripe/handleCheckoutSessionCompleted.ts | 24 ++++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/utils/app/const.ts b/utils/app/const.ts index b54e48d471..7dc3f2420f 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -114,11 +114,10 @@ export const ProPlanPaymentLink = ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; -// TODO: Update the link export const UltraPlanPaymentLink = process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' - : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; + ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' // TODO: Update the link + : 'https://buy.stripe.com/test_00gcOhbrmgLwbkYdR0'; export const Gpt4CreditPurchaseLinks = { '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', @@ -134,3 +133,11 @@ export const V2ChatUpgradeLink = process.env.NEXT_PUBLIC_ENV === 'production' ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU'; + +// STRIPE +export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; +export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; +export const STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION = + 'monthly_pro_plan_subscription'; +export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = + 'one_time_pro_plan_for_1_month'; diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 7a616f7a14..17516709a7 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,3 +1,8 @@ +import { + STRIPE_PLAN_CODE_GPT4_CREDIT, + STRIPE_PLAN_CODE_IMAGE_CREDIT, + STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, +} from '@/utils/app/const'; import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { PluginID } from '@/types/plugin'; @@ -12,14 +17,6 @@ import updateUserAccount from './updateUserAccount'; import dayjs from 'dayjs'; import Stripe from 'stripe'; -const MONTHLY_PRO_PLAN_SUBSCRIPTION = - process.env.STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION; -const ONE_TIME_PRO_PLAN_FOR_1_MONTH = - process.env.STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH; - -const IMAGE_CREDIT = process.env.STRIPE_PLAN_CODE_IMAGE_CREDIT; -const GPT4_CREDIT = process.env.STRIPE_PLAN_CODE_GPT4_CREDIT; - export default async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, ): Promise { @@ -40,13 +37,17 @@ export default async function handleCheckoutSessionCompleted( } const isTopUpCreditRequest = - (planCode === IMAGE_CREDIT || planCode === GPT4_CREDIT) && credit; + (planCode === STRIPE_PLAN_CODE_IMAGE_CREDIT || + planCode === STRIPE_PLAN_CODE_GPT4_CREDIT) && + credit; // Handle TopUp Image Credit / GPT4 Credit if (isTopUpCreditRequest) { return await addCreditToUser( email, +credit, - planCode === IMAGE_CREDIT ? PluginID.IMAGE_GEN : PluginID.GPT4, + planCode === STRIPE_PLAN_CODE_IMAGE_CREDIT + ? PluginID.IMAGE_GEN + : PluginID.GPT4, ); } @@ -119,7 +120,8 @@ async function getProPlanExpirationDate( return dayjs(sinceDate).add(+planGivingWeeks, 'week').toDate(); } } else if ( - planCode?.toUpperCase() === ONE_TIME_PRO_PLAN_FOR_1_MONTH?.toUpperCase() + planCode?.toUpperCase() === + STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH.toUpperCase() ) { // Only store expiration for one month plan return dayjs(sinceDate).add(1, 'month').toDate(); From be82c271afb569362c41f584efab697e42bf02d0 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 16:09:40 +0800 Subject: [PATCH 010/134] chore: Update const file --- utils/app/const.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utils/app/const.ts b/utils/app/const.ts index 7dc3f2420f..c3e3c23fa3 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -116,7 +116,7 @@ export const ProPlanPaymentLink = export const UltraPlanPaymentLink = process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' // TODO: Update the link + ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' // TODO: Update the production link : 'https://buy.stripe.com/test_00gcOhbrmgLwbkYdR0'; export const Gpt4CreditPurchaseLinks = { @@ -134,10 +134,14 @@ export const V2ChatUpgradeLink = ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU'; -// STRIPE +// STRIPE CREDIT CODE export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; + +// STRIPE MONTHLY PLAN CODE export const STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION = 'monthly_pro_plan_subscription'; + +// STRIPE ONE TIME PLAN CODE export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = 'one_time_pro_plan_for_1_month'; From 698896f31ce3859ad44e578f3b51a46ddd97f6d5 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 16:09:59 +0800 Subject: [PATCH 011/134] refactor: Update handleCheckoutSessionCompleted function to use UserProfile type and supabase client --- .../stripe/handleCheckoutSessionCompleted.ts | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 17516709a7..87aaae2073 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -6,6 +6,7 @@ import { import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { PluginID } from '@/types/plugin'; +import { UserProfile } from '@/types/user'; import { addCredit, @@ -17,6 +18,8 @@ import updateUserAccount from './updateUserAccount'; import dayjs from 'dayjs'; import Stripe from 'stripe'; +const supabase = getAdminSupabaseClient(); + export default async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, ): Promise { @@ -28,6 +31,14 @@ export default async function handleCheckoutSessionCompleted( const credit = session.metadata?.credit; const stripeSubscriptionId = session.subscription as string; + console.log({ + userId, + email, + planCode, + planGivingWeeks, + credit, + stripeSubscriptionId, + }); if (!planCode && !planGivingWeeks) { throw new Error('no plan code or plan giving weeks from Stripe webhook'); } @@ -36,6 +47,11 @@ export default async function handleCheckoutSessionCompleted( throw new Error('missing Email from Stripe webhook'); } + const user = await userProfileQuery({ + client: supabase, + email, + }); + const isTopUpCreditRequest = (planCode === STRIPE_PLAN_CODE_IMAGE_CREDIT || planCode === STRIPE_PLAN_CODE_GPT4_CREDIT) && @@ -43,7 +59,7 @@ export default async function handleCheckoutSessionCompleted( // Handle TopUp Image Credit / GPT4 Credit if (isTopUpCreditRequest) { return await addCreditToUser( - email, + user, +credit, planCode === STRIPE_PLAN_CODE_IMAGE_CREDIT ? PluginID.IMAGE_GEN @@ -52,10 +68,12 @@ export default async function handleCheckoutSessionCompleted( } const sinceDate = dayjs.unix(session.created).utc().toDate(); + // Retrieve user profile using email + const proPlanExpirationDate = await getProPlanExpirationDate( planGivingWeeks, planCode, - email, + user, sinceDate, ); @@ -88,25 +106,21 @@ export default async function handleCheckoutSessionCompleted( async function getProPlanExpirationDate( planGivingWeeks: string | undefined, planCode: string | undefined, - email: string, + user: UserProfile, sinceDate: Date, -) { - // Takes plan_giving_weeks priority over plan_code +): Promise { + // Check if planGivingWeeks is defined and is a string if (planGivingWeeks && typeof planGivingWeeks === 'string') { - // Get users' pro expiration date - const supabase = getAdminSupabaseClient(); - const user = await userProfileQuery({ - client: supabase, - email, - }); const userProPlanExpirationDate = user?.proPlanExpirationDate; + + // User has a previous one-time pro plan or a referral trial if (userProPlanExpirationDate) { - // when user bought one-time pro plan previously or user has referral trial return dayjs(userProPlanExpirationDate) .add(+planGivingWeeks, 'week') .toDate(); - } else if (user.plan === 'pro' && !user.proPlanExpirationDate) { - // when user is pro monthly subscriber + } + // Error handling for monthly pro subscribers who should not buy one-time plans + else if (user.plan === 'pro' && !user.proPlanExpirationDate) { throw new Error( 'Monthly Pro subscriber bought one-time pro plan, should not happen', { @@ -115,35 +129,31 @@ async function getProPlanExpirationDate( }, }, ); - } else { - // when user is not pro yet + } + // User is not a pro user yet + else { return dayjs(sinceDate).add(+planGivingWeeks, 'week').toDate(); } - } else if ( + } + // Handle one-month pro plan based on planCode + else if ( planCode?.toUpperCase() === STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH.toUpperCase() ) { - // Only store expiration for one month plan return dayjs(sinceDate).add(1, 'month').toDate(); - } else { - return undefined; } + // Return undefined if no conditions are met + return undefined; } async function addCreditToUser( - email: string, + user: UserProfile, credit: number, creditType: Exclude< PluginID, PluginID.LANGCHAIN_CHAT | PluginID.IMAGE_TO_PROMPT >, ) { - // Get user id by email address - const supabase = getAdminSupabaseClient(); - const user = await userProfileQuery({ - client: supabase, - email, - }); // Check is Pro user if (user.plan === 'free') { throw Error(`A free user try to top up ${creditType}}`, { From a1ac3a1f2576a7a4af0b4d0522dd9a955db15b3a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 16:56:50 +0800 Subject: [PATCH 012/134] feat: Add enums for PaidPlan and TopUpRequest --- types/paid_plan.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 types/paid_plan.ts diff --git a/types/paid_plan.ts b/types/paid_plan.ts new file mode 100644 index 0000000000..b4bd29a88e --- /dev/null +++ b/types/paid_plan.ts @@ -0,0 +1,11 @@ +export enum PaidPlan { + ProMonthly = 'pro-monthly', + ProOneTime = 'pro-one-time', + UltraMonthly = 'ultra-monthly', + UltraOneTime = 'ultra-one-time', +} + +export enum TopUpRequest { + ImageCredit = 'image-credit', + GPT4Credit = 'gpt4-credit', +} From 9f95618981bd27d06821ecfb5f84f23a2fd09b66 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:00:51 +0800 Subject: [PATCH 013/134] refactor: Move strip config to a new file from const file --- utils/app/const.ts | 51 +++++++++++--------------------------- utils/app/paid_plan.ts | 31 +++++++++++++++++++++++ utils/app/stripe_config.ts | 42 +++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 utils/app/paid_plan.ts create mode 100644 utils/app/stripe_config.ts diff --git a/utils/app/const.ts b/utils/app/const.ts index c3e3c23fa3..2ba1a840cb 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -4,6 +4,20 @@ import { SubscriptionPlan } from '@/types/user'; import dayjs from 'dayjs'; import { v4 as uuidv4 } from 'uuid'; +export { + STRIPE_PLAN_CODE_GPT4_CREDIT, + STRIPE_PLAN_CODE_IMAGE_CREDIT, + STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION, + STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION, + STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, + STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH, + PRO_PLAN_PAYMENT_LINK, + ULTRA_PLAN_PAYMENT_LINK, + GPT4_CREDIT_PURCHASE_LINKS, + AI_IMAGE_CREDIT_PURCHASE_LINKS, + V2_CHAT_UPGRADE_LINK, +} from './stripe_config'; + export const DEFAULT_SYSTEM_PROMPT = "You are an AI language model named Chat Everywhere, designed to answer user questions as accurately and helpfully as possible. Always be aware of the current date and time, and make sure to generate responses in the exact same language as the user's query. Adapt your responses to match the user's input language and context, maintaining an informative and supportive communication style. Additionally, format all responses using Markdown syntax, regardless of the input format." + 'If the input includes text such as [lang=xxx], the response should not include this text.' + @@ -108,40 +122,3 @@ export const OrderedSubscriptionPlans: SubscriptionPlan[] = [ 'ultra', 'edu', ]; - -export const ProPlanPaymentLink = - process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' - : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; - -export const UltraPlanPaymentLink = - process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' // TODO: Update the production link - : 'https://buy.stripe.com/test_00gcOhbrmgLwbkYdR0'; - -export const Gpt4CreditPurchaseLinks = { - '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', - '150': 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW', - '300': 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn', -}; -export const AiImageCreditPurchaseLinks = { - '100': 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT', - '500': 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ', -}; - -export const V2ChatUpgradeLink = - process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' - : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU'; - -// STRIPE CREDIT CODE -export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; -export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; - -// STRIPE MONTHLY PLAN CODE -export const STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION = - 'monthly_pro_plan_subscription'; - -// STRIPE ONE TIME PLAN CODE -export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = - 'one_time_pro_plan_for_1_month'; diff --git a/utils/app/paid_plan.ts b/utils/app/paid_plan.ts new file mode 100644 index 0000000000..ae74c2a097 --- /dev/null +++ b/utils/app/paid_plan.ts @@ -0,0 +1,31 @@ +import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; + +import { + STRIPE_PLAN_CODE_GPT4_CREDIT, + STRIPE_PLAN_CODE_IMAGE_CREDIT, + STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION, + STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION, + STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, + STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH, +} from './const'; + +export const getPaidPlan = ( + planCode: string, +): PaidPlan | TopUpRequest | undefined => { + switch (planCode.toUpperCase()) { + case STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION.toUpperCase(): + return PaidPlan.ProMonthly; + case STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH.toUpperCase(): + return PaidPlan.ProOneTime; + case STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION.toUpperCase(): + return PaidPlan.UltraMonthly; + case STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH.toUpperCase(): + return PaidPlan.UltraOneTime; + case STRIPE_PLAN_CODE_IMAGE_CREDIT.toUpperCase(): + return TopUpRequest.ImageCredit; + case STRIPE_PLAN_CODE_GPT4_CREDIT.toUpperCase(): + return TopUpRequest.GPT4Credit; + default: + return undefined; + } +}; diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts new file mode 100644 index 0000000000..48d0df7ffa --- /dev/null +++ b/utils/app/stripe_config.ts @@ -0,0 +1,42 @@ +// P.S. All of the code below is used in the product payment link + +// STRIPE CREDIT CODE +export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; +export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; + +// STRIPE MONTHLY PLAN CODE +export const STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION = + 'monthly_pro_plan_subscription'; +export const STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION = + 'monthly_ultra_plan_subscription'; + +// STRIPE ONE TIME PLAN CODE +export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = + 'one_time_pro_plan_for_1_month'; +export const STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH = + 'one_time_ultra_plan_for_1_month'; + +export const PRO_PLAN_PAYMENT_LINK = + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' + : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; + +export const ULTRA_PLAN_PAYMENT_LINK = + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' // TODO: Update the production link + : 'https://buy.stripe.com/test_00gcOhbrmgLwbkYdR0'; + +export const GPT4_CREDIT_PURCHASE_LINKS = { + '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', + '150': 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW', + '300': 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn', +}; +export const AI_IMAGE_CREDIT_PURCHASE_LINKS = { + '100': 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT', + '500': 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ', +}; + +export const V2_CHAT_UPGRADE_LINK = + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' + : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU'; From a7b08e4674150fd43c6227aa81500ffe9eea2eef Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:01:23 +0800 Subject: [PATCH 014/134] refactor: Update const variable name --- components/User/Settings/PlanComparison.tsx | 8 ++++---- components/User/UsageCreditModel.tsx | 8 ++++---- components/v2Chat/payment-dialog.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 06c2b040e5..62af2c7a60 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import { OrderedSubscriptionPlans, - ProPlanPaymentLink, - UltraPlanPaymentLink, + PRO_PLAN_PAYMENT_LINK, + ULTRA_PLAN_PAYMENT_LINK, } from '@/utils/app/const'; import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; @@ -20,9 +20,9 @@ const PlanComparison = ({ user }: { user: User | null }) => { const upgradeLinkOnClick = (upgradePlan: 'pro' | 'ultra') => { let paymentLink = ''; if (upgradePlan === 'pro') { - paymentLink = ProPlanPaymentLink; + paymentLink = PRO_PLAN_PAYMENT_LINK; } else if (upgradePlan === 'ultra') { - paymentLink = UltraPlanPaymentLink; + paymentLink = ULTRA_PLAN_PAYMENT_LINK; } const userEmail = user?.email; diff --git a/components/User/UsageCreditModel.tsx b/components/User/UsageCreditModel.tsx index c2ab7b9d63..6a9f0cf107 100644 --- a/components/User/UsageCreditModel.tsx +++ b/components/User/UsageCreditModel.tsx @@ -4,8 +4,8 @@ import { FC, Fragment, useContext, useEffect } from 'react'; import { useTranslation } from 'next-i18next'; import { - AiImageCreditPurchaseLinks, - Gpt4CreditPurchaseLinks, + AI_IMAGE_CREDIT_PURCHASE_LINKS, + GPT4_CREDIT_PURCHASE_LINKS, } from '@/utils/app/const'; import { DefaultMonthlyCredits } from '@/utils/config'; @@ -101,7 +101,7 @@ export const UsageCreditModel: FC = ({ onClose }) => { : gpt4Credit} - {Object.entries(Gpt4CreditPurchaseLinks).map( + {Object.entries(GPT4_CREDIT_PURCHASE_LINKS).map( ([key, value]) => ( = ({ onClose }) => { : aiImageCredit} - {Object.entries(AiImageCreditPurchaseLinks).map( + {Object.entries(AI_IMAGE_CREDIT_PURCHASE_LINKS).map( ([key, value]) => ( { - const paymentLink = V2ChatUpgradeLink; + const paymentLink = V2_CHAT_UPGRADE_LINK; trackEvent('v2 Payment link clicked'); From 92964e871c5e46686918b938553c5857692d2153 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:04:29 +0800 Subject: [PATCH 015/134] refactor: Refactor handleCheckoutSessionCompleted function --- .../stripe/handleCheckoutSessionCompleted.ts | 155 ++++++++---------- 1 file changed, 72 insertions(+), 83 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 87aaae2073..a03bafb8e3 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,10 +1,7 @@ -import { - STRIPE_PLAN_CODE_GPT4_CREDIT, - STRIPE_PLAN_CODE_IMAGE_CREDIT, - STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, -} from '@/utils/app/const'; import { serverSideTrackEvent } from '@/utils/app/eventTracking'; +import { getPaidPlan } from '@/utils/app/paid_plan'; +import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; import { PluginID } from '@/types/plugin'; import { UserProfile } from '@/types/user'; @@ -26,21 +23,17 @@ export default async function handleCheckoutSessionCompleted( const userId = session.client_reference_id; const email = session.customer_details?.email; - const planCode = session.metadata?.plan_code; + const planCode = session.metadata?.plan_code + ? getPaidPlan(session.metadata?.plan_code) + : undefined; const planGivingWeeks = session.metadata?.plan_giving_weeks; const credit = session.metadata?.credit; const stripeSubscriptionId = session.subscription as string; - console.log({ - userId, - email, - planCode, - planGivingWeeks, - credit, - stripeSubscriptionId, - }); if (!planCode && !planGivingWeeks) { - throw new Error('no plan code or plan giving weeks from Stripe webhook'); + throw new Error( + 'no plan code and plan giving weeks from Stripe webhook, one of them must be provided', + ); } if (!email) { @@ -53,94 +46,90 @@ export default async function handleCheckoutSessionCompleted( }); const isTopUpCreditRequest = - (planCode === STRIPE_PLAN_CODE_IMAGE_CREDIT || - planCode === STRIPE_PLAN_CODE_GPT4_CREDIT) && - credit; - // Handle TopUp Image Credit / GPT4 Credit + Object.values(TopUpRequest).includes(planCode as TopUpRequest) && credit; + + // # REQUEST: Top Up Image Credit / GPT4 Credit if (isTopUpCreditRequest) { return await addCreditToUser( user, +credit, - planCode === STRIPE_PLAN_CODE_IMAGE_CREDIT + planCode === TopUpRequest.ImageCredit ? PluginID.IMAGE_GEN : PluginID.GPT4, ); } - const sinceDate = dayjs.unix(session.created).utc().toDate(); - // Retrieve user profile using email + // # REQUEST: Upgrade plan + return await (async () => { + const sessionCreatedDate = dayjs.unix(session.created).utc().toDate(); + // Retrieve user profile using email - const proPlanExpirationDate = await getProPlanExpirationDate( - planGivingWeeks, - planCode, - user, - sinceDate, - ); - - serverSideTrackEvent(userId || 'N/A', 'New paying customer', { - paymentDetail: - !session.amount_subtotal || session.amount_subtotal <= 50000 - ? 'One-time' - : 'Monthly', - }); + const proPlanExpirationDate = await getExtendedMembershipExpirationDate( + planGivingWeeks, + planCode, + user, + sessionCreatedDate, + ); - // Update user account by User id - if (userId) { - await updateUserAccount({ - upgrade: true, - userId, - stripeSubscriptionId, - proPlanExpirationDate: proPlanExpirationDate, - }); - } else { - // Update user account by Email - await updateUserAccount({ - upgrade: true, - email: email!, - stripeSubscriptionId, - proPlanExpirationDate: proPlanExpirationDate, + serverSideTrackEvent(userId || 'N/A', 'New paying customer', { + paymentDetail: + !session.amount_subtotal || session.amount_subtotal <= 50000 + ? 'One-time' + : 'Monthly', }); - } + + if (!proPlanExpirationDate) { + throw new Error('undefined extended pro plan expiration date', { + cause: { + user, + }, + }); + } + + // Update user account by User id + if (userId) { + await updateUserAccount({ + upgrade: true, + userId, + stripeSubscriptionId, + proPlanExpirationDate: proPlanExpirationDate, + }); + } else { + // Update user account by Email + await updateUserAccount({ + upgrade: true, + email: email!, + stripeSubscriptionId, + proPlanExpirationDate: proPlanExpirationDate, + }); + } + })(); } -async function getProPlanExpirationDate( +async function getExtendedMembershipExpirationDate( planGivingWeeks: string | undefined, planCode: string | undefined, user: UserProfile, - sinceDate: Date, + sessionCreatedDate: Date, ): Promise { - // Check if planGivingWeeks is defined and is a string - if (planGivingWeeks && typeof planGivingWeeks === 'string') { - const userProPlanExpirationDate = user?.proPlanExpirationDate; + const userProPlanExpirationDate = user?.proPlanExpirationDate; - // User has a previous one-time pro plan or a referral trial - if (userProPlanExpirationDate) { - return dayjs(userProPlanExpirationDate) - .add(+planGivingWeeks, 'week') - .toDate(); - } - // Error handling for monthly pro subscribers who should not buy one-time plans - else if (user.plan === 'pro' && !user.proPlanExpirationDate) { - throw new Error( - 'Monthly Pro subscriber bought one-time pro plan, should not happen', - { - cause: { - user, - }, - }, - ); - } - // User is not a pro user yet - else { - return dayjs(sinceDate).add(+planGivingWeeks, 'week').toDate(); - } + const previousDate = dayjs( + userProPlanExpirationDate || sessionCreatedDate || undefined, + ); + // If has planGivingWeeks, use it to calculate the expiration date + if (planGivingWeeks && typeof planGivingWeeks === 'string') { + return previousDate.add(+planGivingWeeks, 'week').toDate(); } - // Handle one-month pro plan based on planCode - else if ( - planCode?.toUpperCase() === - STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH.toUpperCase() - ) { - return dayjs(sinceDate).add(1, 'month').toDate(); + // else extend the expiration date based on the plan code + else if (planCode === PaidPlan.ProOneTime) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.ProMonthly) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.UltraOneTime) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.UltraMonthly) { + return previousDate.add(1, 'month').toDate(); } // Return undefined if no conditions are met return undefined; From f8fd4a497773220bf8dc1a1f46bcc7a8a38fcc8d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:27:28 +0800 Subject: [PATCH 016/134] refactor: Add error logging to stripe webhook handler --- pages/api/webhooks/stripe.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 968cd7d68d..2267075a6b 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -67,6 +67,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { // Return a response to acknowledge receipt of the event res.json({ received: true }); } catch (error) { + console.log(error); if (error instanceof Error) { const user = error.cause && typeof error.cause === 'object' && 'user' in error.cause From 5a72aa6d35d468a15b439715d239af533d483da0 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:27:51 +0800 Subject: [PATCH 017/134] refactor: Add SubscriptionPlan type and update user.ts to use it --- types/paid_plan.ts | 1 + types/user.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/types/paid_plan.ts b/types/paid_plan.ts index b4bd29a88e..442dcf22bf 100644 --- a/types/paid_plan.ts +++ b/types/paid_plan.ts @@ -9,3 +9,4 @@ export enum TopUpRequest { ImageCredit = 'image-credit', GPT4Credit = 'gpt4-credit', } +export type SubscriptionPlan = 'free' | 'pro' | 'ultra' | 'edu'; diff --git a/types/user.ts b/types/user.ts index 7b55b99f42..829cd0841f 100644 --- a/types/user.ts +++ b/types/user.ts @@ -1,15 +1,16 @@ import { ExportFormatV4 } from './export'; +import { SubscriptionPlan } from './paid_plan'; import { PluginID } from './plugin'; import { SupabaseClient } from '@supabase/supabase-js'; +export type { SubscriptionPlan } from './paid_plan'; + export interface User extends UserProfile { email: string; token: string; } -export type SubscriptionPlan = 'free' | 'pro' | 'ultra' | 'edu'; - export interface UserConversation { id: string; uid: string; From b62a7820693af3e2b37997a0d38bf99cff851743 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:28:00 +0800 Subject: [PATCH 018/134] refactor: Add getSubscriptionPlanByPaidPlan function to utils/app/paid_plan.ts --- utils/app/paid_plan.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/utils/app/paid_plan.ts b/utils/app/paid_plan.ts index ae74c2a097..0fcb1049d5 100644 --- a/utils/app/paid_plan.ts +++ b/utils/app/paid_plan.ts @@ -1,4 +1,4 @@ -import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; +import { PaidPlan, SubscriptionPlan, TopUpRequest } from '@/types/paid_plan'; import { STRIPE_PLAN_CODE_GPT4_CREDIT, @@ -29,3 +29,20 @@ export const getPaidPlan = ( return undefined; } }; + +export const getSubscriptionPlanByPaidPlan = ( + paidPlan: PaidPlan, +): SubscriptionPlan => { + switch (paidPlan) { + case PaidPlan.ProMonthly: + return 'pro'; + case PaidPlan.ProOneTime: + return 'pro'; + case PaidPlan.UltraMonthly: + return 'ultra'; + case PaidPlan.UltraOneTime: + return 'ultra'; + default: + return 'free'; + } +}; From f6f2383fa3f7d7c250d041229fd0406a0f6887ce Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:28:11 +0800 Subject: [PATCH 019/134] refactor: Remove unused constant in stripe_config.ts --- utils/app/stripe_config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 48d0df7ffa..802083546a 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -13,6 +13,8 @@ export const STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION = // STRIPE ONE TIME PLAN CODE export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = 'one_time_pro_plan_for_1_month'; + +// (Not in used) export const STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH = 'one_time_ultra_plan_for_1_month'; From acb74460b6fd15fa39b9991f20e2dfc068b14d90 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:28:21 +0800 Subject: [PATCH 020/134] refactor: Update updateUserAccount function to support extending pro/ultra plan --- utils/server/stripe/updateUserAccount.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/utils/server/stripe/updateUserAccount.ts b/utils/server/stripe/updateUserAccount.ts index edb003ac2e..e10c983502 100644 --- a/utils/server/stripe/updateUserAccount.ts +++ b/utils/server/stripe/updateUserAccount.ts @@ -1,3 +1,5 @@ +import { SubscriptionPlan } from '@/types/paid_plan'; + import { getAdminSupabaseClient } from '../supabase'; // Skip any account operation on Edu accounts @@ -17,14 +19,14 @@ export default async function updateUserAccount(props: UpdateUserAccountProps) { const { error: updatedUserError } = await supabase .from('profiles') .update({ - plan: 'pro', + plan: props.plan, stripe_subscription_id: props.stripeSubscriptionId, pro_plan_expiration_date: props.proPlanExpirationDate || null, }) .eq('id', props.userId); if (updatedUserError) throw updatedUserError; - console.log(`User ${props.userId} updated to pro`); + console.log(`User ${props.userId} updated to ${props.plan}`); } else if (isUpgradeUserAccountByEmailProps(props)) { // Update user account by Email const { data: userProfile } = await supabase @@ -38,13 +40,13 @@ export default async function updateUserAccount(props: UpdateUserAccountProps) { const { error: updatedUserError } = await supabase .from('profiles') .update({ - plan: 'pro', + plan: props.plan, stripe_subscription_id: props.stripeSubscriptionId, pro_plan_expiration_date: props.proPlanExpirationDate || null, }) .eq('email', props.email); if (updatedUserError) throw updatedUserError; - console.log(`User ${props.email} updated to pro`); + console.log(`User ${props.email} updated to ${props.plan}`); } else if (isDowngradeUserAccountProps(props)) { // Downgrade user account @@ -84,13 +86,13 @@ export default async function updateUserAccount(props: UpdateUserAccountProps) { .eq('email', props.email); if (updatedUserError) throw updatedUserError; console.log(`User subscription ${props.email} downgrade back to free`); - } else if (isExtendProPlanProps(props)) { - // Extend pro plan - + } else if (isExtendMembershipProps(props)) { + // Extend pro / ultra plan const { data: userProfile } = await supabase .from('profiles') .select('plan') .eq('stripe_subscription_id', props.stripeSubscriptionId) + .eq('plan', props.plan) .single(); if (userProfile?.plan === 'edu') return; @@ -113,6 +115,7 @@ export default async function updateUserAccount(props: UpdateUserAccountProps) { interface UpgradeUserAccountProps { upgrade: true; userId: string; + plan: SubscriptionPlan; stripeSubscriptionId?: string; proPlanExpirationDate?: Date; } @@ -120,6 +123,7 @@ interface UpgradeUserAccountProps { interface UpgradeUserAccountByEmailProps { upgrade: true; email: string; + plan: SubscriptionPlan; stripeSubscriptionId?: string; proPlanExpirationDate?: Date; } @@ -137,6 +141,7 @@ interface DowngradeUserAccountByEmailProps { interface ExtendProPlanProps { upgrade: true; + plan: SubscriptionPlan; stripeSubscriptionId: string; proPlanExpirationDate: Date | undefined; } @@ -156,6 +161,7 @@ function isUpgradeUserAccountProps( props.upgrade === true && 'userId' in props && typeof props.userId === 'string' && + !!props.plan && (props.proPlanExpirationDate instanceof Date || props.proPlanExpirationDate === undefined) ); @@ -168,6 +174,7 @@ function isUpgradeUserAccountByEmailProps( props.upgrade === true && 'email' in props && typeof props.email === 'string' && + !!props.plan && (props.proPlanExpirationDate instanceof Date || props.proPlanExpirationDate === undefined) ); @@ -197,12 +204,13 @@ function isDowngradeUserAccountByEmailProps( ); } -function isExtendProPlanProps( +function isExtendMembershipProps( props: UpdateUserAccountProps, ): props is ExtendProPlanProps { return ( (props.upgrade === true && typeof props.stripeSubscriptionId === 'string' && + !!props.plan && props.proPlanExpirationDate instanceof Date) || props.proPlanExpirationDate === undefined ); From ac41bd64be078e6c5c386c0ed309e4130be457f3 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:28:28 +0800 Subject: [PATCH 021/134] refactor: Update handleCheckoutSessionCompleted function to use UserProfile type and supabase client --- .../stripe/handleCheckoutSessionCompleted.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index a03bafb8e3..d680a67e63 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,5 +1,8 @@ import { serverSideTrackEvent } from '@/utils/app/eventTracking'; -import { getPaidPlan } from '@/utils/app/paid_plan'; +import { + getPaidPlan, + getSubscriptionPlanByPaidPlan, +} from '@/utils/app/paid_plan'; import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; import { PluginID } from '@/types/plugin'; @@ -20,6 +23,7 @@ const supabase = getAdminSupabaseClient(); export default async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, ): Promise { + console.log('handleCheckoutSessionCompleted'); const userId = session.client_reference_id; const email = session.customer_details?.email; @@ -45,11 +49,12 @@ export default async function handleCheckoutSessionCompleted( email, }); - const isTopUpCreditRequest = - Object.values(TopUpRequest).includes(planCode as TopUpRequest) && credit; + const isTopUpCreditRequest = Object.values(TopUpRequest).includes( + planCode as TopUpRequest, + ); // # REQUEST: Top Up Image Credit / GPT4 Credit - if (isTopUpCreditRequest) { + if (isTopUpCreditRequest && credit) { return await addCreditToUser( user, +credit, @@ -79,7 +84,7 @@ export default async function handleCheckoutSessionCompleted( }); if (!proPlanExpirationDate) { - throw new Error('undefined extended pro plan expiration date', { + throw new Error('undefined extended membership expiration date', { cause: { user, }, @@ -90,6 +95,7 @@ export default async function handleCheckoutSessionCompleted( if (userId) { await updateUserAccount({ upgrade: true, + plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), userId, stripeSubscriptionId, proPlanExpirationDate: proPlanExpirationDate, @@ -98,6 +104,7 @@ export default async function handleCheckoutSessionCompleted( // Update user account by Email await updateUserAccount({ upgrade: true, + plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), email: email!, stripeSubscriptionId, proPlanExpirationDate: proPlanExpirationDate, From 805f2a8d2993b1f88ca88b3a0c6c472c1a1507c3 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:32:02 +0800 Subject: [PATCH 022/134] refactor: Update import statement for getCustomerEmailByCustomerID in handleCustomerSubscriptionDeleted and handleCustomerSubscriptionUpdated --- utils/server/stripe/handleCustomerSubscriptionDeleted.ts | 2 +- utils/server/stripe/handleCustomerSubscriptionUpdated.ts | 2 +- .../stripe/{getCustomerEmailByCustomerID.ts => strip_helper.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename utils/server/stripe/{getCustomerEmailByCustomerID.ts => strip_helper.ts} (100%) diff --git a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts index fdca335ab1..cc07ce80d3 100644 --- a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts +++ b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts @@ -1,4 +1,4 @@ -import getCustomerEmailByCustomerID from './getCustomerEmailByCustomerID'; +import getCustomerEmailByCustomerID from './strip_helper'; import updateUserAccount from './updateUserAccount'; import Stripe from 'stripe'; diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts index 149bfb2fda..557cd8ea8e 100644 --- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts +++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts @@ -1,4 +1,4 @@ -import getCustomerEmailByCustomerID from './getCustomerEmailByCustomerID'; +import getCustomerEmailByCustomerID from './strip_helper'; import updateUserAccount from './updateUserAccount'; import dayjs from 'dayjs'; diff --git a/utils/server/stripe/getCustomerEmailByCustomerID.ts b/utils/server/stripe/strip_helper.ts similarity index 100% rename from utils/server/stripe/getCustomerEmailByCustomerID.ts rename to utils/server/stripe/strip_helper.ts From 174a8e0359cd6cdc6d5cea2b7f5e5fbe8cd45419 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:32:06 +0800 Subject: [PATCH 023/134] refactor: Update subscription event handling in stripe webhook handler --- pages/api/webhooks/stripe.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 2267075a6b..f75bff0a4a 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -43,13 +43,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { switch (event.type) { case 'checkout.session.completed': - // One time payment / Initial Monthly Pro Plan Subscription + // One time payment / Initial Monthly Pro / Ultra Plan Subscription await handleCheckoutSessionCompleted( event.data.object as Stripe.Checkout.Session, ); break; case 'customer.subscription.updated': - // Monthly Pro Plan Subscription recurring payment + // Monthly Pro / Ultra Plan Subscription recurring payment await handleCustomerSubscriptionUpdated( event.data.object as Stripe.Subscription, ); From b4a75d37f7ef2de42f1e4f8527792b402b985306 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 17:33:07 +0800 Subject: [PATCH 024/134] refactor: rename file to paid_plan to paid_plan_helper --- utils/app/{paid_plan.ts => paid_plan_helper.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename utils/app/{paid_plan.ts => paid_plan_helper.ts} (100%) diff --git a/utils/app/paid_plan.ts b/utils/app/paid_plan_helper.ts similarity index 100% rename from utils/app/paid_plan.ts rename to utils/app/paid_plan_helper.ts From 415bf96270312055725bc587dc4fa39b0503a83e Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 28 May 2024 18:09:24 +0800 Subject: [PATCH 025/134] refactor: Update import statement for getCustomerEmailByCustomerID in handleCustomerSubscriptionDeleted and handleCustomerSubscriptionUpdated --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index d680a67e63..ec493fe6f5 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -2,7 +2,7 @@ import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { getPaidPlan, getSubscriptionPlanByPaidPlan, -} from '@/utils/app/paid_plan'; +} from '@/utils/app/paid_plan_helper'; import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; import { PluginID } from '@/types/plugin'; From 97f607288f05022196b1b740632848e529187826 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:47:43 +0800 Subject: [PATCH 026/134] refactor: Update localization for Chinese (Simplified) and Chinese (Traditional) --- public/locales/zh-Hans/model.json | 3 ++- public/locales/zh-Hant/model.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/zh-Hans/model.json b/public/locales/zh-Hans/model.json index af616df90a..a67d587bb7 100644 --- a/public/locales/zh-Hans/model.json +++ b/public/locales/zh-Hans/model.json @@ -232,5 +232,6 @@ "All in tags": "标签中的全部", "Error deleting file": "文件删除失败", "Download failed!": "下载失败", - "Downloading...": "下载中..." + "Downloading...": "下载中...", + "Please sign-up before upgrading to paid plan": "请在注册后再升级到付费方案" } diff --git a/public/locales/zh-Hant/model.json b/public/locales/zh-Hant/model.json index 0cbf2a820f..b6d754846f 100644 --- a/public/locales/zh-Hant/model.json +++ b/public/locales/zh-Hant/model.json @@ -233,5 +233,6 @@ "All in tags": "標籤中的全部", "Error deleting file": "檔案刪除失敗", "Download failed!": "下載失敗", - "Downloading...": "下載中..." + "Downloading...": "下載中...", + "Please sign-up before upgrading to paid plan": "請在註冊後再升級到付費方案" } From a96b756879024f39bb3f118663238b016a003722 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:48:00 +0800 Subject: [PATCH 027/134] refactor: Add Tabs component for UI tab functionality --- components/ui/tabs.tsx | 53 ++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 54 insertions(+) create mode 100644 components/ui/tabs.tsx diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000000..85d83beab3 --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/package.json b/package.json index 6b2189ab45..99087d7c95 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@ramonak/react-progress-bar": "^5.0.3", "@sendgrid/eventwebhook": "^8.0.0", From 208657f2053dcb737cee7e359ab8a344721761a5 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:48:16 +0800 Subject: [PATCH 028/134] refactor: Add UltraYearly option to PaidPlan enum --- types/paid_plan.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/paid_plan.ts b/types/paid_plan.ts index 442dcf22bf..bce0056af9 100644 --- a/types/paid_plan.ts +++ b/types/paid_plan.ts @@ -3,6 +3,7 @@ export enum PaidPlan { ProOneTime = 'pro-one-time', UltraMonthly = 'ultra-monthly', UltraOneTime = 'ultra-one-time', + UltraYearly = 'ultra-yearly', } export enum TopUpRequest { From ce364279b913f6bfcf1204d8cefb55da89d937ad Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:49:13 +0800 Subject: [PATCH 029/134] refactor: Update payment links for Pro and Ultra plans Update the payment links for the Pro and Ultra plans in the stripe_config.ts file. Add separate links for the monthly plans in USD and TWD currencies. Also, add links for the yearly Ultra plan in USD and TWD currencies. Remove the unused constant in the file. --- components/User/Settings/PlanComparison.tsx | 270 +++++++++++++------- utils/app/const.ts | 9 +- utils/app/stripe_config.ts | 37 ++- 3 files changed, 221 insertions(+), 95 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 62af2c7a60..2796912e92 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -1,59 +1,26 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { OrderedSubscriptionPlans, - PRO_PLAN_PAYMENT_LINK, - ULTRA_PLAN_PAYMENT_LINK, + PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD, + PRO_MONTHLY_PLAN_PAYMENT_LINK_USD, + ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD, + ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD, + ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD, + ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD, } from '@/utils/app/const'; import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; import { User } from '@/types/user'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; + import dayjs from 'dayjs'; const PlanComparison = ({ user }: { user: User | null }) => { - const { t } = useTranslation('model'); - - const upgradeLinkOnClick = (upgradePlan: 'pro' | 'ultra') => { - let paymentLink = ''; - if (upgradePlan === 'pro') { - paymentLink = PRO_PLAN_PAYMENT_LINK; - } else if (upgradePlan === 'ultra') { - paymentLink = ULTRA_PLAN_PAYMENT_LINK; - } - - const userEmail = user?.email; - const userId = user?.id; - - trackEvent('Upgrade button clicked'); - - if (!user) { - toast.error('Please sign-up before upgrading to pro plan'); - } else { - window.open( - `${paymentLink}?prefilled_email=${userEmail}&client_reference_id=${userId}`, - '_blank', - ); - } - }; - - const showUpgradeToPro = useMemo(() => { - if (!user) return true; - const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); - const proPlanIndex = OrderedSubscriptionPlans.indexOf('pro'); - return userPlanIndex < proPlanIndex; - }, [user]); - - const showUpgradeToUltra = useMemo(() => { - if (!user) return true; - const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); - const ultraPlanIndex = OrderedSubscriptionPlans.indexOf('ultra'); - return userPlanIndex < ultraPlanIndex; - }, [user]); - return (
{/* Free Plan */} @@ -64,55 +31,11 @@ const PlanComparison = ({ user }: { user: User | null }) => { {/* Pro Plan */} {/* Ultra Plan */}
- - {/* Upgrade button */} - {showUpgradeToUltra && ( - - )} - - {(user?.plan === 'pro' || user?.plan === 'ultra') && - user.proPlanExpirationDate && ( - - )}
); @@ -151,7 +74,34 @@ const FreePlanContent = ({ user }: { user: User | null }) => { ); }; const ProPlanContent = ({ user }: { user: User | null }) => { - const { t } = useTranslation('model'); + const { t, i18n } = useTranslation('model'); + const showUpgradeToPro = useMemo(() => { + if (!user) return true; + const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); + const proPlanIndex = OrderedSubscriptionPlans.indexOf('pro'); + return userPlanIndex < proPlanIndex; + }, [user]); + const upgradeLinkOnClick = () => { + const paymentLink = + i18n.language === 'zh-Hant' || i18n.language === 'zh' + ? PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD + : PRO_MONTHLY_PLAN_PAYMENT_LINK_USD; + + const userEmail = user?.email; + const userId = user?.id; + + trackEvent('Upgrade button clicked'); + + if (!user) { + toast.error(t('Please sign-up before upgrading to paid plan')); + } else { + window.open( + `${paymentLink}?prefilled_email=${userEmail}&client_reference_id=${userId}`, + '_blank', + ); + } + }; + return ( <>
@@ -168,7 +118,7 @@ const ProPlanContent = ({ user }: { user: User | null }) => { {user?.plan === 'pro' && }
- {t('USD$9.99 / month')} +
@@ -176,12 +126,72 @@ const ProPlanContent = ({ user }: { user: User | null }) => { ))}
+ {/* Upgrade button */} + {showUpgradeToPro && ( +
+ + {t('Upgrade')} + +

+ {t('No Strings Attached - Cancel Anytime!')} +

+
+ )} + + {(user?.plan === 'pro' || user?.plan === 'ultra') && + user.proPlanExpirationDate && ( + + )} ); }; const UltraPlanContent = ({ user }: { user: User | null }) => { - const { t } = useTranslation('model'); + const { t, i18n } = useTranslation('model'); + const [priceType, setPriceType] = useState<'monthly' | 'yearly'>('monthly'); + const showUpgradeToUltra = useMemo(() => { + if (!user) return true; + const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); + const ultraPlanIndex = OrderedSubscriptionPlans.indexOf('ultra'); + return userPlanIndex < ultraPlanIndex; + }, [user]); + + const upgradeLinkOnClick = () => { + let paymentLink = ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD; + if (priceType === 'monthly') { + if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { + paymentLink = ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD; + } else { + paymentLink = ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD; + } + } else { + if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { + paymentLink = ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD; + } else { + paymentLink = ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD; + } + } + + const userEmail = user?.email; + const userId = user?.id; + + trackEvent('Upgrade button clicked'); + + if (!user) { + toast.error(t('Please sign-up before upgrading to paid plan')); + } else { + window.open( + `${paymentLink}?prefilled_email=${userEmail}&client_reference_id=${userId}`, + '_blank', + ); + } + }; + return ( <>
@@ -198,7 +208,7 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { {user?.plan === 'ultra' && }
- {t('USD$19.99 / month')} +
@@ -207,6 +217,27 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { ))}
+ {/* Upgrade button */} + {showUpgradeToUltra && ( +
+ + {t('Upgrade')} + +

+ {t('No Strings Attached - Cancel Anytime!')} +

+
+ )} + + {(user?.plan === 'pro' || user?.plan === 'ultra') && + user.proPlanExpirationDate && ( + + )} ); }; @@ -218,3 +249,62 @@ const CurrentTag = () => { ); }; + +const ProPlanPrice = () => { + const { i18n } = useTranslation('model'); + + switch (i18n.language) { + case 'zh-Hant': + case 'zh': + return {'TWD$249.99 / month'}; + default: + return {'USD$9.99 / month'}; + } +}; + +const UltraPlanPrice = ({ + setPriceType, +}: { + setPriceType: (type: 'monthly' | 'yearly') => void; +}) => { + const { i18n } = useTranslation('model'); + + return ( + + + { + setPriceType('monthly'); + }} + > + MONTHLY + + { + setPriceType('yearly'); + }} + > + YEARLY + + + + {i18n.language === 'zh-Hant' || i18n.language === 'zh' ? ( + {'TWD$880 / month'} + ) : ( + {'USD$29.99 / month'} + )} + + + {i18n.language === 'zh-Hant' || i18n.language === 'zh' ? ( + {'TWD$8800 / year'} + ) : ( + {'USD$279.99 / year'} + )} + + + ); +}; diff --git a/utils/app/const.ts b/utils/app/const.ts index 2ba1a840cb..e8a9b83b45 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -9,10 +9,15 @@ export { STRIPE_PLAN_CODE_IMAGE_CREDIT, STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION, STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION, + STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION, STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH, - PRO_PLAN_PAYMENT_LINK, - ULTRA_PLAN_PAYMENT_LINK, + PRO_MONTHLY_PLAN_PAYMENT_LINK_USD, + ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD, + PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD, + ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD, + ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD, + ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD, GPT4_CREDIT_PURCHASE_LINKS, AI_IMAGE_CREDIT_PURCHASE_LINKS, V2_CHAT_UPGRADE_LINK, diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 802083546a..0eab9fe730 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -10,6 +10,10 @@ export const STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION = export const STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION = 'monthly_ultra_plan_subscription'; +// STRIPE YEARLY PLAN CODE +export const STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION = + 'yearly_ultra_plan_subscription'; + // STRIPE ONE TIME PLAN CODE export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = 'one_time_pro_plan_for_1_month'; @@ -18,16 +22,42 @@ export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = export const STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH = 'one_time_ultra_plan_for_1_month'; -export const PRO_PLAN_PAYMENT_LINK = +// =========== PRO PLAN LINKS =========== +// PRO MONTHLY PLAN +export const PRO_MONTHLY_PLAN_PAYMENT_LINK_USD = process.env.NEXT_PUBLIC_ENV === 'production' ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; -export const ULTRA_PLAN_PAYMENT_LINK = +export const PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD = + process.env.NEXT_PUBLIC_ENV === 'production' + ? '' // TODO: Update the production link + : ''; // TODO: Update the test link + +// =========== ULTRA PLAN LINKS =========== +// ULTRA MONTHLY PLAN +export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD = + process.env.NEXT_PUBLIC_ENV === 'production' + ? '' // TODO: Update the production link + : ''; + +export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD = process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' // TODO: Update the production link + ? '' // TODO: Update the production link : 'https://buy.stripe.com/test_00gcOhbrmgLwbkYdR0'; +// ULTRA YEARLY PLAN +export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD = + process.env.NEXT_PUBLIC_ENV === 'production' + ? '' // TODO: Update the production link + : ''; // TODO: Update the test link + +export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD = + process.env.NEXT_PUBLIC_ENV === 'production' + ? '' // TODO: Update the production link + : ''; // TODO: Update the test link + +// =========== TOP UP LINKS =========== export const GPT4_CREDIT_PURCHASE_LINKS = { '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', '150': 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW', @@ -38,6 +68,7 @@ export const AI_IMAGE_CREDIT_PURCHASE_LINKS = { '500': 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ', }; +// =========== V2 UPGRADE LINKS =========== export const V2_CHAT_UPGRADE_LINK = process.env.NEXT_PUBLIC_ENV === 'production' ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd' From 5f25e164863a1ac0d4cc47550911c59e4f31a0d4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:49:30 +0800 Subject: [PATCH 030/134] refactor: Add support for yearly Ultra plan subscription --- utils/app/paid_plan_helper.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index 0fcb1049d5..4735a30e39 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -7,6 +7,7 @@ import { STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION, STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH, + STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION, } from './const'; export const getPaidPlan = ( @@ -19,6 +20,8 @@ export const getPaidPlan = ( return PaidPlan.ProOneTime; case STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION.toUpperCase(): return PaidPlan.UltraMonthly; + case STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION.toUpperCase(): + return PaidPlan.UltraYearly; case STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH.toUpperCase(): return PaidPlan.UltraOneTime; case STRIPE_PLAN_CODE_IMAGE_CREDIT.toUpperCase(): From fd433e5f7c9c734bbaf0854f8dfce5dc9585d814 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:50:01 +0800 Subject: [PATCH 031/134] refactor: Update updateUserAccount function to extend membership --- utils/server/stripe/updateUserAccount.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/utils/server/stripe/updateUserAccount.ts b/utils/server/stripe/updateUserAccount.ts index e10c983502..22fed350b6 100644 --- a/utils/server/stripe/updateUserAccount.ts +++ b/utils/server/stripe/updateUserAccount.ts @@ -92,7 +92,6 @@ export default async function updateUserAccount(props: UpdateUserAccountProps) { .from('profiles') .select('plan') .eq('stripe_subscription_id', props.stripeSubscriptionId) - .eq('plan', props.plan) .single(); if (userProfile?.plan === 'edu') return; @@ -139,9 +138,8 @@ interface DowngradeUserAccountByEmailProps { proPlanExpirationDate?: Date; } -interface ExtendProPlanProps { +interface ExtendMemberShipProps { upgrade: true; - plan: SubscriptionPlan; stripeSubscriptionId: string; proPlanExpirationDate: Date | undefined; } @@ -151,7 +149,7 @@ type UpdateUserAccountProps = | UpgradeUserAccountByEmailProps | DowngradeUserAccountProps | DowngradeUserAccountByEmailProps - | ExtendProPlanProps; + | ExtendMemberShipProps; // Type Assertion functions function isUpgradeUserAccountProps( @@ -206,11 +204,11 @@ function isDowngradeUserAccountByEmailProps( function isExtendMembershipProps( props: UpdateUserAccountProps, -): props is ExtendProPlanProps { +): props is ExtendMemberShipProps { return ( (props.upgrade === true && typeof props.stripeSubscriptionId === 'string' && - !!props.plan && + !Object.keys(props).includes('plan') && props.proPlanExpirationDate instanceof Date) || props.proPlanExpirationDate === undefined ); From 1a73f75be0c3c09498cf6385196ac9618bf78ce0 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 11:57:10 +0800 Subject: [PATCH 032/134] refactor: Update payment links for Pro and Ultra plans --- utils/app/stripe_config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 0eab9fe730..3964c7102b 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -32,14 +32,14 @@ export const PRO_MONTHLY_PLAN_PAYMENT_LINK_USD = export const PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD = process.env.NEXT_PUBLIC_ENV === 'production' ? '' // TODO: Update the production link - : ''; // TODO: Update the test link + : 'https://buy.stripe.com/test_00gcOhdzucvg1Ko00e'; // =========== ULTRA PLAN LINKS =========== // ULTRA MONTHLY PLAN export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD = process.env.NEXT_PUBLIC_ENV === 'production' ? '' // TODO: Update the production link - : ''; + : 'https://buy.stripe.com/test_6oEdSl67266SexafZ9'; export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD = process.env.NEXT_PUBLIC_ENV === 'production' @@ -50,12 +50,12 @@ export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD = export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD = process.env.NEXT_PUBLIC_ENV === 'production' ? '' // TODO: Update the production link - : ''; // TODO: Update the test link + : 'https://buy.stripe.com/test_14k7tX7b6brcexa4gs'; export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD = process.env.NEXT_PUBLIC_ENV === 'production' ? '' // TODO: Update the production link - : ''; // TODO: Update the test link + : 'https://buy.stripe.com/test_eVa15z6720Myexa7sF'; // =========== TOP UP LINKS =========== export const GPT4_CREDIT_PURCHASE_LINKS = { From 3f1a88a1d8d575b6119c9c31586a662a721ce3b8 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:08:28 +0800 Subject: [PATCH 033/134] Refactor: Remove the updateUserAccount and make the function more atomic --- utils/server/stripe/strip_helper.ts | 130 ++++++++++++++ utils/server/stripe/updateUserAccount.ts | 215 ----------------------- 2 files changed, 130 insertions(+), 215 deletions(-) delete mode 100644 utils/server/stripe/updateUserAccount.ts diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 609a3658d0..04b4b042e4 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -1,5 +1,9 @@ +import { getAdminSupabaseClient } from '../supabase'; + import Stripe from 'stripe'; +const supabase = getAdminSupabaseClient(); + export default async function getCustomerEmailByCustomerID( customerID: string, ): Promise { @@ -22,3 +26,129 @@ export default async function getCustomerEmailByCustomerID( throw new Error(`getCustomerEmailByCustomerID failed: ${e}`); } } + +export async function updateUserAccountById({ + userId, + plan, + stripeSubscriptionId, + proPlanExpirationDate, +}: { + userId: string; + plan?: string; + stripeSubscriptionId?: string; + proPlanExpirationDate?: Date; +}) { + // Update user account by User ID + const { data: userProfile } = await supabase + .from('profiles') + .select('plan') + .eq('id', userId) + .single(); + + if (userProfile?.plan === 'edu') return; + + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + plan, + stripe_subscription_id: stripeSubscriptionId, + pro_plan_expiration_date: proPlanExpirationDate, + }) + .eq('id', userId); + if (updatedUserError) throw updatedUserError; + console.log(`User ${userId} updated to ${plan}`); +} + +export async function updateUserAccountByEmail({ + email, + plan, + stripeSubscriptionId, + proPlanExpirationDate, +}: { + email: string; + plan?: string; + stripeSubscriptionId?: string; + proPlanExpirationDate?: Date; +}) { + const { data: userProfile } = await supabase + .from('profiles') + .select('plan') + .eq('email', email) + .single(); + + if (userProfile?.plan === 'edu') return; + + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + plan, + stripe_subscription_id: stripeSubscriptionId, + pro_plan_expiration_date: proPlanExpirationDate, + }) + .eq('email', email); + if (updatedUserError) throw updatedUserError; + console.log(`User ${email} updated to ${plan}`); +} + +export async function downgradeUserAccount({ + email, + stripeSubscriptionId, +}: { + email?: string; + stripeSubscriptionId?: string; +}) { + if (!email && !stripeSubscriptionId) { + throw new Error('Either email or stripeSubscriptionId must be provided'); + } + + const { data: userProfile } = await supabase + .from('profiles') + .select('plan') + .eq( + stripeSubscriptionId ? 'stripe_subscription_id' : 'email', + stripeSubscriptionId || email, + ) + .single(); + + if (userProfile?.plan === 'edu') return; + + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + plan: 'free', + }) + .eq( + stripeSubscriptionId ? 'stripe_subscription_id' : 'email', + stripeSubscriptionId || email, + ); + if (updatedUserError) throw updatedUserError; + console.log(`User ${email || stripeSubscriptionId} downgraded to free plan`); +} + +export async function extendMembershipByStripeSubscriptionId({ + stripeSubscriptionId, + proPlanExpirationDate, +}: { + stripeSubscriptionId: string; + proPlanExpirationDate: Date; +}) { + // Extend pro / ultra plan + const { data: userProfile } = await supabase + .from('profiles') + .select('plan, email') + .eq('stripe_subscription_id', stripeSubscriptionId) + .single(); + + if (userProfile?.plan === 'edu') return; + + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + pro_plan_expiration_date: proPlanExpirationDate, + }) + .eq('stripe_subscription_id', stripeSubscriptionId); + if (updatedUserError) throw updatedUserError; + console.log( + `User ${userProfile?.email} with plan ${userProfile?.plan} extended to ${proPlanExpirationDate}`, + ); +} diff --git a/utils/server/stripe/updateUserAccount.ts b/utils/server/stripe/updateUserAccount.ts deleted file mode 100644 index 22fed350b6..0000000000 --- a/utils/server/stripe/updateUserAccount.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { SubscriptionPlan } from '@/types/paid_plan'; - -import { getAdminSupabaseClient } from '../supabase'; - -// Skip any account operation on Edu accounts -export default async function updateUserAccount(props: UpdateUserAccountProps) { - const supabase = getAdminSupabaseClient(); - - if (isUpgradeUserAccountProps(props)) { - // Update user account - const { data: userProfile } = await supabase - .from('profiles') - .select('plan') - .eq('id', props.userId) - .single(); - - if (userProfile?.plan === 'edu') return; - - const { error: updatedUserError } = await supabase - .from('profiles') - .update({ - plan: props.plan, - stripe_subscription_id: props.stripeSubscriptionId, - pro_plan_expiration_date: props.proPlanExpirationDate || null, - }) - .eq('id', props.userId); - - if (updatedUserError) throw updatedUserError; - console.log(`User ${props.userId} updated to ${props.plan}`); - } else if (isUpgradeUserAccountByEmailProps(props)) { - // Update user account by Email - const { data: userProfile } = await supabase - .from('profiles') - .select('plan') - .eq('email', props.email) - .single(); - - if (userProfile?.plan === 'edu') return; - - const { error: updatedUserError } = await supabase - .from('profiles') - .update({ - plan: props.plan, - stripe_subscription_id: props.stripeSubscriptionId, - pro_plan_expiration_date: props.proPlanExpirationDate || null, - }) - .eq('email', props.email); - if (updatedUserError) throw updatedUserError; - console.log(`User ${props.email} updated to ${props.plan}`); - } else if (isDowngradeUserAccountProps(props)) { - // Downgrade user account - - const { data: userProfile } = await supabase - .from('profiles') - .select('plan') - .eq('stripe_subscription_id', props.stripeSubscriptionId) - .single(); - - if (userProfile?.plan === 'edu') return; - - const { error: updatedUserError } = await supabase - .from('profiles') - .update({ - plan: 'free', - }) - .eq('stripe_subscription_id', props.stripeSubscriptionId); - if (updatedUserError) throw updatedUserError; - console.log( - `User subscription ${props.stripeSubscriptionId} downgrade back to free`, - ); - } else if (isDowngradeUserAccountByEmailProps(props)) { - // Downgrade user account - const { data: userProfile } = await supabase - .from('profiles') - .select('plan') - .eq('email', props.email) - .single(); - - if (userProfile?.plan === 'edu') return; - - const { error: updatedUserError } = await supabase - .from('profiles') - .update({ - plan: 'free', - }) - .eq('email', props.email); - if (updatedUserError) throw updatedUserError; - console.log(`User subscription ${props.email} downgrade back to free`); - } else if (isExtendMembershipProps(props)) { - // Extend pro / ultra plan - const { data: userProfile } = await supabase - .from('profiles') - .select('plan') - .eq('stripe_subscription_id', props.stripeSubscriptionId) - .single(); - - if (userProfile?.plan === 'edu') return; - - const { error: updatedUserError } = await supabase - .from('profiles') - .update({ - pro_plan_expiration_date: props.proPlanExpirationDate, - }) - .eq('stripe_subscription_id', props.stripeSubscriptionId); - if (updatedUserError) throw updatedUserError; - console.log( - `User subscription ${props.stripeSubscriptionId} extended to ${props.proPlanExpirationDate}`, - ); - } else { - throw new Error('Invalid props object'); - } -} - -interface UpgradeUserAccountProps { - upgrade: true; - userId: string; - plan: SubscriptionPlan; - stripeSubscriptionId?: string; - proPlanExpirationDate?: Date; -} - -interface UpgradeUserAccountByEmailProps { - upgrade: true; - email: string; - plan: SubscriptionPlan; - stripeSubscriptionId?: string; - proPlanExpirationDate?: Date; -} - -interface DowngradeUserAccountProps { - upgrade: false; - stripeSubscriptionId: string; - proPlanExpirationDate?: Date; -} -interface DowngradeUserAccountByEmailProps { - upgrade: false; - email: string; - proPlanExpirationDate?: Date; -} - -interface ExtendMemberShipProps { - upgrade: true; - stripeSubscriptionId: string; - proPlanExpirationDate: Date | undefined; -} - -type UpdateUserAccountProps = - | UpgradeUserAccountProps - | UpgradeUserAccountByEmailProps - | DowngradeUserAccountProps - | DowngradeUserAccountByEmailProps - | ExtendMemberShipProps; - -// Type Assertion functions -function isUpgradeUserAccountProps( - props: UpdateUserAccountProps, -): props is UpgradeUserAccountProps { - return ( - props.upgrade === true && - 'userId' in props && - typeof props.userId === 'string' && - !!props.plan && - (props.proPlanExpirationDate instanceof Date || - props.proPlanExpirationDate === undefined) - ); -} - -function isUpgradeUserAccountByEmailProps( - props: UpdateUserAccountProps, -): props is UpgradeUserAccountByEmailProps { - return ( - props.upgrade === true && - 'email' in props && - typeof props.email === 'string' && - !!props.plan && - (props.proPlanExpirationDate instanceof Date || - props.proPlanExpirationDate === undefined) - ); -} - -function isDowngradeUserAccountProps( - props: UpdateUserAccountProps, -): props is DowngradeUserAccountProps { - return ( - props.upgrade === false && - 'stripeSubscriptionId' in props && - typeof props.stripeSubscriptionId === 'string' && - (props.proPlanExpirationDate === undefined || - props.proPlanExpirationDate instanceof Date) - ); -} - -function isDowngradeUserAccountByEmailProps( - props: UpdateUserAccountProps, -): props is DowngradeUserAccountByEmailProps { - return ( - props.upgrade === false && - 'email' in props && - typeof props.email === 'string' && - (props.proPlanExpirationDate === undefined || - props.proPlanExpirationDate instanceof Date) - ); -} - -function isExtendMembershipProps( - props: UpdateUserAccountProps, -): props is ExtendMemberShipProps { - return ( - (props.upgrade === true && - typeof props.stripeSubscriptionId === 'string' && - !Object.keys(props).includes('plan') && - props.proPlanExpirationDate instanceof Date) || - props.proPlanExpirationDate === undefined - ); -} From 5b77027f4161398e554e137dad29fb52c811ca35 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:09:00 +0800 Subject: [PATCH 034/134] refactor: Update handleCheckoutSessionCompleted to use strip_helper functions for updating user account --- .../stripe/handleCheckoutSessionCompleted.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index ec493fe6f5..d240a69243 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -13,7 +13,10 @@ import { getAdminSupabaseClient, userProfileQuery, } from '../supabase'; -import updateUserAccount from './updateUserAccount'; +import { + updateUserAccountByEmail, + updateUserAccountById, +} from './strip_helper'; import dayjs from 'dayjs'; import Stripe from 'stripe'; @@ -23,7 +26,6 @@ const supabase = getAdminSupabaseClient(); export default async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, ): Promise { - console.log('handleCheckoutSessionCompleted'); const userId = session.client_reference_id; const email = session.customer_details?.email; @@ -93,19 +95,17 @@ export default async function handleCheckoutSessionCompleted( // Update user account by User id if (userId) { - await updateUserAccount({ - upgrade: true, - plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), + await updateUserAccountById({ userId, + plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), stripeSubscriptionId, proPlanExpirationDate: proPlanExpirationDate, }); } else { // Update user account by Email - await updateUserAccount({ - upgrade: true, - plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), + await updateUserAccountByEmail({ email: email!, + plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), stripeSubscriptionId, proPlanExpirationDate: proPlanExpirationDate, }); From 8e801fb898e4d02f6e84e217075d123427d4261e Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:09:04 +0800 Subject: [PATCH 035/134] refactor: Update handleCustomerSubscriptionUpdated to use strip_helper functions for updating user account --- .../handleCustomerSubscriptionUpdated.ts | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts index 557cd8ea8e..c55bbbb7f8 100644 --- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts +++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts @@ -1,5 +1,7 @@ -import getCustomerEmailByCustomerID from './strip_helper'; -import updateUserAccount from './updateUserAccount'; +import getCustomerEmailByCustomerID, { + downgradeUserAccount, + extendMembershipByStripeSubscriptionId, +} from './strip_helper'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; @@ -12,42 +14,48 @@ export default async function handleCustomerSubscriptionUpdated( ): Promise { const stripeSubscriptionId = session.id; - if (!session.cancel_at) { - return; - } + console.log('handleCustomerSubscriptionUpdated'); + console.log({ session }); + + const currentPeriodStart = dayjs + .unix(session.current_period_start) + .utc() + .toDate(); + const currentPeriodEnd = dayjs + .unix(session.current_period_end) + .utc() + .toDate(); - const cancelAtDate = dayjs.unix(session.cancel_at!).utc().toDate(); + console.log({ + currentPeriodStart, + currentPeriodEnd, + }); + const cancelAtDate = session.cancel_at + ? dayjs.unix(session.cancel_at).utc().toDate() + : null; const today = dayjs().utc().toDate(); - if (cancelAtDate < today) { + if (cancelAtDate && cancelAtDate < today) { // Downgrade to free plan if (!stripeSubscriptionId) { const customerId = session.customer as string; const email = await getCustomerEmailByCustomerID(customerId); - await updateUserAccount({ - upgrade: false, + await downgradeUserAccount({ email, }); } else { - await updateUserAccount({ - upgrade: false, + await downgradeUserAccount({ stripeSubscriptionId, }); } } else { - // Monthly Pro Plan Subscription recurring payment, extend expiration date + // Monthly Pro / Ultra Plan Subscription recurring payment, extend expiration date if (!stripeSubscriptionId) { - const customerId = session.customer as string; - const email = await getCustomerEmailByCustomerID(customerId); - await updateUserAccount({ - upgrade: true, - email, - }); + throw new Error('Stripe subscription ID not found'); } else { - await updateUserAccount({ - upgrade: true, + await extendMembershipByStripeSubscriptionId({ stripeSubscriptionId, - proPlanExpirationDate: undefined, + proPlanExpirationDate: currentPeriodEnd, }); } } From ea625c4fb42ad480942ee2fea59c6e705b8ad70a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:09:14 +0800 Subject: [PATCH 036/134] refactor: Update handleCustomerSubscriptionDeleted to use downgradeUserAccount from strip_helper --- .../stripe/handleCustomerSubscriptionDeleted.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts index cc07ce80d3..e22af2c2f5 100644 --- a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts +++ b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts @@ -1,5 +1,6 @@ -import getCustomerEmailByCustomerID from './strip_helper'; -import updateUserAccount from './updateUserAccount'; +import getCustomerEmailByCustomerID, { + downgradeUserAccount, +} from './strip_helper'; import Stripe from 'stripe'; @@ -11,13 +12,11 @@ export default async function handleCustomerSubscriptionDeleted( if (!stripeSubscriptionId) { const customerId = session.customer as string; const email = await getCustomerEmailByCustomerID(customerId); - await updateUserAccount({ - upgrade: false, + await downgradeUserAccount({ email, }); } else { - await updateUserAccount({ - upgrade: false, + await downgradeUserAccount({ stripeSubscriptionId, }); } From c87f6c48848bb0b6bd383a1127a46bbda3534eb6 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:23:27 +0800 Subject: [PATCH 037/134] refactor: Update handleCustomerSubscriptionDeleted to use downgradeUserAccount from strip_helper --- components/User/Settings/PlanComparison.tsx | 8 ++++---- pages/api/webhooks/stripe.ts | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 2796912e92..b765b97142 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -63,7 +63,7 @@ const FreePlanContent = ({ user }: { user: User | null }) => { <>
Free - {(user?.plan === 'free' || !user) && } + {(user?.plan === 'free' || !user) && }
{PlanDetail.free.features.map((feature, index) => ( @@ -116,7 +116,7 @@ const ProPlanContent = ({ user }: { user: User | null }) => { > Pro - {user?.plan === 'pro' && } + {user?.plan === 'pro' && }
@@ -206,7 +206,7 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { > Ultra - {user?.plan === 'ultra' && } + {user?.plan === 'ultra' && }
@@ -242,7 +242,7 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { ); }; -const CurrentTag = () => { +const CurrentPlanTag = () => { return ( CURRENT PLAN diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index f75bff0a4a..871443aa65 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -44,18 +44,21 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { switch (event.type) { case 'checkout.session.completed': // One time payment / Initial Monthly Pro / Ultra Plan Subscription + console.log('✅ checkout.session.completed'); await handleCheckoutSessionCompleted( event.data.object as Stripe.Checkout.Session, ); break; case 'customer.subscription.updated': // Monthly Pro / Ultra Plan Subscription recurring payment + console.log('✅ customer.subscription.updated'); await handleCustomerSubscriptionUpdated( event.data.object as Stripe.Subscription, ); break; case 'customer.subscription.deleted': // Monthly Pro Plan Subscription removal + console.log('✅ customer.subscription.deleted'); await handleCustomerSubscriptionDeleted( event.data.object as Stripe.Subscription, ); From ad40b13137e8c4183e4072a1a42d9dc48afaf73a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:53:46 +0800 Subject: [PATCH 038/134] refactor: Add support for monthly and yearly payment options in Chinese localization --- components/User/Settings/PlanComparison.tsx | 6 +++--- public/locales/zh-Hant/model.json | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index b765b97142..ae288f3386 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -267,7 +267,7 @@ const UltraPlanPrice = ({ }: { setPriceType: (type: 'monthly' | 'yearly') => void; }) => { - const { i18n } = useTranslation('model'); + const { t, i18n } = useTranslation('model'); return ( @@ -279,7 +279,7 @@ const UltraPlanPrice = ({ setPriceType('monthly'); }} > - MONTHLY + {t('MONTHLY')} - YEARLY + {t('YEARLY')} diff --git a/public/locales/zh-Hant/model.json b/public/locales/zh-Hant/model.json index b6d754846f..22544c6f81 100644 --- a/public/locales/zh-Hant/model.json +++ b/public/locales/zh-Hant/model.json @@ -234,5 +234,7 @@ "Error deleting file": "檔案刪除失敗", "Download failed!": "下載失敗", "Downloading...": "下載中...", - "Please sign-up before upgrading to paid plan": "請在註冊後再升級到付費方案" + "Please sign-up before upgrading to paid plan": "請在註冊後再升級到付費方案", + "MONTHLY": "月費", + "YEARLY": "年費" } From 246896baf8d72285b0c698594b805f23cf2b7037 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:54:02 +0800 Subject: [PATCH 039/134] feat: Add logic to handle one-time plan purchase for users already on a paid subscription plan --- .../stripe/handleCheckoutSessionCompleted.ts | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index d240a69243..e5ffec278c 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -69,12 +69,34 @@ export default async function handleCheckoutSessionCompleted( // # REQUEST: Upgrade plan return await (async () => { const sessionCreatedDate = dayjs.unix(session.created).utc().toDate(); - // Retrieve user profile using email + const userIsInPaidPlan = user.plan !== 'free' && user.plan !== 'edu'; + const isBuyingOneTimePlan = + planCode === PaidPlan.ProOneTime || planCode === PaidPlan.UltraOneTime; + if (userIsInPaidPlan && isBuyingOneTimePlan) { + throw new Error( + 'One-time plan purchase is disallowed for users already on a paid subscription plan', + { + cause: { + user, + }, + }, + ); + } + if (userIsInPaidPlan) { + throw new Error( + 'User is already in a paid plan, cannot purchase a new plan, should issue an refund', + { + cause: { + user, + }, + }, + ); + } - const proPlanExpirationDate = await getExtendedMembershipExpirationDate( + // Extend membership expiration date if user has a pro plan expiration date already + const proPlanExpirationDate = await calculateMembershipExpirationDate( planGivingWeeks, planCode, - user, sessionCreatedDate, ); @@ -113,17 +135,12 @@ export default async function handleCheckoutSessionCompleted( })(); } -async function getExtendedMembershipExpirationDate( +async function calculateMembershipExpirationDate( planGivingWeeks: string | undefined, planCode: string | undefined, - user: UserProfile, sessionCreatedDate: Date, ): Promise { - const userProPlanExpirationDate = user?.proPlanExpirationDate; - - const previousDate = dayjs( - userProPlanExpirationDate || sessionCreatedDate || undefined, - ); + const previousDate = dayjs(sessionCreatedDate || undefined); // If has planGivingWeeks, use it to calculate the expiration date if (planGivingWeeks && typeof planGivingWeeks === 'string') { return previousDate.add(+planGivingWeeks, 'week').toDate(); From 27381da9173da4be070a9e0be9e105033a1dbd6e Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 12:58:18 +0800 Subject: [PATCH 040/134] refactor: Update payment links for staging --- utils/app/stripe_config.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 3964c7102b..db85232389 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -59,13 +59,28 @@ export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD = // =========== TOP UP LINKS =========== export const GPT4_CREDIT_PURCHASE_LINKS = { - '50': 'https://buy.stripe.com/28o03Z0vE3Glak09AJ', - '150': 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW', - '300': 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn', + '50': + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/28o03Z0vE3Glak09AJ' + : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b', + '150': + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW' + : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b', + '300': + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn' + : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b', }; export const AI_IMAGE_CREDIT_PURCHASE_LINKS = { - '100': 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT', - '500': 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ', + '100': + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT' + : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b', + '500': + process.env.NEXT_PUBLIC_ENV === 'production' + ? 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ' + : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b', }; // =========== V2 UPGRADE LINKS =========== From 30be37fea9f0fbba2a5dacb807502ca1eea0325d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 13:12:23 +0800 Subject: [PATCH 041/134] fix: Calculate membership expiration date for Ultra Yearly plan --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index e5ffec278c..9c27680004 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -108,7 +108,7 @@ export default async function handleCheckoutSessionCompleted( }); if (!proPlanExpirationDate) { - throw new Error('undefined extended membership expiration date', { + throw new Error('calculate membership expiration date: undefined ', { cause: { user, }, @@ -154,6 +154,8 @@ async function calculateMembershipExpirationDate( return previousDate.add(1, 'month').toDate(); } else if (planCode === PaidPlan.UltraMonthly) { return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.UltraYearly) { + return previousDate.add(1, 'year').toDate(); } // Return undefined if no conditions are met return undefined; From e4dc0c5499ee40f0f681dd43257266ecb1034308 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 13:12:35 +0800 Subject: [PATCH 042/134] refactor: Update getSubscriptionPlanByPaidPlan to handle Ultra Yearly plan --- utils/app/paid_plan_helper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index 4735a30e39..1b56abb93f 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -41,6 +41,8 @@ export const getSubscriptionPlanByPaidPlan = ( return 'pro'; case PaidPlan.ProOneTime: return 'pro'; + case PaidPlan.UltraYearly: + return 'ultra'; case PaidPlan.UltraMonthly: return 'ultra'; case PaidPlan.UltraOneTime: From 4d816e9e2f949452548984479d7231ac59902b71 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 13:18:35 +0800 Subject: [PATCH 043/134] refactor: Remove console.log statements from handleCustomerSubscriptionUpdated function --- utils/server/stripe/handleCustomerSubscriptionUpdated.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts index c55bbbb7f8..4c291f88a1 100644 --- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts +++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts @@ -14,9 +14,6 @@ export default async function handleCustomerSubscriptionUpdated( ): Promise { const stripeSubscriptionId = session.id; - console.log('handleCustomerSubscriptionUpdated'); - console.log({ session }); - const currentPeriodStart = dayjs .unix(session.current_period_start) .utc() From a586f0278d0876b2ac0132e52a4bb6ca47897edd Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 14:17:12 +0800 Subject: [PATCH 044/134] refactor: Update PlanComparison component to improve UI and readability --- components/User/Settings/PlanComparison.tsx | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index ae288f3386..7a2c5c983a 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -18,6 +18,7 @@ import { User } from '@/types/user'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/lib/utils'; import dayjs from 'dayjs'; const PlanComparison = ({ user }: { user: User | null }) => { @@ -51,8 +52,10 @@ const PlanExpirationDate: React.FC<{ expirationDate: string }> = ({ }); const { t } = useTranslation('model'); return ( -
- {`${t('Expires on')}: ${dayjs(expirationDate).format('ll')}`} +
+
+ {`${t('Expires on')}: ${dayjs(expirationDate).format('ll')}`} +
); }; @@ -143,10 +146,9 @@ const ProPlanContent = ({ user }: { user: User | null }) => {
)} - {(user?.plan === 'pro' || user?.plan === 'ultra') && - user.proPlanExpirationDate && ( - - )} + {user?.plan === 'pro' && user.proPlanExpirationDate && ( + + )} ); }; @@ -208,7 +210,7 @@ const UltraPlanContent = ({ user }: { user: User | null }) => {
{user?.plan === 'ultra' && }
- + {user?.plan !== 'ultra' && }
@@ -234,10 +236,9 @@ const UltraPlanContent = ({ user }: { user: User | null }) => {
)} - {(user?.plan === 'pro' || user?.plan === 'ultra') && - user.proPlanExpirationDate && ( - - )} + {user?.plan === 'ultra' && user.proPlanExpirationDate && ( + + )} ); }; From d3ab5dfd51c65832041bad2a31e78bd6835a589c Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 14:22:21 +0800 Subject: [PATCH 045/134] refactor: Improve UI and readability of PlanComparison component --- components/User/Settings/PlanComparison.tsx | 10 +++++----- public/locales/zh-Hans/model.json | 1 + public/locales/zh-Hant/model.json | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 7a2c5c983a..b1fc6851d1 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -131,12 +131,12 @@ const ProPlanContent = ({ user }: { user: User | null }) => {
{/* Upgrade button */} {showUpgradeToPro && ( -
+
{t('Upgrade')} @@ -221,14 +221,14 @@ const UltraPlanContent = ({ user }: { user: User | null }) => {
{/* Upgrade button */} {showUpgradeToUltra && ( -
+
- {t('Upgrade')} + {t('Upgrade to Ultra')}

{t('No Strings Attached - Cancel Anytime!')} diff --git a/public/locales/zh-Hans/model.json b/public/locales/zh-Hans/model.json index a67d587bb7..3604280902 100644 --- a/public/locales/zh-Hans/model.json +++ b/public/locales/zh-Hans/model.json @@ -46,6 +46,7 @@ "Please sign-up before upgrading to pro plan": "请先注册和登入帐号", "Upgrade": "升级为专业版", "Upgrade for one month only": "只购买一个月的专业版", + "Upgrade to Ultra": "升級到Ultra专业版", "Unlock all the amazing features by upgrading to our Pro plan, cancel anytime!": "升级为专业版,解锁以下所有功能!", "AI Image": "AI图像生成", "Creative": "创意", diff --git a/public/locales/zh-Hant/model.json b/public/locales/zh-Hant/model.json index 22544c6f81..40017bd361 100644 --- a/public/locales/zh-Hant/model.json +++ b/public/locales/zh-Hant/model.json @@ -44,6 +44,7 @@ "Please sign-up before upgrading to pro plan": "請先註冊和登入帳號", "Upgrade": "升級到Pro專業版", "Upgrade for one month only": "只購買一個月的Pro專業版", + "Upgrade to Ultra": "升級到Ultra專業版", "Unlock all the amazing features by upgrading to our Pro plan, cancel anytime!": "升級為專業版,解鎖以下的所有功能!", "Creative": "創意", "Balanced": "平衡", From 10b18df4a4684f283dc1ce17a1db300959853bbe Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 17:46:40 +0800 Subject: [PATCH 046/134] refactor: update the stripe product type format --- components/User/Settings/PlanComparison.tsx | 46 ++-- types/paid_plan.ts | 1 + utils/app/const.ts | 12 +- utils/app/paid_plan_helper.ts | 25 +- utils/app/stripe_config.ts | 261 ++++++++++++++++---- 5 files changed, 262 insertions(+), 83 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index b1fc6851d1..51fa8284e3 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -2,15 +2,9 @@ import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { - OrderedSubscriptionPlans, - PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD, - PRO_MONTHLY_PLAN_PAYMENT_LINK_USD, - ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD, - ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD, - ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD, - ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD, -} from '@/utils/app/const'; +import { useChangeSubscriptionPlan } from '@/hooks/useChangeSubscriptionPlan'; + +import { OrderedSubscriptionPlans, STRIPE_PRODUCTS } from '@/utils/app/const'; import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; @@ -84,11 +78,18 @@ const ProPlanContent = ({ user }: { user: User | null }) => { const proPlanIndex = OrderedSubscriptionPlans.indexOf('pro'); return userPlanIndex < proPlanIndex; }, [user]); + const isPaidUser = user?.plan === 'pro' || user?.plan === 'ultra'; + const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); + const upgradeLinkOnClick = () => { + if (isPaidUser) { + changeSubscriptionPlan(); + return; + } const paymentLink = i18n.language === 'zh-Hant' || i18n.language === 'zh' - ? PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD - : PRO_MONTHLY_PLAN_PAYMENT_LINK_USD; + ? STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.currencies.TWD.link + : STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.currencies.USD.link; const userEmail = user?.email; const userId = user?.id; @@ -163,19 +164,32 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { return userPlanIndex < ultraPlanIndex; }, [user]); + const isPaidUser = user?.plan === 'pro' || user?.plan === 'ultra'; + const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); + const upgradeLinkOnClick = () => { - let paymentLink = ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD; + if (isPaidUser) { + changeSubscriptionPlan(); + return; + } + + let paymentLink = + STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.USD.link; if (priceType === 'monthly') { if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { - paymentLink = ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD; + paymentLink = + STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.TWD.link; } else { - paymentLink = ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD; + paymentLink = + STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.USD.link; } } else { if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { - paymentLink = ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD; + paymentLink = + STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.yearly.currencies.TWD.link; } else { - paymentLink = ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD; + paymentLink = + STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.yearly.currencies.USD.link; } } diff --git a/types/paid_plan.ts b/types/paid_plan.ts index bce0056af9..7c3e04e39e 100644 --- a/types/paid_plan.ts +++ b/types/paid_plan.ts @@ -1,6 +1,7 @@ export enum PaidPlan { ProMonthly = 'pro-monthly', ProOneTime = 'pro-one-time', + ProYearly = 'pro-yearly', UltraMonthly = 'ultra-monthly', UltraOneTime = 'ultra-one-time', UltraYearly = 'ultra-yearly', diff --git a/utils/app/const.ts b/utils/app/const.ts index e8a9b83b45..152691a0d1 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -5,19 +5,9 @@ import dayjs from 'dayjs'; import { v4 as uuidv4 } from 'uuid'; export { + STRIPE_PRODUCTS, STRIPE_PLAN_CODE_GPT4_CREDIT, STRIPE_PLAN_CODE_IMAGE_CREDIT, - STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION, - STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION, - STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION, - STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, - STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH, - PRO_MONTHLY_PLAN_PAYMENT_LINK_USD, - ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD, - PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD, - ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD, - ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD, - ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD, GPT4_CREDIT_PURCHASE_LINKS, AI_IMAGE_CREDIT_PURCHASE_LINKS, V2_CHAT_UPGRADE_LINK, diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index 1b56abb93f..3ab8abbb3b 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -3,27 +3,30 @@ import { PaidPlan, SubscriptionPlan, TopUpRequest } from '@/types/paid_plan'; import { STRIPE_PLAN_CODE_GPT4_CREDIT, STRIPE_PLAN_CODE_IMAGE_CREDIT, - STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION, - STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION, - STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH, - STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH, - STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION, + STRIPE_PRODUCTS, } from './const'; export const getPaidPlan = ( planCode: string, ): PaidPlan | TopUpRequest | undefined => { switch (planCode.toUpperCase()) { - case STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION.toUpperCase(): + case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.plan_code.toUpperCase(): return PaidPlan.ProMonthly; - case STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH.toUpperCase(): + case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro[ + 'one-time' + ].plan_code.toUpperCase(): return PaidPlan.ProOneTime; - case STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION.toUpperCase(): + case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro['yearly'].plan_code.toUpperCase(): + return PaidPlan.ProYearly; + case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.plan_code.toUpperCase(): return PaidPlan.UltraMonthly; - case STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION.toUpperCase(): + case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.yearly.plan_code.toUpperCase(): return PaidPlan.UltraYearly; - case STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH.toUpperCase(): + case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra[ + 'one-time' + ].plan_code.toUpperCase(): return PaidPlan.UltraOneTime; + case STRIPE_PLAN_CODE_IMAGE_CREDIT.toUpperCase(): return TopUpRequest.ImageCredit; case STRIPE_PLAN_CODE_GPT4_CREDIT.toUpperCase(): @@ -41,6 +44,8 @@ export const getSubscriptionPlanByPaidPlan = ( return 'pro'; case PaidPlan.ProOneTime: return 'pro'; + case PaidPlan.ProYearly: + return 'pro'; case PaidPlan.UltraYearly: return 'ultra'; case PaidPlan.UltraMonthly: diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index db85232389..487e025fac 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -1,61 +1,230 @@ -// P.S. All of the code below is used in the product payment link - // STRIPE CREDIT CODE export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; -// STRIPE MONTHLY PLAN CODE -export const STRIPE_PLAN_CODE_MONTHLY_PRO_PLAN_SUBSCRIPTION = - 'monthly_pro_plan_subscription'; -export const STRIPE_PLAN_CODE_MONTHLY_ULTRA_PLAN_SUBSCRIPTION = - 'monthly_ultra_plan_subscription'; - -// STRIPE YEARLY PLAN CODE -export const STRIPE_PLAN_CODE_YEARLY_ULTRA_PLAN_SUBSCRIPTION = - 'yearly_ultra_plan_subscription'; - -// STRIPE ONE TIME PLAN CODE -export const STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH = - 'one_time_pro_plan_for_1_month'; - -// (Not in used) -export const STRIPE_PLAN_CODE_ONE_TIME_ULTRA_PLAN_FOR_1_MONTH = - 'one_time_ultra_plan_for_1_month'; - // =========== PRO PLAN LINKS =========== // PRO MONTHLY PLAN -export const PRO_MONTHLY_PLAN_PAYMENT_LINK_USD = - process.env.NEXT_PUBLIC_ENV === 'production' - ? 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1' - : 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY'; +type MemberShipPlanPeriodType = 'monthly' | 'yearly' | 'one-time'; +type MemberShipPlanCurrencyType = 'USD' | 'TWD'; -export const PRO_MONTHLY_PLAN_PAYMENT_LINK_TWD = - process.env.NEXT_PUBLIC_ENV === 'production' - ? '' // TODO: Update the production link - : 'https://buy.stripe.com/test_00gcOhdzucvg1Ko00e'; +// P.S. All of the code below is used in the product payment link +type PlanCode = + | 'one_time_pro_plan_for_1_month' + | 'one_time_ultra_plan_for_1_month' + | 'monthly_pro_plan_subscription' + | 'monthly_ultra_plan_subscription' + | 'yearly_pro_plan_subscription' + | 'yearly_ultra_plan_subscription'; -// =========== ULTRA PLAN LINKS =========== -// ULTRA MONTHLY PLAN -export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_USD = - process.env.NEXT_PUBLIC_ENV === 'production' - ? '' // TODO: Update the production link - : 'https://buy.stripe.com/test_6oEdSl67266SexafZ9'; +interface MemberShipPlanItem { + link: string; + price_id: string; +} -export const ULTRA_MONTHLY_PLAN_PAYMENT_LINK_TWD = - process.env.NEXT_PUBLIC_ENV === 'production' - ? '' // TODO: Update the production link - : 'https://buy.stripe.com/test_00gcOhbrmgLwbkYdR0'; +interface PlanDetails { + plan_code: PlanCode; + currencies: { + [currency in MemberShipPlanCurrencyType]: MemberShipPlanItem; + }; +} -// ULTRA YEARLY PLAN -export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_USD = - process.env.NEXT_PUBLIC_ENV === 'production' - ? '' // TODO: Update the production link - : 'https://buy.stripe.com/test_14k7tX7b6brcexa4gs'; +interface MemberShipPlan { + pro: { + [period in MemberShipPlanPeriodType]: PlanDetails; + }; + ultra: { + [period in MemberShipPlanPeriodType]: PlanDetails; + }; +} +interface StripeProduct { + MEMBERSHIP_PLAN: MemberShipPlan; +} + +const STRIPE_PRODUCTS_PRODUCTION: StripeProduct = { + MEMBERSHIP_PLAN: { + pro: { + monthly: { + // META DATA use in the payment link + plan_code: 'monthly_pro_plan_subscription', + currencies: { + USD: { + link: 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + // NOTE: NOT IN USED IN APP + 'one-time': { + plan_code: 'one_time_pro_plan_for_1_month', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + // NOTE: NOT IN USED IN APP + yearly: { + plan_code: 'yearly_pro_plan_subscription', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + }, + ultra: { + 'one-time': { + plan_code: 'one_time_ultra_plan_for_1_month', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + monthly: { + plan_code: 'monthly_ultra_plan_subscription', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + yearly: { + plan_code: 'yearly_ultra_plan_subscription', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + }, + }, +}; + +const STRIPE_PRODUCTS_STAGING: StripeProduct = { + MEMBERSHIP_PLAN: { + pro: { + // NOTE: NOT IN USED IN APP + 'one-time': { + plan_code: 'one_time_pro_plan_for_1_month', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + monthly: { + // META DATA use in the payment link + plan_code: 'monthly_pro_plan_subscription', + currencies: { + USD: { + link: 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY', + price_id: 'price_1N09fTEEvfd1BzvuJwBCAfg2', + }, + TWD: { + link: 'https://buy.stripe.com/test_6oE01v1QM66S74I7sH', + price_id: 'price_1PLhJREEvfd1BzvuxCM477DD', + }, + }, + }, + // NOTE: NOT IN USED IN APP + yearly: { + plan_code: 'yearly_pro_plan_subscription', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + }, + ultra: { + // NOTE: NOT IN USED IN APP + 'one-time': { + plan_code: 'one_time_ultra_plan_for_1_month', + currencies: { + USD: { + link: '', + price_id: '', + }, + TWD: { + link: '', + price_id: '', + }, + }, + }, + monthly: { + plan_code: 'monthly_ultra_plan_subscription', + currencies: { + USD: { + link: 'https://buy.stripe.com/test_cN29C5dzu8f0dt6fZe', + price_id: 'price_1PLhlhEEvfd1Bzvu0UEqwm9y', + }, + TWD: { + link: 'https://buy.stripe.com/test_fZe6pT1QM1QC2Os6oF', + price_id: 'price_1PLiWBEEvfd1BzvunVr1yZ55', + }, + }, + }, + yearly: { + plan_code: 'yearly_ultra_plan_subscription', + currencies: { + USD: { + link: 'https://buy.stripe.com/test_3csaG952Y2UG74IfZg', + price_id: 'price_1PLiWmEEvfd1BzvuDFmiLKI6', + }, + TWD: { + link: 'https://buy.stripe.com/test_8wM9C5fHCan8agUdR9', + price_id: 'price_1PLiWVEEvfd1Bzvu7voi21Jw', + }, + }, + }, + }, + }, +}; -export const ULTRA_YEARLY_PLAN_PAYMENT_LINK_TWD = +export const STRIPE_PRODUCTS = process.env.NEXT_PUBLIC_ENV === 'production' - ? '' // TODO: Update the production link - : 'https://buy.stripe.com/test_eVa15z6720Myexa7sF'; + ? STRIPE_PRODUCTS_PRODUCTION + : STRIPE_PRODUCTS_STAGING; // =========== TOP UP LINKS =========== export const GPT4_CREDIT_PURCHASE_LINKS = { From fb28020a923231138c9fdf808f4eac62ca6b60a4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 17:46:54 +0800 Subject: [PATCH 047/134] refactor: Add API endpoint for changing subscription plan --- hooks/useChangeSubscriptionPlan.ts | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 hooks/useChangeSubscriptionPlan.ts diff --git a/hooks/useChangeSubscriptionPlan.ts b/hooks/useChangeSubscriptionPlan.ts new file mode 100644 index 0000000000..ed858f4d96 --- /dev/null +++ b/hooks/useChangeSubscriptionPlan.ts @@ -0,0 +1,38 @@ +import { useSupabaseClient } from '@supabase/auth-helpers-react'; +import { useMutation } from '@tanstack/react-query'; +import { useContext } from 'react'; + +import HomeContext from '@/components/home/home.context'; + +export const useChangeSubscriptionPlan = () => { + const supabase = useSupabaseClient(); + const { + state: { user }, + } = useContext(HomeContext); + + const changeSubscriptionPlan = async () => { + if (!user) { + throw new Error('User is not authenticated'); + } + const accessToken = (await supabase.auth.getSession()).data.session + ?.access_token!; + const response = await fetch('/api/stripe/change-subscription-plan', { + method: 'POST', + headers: { + 'access-token': accessToken, + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + return data.subscription; + }; + + return useMutation(changeSubscriptionPlan, { + onError: (error) => { + console.error('There was a problem with your mutation operation:', error); + }, + }); +}; From 9d89727a757166e1bb9ed45f4ccadde8a3015270 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 17:46:58 +0800 Subject: [PATCH 048/134] refactor: Add API endpoint for changing subscription plan --- pages/api/stripe/change-subscription-plan.ts | 66 ++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 pages/api/stripe/change-subscription-plan.ts diff --git a/pages/api/stripe/change-subscription-plan.ts b/pages/api/stripe/change-subscription-plan.ts new file mode 100644 index 0000000000..4e9e8b7855 --- /dev/null +++ b/pages/api/stripe/change-subscription-plan.ts @@ -0,0 +1,66 @@ +import { fetchUserProfileWithAccessToken } from '@/utils/server/auth'; +import { fetchSubscriptionIdByUserId } from '@/utils/server/stripe/strip_helper'; + +import Stripe from 'stripe'; + +export const config = { + runtime: 'edge', +}; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2022-11-15', +}); + +const handler = async (req: Request) => { + if (req.method !== 'POST') { + return new Response('Method Not Allowed', { status: 405 }); + } + + try { + // Step 1: Get User Profile and Subscription ID + const userProfile = await fetchUserProfileWithAccessToken(req); + if (userProfile.plan !== 'pro' && userProfile.plan !== 'ultra') { + throw new Error('User does not have a paid plan'); + } + const subscriptionId = await fetchSubscriptionIdByUserId(userProfile.id); + if (!subscriptionId) { + throw new Error('User does not have a valid subscription in Stripe'); + } + + // Step 2: Retrieve Current Subscription + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + console.log({ + subscription, + }); + + // // Step 3: Calculate Proration + // const prorationDate = Math.floor(Date.now() / 1000); + + // // Step 4: Update Subscription + // const updatedSubscription = await stripe.subscriptions.update( + // subscriptionId, + // { + // items: [ + // { + // id: subscription.items.data[0].id, + // price: newPlanId, + // }, + // ], + // proration_behavior: 'create_prorations', + // proration_date: prorationDate, + // }, + // ); + + // Step 5: Notify User + // In a real-world application, you might send an email or in-app notification here + // For this example, we'll just return the updated subscription details + return new Response(JSON.stringify({ subscription: subscription }), { + status: 200, + }); + } catch (error) { + console.error(error); + return new Response('Internal Server Error', { status: 500 }); + } +}; + +export default handler; From 97406b784cd1fd928fb84b206e30a2422c46c5ac Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 17:47:05 +0800 Subject: [PATCH 049/134] refactor: Update handleCustomerSubscriptionDeleted to use getCustomerEmailByCustomerID from strip_helper --- utils/server/stripe/handleCustomerSubscriptionDeleted.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts index e22af2c2f5..6b74093543 100644 --- a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts +++ b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts @@ -1,5 +1,6 @@ -import getCustomerEmailByCustomerID, { +import { downgradeUserAccount, + getCustomerEmailByCustomerID, } from './strip_helper'; import Stripe from 'stripe'; From 4505d49909a9152d8275385f58d485a398b7ac11 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 17:47:10 +0800 Subject: [PATCH 050/134] refactor: Update handleCustomerSubscriptionUpdated to use getCustomerEmailByCustomerID from strip_helper --- utils/server/stripe/handleCustomerSubscriptionUpdated.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts index 4c291f88a1..ef3098e904 100644 --- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts +++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts @@ -1,6 +1,7 @@ -import getCustomerEmailByCustomerID, { +import { downgradeUserAccount, extendMembershipByStripeSubscriptionId, + getCustomerEmailByCustomerID, } from './strip_helper'; import dayjs from 'dayjs'; From e72ed64a4e45fc1e0689f9a00efbd3f0c049a6f8 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Wed, 29 May 2024 17:47:20 +0800 Subject: [PATCH 051/134] refactor: Update getCustomerEmailByCustomerID function in strip_helper --- utils/server/stripe/strip_helper.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 04b4b042e4..0246ba770f 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -4,7 +4,18 @@ import Stripe from 'stripe'; const supabase = getAdminSupabaseClient(); -export default async function getCustomerEmailByCustomerID( +export async function fetchSubscriptionIdByUserId( + userId: string, +): Promise { + const { data: userProfile } = await supabase + .from('profiles') + .select('stripe_subscription_id') + .eq('id', userId) + .single(); + return userProfile?.stripe_subscription_id; +} + +export async function getCustomerEmailByCustomerID( customerID: string, ): Promise { try { From 5465e0570621d198920acc15178cda3cbceeddae Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 10:33:16 +0800 Subject: [PATCH 052/134] refactor: Improve PlanComparison component UI and readability --- components/User/Settings/PlanComparison.tsx | 37 ++++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 51fa8284e3..3fe7419418 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -12,7 +12,6 @@ import { User } from '@/types/user'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { cn } from '@/lib/utils'; import dayjs from 'dayjs'; const PlanComparison = ({ user }: { user: User | null }) => { @@ -78,14 +77,9 @@ const ProPlanContent = ({ user }: { user: User | null }) => { const proPlanIndex = OrderedSubscriptionPlans.indexOf('pro'); return userPlanIndex < proPlanIndex; }, [user]); - const isPaidUser = user?.plan === 'pro' || user?.plan === 'ultra'; const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); const upgradeLinkOnClick = () => { - if (isPaidUser) { - changeSubscriptionPlan(); - return; - } const paymentLink = i18n.language === 'zh-Hant' || i18n.language === 'zh' ? STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.currencies.TWD.link @@ -146,6 +140,18 @@ const ProPlanContent = ({ user }: { user: User | null }) => {

)} + {user?.plan === 'ultra' && user.proPlanExpirationDate && ( + + )} {user?.plan === 'pro' && user.proPlanExpirationDate && ( @@ -164,15 +170,9 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { return userPlanIndex < ultraPlanIndex; }, [user]); - const isPaidUser = user?.plan === 'pro' || user?.plan === 'ultra'; const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); const upgradeLinkOnClick = () => { - if (isPaidUser) { - changeSubscriptionPlan(); - return; - } - let paymentLink = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.USD.link; if (priceType === 'monthly') { @@ -250,6 +250,19 @@ const UltraPlanContent = ({ user }: { user: User | null }) => {
)} + {user?.plan === 'pro' && user.proPlanExpirationDate && ( + + )} + {user?.plan === 'ultra' && user.proPlanExpirationDate && ( )} From 06dc18c0cf1af0a7a3b67cd8e1f42285bc2e6dd9 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 10:33:22 +0800 Subject: [PATCH 053/134] refactor: Update localization for Chinese (Simplified) and Chinese (Traditional) --- public/locales/zh-Hans/model.json | 5 ++++- public/locales/zh-Hant/model.json | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/locales/zh-Hans/model.json b/public/locales/zh-Hans/model.json index 3604280902..68d98116aa 100644 --- a/public/locales/zh-Hans/model.json +++ b/public/locales/zh-Hans/model.json @@ -234,5 +234,8 @@ "Error deleting file": "文件删除失败", "Download failed!": "下载失败", "Downloading...": "下载中...", - "Please sign-up before upgrading to paid plan": "请在注册后再升级到付费方案" + "Please sign-up before upgrading to paid plan": "请在注册后再升级到付费方案", + "MONTHLY": "月费", + "YEARLY": "年费", + "Chat with document": "文档对话" } diff --git a/public/locales/zh-Hant/model.json b/public/locales/zh-Hant/model.json index 40017bd361..8898cb812a 100644 --- a/public/locales/zh-Hant/model.json +++ b/public/locales/zh-Hant/model.json @@ -237,5 +237,6 @@ "Downloading...": "下載中...", "Please sign-up before upgrading to paid plan": "請在註冊後再升級到付費方案", "MONTHLY": "月費", - "YEARLY": "年費" + "YEARLY": "年費", + "Chat with document": "文檔對話" } From d2f6c34ca7a4890c176776cd0c75a2fa927a348b Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 10:33:39 +0800 Subject: [PATCH 054/134] refactor: Update stripe_config.ts to import StripeProduct type from '@/types/stripe-product' --- utils/app/stripe_config.ts | 40 ++------------------------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 487e025fac..9854eb4c89 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -1,45 +1,9 @@ +import { StripeProduct } from '@/types/stripe-product'; + // STRIPE CREDIT CODE export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; -// =========== PRO PLAN LINKS =========== -// PRO MONTHLY PLAN -type MemberShipPlanPeriodType = 'monthly' | 'yearly' | 'one-time'; -type MemberShipPlanCurrencyType = 'USD' | 'TWD'; - -// P.S. All of the code below is used in the product payment link -type PlanCode = - | 'one_time_pro_plan_for_1_month' - | 'one_time_ultra_plan_for_1_month' - | 'monthly_pro_plan_subscription' - | 'monthly_ultra_plan_subscription' - | 'yearly_pro_plan_subscription' - | 'yearly_ultra_plan_subscription'; - -interface MemberShipPlanItem { - link: string; - price_id: string; -} - -interface PlanDetails { - plan_code: PlanCode; - currencies: { - [currency in MemberShipPlanCurrencyType]: MemberShipPlanItem; - }; -} - -interface MemberShipPlan { - pro: { - [period in MemberShipPlanPeriodType]: PlanDetails; - }; - ultra: { - [period in MemberShipPlanPeriodType]: PlanDetails; - }; -} -interface StripeProduct { - MEMBERSHIP_PLAN: MemberShipPlan; -} - const STRIPE_PRODUCTS_PRODUCTION: StripeProduct = { MEMBERSHIP_PLAN: { pro: { From 5b95c054dac34091c70a5020e199bf0b3834c13e Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 10:34:39 +0800 Subject: [PATCH 055/134] refactor: Update stripe product type format --- types/stripe-product.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 types/stripe-product.ts diff --git a/types/stripe-product.ts b/types/stripe-product.ts new file mode 100644 index 0000000000..eb8f5e484f --- /dev/null +++ b/types/stripe-product.ts @@ -0,0 +1,35 @@ +export type MemberShipPlanPeriodType = 'monthly' | 'yearly' | 'one-time'; +export type MemberShipPlanCurrencyType = 'USD' | 'TWD'; + +// P.S. All of the code below is used in the product payment link +export type PlanCode = + | 'one_time_pro_plan_for_1_month' + | 'one_time_ultra_plan_for_1_month' + | 'monthly_pro_plan_subscription' + | 'monthly_ultra_plan_subscription' + | 'yearly_pro_plan_subscription' + | 'yearly_ultra_plan_subscription'; + +export interface MemberShipPlanItem { + link: string; + price_id: string; +} + +export interface PlanDetails { + plan_code: PlanCode; + currencies: { + [currency in MemberShipPlanCurrencyType]: MemberShipPlanItem; + }; +} + +export interface MemberShipPlan { + pro: { + [period in MemberShipPlanPeriodType]: PlanDetails; + }; + ultra: { + [period in MemberShipPlanPeriodType]: PlanDetails; + }; +} +export interface StripeProduct { + MEMBERSHIP_PLAN: MemberShipPlan; +} From 80837ced15f8266cff75fc9b63685fc3769f92b4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 10:36:05 +0800 Subject: [PATCH 056/134] refactor: Update getPaidPlan function name to getDbSubscriptionPlanByPaidPlan --- utils/app/paid_plan_helper.ts | 2 +- utils/server/stripe/handleCheckoutSessionCompleted.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index 3ab8abbb3b..86a37c1056 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -36,7 +36,7 @@ export const getPaidPlan = ( } }; -export const getSubscriptionPlanByPaidPlan = ( +export const getDbSubscriptionPlanByPaidPlan = ( paidPlan: PaidPlan, ): SubscriptionPlan => { switch (paidPlan) { diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 9c27680004..bc5bc83460 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,7 +1,7 @@ import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { + getDbSubscriptionPlanByPaidPlan, getPaidPlan, - getSubscriptionPlanByPaidPlan, } from '@/utils/app/paid_plan_helper'; import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; @@ -119,7 +119,7 @@ export default async function handleCheckoutSessionCompleted( if (userId) { await updateUserAccountById({ userId, - plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), + plan: getDbSubscriptionPlanByPaidPlan(planCode as PaidPlan), stripeSubscriptionId, proPlanExpirationDate: proPlanExpirationDate, }); @@ -127,7 +127,7 @@ export default async function handleCheckoutSessionCompleted( // Update user account by Email await updateUserAccountByEmail({ email: email!, - plan: getSubscriptionPlanByPaidPlan(planCode as PaidPlan), + plan: getDbSubscriptionPlanByPaidPlan(planCode as PaidPlan), stripeSubscriptionId, proPlanExpirationDate: proPlanExpirationDate, }); From 15db06ce605a05464c18e521aa5f152ad3f33c22 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 10:53:32 +0800 Subject: [PATCH 057/134] refactor: Update getPaidPlan function name to getPaidPlanByPlanCode --- utils/app/paid_plan_helper.ts | 25 +++++++++++-------- .../stripe/handleCheckoutSessionCompleted.ts | 4 +-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index 86a37c1056..6bf3d8e086 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -6,27 +6,30 @@ import { STRIPE_PRODUCTS, } from './const'; -export const getPaidPlan = ( +export const getPaidPlanByPlanCode = ( planCode: string, ): PaidPlan | TopUpRequest | undefined => { + const PRO = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro; + const ULTRA = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra; + switch (planCode.toUpperCase()) { - case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.plan_code.toUpperCase(): + // PRO + case PRO['monthly'].plan_code.toUpperCase(): return PaidPlan.ProMonthly; - case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro[ - 'one-time' - ].plan_code.toUpperCase(): + case PRO['one-time'].plan_code.toUpperCase(): return PaidPlan.ProOneTime; - case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro['yearly'].plan_code.toUpperCase(): + case PRO['yearly'].plan_code.toUpperCase(): return PaidPlan.ProYearly; - case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.plan_code.toUpperCase(): + + // ULTRA + case ULTRA['monthly'].plan_code.toUpperCase(): return PaidPlan.UltraMonthly; - case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.yearly.plan_code.toUpperCase(): + case ULTRA['yearly'].plan_code.toUpperCase(): return PaidPlan.UltraYearly; - case STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra[ - 'one-time' - ].plan_code.toUpperCase(): + case ULTRA['one-time'].plan_code.toUpperCase(): return PaidPlan.UltraOneTime; + // TOP UP REQUEST case STRIPE_PLAN_CODE_IMAGE_CREDIT.toUpperCase(): return TopUpRequest.ImageCredit; case STRIPE_PLAN_CODE_GPT4_CREDIT.toUpperCase(): diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index bc5bc83460..79762dc0cd 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,7 +1,7 @@ import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { getDbSubscriptionPlanByPaidPlan, - getPaidPlan, + getPaidPlanByPlanCode, } from '@/utils/app/paid_plan_helper'; import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; @@ -30,7 +30,7 @@ export default async function handleCheckoutSessionCompleted( const email = session.customer_details?.email; const planCode = session.metadata?.plan_code - ? getPaidPlan(session.metadata?.plan_code) + ? getPaidPlanByPlanCode(session.metadata?.plan_code) : undefined; const planGivingWeeks = session.metadata?.plan_giving_weeks; const credit = session.metadata?.credit; From 33345078c868aeef671093406f9d3c2df2b3d441 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 11:19:32 +0800 Subject: [PATCH 058/134] refactor: Extract calculateMembershipExpirationDate function to strip_helper --- .../stripe/handleCheckoutSessionCompleted.ts | 27 +--------------- utils/server/stripe/strip_helper.ts | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 79762dc0cd..faf886be11 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -14,6 +14,7 @@ import { userProfileQuery, } from '../supabase'; import { + calculateMembershipExpirationDate, updateUserAccountByEmail, updateUserAccountById, } from './strip_helper'; @@ -135,32 +136,6 @@ export default async function handleCheckoutSessionCompleted( })(); } -async function calculateMembershipExpirationDate( - planGivingWeeks: string | undefined, - planCode: string | undefined, - sessionCreatedDate: Date, -): Promise { - const previousDate = dayjs(sessionCreatedDate || undefined); - // If has planGivingWeeks, use it to calculate the expiration date - if (planGivingWeeks && typeof planGivingWeeks === 'string') { - return previousDate.add(+planGivingWeeks, 'week').toDate(); - } - // else extend the expiration date based on the plan code - else if (planCode === PaidPlan.ProOneTime) { - return previousDate.add(1, 'month').toDate(); - } else if (planCode === PaidPlan.ProMonthly) { - return previousDate.add(1, 'month').toDate(); - } else if (planCode === PaidPlan.UltraOneTime) { - return previousDate.add(1, 'month').toDate(); - } else if (planCode === PaidPlan.UltraMonthly) { - return previousDate.add(1, 'month').toDate(); - } else if (planCode === PaidPlan.UltraYearly) { - return previousDate.add(1, 'year').toDate(); - } - // Return undefined if no conditions are met - return undefined; -} - async function addCreditToUser( user: UserProfile, credit: number, diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 0246ba770f..632c165a5b 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -1,7 +1,13 @@ +import { PaidPlan } from '@/types/paid_plan'; + import { getAdminSupabaseClient } from '../supabase'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import Stripe from 'stripe'; +dayjs.extend(utc); + const supabase = getAdminSupabaseClient(); export async function fetchSubscriptionIdByUserId( @@ -163,3 +169,29 @@ export async function extendMembershipByStripeSubscriptionId({ `User ${userProfile?.email} with plan ${userProfile?.plan} extended to ${proPlanExpirationDate}`, ); } + +export async function calculateMembershipExpirationDate( + planGivingWeeks: string | undefined, + planCode: string | undefined, + sessionCreatedDate: Date, +): Promise { + const previousDate = dayjs(sessionCreatedDate || undefined); + // If has planGivingWeeks, use it to calculate the expiration date + if (planGivingWeeks && typeof planGivingWeeks === 'string') { + return previousDate.add(+planGivingWeeks, 'week').toDate(); + } + // else extend the expiration date based on the plan code + else if (planCode === PaidPlan.ProOneTime) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.ProMonthly) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.UltraOneTime) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.UltraMonthly) { + return previousDate.add(1, 'month').toDate(); + } else if (planCode === PaidPlan.UltraYearly) { + return previousDate.add(1, 'year').toDate(); + } + // Return undefined if no conditions are met + return undefined; +} From 1e525da5bea5a4f672b1ca48380cfba6fb6714a4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 11:20:05 +0800 Subject: [PATCH 059/134] refactor: Extract getPriceIdByPaidPlan function to paid_plan_helper --- utils/app/paid_plan_helper.ts | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index 6bf3d8e086..a63262ea84 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -1,4 +1,8 @@ import { PaidPlan, SubscriptionPlan, TopUpRequest } from '@/types/paid_plan'; +import { + MemberShipPlanCurrencyType, + MemberShipPlanItem, +} from '@/types/stripe-product'; import { STRIPE_PLAN_CODE_GPT4_CREDIT, @@ -6,6 +10,34 @@ import { STRIPE_PRODUCTS, } from './const'; +export const getPriceIdByPaidPlan = ( + paidPlan: PaidPlan, + currency: MemberShipPlanCurrencyType, +): MemberShipPlanItem['price_id'] | undefined => { + const PRO = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro; + const ULTRA = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra; + + switch (paidPlan) { + // PRO + case PaidPlan.ProMonthly: + return PRO['monthly'].currencies[currency].price_id; + case PaidPlan.ProOneTime: + return PRO['one-time'].currencies[currency].price_id; + case PaidPlan.ProYearly: + return PRO['yearly'].currencies[currency].price_id; + + // ULTRA + case PaidPlan.UltraMonthly: + return ULTRA['monthly'].currencies[currency].price_id; + case PaidPlan.UltraYearly: + return ULTRA['yearly'].currencies[currency].price_id; + case PaidPlan.UltraOneTime: + return ULTRA['one-time'].currencies[currency].price_id; + + default: + return undefined; + } +}; export const getPaidPlanByPlanCode = ( planCode: string, ): PaidPlan | TopUpRequest | undefined => { @@ -59,3 +91,26 @@ export const getDbSubscriptionPlanByPaidPlan = ( return 'free'; } }; + +export const getPaidPlanByPriceId = (priceId: string): PaidPlan | undefined => { + for (const planType of ['pro', 'ultra'] as const) { + for (const period of ['monthly', 'yearly', 'one-time'] as const) { + const planDetails = STRIPE_PRODUCTS.MEMBERSHIP_PLAN[planType][period]; + if (planDetails) { + for (const currency of Object.keys( + planDetails.currencies, + ) as MemberShipPlanCurrencyType[]) { + if (planDetails.currencies[currency].price_id === priceId) { + console.log( + 'current plan', + getPaidPlanByPlanCode(planDetails.plan_code) as PaidPlan, + ); + console.log('current plan currency ', currency); + return getPaidPlanByPlanCode(planDetails.plan_code) as PaidPlan; + } + } + } + } + } + return undefined; +}; From fd8c7888fd85f6125330892bb9070bb413c2f888 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 11:20:19 +0800 Subject: [PATCH 060/134] refactor: Import dayjs plugin for UTC in handleCheckoutSessionCompleted --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index faf886be11..03ec348592 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -20,10 +20,13 @@ import { } from './strip_helper'; import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import Stripe from 'stripe'; const supabase = getAdminSupabaseClient(); +dayjs.extend(utc); + export default async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, ): Promise { From d65fd4c857165c08e969d59c395425efd2dda50b Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 12:18:22 +0800 Subject: [PATCH 061/134] refactor: Add newPaidPlan validation and update subscription plan in change-subscription-plan API --- hooks/useChangeSubscriptionPlan.ts | 1 + pages/api/stripe/change-subscription-plan.ts | 80 +++++++++++++++----- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/hooks/useChangeSubscriptionPlan.ts b/hooks/useChangeSubscriptionPlan.ts index ed858f4d96..b4ac0cb7eb 100644 --- a/hooks/useChangeSubscriptionPlan.ts +++ b/hooks/useChangeSubscriptionPlan.ts @@ -27,6 +27,7 @@ export const useChangeSubscriptionPlan = () => { throw new Error('Network response was not ok'); } const data = await response.json(); + console.log(data); return data.subscription; }; diff --git a/pages/api/stripe/change-subscription-plan.ts b/pages/api/stripe/change-subscription-plan.ts index 4e9e8b7855..ad6a425d77 100644 --- a/pages/api/stripe/change-subscription-plan.ts +++ b/pages/api/stripe/change-subscription-plan.ts @@ -1,6 +1,13 @@ +import { + getPaidPlanByPriceId, + getPriceIdByPaidPlan, +} from '@/utils/app/paid_plan_helper'; import { fetchUserProfileWithAccessToken } from '@/utils/server/auth'; import { fetchSubscriptionIdByUserId } from '@/utils/server/stripe/strip_helper'; +import { PaidPlan } from '@/types/paid_plan'; +import { MemberShipPlanCurrencyType } from '@/types/stripe-product'; + import Stripe from 'stripe'; export const config = { @@ -17,6 +24,15 @@ const handler = async (req: Request) => { } try { + // TODO: add zod validation to validate if the request newPaidPlan is the PaidPlan type + // TODO: add zod validation to validate if the request currency is the MemberShipPlanCurrencyType type + const newPaidPlan = PaidPlan.UltraMonthly; + const currency = 'TWD' as MemberShipPlanCurrencyType; + const newPriceId = getPriceIdByPaidPlan(newPaidPlan, currency); + if (!newPriceId) { + throw new Error('New plan is not a valid plan'); + } + // Step 1: Get User Profile and Subscription ID const userProfile = await fetchUserProfileWithAccessToken(req); if (userProfile.plan !== 'pro' && userProfile.plan !== 'ultra') { @@ -32,31 +48,55 @@ const handler = async (req: Request) => { console.log({ subscription, }); + // Check if user has any active items + if (!(subscription.items.data?.[0].object === 'subscription_item')) { + throw new Error('Subscription has no active subscription plan'); + } + const currentPaidPlan = getPaidPlanByPriceId( + subscription.items.data[0].plan.id, + ); + + if (!currentPaidPlan) { + throw new Error('Subscription has no active subscription plan'); + } + if (currentPaidPlan === newPaidPlan) { + throw new Error( + 'The current Subscription plan is the same as the new plan', + ); + } - // // Step 3: Calculate Proration - // const prorationDate = Math.floor(Date.now() / 1000); + // Step 3: Calculate Proration + const prorationDate = Math.floor(Date.now() / 1000); - // // Step 4: Update Subscription - // const updatedSubscription = await stripe.subscriptions.update( - // subscriptionId, - // { - // items: [ - // { - // id: subscription.items.data[0].id, - // price: newPlanId, - // }, - // ], - // proration_behavior: 'create_prorations', - // proration_date: prorationDate, - // }, - // ); + // Step 4: Update Subscription + const updatedSubscription = await stripe.subscriptions.update( + subscriptionId, + { + items: [ + { + id: subscription.items.data[0].id, + price: newPriceId, + }, + ], + proration_behavior: 'create_prorations', + }, + ); // Step 5: Notify User - // In a real-world application, you might send an email or in-app notification here // For this example, we'll just return the updated subscription details - return new Response(JSON.stringify({ subscription: subscription }), { - status: 200, - }); + + // TODO:Lets see if it called the stripe webhooks to update our db + + return new Response( + JSON.stringify({ + previousSubscription: subscription, + newSubscription: updatedSubscription, + prorationDate, + }), + { + status: 200, + }, + ); } catch (error) { console.error(error); return new Response('Internal Server Error', { status: 500 }); From 1c0fe2591338c33fd3cd0e2b785d82f2d97f4c2b Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:25:01 +0800 Subject: [PATCH 062/134] refactor: Update stripe product type format --- types/stripe-product.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/types/stripe-product.ts b/types/stripe-product.ts index eb8f5e484f..07aaa8d937 100644 --- a/types/stripe-product.ts +++ b/types/stripe-product.ts @@ -1,3 +1,40 @@ +import { SubscriptionPlan } from './paid_plan'; + +export type StripeProductType = 'top_up' | 'paid_plan'; + +export type StripeProductPaidPlanType = Exclude< + SubscriptionPlan, + 'free' | 'edu' +>; +export type StripeProductTopUpType = + | '50_GPT4_CREDIT' + | '150_GPT4_CREDIT' + | '300_GPT4_CREDIT' + | '100_IMAGE_CREDIT' + | '500_IMAGE_CREDIT'; + +export type StripeProductName = + | StripeProductPaidPlanType + | StripeProductTopUpType; + +export interface BaseStripeProduct { + type: StripeProductType; + productName: StripeProductName; + productId: string; +} + +export interface StripeTopUpProduct extends BaseStripeProduct { + type: 'top_up'; + credit: number; +} + +export interface StripePaidPlanProduct extends BaseStripeProduct { + type: 'paid_plan'; +} + +export type NewStripeProduct = StripeTopUpProduct | StripePaidPlanProduct; + +// TODO: To be remove START ================================ export type MemberShipPlanPeriodType = 'monthly' | 'yearly' | 'one-time'; export type MemberShipPlanCurrencyType = 'USD' | 'TWD'; @@ -33,3 +70,4 @@ export interface MemberShipPlan { export interface StripeProduct { MEMBERSHIP_PLAN: MemberShipPlan; } +// TODO: To be remove END ============================================= From 2c10c83b1b6a9a84455e5fe89138c59316bbc920 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:25:11 +0800 Subject: [PATCH 063/134] refactor: Update getPaidPlan function name to getDbSubscriptionPlanByPaidPlan --- utils/app/paid_plan_helper.ts | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts index a63262ea84..c7d4a85317 100644 --- a/utils/app/paid_plan_helper.ts +++ b/utils/app/paid_plan_helper.ts @@ -1,4 +1,4 @@ -import { PaidPlan, SubscriptionPlan, TopUpRequest } from '@/types/paid_plan'; +import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; import { MemberShipPlanCurrencyType, MemberShipPlanItem, @@ -71,27 +71,6 @@ export const getPaidPlanByPlanCode = ( } }; -export const getDbSubscriptionPlanByPaidPlan = ( - paidPlan: PaidPlan, -): SubscriptionPlan => { - switch (paidPlan) { - case PaidPlan.ProMonthly: - return 'pro'; - case PaidPlan.ProOneTime: - return 'pro'; - case PaidPlan.ProYearly: - return 'pro'; - case PaidPlan.UltraYearly: - return 'ultra'; - case PaidPlan.UltraMonthly: - return 'ultra'; - case PaidPlan.UltraOneTime: - return 'ultra'; - default: - return 'free'; - } -}; - export const getPaidPlanByPriceId = (priceId: string): PaidPlan | undefined => { for (const planType of ['pro', 'ultra'] as const) { for (const period of ['monthly', 'yearly', 'one-time'] as const) { @@ -101,10 +80,6 @@ export const getPaidPlanByPriceId = (priceId: string): PaidPlan | undefined => { planDetails.currencies, ) as MemberShipPlanCurrencyType[]) { if (planDetails.currencies[currency].price_id === priceId) { - console.log( - 'current plan', - getPaidPlanByPlanCode(planDetails.plan_code) as PaidPlan, - ); console.log('current plan currency ', currency); return getPaidPlanByPlanCode(planDetails.plan_code) as PaidPlan; } From b24fa08cd0be0a8267c2d7070978c35cb12e3661 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:29:49 +0800 Subject: [PATCH 064/134] refactor: Update stripe helper functions and import StripeProduct type --- .../stripe/handleCheckoutSessionCompleted.ts | 178 +++++++++--------- utils/server/stripe/strip_helper.ts | 58 ++++++ 2 files changed, 147 insertions(+), 89 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 03ec348592..a395275eec 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,11 +1,7 @@ import { serverSideTrackEvent } from '@/utils/app/eventTracking'; -import { - getDbSubscriptionPlanByPaidPlan, - getPaidPlanByPlanCode, -} from '@/utils/app/paid_plan_helper'; -import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; import { PluginID } from '@/types/plugin'; +import { NewStripeProduct } from '@/types/stripe-product'; import { UserProfile } from '@/types/user'; import { @@ -13,8 +9,7 @@ import { getAdminSupabaseClient, userProfileQuery, } from '../supabase'; -import { - calculateMembershipExpirationDate, +import StripeHelper, { updateUserAccountByEmail, updateUserAccountById, } from './strip_helper'; @@ -33,110 +28,62 @@ export default async function handleCheckoutSessionCompleted( const userId = session.client_reference_id; const email = session.customer_details?.email; - const planCode = session.metadata?.plan_code - ? getPaidPlanByPlanCode(session.metadata?.plan_code) - : undefined; - const planGivingWeeks = session.metadata?.plan_giving_weeks; - const credit = session.metadata?.credit; const stripeSubscriptionId = session.subscription as string; - if (!planCode && !planGivingWeeks) { - throw new Error( - 'no plan code and plan giving weeks from Stripe webhook, one of them must be provided', - ); - } + const sessionId = session.id; + const product = await StripeHelper.product.getProductBySessionId(sessionId); if (!email) { throw new Error('missing Email from Stripe webhook'); } - const user = await userProfileQuery({ client: supabase, email, }); - const isTopUpCreditRequest = Object.values(TopUpRequest).includes( - planCode as TopUpRequest, - ); - - // # REQUEST: Top Up Image Credit / GPT4 Credit - if (isTopUpCreditRequest && credit) { - return await addCreditToUser( - user, - +credit, - planCode === TopUpRequest.ImageCredit - ? PluginID.IMAGE_GEN - : PluginID.GPT4, - ); - } - - // # REQUEST: Upgrade plan - return await (async () => { - const sessionCreatedDate = dayjs.unix(session.created).utc().toDate(); - const userIsInPaidPlan = user.plan !== 'free' && user.plan !== 'edu'; - const isBuyingOneTimePlan = - planCode === PaidPlan.ProOneTime || planCode === PaidPlan.UltraOneTime; - if (userIsInPaidPlan && isBuyingOneTimePlan) { - throw new Error( - 'One-time plan purchase is disallowed for users already on a paid subscription plan', - { - cause: { - user, - }, - }, + // # Upgrade plan flow + if (product.type === 'paid_plan') { + if (session.mode === 'subscription') { + // Recurring payment flow + await handleSubscription( + session, + user, + product, + stripeSubscriptionId, + userId || undefined, + email || undefined, ); - } - if (userIsInPaidPlan) { + } else if (session.mode === 'payment') { + // One-time payment flow throw new Error( - 'User is already in a paid plan, cannot purchase a new plan, should issue an refund', + 'One-time payment flow not implemented, need to setup user account manually', { cause: { - user, + email, + product, + session, }, }, ); - } - - // Extend membership expiration date if user has a pro plan expiration date already - const proPlanExpirationDate = await calculateMembershipExpirationDate( - planGivingWeeks, - planCode, - sessionCreatedDate, - ); - - serverSideTrackEvent(userId || 'N/A', 'New paying customer', { - paymentDetail: - !session.amount_subtotal || session.amount_subtotal <= 50000 - ? 'One-time' - : 'Monthly', - }); - - if (!proPlanExpirationDate) { - throw new Error('calculate membership expiration date: undefined ', { + } else { + throw new Error(`Unhandled session mode ${session.mode}`, { cause: { - user, + session, + product, }, }); } - - // Update user account by User id - if (userId) { - await updateUserAccountById({ - userId, - plan: getDbSubscriptionPlanByPaidPlan(planCode as PaidPlan), - stripeSubscriptionId, - proPlanExpirationDate: proPlanExpirationDate, - }); - } else { - // Update user account by Email - await updateUserAccountByEmail({ - email: email!, - plan: getDbSubscriptionPlanByPaidPlan(planCode as PaidPlan), - stripeSubscriptionId, - proPlanExpirationDate: proPlanExpirationDate, - }); - } - })(); + } else { + // Top Up Credit flow + return await addCreditToUser( + user, + product.credit, + product.productName === '500_IMAGE_CREDIT' || + product.productName === '100_IMAGE_CREDIT' + ? PluginID.IMAGE_GEN + : PluginID.GPT4, + ); + } } async function addCreditToUser( @@ -173,3 +120,56 @@ async function addCreditToUser( credit, ); } + +async function handleSubscription( + session: Stripe.Checkout.Session, + user: UserProfile, + product: NewStripeProduct, + stripeSubscriptionId: string, + userId: string | undefined, + email: string | undefined, +) { + const subscription = await StripeHelper.subscription.getSubscriptionById( + stripeSubscriptionId, + ); + const currentPeriodEnd = dayjs + .unix(subscription.current_period_end) + .utc() + .toDate(); + + const userIsInPaidPlan = user.plan !== 'free' && user.plan !== 'edu'; + if (userIsInPaidPlan) { + throw new Error( + 'User is already in a paid plan, cannot purchase a new plan, should issue an refund', + { + cause: { + user, + }, + }, + ); + } + serverSideTrackEvent(userId || 'N/A', 'New paying customer', { + paymentDetail: + !session.amount_subtotal || session.amount_subtotal <= 50000 + ? 'One-time' + : 'Monthly', + }); + + // Update user account by User id + if (userId) { + await updateUserAccountById({ + userId, + plan: product.productName, + stripeSubscriptionId, + proPlanExpirationDate: currentPeriodEnd, + }); + } else { + // Update user account by Email + await updateUserAccountByEmail({ + email: email!, + plan: product.productName, + stripeSubscriptionId, + proPlanExpirationDate: currentPeriodEnd, + }); + } +} diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 632c165a5b..01b00dfcdb 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -1,3 +1,5 @@ +import { STRIPE_PRODUCT_LIST } from '@/utils/app/stripe_config'; + import { PaidPlan } from '@/types/paid_plan'; import { getAdminSupabaseClient } from '../supabase'; @@ -10,6 +12,62 @@ dayjs.extend(utc); const supabase = getAdminSupabaseClient(); +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2022-11-15', +}); + +async function getSubscriptionById(subscriptionId: string) { + try { + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + return subscription; + } catch (error) { + console.error('Error retrieving subscription:', error); + throw error; + } +} +const StripeHelper = { + subscription: { + getSubscriptionById, + }, + product: { + getProductBySessionId, + }, +}; +export default StripeHelper; + +export async function getProductBySessionId(sessionId: string) { + const productId = await getProductIdBySessionId(sessionId); + const product = STRIPE_PRODUCT_LIST.find( + (product) => product.productId === productId, + ); + if (!product) { + throw new Error('The product id does not exist in our codebase', { + cause: { + productId, + sessionId, + }, + }); + } + return product; +} + +async function getProductIdBySessionId(sessionId: string): Promise { + const session = await stripe.checkout.sessions.retrieve(sessionId, { + expand: ['line_items'], + }); + const lineItems = session.line_items; + + const productId = lineItems?.data[0]?.price?.product; + if (!productId || typeof productId !== 'string') { + throw new Error('The session does not have a product id', { + cause: { + session, + }, + }); + } + return productId; +} + export async function fetchSubscriptionIdByUserId( userId: string, ): Promise { From 315958d1226f4dc7854a6a7bb953b559fd4e2a36 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:35:04 +0800 Subject: [PATCH 065/134] refactor: Update BaseStripeProduct property name to productValue --- types/stripe-product.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/stripe-product.ts b/types/stripe-product.ts index 07aaa8d937..130040e2fb 100644 --- a/types/stripe-product.ts +++ b/types/stripe-product.ts @@ -19,8 +19,9 @@ export type StripeProductName = export interface BaseStripeProduct { type: StripeProductType; - productName: StripeProductName; + productValue: StripeProductName; productId: string; + note?: string; } export interface StripeTopUpProduct extends BaseStripeProduct { From f9f205b4eb820e3f6bac087bd7b64f618af4986d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:35:16 +0800 Subject: [PATCH 066/134] refactor: Update handleCheckoutSessionCompleted to use productValue instead of productName --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index a395275eec..f9cfb7e12f 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -78,8 +78,8 @@ export default async function handleCheckoutSessionCompleted( return await addCreditToUser( user, product.credit, - product.productName === '500_IMAGE_CREDIT' || - product.productName === '100_IMAGE_CREDIT' + product.productValue === '500_IMAGE_CREDIT' || + product.productValue === '100_IMAGE_CREDIT' ? PluginID.IMAGE_GEN : PluginID.GPT4, ); @@ -159,7 +159,7 @@ async function handleSubscription( if (userId) { await updateUserAccountById({ userId, - plan: product.productName, + plan: product.productValue, stripeSubscriptionId, proPlanExpirationDate: currentPeriodEnd, }); @@ -167,7 +167,7 @@ async function handleSubscription( // Update user account by Email await updateUserAccountByEmail({ email: email!, - plan: product.productName, + plan: product.productValue, stripeSubscriptionId, proPlanExpirationDate: currentPeriodEnd, }); From 6f4a5d3724953626a8c4f880cdaba460a108436d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:37:33 +0800 Subject: [PATCH 067/134] refactor: Update stripe product list based on environment --- utils/app/stripe_config.ts | 100 ++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 9854eb4c89..472b07a55d 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -1,9 +1,107 @@ -import { StripeProduct } from '@/types/stripe-product'; +import { NewStripeProduct, StripeProduct } from '@/types/stripe-product'; // STRIPE CREDIT CODE export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; +// Add production one +const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [ + { + type: 'paid_plan', + productValue: 'ultra', + productId: 'prod_QC5xRJFNyaB3h7', + }, + { + type: 'paid_plan', + productValue: 'pro', + productId: 'prod_Nlh3dRKPO799ja', + }, + { + type: 'top_up', + productValue: '50_GPT4_CREDIT', + productId: 'prod_OKKu2YQZyaJTYN', + credit: 50, + }, + { + type: 'top_up', + productValue: '150_GPT4_CREDIT', + productId: 'prod_OKKu2YQZyaJTYN', + credit: 150, + }, + { + type: 'top_up', + productValue: '300_GPT4_CREDIT', + productId: 'prod_OKKu2YQZyaJTYN', + credit: 300, + }, + { + type: 'top_up', + productValue: '500_IMAGE_CREDIT', + productId: 'prod_OKJgVwM66OOWuR', + credit: 500, + }, + { + type: 'top_up', + productValue: '100_IMAGE_CREDIT', + productId: 'prod_OKJgVwM66OOWuR', + credit: 100, + }, +]; +const STRIPE_PRODUCT_LIST_PRODUCTION: NewStripeProduct[] = [ + { + type: 'paid_plan', + productValue: 'ultra', + productId: 'prod_Q6j96oOouZMFN4', + }, + { + type: 'paid_plan', + productValue: 'ultra', + productId: 'prod_PGES2QxP0aYFr4', + note: 'This is the pre-sell plan for the Ultra plan', + }, + + { + type: 'paid_plan', + productValue: 'pro', + productId: 'prod_NlR6az1csuoBHl', + }, + { + type: 'top_up', + productValue: '50_GPT4_CREDIT', + productId: 'prod_Nofw6ncuYiYD81', + credit: 50, + }, + { + type: 'top_up', + productValue: '150_GPT4_CREDIT', + productId: 'prod_Nog8i22B8eLX6y', + credit: 150, + }, + { + type: 'top_up', + productValue: '300_GPT4_CREDIT', + productId: 'prod_Nog91rmXzSJY1w', + credit: 300, + }, + { + type: 'top_up', + productValue: '100_IMAGE_CREDIT', + productId: 'prod_OKYWXZGkysAtFS', + credit: 100, + }, + { + type: 'top_up', + productValue: '500_IMAGE_CREDIT', + productId: 'prod_OKYouQss6inD9q', + credit: 500, + }, +]; + +export const STRIPE_PRODUCT_LIST = + process.env.NEXT_PUBLIC_ENV === 'production' + ? STRIPE_PRODUCT_LIST_PRODUCTION + : STRIPE_PRODUCT_LIST_STAGING; + const STRIPE_PRODUCTS_PRODUCTION: StripeProduct = { MEMBERSHIP_PLAN: { pro: { From 5f904465d6394ba2c02593e80ef4f03c353cd4f1 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 18:44:51 +0800 Subject: [PATCH 068/134] refactor: Update handleCheckoutSessionCompleted to include Top Up Request in comment --- pages/api/webhooks/stripe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 871443aa65..3686b453fb 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -43,7 +43,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { switch (event.type) { case 'checkout.session.completed': - // One time payment / Initial Monthly Pro / Ultra Plan Subscription + // One time payment / Initial Monthly [Pro / Ultra] Plan Subscription / Top Up Request console.log('✅ checkout.session.completed'); await handleCheckoutSessionCompleted( event.data.object as Stripe.Checkout.Session, From 284120ca4f8abea1186385f5568584cb6d1d1bc9 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 19:23:45 +0800 Subject: [PATCH 069/134] refactor: Update stripe helper functions and import StripeProduct type --- utils/server/stripe/strip_helper.ts | 47 ++++++----------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 01b00dfcdb..161bc977c6 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -30,13 +30,17 @@ const StripeHelper = { getSubscriptionById, }, product: { - getProductBySessionId, + getProductByCheckoutSessionId: getProductByCheckoutSessionId, + getProductByProductId: getProductByProductId, }, }; export default StripeHelper; -export async function getProductBySessionId(sessionId: string) { - const productId = await getProductIdBySessionId(sessionId); +async function getProductByCheckoutSessionId(sessionId: string) { + const productId = await getProductIdByCheckoutSessionId(sessionId); + return getProductByProductId(productId); +} +async function getProductByProductId(productId: string) { const product = STRIPE_PRODUCT_LIST.find( (product) => product.productId === productId, ); @@ -44,14 +48,15 @@ export async function getProductBySessionId(sessionId: string) { throw new Error('The product id does not exist in our codebase', { cause: { productId, - sessionId, }, }); } return product; } -async function getProductIdBySessionId(sessionId: string): Promise { +async function getProductIdByCheckoutSessionId( + sessionId: string, +): Promise { const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['line_items'], }); @@ -83,10 +88,6 @@ export async function getCustomerEmailByCustomerID( customerID: string, ): Promise { try { - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: '2022-11-15', - }); - // We get the customer id from webhook, so we know the customer is not deleted const customer = (await stripe.customers.retrieve( customerID, @@ -200,34 +201,6 @@ export async function downgradeUserAccount({ console.log(`User ${email || stripeSubscriptionId} downgraded to free plan`); } -export async function extendMembershipByStripeSubscriptionId({ - stripeSubscriptionId, - proPlanExpirationDate, -}: { - stripeSubscriptionId: string; - proPlanExpirationDate: Date; -}) { - // Extend pro / ultra plan - const { data: userProfile } = await supabase - .from('profiles') - .select('plan, email') - .eq('stripe_subscription_id', stripeSubscriptionId) - .single(); - - if (userProfile?.plan === 'edu') return; - - const { error: updatedUserError } = await supabase - .from('profiles') - .update({ - pro_plan_expiration_date: proPlanExpirationDate, - }) - .eq('stripe_subscription_id', stripeSubscriptionId); - if (updatedUserError) throw updatedUserError; - console.log( - `User ${userProfile?.email} with plan ${userProfile?.plan} extended to ${proPlanExpirationDate}`, - ); -} - export async function calculateMembershipExpirationDate( planGivingWeeks: string | undefined, planCode: string | undefined, From 1b2045ad40a0fbbd566d11209330ea61d2f20d1a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 19:23:55 +0800 Subject: [PATCH 070/134] refactor: Update handleCheckoutSessionCompleted to use getProductByCheckoutSessionId instead of getProductBySessionId --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index f9cfb7e12f..58255f2f6d 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -31,7 +31,9 @@ export default async function handleCheckoutSessionCompleted( const stripeSubscriptionId = session.subscription as string; const sessionId = session.id; - const product = await StripeHelper.product.getProductBySessionId(sessionId); + const product = await StripeHelper.product.getProductByCheckoutSessionId( + sessionId, + ); if (!email) { throw new Error('missing Email from Stripe webhook'); From d303329440d313139f98b2fd6d84a05a1772b9a3 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 30 May 2024 19:24:02 +0800 Subject: [PATCH 071/134] refactor: Extend expiration date or change plan for Monthly Pro/Ultra Plan Subscription --- .../handleCustomerSubscriptionUpdated.ts | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts index ef3098e904..3a717fe027 100644 --- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts +++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts @@ -1,6 +1,6 @@ -import { +import { getAdminSupabaseClient } from '../supabase'; +import StripeHelper, { downgradeUserAccount, - extendMembershipByStripeSubscriptionId, getCustomerEmailByCustomerID, } from './strip_helper'; @@ -8,6 +8,7 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import Stripe from 'stripe'; +const supabase = getAdminSupabaseClient(); dayjs.extend(utc); export default async function handleCustomerSubscriptionUpdated( @@ -47,14 +48,30 @@ export default async function handleCustomerSubscriptionUpdated( }); } } else { - // Monthly Pro / Ultra Plan Subscription recurring payment, extend expiration date + // Monthly Pro / Ultra Plan Subscription recurring payment, extend expiration date / change to new plan if (!stripeSubscriptionId) { throw new Error('Stripe subscription ID not found'); - } else { - await extendMembershipByStripeSubscriptionId({ - stripeSubscriptionId, - proPlanExpirationDate: currentPeriodEnd, + } + + const userSubscription = + await StripeHelper.subscription.getSubscriptionById(stripeSubscriptionId); + const productId = userSubscription.items.data[0].price.product; + if (!productId || typeof productId !== 'string') { + throw new Error('The session does not have a product id', { + cause: { + session, + }, }); } + const product = await StripeHelper.product.getProductByProductId(productId); + if (product.type !== 'paid_plan') return; + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + plan: product.productValue, + pro_plan_expiration_date: currentPeriodEnd, + }) + .eq('stripe_subscription_id', stripeSubscriptionId); + if (updatedUserError) throw updatedUserError; } } From 0ba779e84a9cf99931542307d2da61ae76a28b84 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:39:27 +0800 Subject: [PATCH 072/134] refactor: Add second receiver email for resend functionality --- .env.local.example | 1 + utils/server/resend.ts | 85 ++++++++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/.env.local.example b/.env.local.example index 62d7735d02..404282dbc1 100644 --- a/.env.local.example +++ b/.env.local.example @@ -43,6 +43,7 @@ WEB_CONTENT_FUNCTION_SECRET=SECRET # Resned email api key RESEND_EMAIL_API_KEY=KEY RESEND_EMAIL_RECEIVER=EMAIL +RESEND_EMAIL_RECEIVER2=EMAIL # SendGrid SENDGRID_API_KEY=KEY diff --git a/utils/server/resend.ts b/utils/server/resend.ts index 9178cf2e7e..13a8a31fe9 100644 --- a/utils/server/resend.ts +++ b/utils/server/resend.ts @@ -11,15 +11,27 @@ dayjs.extend(timezone); const resend = new Resend(process.env.RESEND_EMAIL_API_KEY); const receiverEmail = process.env.RESEND_EMAIL_RECEIVER!; +const receiverEmail2 = process.env.RESEND_EMAIL_RECEIVER2!; export async function sendReport(subject = '', html = '') { try { - await resend.emails.send({ - from: 'team@chateverywhere.app', - to: receiverEmail, - subject, - html, - }); + const emails = [ + { + from: 'team@chateverywhere.app', + to: receiverEmail, + subject, + html, + }, + ]; + if (receiverEmail2) { + emails.push({ + from: 'team@chateverywhere.app', + to: receiverEmail2, + subject, + html, + }); + } + await resend.batch.send(emails); } catch (error) { console.error(error); if (error instanceof Error) { @@ -33,48 +45,55 @@ export async function sendReportForStripeWebhookError( subject = '', event: Stripe.Event, user?: UserProfile, + cause?: any, ) { const session = event.data.object as Stripe.Checkout.Session; const stripeSubscriptionId = session.subscription as string; - const paymentDateInCanadaTime = dayjs - .tz(session.created * 1000, 'America/Toronto') - .format('YYYY-MM-DD HH:mm:ss'); - const paymentDateInTaiwanTime = dayjs - .tz(session.created * 1000, 'Asia/Taipei') - .format('YYYY-MM-DD HH:mm:ss'); + + const formatDate = (timestamp: number, timezone: string) => + dayjs.tz(timestamp * 1000, timezone).format('YYYY-MM-DD HH:mm:ss'); + + const paymentDateInCanadaTime = formatDate( + session.created, + 'America/Toronto', + ); + const paymentDateInTaiwanTime = formatDate(session.created, 'Asia/Taipei'); + const paymentDateHTML = ` -

Stripe Session Date(payment date): ${paymentDateInCanadaTime} (Canada Time)

-

Stripe Session Date(payment date): ${paymentDateInTaiwanTime} (Taiwan Time)

+

Stripe Session Date(payment date): ${paymentDateInCanadaTime} (Canada Time)

+

Stripe Session Date(payment date): ${paymentDateInTaiwanTime} (Taiwan Time)

`; const userDataHTML = user ? ` -

User id: ${user?.id}

-

User Email: ${user?.email}

- ` +

User id: ${user.id}

+

User Email: ${user.email}

+ ` : ''; + const referenceDataHTML = session ? ` -

Stripe Session Reference

-
-  ${JSON.stringify(session)}
-
` - : ` -

Stripe Event

-
-${JSON.stringify(event)}
-
-`; - await sendReport( - `Stripe Webhook Error - ${subject}`, +

Stripe Session Reference

+
${JSON.stringify(session, null, 2)}
+

Cause data

+
${JSON.stringify(cause, null, 2)}
` + : ` +

Stripe Event

+
${JSON.stringify(event, null, 2)}
+

Cause data

+
${JSON.stringify(cause, null, 2)}
+ `; + + const emailHTML = `

Stripe Webhook Error - ${subject}

Stripe Session Id: ${session.id}

${paymentDateHTML} -

Stripe event type : ${event.type}

-

Stripe Strip Subscription Id: ${stripeSubscriptionId}

+

Stripe event type: ${event.type}

+

Stripe Subscription Id: ${stripeSubscriptionId}

${userDataHTML} ${referenceDataHTML} - `, - ); + `; + + await sendReport(`Stripe Webhook Error - ${subject}`, emailHTML); } From 3eed887d6e6120c812b5ed6705c8e0d9f54788fe Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:39:37 +0800 Subject: [PATCH 073/134] refactor: Update StripeProduct type and add mode property --- types/stripe-product.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/types/stripe-product.ts b/types/stripe-product.ts index 130040e2fb..22364457ae 100644 --- a/types/stripe-product.ts +++ b/types/stripe-product.ts @@ -1,5 +1,7 @@ import { SubscriptionPlan } from './paid_plan'; +import Stripe from 'stripe'; + export type StripeProductType = 'top_up' | 'paid_plan'; export type StripeProductPaidPlanType = Exclude< @@ -19,6 +21,7 @@ export type StripeProductName = export interface BaseStripeProduct { type: StripeProductType; + mode: Stripe.Checkout.Session.Mode; productValue: StripeProductName; productId: string; note?: string; @@ -29,8 +32,17 @@ export interface StripeTopUpProduct extends BaseStripeProduct { credit: number; } -export interface StripePaidPlanProduct extends BaseStripeProduct { +export type StripePaidPlanProduct = + | StripeSubscriptionPaidPlanProduct + | StripeOneTimePaidPlanProduct; +export interface StripeSubscriptionPaidPlanProduct extends BaseStripeProduct { + type: 'paid_plan'; + mode: 'subscription'; +} +export interface StripeOneTimePaidPlanProduct extends BaseStripeProduct { type: 'paid_plan'; + mode: 'payment'; + givenDays: number; } export type NewStripeProduct = StripeTopUpProduct | StripePaidPlanProduct; From ce7c568f481fae94277e39c4f68b0ad3fc265181 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:39:50 +0800 Subject: [PATCH 074/134] refactor: Update sendReportForStripeWebhookError to include error cause --- pages/api/webhooks/stripe.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 3686b453fb..b823992ff0 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -78,7 +78,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { : undefined; try { - await sendReportForStripeWebhookError(error.message, event, user); + await sendReportForStripeWebhookError( + error.message, + event, + user, + error.cause, + ); } catch (error) { console.error(error); throw error; From 850f3dd646351f7f4abffa40a4274b3a5c7b2758 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:40:13 +0800 Subject: [PATCH 075/134] refactor: Update stripe product list based on environment --- utils/app/stripe_config.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/utils/app/stripe_config.ts b/utils/app/stripe_config.ts index 472b07a55d..0417817c9d 100644 --- a/utils/app/stripe_config.ts +++ b/utils/app/stripe_config.ts @@ -8,40 +8,55 @@ export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [ { type: 'paid_plan', + mode: 'payment', + givenDays: 7, + productValue: 'pro', + productId: 'prod_P39kXR1vgNBVZh', + note: 'This is a v2 weekly Pro plan', + }, + { + type: 'paid_plan', + mode: 'subscription', productValue: 'ultra', productId: 'prod_QC5xRJFNyaB3h7', }, { type: 'paid_plan', + mode: 'subscription', productValue: 'pro', productId: 'prod_Nlh3dRKPO799ja', }, { type: 'top_up', + mode: 'payment', productValue: '50_GPT4_CREDIT', productId: 'prod_OKKu2YQZyaJTYN', credit: 50, }, { type: 'top_up', + mode: 'payment', productValue: '150_GPT4_CREDIT', productId: 'prod_OKKu2YQZyaJTYN', credit: 150, }, { type: 'top_up', + mode: 'payment', productValue: '300_GPT4_CREDIT', productId: 'prod_OKKu2YQZyaJTYN', credit: 300, }, { type: 'top_up', + mode: 'payment', productValue: '500_IMAGE_CREDIT', productId: 'prod_OKJgVwM66OOWuR', credit: 500, }, { type: 'top_up', + mode: 'payment', productValue: '100_IMAGE_CREDIT', productId: 'prod_OKJgVwM66OOWuR', credit: 100, @@ -50,47 +65,62 @@ const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [ const STRIPE_PRODUCT_LIST_PRODUCTION: NewStripeProduct[] = [ { type: 'paid_plan', + mode: 'payment', + givenDays: 7, + productValue: 'pro', + productId: 'prod_P39h2AVZANAN1M', + note: 'This is a v2 weekly Pro plan', + }, + { + type: 'paid_plan', + mode: 'subscription', productValue: 'ultra', productId: 'prod_Q6j96oOouZMFN4', }, { type: 'paid_plan', + mode: 'subscription', productValue: 'ultra', productId: 'prod_PGES2QxP0aYFr4', note: 'This is the pre-sell plan for the Ultra plan', }, - { type: 'paid_plan', + mode: 'subscription', productValue: 'pro', productId: 'prod_NlR6az1csuoBHl', }, { type: 'top_up', + mode: 'payment', productValue: '50_GPT4_CREDIT', productId: 'prod_Nofw6ncuYiYD81', credit: 50, }, { type: 'top_up', + mode: 'payment', productValue: '150_GPT4_CREDIT', productId: 'prod_Nog8i22B8eLX6y', credit: 150, }, { type: 'top_up', + mode: 'payment', productValue: '300_GPT4_CREDIT', productId: 'prod_Nog91rmXzSJY1w', credit: 300, }, { type: 'top_up', + mode: 'payment', productValue: '100_IMAGE_CREDIT', productId: 'prod_OKYWXZGkysAtFS', credit: 100, }, { type: 'top_up', + mode: 'payment', productValue: '500_IMAGE_CREDIT', productId: 'prod_OKYouQss6inD9q', credit: 500, From c833e776749b6f88c1c2cc264c5b110d40f520a1 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:40:20 +0800 Subject: [PATCH 076/134] refactor: Update getProductByCheckoutSessionId to include mode parameter --- utils/server/stripe/strip_helper.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 161bc977c6..df154f8ea7 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -36,13 +36,19 @@ const StripeHelper = { }; export default StripeHelper; -async function getProductByCheckoutSessionId(sessionId: string) { +async function getProductByCheckoutSessionId( + sessionId: string, + mode: Stripe.Checkout.Session.Mode, +) { const productId = await getProductIdByCheckoutSessionId(sessionId); - return getProductByProductId(productId); + return getProductByProductId(productId, mode); } -async function getProductByProductId(productId: string) { +async function getProductByProductId( + productId: string, + mode: Stripe.Checkout.Session.Mode, +) { const product = STRIPE_PRODUCT_LIST.find( - (product) => product.productId === productId, + (product) => product.productId === productId && product.mode === mode, ); if (!product) { throw new Error('The product id does not exist in our codebase', { From 8217956ba1b132da0f3f72cd136be6b46f46cf50 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:40:26 +0800 Subject: [PATCH 077/134] refactor: Update handleCustomerSubscriptionUpdated to include mode parameter in getProductByProductId --- utils/server/stripe/handleCustomerSubscriptionUpdated.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts index 3a717fe027..f37671e82d 100644 --- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts +++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts @@ -63,7 +63,10 @@ export default async function handleCustomerSubscriptionUpdated( }, }); } - const product = await StripeHelper.product.getProductByProductId(productId); + const product = await StripeHelper.product.getProductByProductId( + productId, + 'subscription', + ); if (product.type !== 'paid_plan') return; const { error: updatedUserError } = await supabase .from('profiles') From e8b53c1521b2f521834cb2299d375d6a91bf1090 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:41:32 +0800 Subject: [PATCH 078/134] refactor: Update handleCheckoutSessionCompleted to include session mode in getProductByCheckoutSessionId --- .../stripe/handleCheckoutSessionCompleted.ts | 92 ++++++++++++++++--- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 58255f2f6d..33fd19fa0b 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -1,7 +1,10 @@ import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { PluginID } from '@/types/plugin'; -import { NewStripeProduct } from '@/types/stripe-product'; +import { + NewStripeProduct, + StripeOneTimePaidPlanProduct, +} from '@/types/stripe-product'; import { UserProfile } from '@/types/user'; import { @@ -33,6 +36,7 @@ export default async function handleCheckoutSessionCompleted( const sessionId = session.id; const product = await StripeHelper.product.getProductByCheckoutSessionId( sessionId, + session.mode, ); if (!email) { @@ -57,15 +61,13 @@ export default async function handleCheckoutSessionCompleted( ); } else if (session.mode === 'payment') { // One-time payment flow - throw new Error( - 'One-time payment flow not implemented, need to setup user account manually', - { - cause: { - email, - product, - session, - }, - }, + await handleOneTimePayment( + session, + user, + product as StripeOneTimePaidPlanProduct, + stripeSubscriptionId, + userId || undefined, + email || undefined, ); } else { throw new Error(`Unhandled session mode ${session.mode}`, { @@ -75,7 +77,7 @@ export default async function handleCheckoutSessionCompleted( }, }); } - } else { + } else if (product.type === 'top_up') { // Top Up Credit flow return await addCreditToUser( user, @@ -146,15 +148,75 @@ async function handleSubscription( { cause: { user, + buyingProduct: product, + session, + }, + }, + ); + } + const subscriptionBillPeriod = (() => { + switch (subscription.items.data?.[0].plan?.interval) { + case 'month': + return 'Monthly'; + case 'year': + return 'Yearly'; + case 'week': + return 'Weekly'; + case 'day': + return 'Daily'; + default: + return 'Monthly'; + } + })(); + serverSideTrackEvent(userId || 'N/A', 'New paying customer', { + paymentDetail: subscriptionBillPeriod, + }); + + // Update user account by User id + if (userId) { + await updateUserAccountById({ + userId, + plan: product.productValue, + stripeSubscriptionId, + proPlanExpirationDate: currentPeriodEnd, + }); + } else { + // Update user account by Email + await updateUserAccountByEmail({ + email: email!, + plan: product.productValue, + stripeSubscriptionId, + proPlanExpirationDate: currentPeriodEnd, + }); + } +} + +async function handleOneTimePayment( + session: Stripe.Checkout.Session, + user: UserProfile, + product: StripeOneTimePaidPlanProduct, + stripeSubscriptionId: string, + userId: string | undefined, + email: string | undefined, +) { + const productGivenDays = product.givenDays; + const currentPeriodEnd = dayjs().add(productGivenDays, 'day').utc().toDate(); + + const userIsInPaidPlan = user.plan !== 'free' && user.plan !== 'edu'; + if (userIsInPaidPlan) { + throw new Error( + 'User is already in a paid plan, cannot purchase a new plan, should issue an refund', + { + cause: { + user, + buyingProduct: product, + session, }, }, ); } serverSideTrackEvent(userId || 'N/A', 'New paying customer', { - paymentDetail: - !session.amount_subtotal || session.amount_subtotal <= 50000 - ? 'One-time' - : 'Monthly', + paymentDetail: 'One-time', }); // Update user account by User id From 9ede547f60a1d890b6b0b23701143531efd9ce50 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 11:41:38 +0800 Subject: [PATCH 079/134] refactor: Update error message in getProductByProductId to improve clarity --- utils/server/stripe/strip_helper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index df154f8ea7..8faac6c5fb 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -51,9 +51,11 @@ async function getProductByProductId( (product) => product.productId === productId && product.mode === mode, ); if (!product) { - throw new Error('The product id does not exist in our codebase', { + throw new Error('No product found in our codebase', { cause: { productId, + mode, + STRIPE_PRODUCT_LIST, }, }); } From 9c46dab26a82db037168d75a9bf5addc10adb342 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 13:46:01 +0800 Subject: [PATCH 080/134] refactor: Update localization files for Chinese translations --- public/locales/zh-Hans/common.json | 3 +- public/locales/zh-Hant/common.json | 3 +- sub.json | 170 +++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 sub.json diff --git a/public/locales/zh-Hans/common.json b/public/locales/zh-Hans/common.json index ad1074807d..27b4db61a6 100644 --- a/public/locales/zh-Hans/common.json +++ b/public/locales/zh-Hans/common.json @@ -10,5 +10,6 @@ "Thank you for your understanding and patience.": "感谢您的理解和耐心。", "Upload": "上传", "File uploaded successfully": "文件上传成功", - "File upload failed": "文件上传失败" + "File upload failed": "文件上传失败", + "There was a problem when changing subscription plan, please contact support team": "当您更改订阅方案时,发生问题,请联系支援团队" } diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json index 9fbc307122..3d04c90a38 100644 --- a/public/locales/zh-Hant/common.json +++ b/public/locales/zh-Hant/common.json @@ -10,5 +10,6 @@ "Thank you for your understanding and patience.": "感謝您的理解與耐心。", "Upload": "上傳", "File uploaded successfully": "檔案上傳成功", - "File upload failed": "檔案上傳失敗" + "File upload failed": "檔案上傳失敗", + "There was a problem when changing subscription plan, please contact support team": "當您更改訂閱方案時,發生問題,請聯絡支援團隊" } diff --git a/sub.json b/sub.json new file mode 100644 index 0000000000..38d73ba81a --- /dev/null +++ b/sub.json @@ -0,0 +1,170 @@ +{ + "id": "sub_1PMO44EEvfd1BzvuNTHhGZ6N", + "object": "subscription", + "application": null, + "application_fee_percent": null, + "automatic_tax": { + "enabled": false, + "liability": null + }, + "billing_cycle_anchor": 1717133504, + "billing_cycle_anchor_config": null, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": null, + "cancellation_details": { + "comment": null, + "feedback": null, + "reason": null + }, + "collection_method": "charge_automatically", + "created": 1717133504, + "currency": "twd", + "current_period_end": 1719725504, + "current_period_start": 1717133504, + "customer": "cus_QCnfro8d7zTJvT", + "days_until_due": null, + "default_payment_method": "pm_1PMO43EEvfd1BzvueE4yD0mH", + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "ended_at": null, + "invoice_settings": { + "account_tax_ids": null, + "issuer": { + "type": "self" + } + }, + "items": { + "object": "list", + "data": [ + { + "id": "si_QCnfJ3kKQPjtGQ", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1717133505, + "discounts": [], + "metadata": {}, + "plan": { + "id": "price_1PLhJREEvfd1BzvuxCM477DD", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 24999, + "amount_decimal": "24999", + "billing_scheme": "per_unit", + "created": 1716969165, + "currency": "twd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_Nlh3dRKPO799ja", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1PLhJREEvfd1BzvuxCM477DD", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1716969165, + "currency": "twd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_Nlh3dRKPO799ja", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "meter": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "exclusive", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 24999, + "unit_amount_decimal": "24999" + }, + "quantity": 1, + "subscription": "sub_1PMO44EEvfd1BzvuNTHhGZ6N", + "tax_rates": [] + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_1PMO44EEvfd1BzvuNTHhGZ6N" + }, + "latest_invoice": "in_1PMO44EEvfd1BzvuT0KgMAcF", + "livemode": false, + "metadata": {}, + "next_pending_invoice_item_invoice": null, + "on_behalf_of": null, + "pause_collection": null, + "payment_settings": { + "payment_method_options": { + "acss_debit": null, + "bancontact": null, + "card": { + "network": null, + "request_three_d_secure": "automatic" + }, + "customer_balance": null, + "konbini": null, + "sepa_debit": null, + "us_bank_account": null + }, + "payment_method_types": null, + "save_default_payment_method": "off" + }, + "pending_invoice_item_interval": null, + "pending_setup_intent": null, + "pending_update": null, + "plan": { + "id": "price_1PLhJREEvfd1BzvuxCM477DD", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 24999, + "amount_decimal": "24999", + "billing_scheme": "per_unit", + "created": 1716969165, + "currency": "twd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_Nlh3dRKPO799ja", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "schedule": null, + "start_date": 1717133504, + "status": "active", + "test_clock": null, + "transfer_data": null, + "trial_end": null, + "trial_settings": { + "end_behavior": { + "missing_payment_method": "create_invoice" + } + }, + "trial_start": null +} From 0b93c06e4c1cb1a6633debcd599748ac3968ef8f Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 13:46:41 +0800 Subject: [PATCH 081/134] refactor: Update change-subscription-plan API to use new price id for ULTRA monthly subscription --- pages/api/stripe/change-subscription-plan.ts | 72 +++++++++----------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/pages/api/stripe/change-subscription-plan.ts b/pages/api/stripe/change-subscription-plan.ts index ad6a425d77..63239067ad 100644 --- a/pages/api/stripe/change-subscription-plan.ts +++ b/pages/api/stripe/change-subscription-plan.ts @@ -1,13 +1,6 @@ -import { - getPaidPlanByPriceId, - getPriceIdByPaidPlan, -} from '@/utils/app/paid_plan_helper'; import { fetchUserProfileWithAccessToken } from '@/utils/server/auth'; import { fetchSubscriptionIdByUserId } from '@/utils/server/stripe/strip_helper'; -import { PaidPlan } from '@/types/paid_plan'; -import { MemberShipPlanCurrencyType } from '@/types/stripe-product'; - import Stripe from 'stripe'; export const config = { @@ -24,14 +17,15 @@ const handler = async (req: Request) => { } try { - // TODO: add zod validation to validate if the request newPaidPlan is the PaidPlan type - // TODO: add zod validation to validate if the request currency is the MemberShipPlanCurrencyType type - const newPaidPlan = PaidPlan.UltraMonthly; - const currency = 'TWD' as MemberShipPlanCurrencyType; - const newPriceId = getPriceIdByPaidPlan(newPaidPlan, currency); - if (!newPriceId) { - throw new Error('New plan is not a valid plan'); - } + // TODO: add zod validation to validate if the request PriceId is the PaidPlan type + // ULTRA yearly price id USD + // const newPaidPlanPriceId = 'price_1PLiWmEEvfd1BzvuDFmiLKI6'; + + // ULTRA monthly price id TWD + const newPaidPlanPriceId = 'price_1PLiWBEEvfd1BzvunVr1yZ55'; + + // ULTRA monthly price id USD + // const newPaidPlanPriceId = 'price_1PLhlhEEvfd1Bzvu0UEqwm9y'; // Step 1: Get User Profile and Subscription ID const userProfile = await fetchUserProfileWithAccessToken(req); @@ -43,30 +37,23 @@ const handler = async (req: Request) => { throw new Error('User does not have a valid subscription in Stripe'); } - // Step 2: Retrieve Current Subscription + // Step 2: Check if the new price id is valid + const newPricePlan = await stripe.prices.retrieve(newPaidPlanPriceId); + if (!newPricePlan) { + throw new Error('New price id is not valid'); + } + + // Step 3: Retrieve Current Subscription const subscription = await stripe.subscriptions.retrieve(subscriptionId); - console.log({ - subscription, - }); + // Check if user has any active items if (!(subscription.items.data?.[0].object === 'subscription_item')) { throw new Error('Subscription has no active subscription plan'); } - const currentPaidPlan = getPaidPlanByPriceId( - subscription.items.data[0].plan.id, - ); - - if (!currentPaidPlan) { + const currentPriceId = subscription.items.data?.[0].price; + if (!currentPriceId) { throw new Error('Subscription has no active subscription plan'); } - if (currentPaidPlan === newPaidPlan) { - throw new Error( - 'The current Subscription plan is the same as the new plan', - ); - } - - // Step 3: Calculate Proration - const prorationDate = Math.floor(Date.now() / 1000); // Step 4: Update Subscription const updatedSubscription = await stripe.subscriptions.update( @@ -75,23 +62,22 @@ const handler = async (req: Request) => { items: [ { id: subscription.items.data[0].id, - price: newPriceId, + price: newPaidPlanPriceId, }, ], - proration_behavior: 'create_prorations', + proration_behavior: 'always_invoice', }, ); - // Step 5: Notify User - // For this example, we'll just return the updated subscription details - - // TODO:Lets see if it called the stripe webhooks to update our db + // Step 5: Retrieve Invoice URL + const invoice = await stripe.invoices.retrieve( + updatedSubscription.latest_invoice as string, + ); + const invoiceUrl = invoice.hosted_invoice_url; return new Response( JSON.stringify({ - previousSubscription: subscription, - newSubscription: updatedSubscription, - prorationDate, + invoiceUrl: invoiceUrl, }), { status: 200, @@ -99,6 +85,10 @@ const handler = async (req: Request) => { ); } catch (error) { console.error(error); + if (error instanceof Error) { + return new Response(error.message, { status: 500 }); + } + return new Response('Internal Server Error', { status: 500 }); } }; From 1200e399627927ff79f7ffcc00f4ad17b850fe55 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 13:46:49 +0800 Subject: [PATCH 082/134] refactor: Update useChangeSubscriptionPlan to include toast error message and translation support --- hooks/useChangeSubscriptionPlan.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hooks/useChangeSubscriptionPlan.ts b/hooks/useChangeSubscriptionPlan.ts index b4ac0cb7eb..dd98db2fed 100644 --- a/hooks/useChangeSubscriptionPlan.ts +++ b/hooks/useChangeSubscriptionPlan.ts @@ -1,6 +1,8 @@ import { useSupabaseClient } from '@supabase/auth-helpers-react'; import { useMutation } from '@tanstack/react-query'; import { useContext } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; import HomeContext from '@/components/home/home.context'; @@ -9,6 +11,7 @@ export const useChangeSubscriptionPlan = () => { const { state: { user }, } = useContext(HomeContext); + const { t } = useTranslation('common'); const changeSubscriptionPlan = async () => { if (!user) { @@ -33,7 +36,14 @@ export const useChangeSubscriptionPlan = () => { return useMutation(changeSubscriptionPlan, { onError: (error) => { - console.error('There was a problem with your mutation operation:', error); + if (error instanceof Error) { + console.log(error.message); + } + toast.error( + t( + 'There was a problem when changing subscription plan, please contact support team', + ), + ); }, }); }; From abb191074818f986ded154789bb788b9502b41bf Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 17:21:24 +0800 Subject: [PATCH 083/134] refactor: Update stripe_config imports to reflect new file structure --- utils/app/const.ts | 2 +- utils/app/{ => stripe}/stripe_config.ts | 0 utils/server/stripe/strip_helper.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename utils/app/{ => stripe}/stripe_config.ts (100%) diff --git a/utils/app/const.ts b/utils/app/const.ts index 152691a0d1..0d623f8184 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -11,7 +11,7 @@ export { GPT4_CREDIT_PURCHASE_LINKS, AI_IMAGE_CREDIT_PURCHASE_LINKS, V2_CHAT_UPGRADE_LINK, -} from './stripe_config'; +} from './stripe/stripe_config'; export const DEFAULT_SYSTEM_PROMPT = "You are an AI language model named Chat Everywhere, designed to answer user questions as accurately and helpfully as possible. Always be aware of the current date and time, and make sure to generate responses in the exact same language as the user's query. Adapt your responses to match the user's input language and context, maintaining an informative and supportive communication style. Additionally, format all responses using Markdown syntax, regardless of the input format." + diff --git a/utils/app/stripe_config.ts b/utils/app/stripe/stripe_config.ts similarity index 100% rename from utils/app/stripe_config.ts rename to utils/app/stripe/stripe_config.ts diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index 8faac6c5fb..e511f081ee 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -1,4 +1,4 @@ -import { STRIPE_PRODUCT_LIST } from '@/utils/app/stripe_config'; +import { STRIPE_PRODUCT_LIST } from '@/utils/app/stripe/stripe_config'; import { PaidPlan } from '@/types/paid_plan'; From 42f3318a6443b132336741ebfd0065c2dddd508a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 17:50:19 +0800 Subject: [PATCH 084/134] refactor: Update stripe product list based on environment --- types/stripe-product.ts | 47 +-- utils/app/stripe/stripe_config.ts | 321 +----------------- .../stripe/stripe_paid_plan_links_config.ts | 67 ++++ .../app/stripe/stripe_product_list_config.ts | 123 +++++++ 4 files changed, 215 insertions(+), 343 deletions(-) create mode 100644 utils/app/stripe/stripe_paid_plan_links_config.ts create mode 100644 utils/app/stripe/stripe_product_list_config.ts diff --git a/types/stripe-product.ts b/types/stripe-product.ts index 22364457ae..4ca60e4755 100644 --- a/types/stripe-product.ts +++ b/types/stripe-product.ts @@ -47,40 +47,17 @@ export interface StripeOneTimePaidPlanProduct extends BaseStripeProduct { export type NewStripeProduct = StripeTopUpProduct | StripePaidPlanProduct; -// TODO: To be remove START ================================ -export type MemberShipPlanPeriodType = 'monthly' | 'yearly' | 'one-time'; -export type MemberShipPlanCurrencyType = 'USD' | 'TWD'; - -// P.S. All of the code below is used in the product payment link -export type PlanCode = - | 'one_time_pro_plan_for_1_month' - | 'one_time_ultra_plan_for_1_month' - | 'monthly_pro_plan_subscription' - | 'monthly_ultra_plan_subscription' - | 'yearly_pro_plan_subscription' - | 'yearly_ultra_plan_subscription'; - -export interface MemberShipPlanItem { - link: string; - price_id: string; -} - -export interface PlanDetails { - plan_code: PlanCode; - currencies: { - [currency in MemberShipPlanCurrencyType]: MemberShipPlanItem; +export type PaidPlanCurrencyType = 'usd' | 'twd'; +export type PaidAvailablePlanType = + | 'ultra-yearly' + | 'ultra-monthly' + | 'pro-monthly'; + +export type PaidPlanLink = { + [currency in PaidPlanCurrencyType]: { + price_id: string; + link: string; }; -} +}; -export interface MemberShipPlan { - pro: { - [period in MemberShipPlanPeriodType]: PlanDetails; - }; - ultra: { - [period in MemberShipPlanPeriodType]: PlanDetails; - }; -} -export interface StripeProduct { - MEMBERSHIP_PLAN: MemberShipPlan; -} -// TODO: To be remove END ============================================= +export type PaidPlanLinks = Record; diff --git a/utils/app/stripe/stripe_config.ts b/utils/app/stripe/stripe_config.ts index 0417817c9d..41bef8c748 100644 --- a/utils/app/stripe/stripe_config.ts +++ b/utils/app/stripe/stripe_config.ts @@ -1,322 +1,27 @@ -import { NewStripeProduct, StripeProduct } from '@/types/stripe-product'; +import { PaidPlanLinks } from '@/types/stripe-product'; + +import { + STRIPE_PAID_PLAN_LINKS_PRODUCTION, + STRIPE_PAID_PLAN_LINKS_STAGING, +} from './stripe_paid_plan_links_config'; +import { + STRIPE_PRODUCT_LIST_PRODUCTION, + STRIPE_PRODUCT_LIST_STAGING, +} from './stripe_product_list_config'; // STRIPE CREDIT CODE export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT'; export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT'; -// Add production one -const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [ - { - type: 'paid_plan', - mode: 'payment', - givenDays: 7, - productValue: 'pro', - productId: 'prod_P39kXR1vgNBVZh', - note: 'This is a v2 weekly Pro plan', - }, - { - type: 'paid_plan', - mode: 'subscription', - productValue: 'ultra', - productId: 'prod_QC5xRJFNyaB3h7', - }, - { - type: 'paid_plan', - mode: 'subscription', - productValue: 'pro', - productId: 'prod_Nlh3dRKPO799ja', - }, - { - type: 'top_up', - mode: 'payment', - productValue: '50_GPT4_CREDIT', - productId: 'prod_OKKu2YQZyaJTYN', - credit: 50, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '150_GPT4_CREDIT', - productId: 'prod_OKKu2YQZyaJTYN', - credit: 150, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '300_GPT4_CREDIT', - productId: 'prod_OKKu2YQZyaJTYN', - credit: 300, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '500_IMAGE_CREDIT', - productId: 'prod_OKJgVwM66OOWuR', - credit: 500, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '100_IMAGE_CREDIT', - productId: 'prod_OKJgVwM66OOWuR', - credit: 100, - }, -]; -const STRIPE_PRODUCT_LIST_PRODUCTION: NewStripeProduct[] = [ - { - type: 'paid_plan', - mode: 'payment', - givenDays: 7, - productValue: 'pro', - productId: 'prod_P39h2AVZANAN1M', - note: 'This is a v2 weekly Pro plan', - }, - { - type: 'paid_plan', - mode: 'subscription', - productValue: 'ultra', - productId: 'prod_Q6j96oOouZMFN4', - }, - { - type: 'paid_plan', - mode: 'subscription', - productValue: 'ultra', - productId: 'prod_PGES2QxP0aYFr4', - note: 'This is the pre-sell plan for the Ultra plan', - }, - { - type: 'paid_plan', - mode: 'subscription', - productValue: 'pro', - productId: 'prod_NlR6az1csuoBHl', - }, - { - type: 'top_up', - mode: 'payment', - productValue: '50_GPT4_CREDIT', - productId: 'prod_Nofw6ncuYiYD81', - credit: 50, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '150_GPT4_CREDIT', - productId: 'prod_Nog8i22B8eLX6y', - credit: 150, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '300_GPT4_CREDIT', - productId: 'prod_Nog91rmXzSJY1w', - credit: 300, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '100_IMAGE_CREDIT', - productId: 'prod_OKYWXZGkysAtFS', - credit: 100, - }, - { - type: 'top_up', - mode: 'payment', - productValue: '500_IMAGE_CREDIT', - productId: 'prod_OKYouQss6inD9q', - credit: 500, - }, -]; - export const STRIPE_PRODUCT_LIST = process.env.NEXT_PUBLIC_ENV === 'production' ? STRIPE_PRODUCT_LIST_PRODUCTION : STRIPE_PRODUCT_LIST_STAGING; -const STRIPE_PRODUCTS_PRODUCTION: StripeProduct = { - MEMBERSHIP_PLAN: { - pro: { - monthly: { - // META DATA use in the payment link - plan_code: 'monthly_pro_plan_subscription', - currencies: { - USD: { - link: 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - // NOTE: NOT IN USED IN APP - 'one-time': { - plan_code: 'one_time_pro_plan_for_1_month', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - // NOTE: NOT IN USED IN APP - yearly: { - plan_code: 'yearly_pro_plan_subscription', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - }, - ultra: { - 'one-time': { - plan_code: 'one_time_ultra_plan_for_1_month', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - monthly: { - plan_code: 'monthly_ultra_plan_subscription', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - yearly: { - plan_code: 'yearly_ultra_plan_subscription', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - }, - }, -}; - -const STRIPE_PRODUCTS_STAGING: StripeProduct = { - MEMBERSHIP_PLAN: { - pro: { - // NOTE: NOT IN USED IN APP - 'one-time': { - plan_code: 'one_time_pro_plan_for_1_month', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - monthly: { - // META DATA use in the payment link - plan_code: 'monthly_pro_plan_subscription', - currencies: { - USD: { - link: 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY', - price_id: 'price_1N09fTEEvfd1BzvuJwBCAfg2', - }, - TWD: { - link: 'https://buy.stripe.com/test_6oE01v1QM66S74I7sH', - price_id: 'price_1PLhJREEvfd1BzvuxCM477DD', - }, - }, - }, - // NOTE: NOT IN USED IN APP - yearly: { - plan_code: 'yearly_pro_plan_subscription', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - }, - ultra: { - // NOTE: NOT IN USED IN APP - 'one-time': { - plan_code: 'one_time_ultra_plan_for_1_month', - currencies: { - USD: { - link: '', - price_id: '', - }, - TWD: { - link: '', - price_id: '', - }, - }, - }, - monthly: { - plan_code: 'monthly_ultra_plan_subscription', - currencies: { - USD: { - link: 'https://buy.stripe.com/test_cN29C5dzu8f0dt6fZe', - price_id: 'price_1PLhlhEEvfd1Bzvu0UEqwm9y', - }, - TWD: { - link: 'https://buy.stripe.com/test_fZe6pT1QM1QC2Os6oF', - price_id: 'price_1PLiWBEEvfd1BzvunVr1yZ55', - }, - }, - }, - yearly: { - plan_code: 'yearly_ultra_plan_subscription', - currencies: { - USD: { - link: 'https://buy.stripe.com/test_3csaG952Y2UG74IfZg', - price_id: 'price_1PLiWmEEvfd1BzvuDFmiLKI6', - }, - TWD: { - link: 'https://buy.stripe.com/test_8wM9C5fHCan8agUdR9', - price_id: 'price_1PLiWVEEvfd1Bzvu7voi21Jw', - }, - }, - }, - }, - }, -}; - -export const STRIPE_PRODUCTS = +export const STRIPE_PAID_PLAN_LINKS = process.env.NEXT_PUBLIC_ENV === 'production' - ? STRIPE_PRODUCTS_PRODUCTION - : STRIPE_PRODUCTS_STAGING; + ? STRIPE_PAID_PLAN_LINKS_PRODUCTION + : STRIPE_PAID_PLAN_LINKS_STAGING; // =========== TOP UP LINKS =========== export const GPT4_CREDIT_PURCHASE_LINKS = { diff --git a/utils/app/stripe/stripe_paid_plan_links_config.ts b/utils/app/stripe/stripe_paid_plan_links_config.ts new file mode 100644 index 0000000000..e908d1889a --- /dev/null +++ b/utils/app/stripe/stripe_paid_plan_links_config.ts @@ -0,0 +1,67 @@ +import { PaidPlanLinks } from '@/types/stripe-product'; + +export const STRIPE_PAID_PLAN_LINKS_PRODUCTION: PaidPlanLinks = { + 'ultra-yearly': { + usd: { + link: '', + price_id: '', + }, + twd: { + link: '', + price_id: '', + }, + }, + 'ultra-monthly': { + twd: { + link: '', + price_id: '', + }, + usd: { + link: '', + price_id: '', + }, + }, + 'pro-monthly': { + twd: { + link: '', + price_id: '', + }, + usd: { + link: 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1', + price_id: '', + }, + }, +}; + +export const STRIPE_PAID_PLAN_LINKS_STAGING: PaidPlanLinks = { + 'ultra-yearly': { + usd: { + link: 'https://buy.stripe.com/test_3csaG952Y2UG74IfZg', + price_id: 'price_1PLiWmEEvfd1BzvuDFmiLKI6', + }, + twd: { + link: 'https://buy.stripe.com/test_8wM9C5fHCan8agUdR9', + price_id: 'price_1PLiWVEEvfd1Bzvu7voi21Jw', + }, + }, + 'ultra-monthly': { + twd: { + link: 'https://buy.stripe.com/test_fZe6pT1QM1QC2Os6oF', + price_id: 'price_1PLiWBEEvfd1BzvunVr1yZ55', + }, + usd: { + link: 'https://buy.stripe.com/test_cN29C5dzu8f0dt6fZe', + price_id: 'price_1PLhlhEEvfd1Bzvu0UEqwm9y', + }, + }, + 'pro-monthly': { + twd: { + link: 'https://buy.stripe.com/test_6oE01v1QM66S74I7sH', + price_id: 'price_1PLhJREEvfd1BzvuxCM477DD', + }, + usd: { + link: 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY', + price_id: 'price_1N09fTEEvfd1BzvuJwBCAfg2', + }, + }, +}; diff --git a/utils/app/stripe/stripe_product_list_config.ts b/utils/app/stripe/stripe_product_list_config.ts new file mode 100644 index 0000000000..114b58724b --- /dev/null +++ b/utils/app/stripe/stripe_product_list_config.ts @@ -0,0 +1,123 @@ +import { NewStripeProduct } from '@/types/stripe-product'; + +export const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [ + { + type: 'paid_plan', + mode: 'payment', + givenDays: 7, + productValue: 'pro', + productId: 'prod_P39kXR1vgNBVZh', + note: 'This is a v2 weekly Pro plan', + }, + { + type: 'paid_plan', + mode: 'subscription', + productValue: 'ultra', + productId: 'prod_QC5xRJFNyaB3h7', + }, + { + type: 'paid_plan', + mode: 'subscription', + productValue: 'pro', + productId: 'prod_Nlh3dRKPO799ja', + }, + { + type: 'top_up', + mode: 'payment', + productValue: '50_GPT4_CREDIT', + productId: 'prod_OKKu2YQZyaJTYN', + credit: 50, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '150_GPT4_CREDIT', + productId: 'prod_OKKu2YQZyaJTYN', + credit: 150, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '300_GPT4_CREDIT', + productId: 'prod_OKKu2YQZyaJTYN', + credit: 300, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '500_IMAGE_CREDIT', + productId: 'prod_OKJgVwM66OOWuR', + credit: 500, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '100_IMAGE_CREDIT', + productId: 'prod_OKJgVwM66OOWuR', + credit: 100, + }, +]; +export const STRIPE_PRODUCT_LIST_PRODUCTION: NewStripeProduct[] = [ + { + type: 'paid_plan', + mode: 'payment', + givenDays: 7, + productValue: 'pro', + productId: 'prod_P39h2AVZANAN1M', + note: 'This is a v2 weekly Pro plan', + }, + { + type: 'paid_plan', + mode: 'subscription', + productValue: 'ultra', + productId: 'prod_Q6j96oOouZMFN4', + }, + { + type: 'paid_plan', + mode: 'subscription', + productValue: 'ultra', + productId: 'prod_PGES2QxP0aYFr4', + note: 'This is the pre-sell plan for the Ultra plan', + }, + { + type: 'paid_plan', + mode: 'subscription', + productValue: 'pro', + productId: 'prod_NlR6az1csuoBHl', + }, + { + type: 'top_up', + mode: 'payment', + productValue: '50_GPT4_CREDIT', + productId: 'prod_Nofw6ncuYiYD81', + credit: 50, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '150_GPT4_CREDIT', + productId: 'prod_Nog8i22B8eLX6y', + credit: 150, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '300_GPT4_CREDIT', + productId: 'prod_Nog91rmXzSJY1w', + credit: 300, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '100_IMAGE_CREDIT', + productId: 'prod_OKYWXZGkysAtFS', + credit: 100, + }, + { + type: 'top_up', + mode: 'payment', + productValue: '500_IMAGE_CREDIT', + productId: 'prod_OKYouQss6inD9q', + credit: 500, + }, +]; From 249acf327970a11d58a3163d7ca6b8dfd751e735 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 17:50:25 +0800 Subject: [PATCH 085/134] refactor: Update ULTRA monthly subscription payment links --- components/User/Settings/PlanComparison.tsx | 24 +++--- utils/app/paid_plan_helper.ts | 91 --------------------- 2 files changed, 11 insertions(+), 104 deletions(-) delete mode 100644 utils/app/paid_plan_helper.ts diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 3fe7419418..1405e1ef89 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -4,7 +4,10 @@ import { useTranslation } from 'react-i18next'; import { useChangeSubscriptionPlan } from '@/hooks/useChangeSubscriptionPlan'; -import { OrderedSubscriptionPlans, STRIPE_PRODUCTS } from '@/utils/app/const'; +import { + OrderedSubscriptionPlans, + STRIPE_PAID_PLAN_LINKS, +} from '@/utils/app/const'; import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; @@ -82,8 +85,8 @@ const ProPlanContent = ({ user }: { user: User | null }) => { const upgradeLinkOnClick = () => { const paymentLink = i18n.language === 'zh-Hant' || i18n.language === 'zh' - ? STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.currencies.TWD.link - : STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro.monthly.currencies.USD.link; + ? STRIPE_PAID_PLAN_LINKS['pro-monthly'].twd.link + : STRIPE_PAID_PLAN_LINKS['pro-monthly'].usd.link; const userEmail = user?.email; const userId = user?.id; @@ -173,23 +176,18 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); const upgradeLinkOnClick = () => { - let paymentLink = - STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.USD.link; + let paymentLink = STRIPE_PAID_PLAN_LINKS['ultra-monthly'].usd.link; if (priceType === 'monthly') { if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { - paymentLink = - STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.TWD.link; + paymentLink = STRIPE_PAID_PLAN_LINKS['ultra-monthly'].twd.link; } else { - paymentLink = - STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.monthly.currencies.USD.link; + paymentLink = STRIPE_PAID_PLAN_LINKS['ultra-monthly'].usd.link; } } else { if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { - paymentLink = - STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.yearly.currencies.TWD.link; + paymentLink = STRIPE_PAID_PLAN_LINKS['ultra-yearly'].twd.link; } else { - paymentLink = - STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra.yearly.currencies.USD.link; + paymentLink = STRIPE_PAID_PLAN_LINKS['ultra-yearly'].usd.link; } } diff --git a/utils/app/paid_plan_helper.ts b/utils/app/paid_plan_helper.ts deleted file mode 100644 index c7d4a85317..0000000000 --- a/utils/app/paid_plan_helper.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { PaidPlan, TopUpRequest } from '@/types/paid_plan'; -import { - MemberShipPlanCurrencyType, - MemberShipPlanItem, -} from '@/types/stripe-product'; - -import { - STRIPE_PLAN_CODE_GPT4_CREDIT, - STRIPE_PLAN_CODE_IMAGE_CREDIT, - STRIPE_PRODUCTS, -} from './const'; - -export const getPriceIdByPaidPlan = ( - paidPlan: PaidPlan, - currency: MemberShipPlanCurrencyType, -): MemberShipPlanItem['price_id'] | undefined => { - const PRO = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro; - const ULTRA = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra; - - switch (paidPlan) { - // PRO - case PaidPlan.ProMonthly: - return PRO['monthly'].currencies[currency].price_id; - case PaidPlan.ProOneTime: - return PRO['one-time'].currencies[currency].price_id; - case PaidPlan.ProYearly: - return PRO['yearly'].currencies[currency].price_id; - - // ULTRA - case PaidPlan.UltraMonthly: - return ULTRA['monthly'].currencies[currency].price_id; - case PaidPlan.UltraYearly: - return ULTRA['yearly'].currencies[currency].price_id; - case PaidPlan.UltraOneTime: - return ULTRA['one-time'].currencies[currency].price_id; - - default: - return undefined; - } -}; -export const getPaidPlanByPlanCode = ( - planCode: string, -): PaidPlan | TopUpRequest | undefined => { - const PRO = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.pro; - const ULTRA = STRIPE_PRODUCTS.MEMBERSHIP_PLAN.ultra; - - switch (planCode.toUpperCase()) { - // PRO - case PRO['monthly'].plan_code.toUpperCase(): - return PaidPlan.ProMonthly; - case PRO['one-time'].plan_code.toUpperCase(): - return PaidPlan.ProOneTime; - case PRO['yearly'].plan_code.toUpperCase(): - return PaidPlan.ProYearly; - - // ULTRA - case ULTRA['monthly'].plan_code.toUpperCase(): - return PaidPlan.UltraMonthly; - case ULTRA['yearly'].plan_code.toUpperCase(): - return PaidPlan.UltraYearly; - case ULTRA['one-time'].plan_code.toUpperCase(): - return PaidPlan.UltraOneTime; - - // TOP UP REQUEST - case STRIPE_PLAN_CODE_IMAGE_CREDIT.toUpperCase(): - return TopUpRequest.ImageCredit; - case STRIPE_PLAN_CODE_GPT4_CREDIT.toUpperCase(): - return TopUpRequest.GPT4Credit; - default: - return undefined; - } -}; - -export const getPaidPlanByPriceId = (priceId: string): PaidPlan | undefined => { - for (const planType of ['pro', 'ultra'] as const) { - for (const period of ['monthly', 'yearly', 'one-time'] as const) { - const planDetails = STRIPE_PRODUCTS.MEMBERSHIP_PLAN[planType][period]; - if (planDetails) { - for (const currency of Object.keys( - planDetails.currencies, - ) as MemberShipPlanCurrencyType[]) { - if (planDetails.currencies[currency].price_id === priceId) { - console.log('current plan currency ', currency); - return getPaidPlanByPlanCode(planDetails.plan_code) as PaidPlan; - } - } - } - } - } - return undefined; -}; From cd7a2a09f7588f0e31b263568d26340f19b41483 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 17:50:30 +0800 Subject: [PATCH 086/134] refactor: Update STRIPE_PAID_PLAN_LINKS in const.ts --- utils/app/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/app/const.ts b/utils/app/const.ts index 0d623f8184..cba8564f15 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -5,7 +5,7 @@ import dayjs from 'dayjs'; import { v4 as uuidv4 } from 'uuid'; export { - STRIPE_PRODUCTS, + STRIPE_PAID_PLAN_LINKS, STRIPE_PLAN_CODE_GPT4_CREDIT, STRIPE_PLAN_CODE_IMAGE_CREDIT, GPT4_CREDIT_PURCHASE_LINKS, From 80a29add5f0b6072245a8bc1c97dcbbe7d48f8aa Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 17:50:34 +0800 Subject: [PATCH 087/134] refactor: Update package.json with type-check script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 99087d7c95..deda827124 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "gen-supabase-type": "supabase gen types typescript --local --schema public > types/supabase.ts", "coverage": "vitest run --coverage", "deploy-firebase-function": "npm run deploy --prefix firebase/functions/", - "deploy-firebase-function-production": "npm run deploy-production --prefix firebase/functions/" + "deploy-firebase-function-production": "npm run deploy-production --prefix firebase/functions/", + "type-check": "tsc --noEmit" }, "dependencies": { "@ctrl/react-adsense": "^1.6.2", From f34f0173a79a8a83f9bc2700864719685286b3e4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 18:03:39 +0800 Subject: [PATCH 088/134] refactor: Update ULTRA yearly and monthly subscription payment links --- .../stripe/stripe_paid_plan_links_config.ts | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/utils/app/stripe/stripe_paid_plan_links_config.ts b/utils/app/stripe/stripe_paid_plan_links_config.ts index e908d1889a..8246b49d12 100644 --- a/utils/app/stripe/stripe_paid_plan_links_config.ts +++ b/utils/app/stripe/stripe_paid_plan_links_config.ts @@ -2,60 +2,71 @@ import { PaidPlanLinks } from '@/types/stripe-product'; export const STRIPE_PAID_PLAN_LINKS_PRODUCTION: PaidPlanLinks = { 'ultra-yearly': { - usd: { - link: '', - price_id: '', - }, twd: { - link: '', - price_id: '', + // $8,800.00 TWD / year + link: 'https://buy.stripe.com/6oEaID6U2b8Ncs85kK', + price_id: 'price_1PGVh6EEvfd1Bzvu3OyJGTZ2', + }, + usd: { + // $279.99 USD / year + link: 'https://buy.stripe.com/fZebMH0vEdgVeAg3cF', + price_id: 'price_1PMSCyEEvfd1Bzvuk7VHjx6S', }, }, 'ultra-monthly': { twd: { - link: '', - price_id: '', + // $880.00 TWD / month + link: 'https://buy.stripe.com/8wMeYT92aekZ0Jq9B1', + price_id: 'price_1PMS9KEEvfd1BzvuBCA4LAJA', }, usd: { - link: '', - price_id: '', + // $29.99 USD / month + link: 'https://buy.stripe.com/4gwbMH6U27WB9fW9B2', + price_id: 'price_1PMSBdEEvfd1BzvuqUuMvUv7', }, }, 'pro-monthly': { twd: { - link: '', - price_id: '', + // $249.99 TWD / month + link: 'https://buy.stripe.com/dR65oj2DM90FeAgcNg', + price_id: 'price_1PMSIDEEvfd1BzvuegdR9cyP', }, usd: { + // $9.99 USD / month link: 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1', - price_id: '', + price_id: 'price_1N1VMjEEvfd1BzvuWqqVu9YZ', }, }, }; export const STRIPE_PAID_PLAN_LINKS_STAGING: PaidPlanLinks = { 'ultra-yearly': { - usd: { - link: 'https://buy.stripe.com/test_3csaG952Y2UG74IfZg', - price_id: 'price_1PLiWmEEvfd1BzvuDFmiLKI6', - }, twd: { + // $8,800.00 TWD / year link: 'https://buy.stripe.com/test_8wM9C5fHCan8agUdR9', price_id: 'price_1PLiWVEEvfd1Bzvu7voi21Jw', }, + usd: { + // $279.99 USD / year + link: 'https://buy.stripe.com/test_3csaG952Y2UG74IfZg', + price_id: 'price_1PLiWmEEvfd1BzvuDFmiLKI6', + }, }, 'ultra-monthly': { twd: { + // $880.00 TWD / month link: 'https://buy.stripe.com/test_fZe6pT1QM1QC2Os6oF', price_id: 'price_1PLiWBEEvfd1BzvunVr1yZ55', }, usd: { + // $29.99 USD / month link: 'https://buy.stripe.com/test_cN29C5dzu8f0dt6fZe', price_id: 'price_1PLhlhEEvfd1Bzvu0UEqwm9y', }, }, 'pro-monthly': { twd: { + // link: 'https://buy.stripe.com/test_6oE01v1QM66S74I7sH', price_id: 'price_1PLhJREEvfd1BzvuxCM477DD', }, From cb07ff393335bf675d9b6f8ecb61df90fb9fd834 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 18:04:14 +0800 Subject: [PATCH 089/134] refactor: Update STRIPE_PAID_PLAN_LINKS in const.ts --- utils/app/stripe/stripe_paid_plan_links_config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/app/stripe/stripe_paid_plan_links_config.ts b/utils/app/stripe/stripe_paid_plan_links_config.ts index 8246b49d12..2a65fada4a 100644 --- a/utils/app/stripe/stripe_paid_plan_links_config.ts +++ b/utils/app/stripe/stripe_paid_plan_links_config.ts @@ -66,11 +66,12 @@ export const STRIPE_PAID_PLAN_LINKS_STAGING: PaidPlanLinks = { }, 'pro-monthly': { twd: { - // + // $249.99 TWD / month link: 'https://buy.stripe.com/test_6oE01v1QM66S74I7sH', price_id: 'price_1PLhJREEvfd1BzvuxCM477DD', }, usd: { + // $9.99 USD / month link: 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY', price_id: 'price_1N09fTEEvfd1BzvuJwBCAfg2', }, From 9c9d534e4a36cc02fe3fba68700a23ed3f6d2902 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 18:07:41 +0800 Subject: [PATCH 090/134] refactor: Remove unused subscription JSON file --- sub.json | 170 ------------------------------------------------------- 1 file changed, 170 deletions(-) delete mode 100644 sub.json diff --git a/sub.json b/sub.json deleted file mode 100644 index 38d73ba81a..0000000000 --- a/sub.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "id": "sub_1PMO44EEvfd1BzvuNTHhGZ6N", - "object": "subscription", - "application": null, - "application_fee_percent": null, - "automatic_tax": { - "enabled": false, - "liability": null - }, - "billing_cycle_anchor": 1717133504, - "billing_cycle_anchor_config": null, - "billing_thresholds": null, - "cancel_at": null, - "cancel_at_period_end": false, - "canceled_at": null, - "cancellation_details": { - "comment": null, - "feedback": null, - "reason": null - }, - "collection_method": "charge_automatically", - "created": 1717133504, - "currency": "twd", - "current_period_end": 1719725504, - "current_period_start": 1717133504, - "customer": "cus_QCnfro8d7zTJvT", - "days_until_due": null, - "default_payment_method": "pm_1PMO43EEvfd1BzvueE4yD0mH", - "default_source": null, - "default_tax_rates": [], - "description": null, - "discount": null, - "discounts": [], - "ended_at": null, - "invoice_settings": { - "account_tax_ids": null, - "issuer": { - "type": "self" - } - }, - "items": { - "object": "list", - "data": [ - { - "id": "si_QCnfJ3kKQPjtGQ", - "object": "subscription_item", - "billing_thresholds": null, - "created": 1717133505, - "discounts": [], - "metadata": {}, - "plan": { - "id": "price_1PLhJREEvfd1BzvuxCM477DD", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 24999, - "amount_decimal": "24999", - "billing_scheme": "per_unit", - "created": 1716969165, - "currency": "twd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": {}, - "meter": null, - "nickname": null, - "product": "prod_Nlh3dRKPO799ja", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "price": { - "id": "price_1PLhJREEvfd1BzvuxCM477DD", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1716969165, - "currency": "twd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": {}, - "nickname": null, - "product": "prod_Nlh3dRKPO799ja", - "recurring": { - "aggregate_usage": null, - "interval": "month", - "interval_count": 1, - "meter": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "exclusive", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 24999, - "unit_amount_decimal": "24999" - }, - "quantity": 1, - "subscription": "sub_1PMO44EEvfd1BzvuNTHhGZ6N", - "tax_rates": [] - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/subscription_items?subscription=sub_1PMO44EEvfd1BzvuNTHhGZ6N" - }, - "latest_invoice": "in_1PMO44EEvfd1BzvuT0KgMAcF", - "livemode": false, - "metadata": {}, - "next_pending_invoice_item_invoice": null, - "on_behalf_of": null, - "pause_collection": null, - "payment_settings": { - "payment_method_options": { - "acss_debit": null, - "bancontact": null, - "card": { - "network": null, - "request_three_d_secure": "automatic" - }, - "customer_balance": null, - "konbini": null, - "sepa_debit": null, - "us_bank_account": null - }, - "payment_method_types": null, - "save_default_payment_method": "off" - }, - "pending_invoice_item_interval": null, - "pending_setup_intent": null, - "pending_update": null, - "plan": { - "id": "price_1PLhJREEvfd1BzvuxCM477DD", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 24999, - "amount_decimal": "24999", - "billing_scheme": "per_unit", - "created": 1716969165, - "currency": "twd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": {}, - "meter": null, - "nickname": null, - "product": "prod_Nlh3dRKPO799ja", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "quantity": 1, - "schedule": null, - "start_date": 1717133504, - "status": "active", - "test_clock": null, - "transfer_data": null, - "trial_end": null, - "trial_settings": { - "end_behavior": { - "missing_payment_method": "create_invoice" - } - }, - "trial_start": null -} From 5230cfd3a4dd16405461aee9d35fcdde0ce51c3d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 31 May 2024 18:22:38 +0800 Subject: [PATCH 091/134] refactor: Update useChangeSubscriptionPlan to accept priceId parameter --- hooks/useChangeSubscriptionPlan.ts | 3 ++- pages/api/stripe/change-subscription-plan.ts | 21 +++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/hooks/useChangeSubscriptionPlan.ts b/hooks/useChangeSubscriptionPlan.ts index dd98db2fed..d7ffc6cc75 100644 --- a/hooks/useChangeSubscriptionPlan.ts +++ b/hooks/useChangeSubscriptionPlan.ts @@ -13,7 +13,7 @@ export const useChangeSubscriptionPlan = () => { } = useContext(HomeContext); const { t } = useTranslation('common'); - const changeSubscriptionPlan = async () => { + const changeSubscriptionPlan = async (priceId: string) => { if (!user) { throw new Error('User is not authenticated'); } @@ -25,6 +25,7 @@ export const useChangeSubscriptionPlan = () => { 'access-token': accessToken, 'Content-Type': 'application/json', }, + body: JSON.stringify({ priceId }), }); if (!response.ok) { throw new Error('Network response was not ok'); diff --git a/pages/api/stripe/change-subscription-plan.ts b/pages/api/stripe/change-subscription-plan.ts index 63239067ad..84221761d1 100644 --- a/pages/api/stripe/change-subscription-plan.ts +++ b/pages/api/stripe/change-subscription-plan.ts @@ -2,6 +2,7 @@ import { fetchUserProfileWithAccessToken } from '@/utils/server/auth'; import { fetchSubscriptionIdByUserId } from '@/utils/server/stripe/strip_helper'; import Stripe from 'stripe'; +import { z } from 'zod'; export const config = { runtime: 'edge', @@ -17,15 +18,17 @@ const handler = async (req: Request) => { } try { - // TODO: add zod validation to validate if the request PriceId is the PaidPlan type - // ULTRA yearly price id USD - // const newPaidPlanPriceId = 'price_1PLiWmEEvfd1BzvuDFmiLKI6'; - - // ULTRA monthly price id TWD - const newPaidPlanPriceId = 'price_1PLiWBEEvfd1BzvunVr1yZ55'; - - // ULTRA monthly price id USD - // const newPaidPlanPriceId = 'price_1PLhlhEEvfd1Bzvu0UEqwm9y'; + const requestBodySchema = z.object({ + priceId: z.string(), + }); + + const body = await req.json(); + const result = requestBodySchema.safeParse(body); + if (!result.success) { + return new Response('Invalid request body', { status: 400 }); + } + const { priceId } = result.data; + const newPaidPlanPriceId = priceId; // Step 1: Get User Profile and Subscription ID const userProfile = await fetchUserProfileWithAccessToken(req); From a6047bde5dc61ccea2f143ee2cab7ce3943bd1f2 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:57:39 +0800 Subject: [PATCH 092/134] refactor: Add useUserSubscriptionDetail hook for fetching user subscription details --- .../useUserSubscriptionDetail.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 hooks/stripeSubscription/useUserSubscriptionDetail.ts diff --git a/hooks/stripeSubscription/useUserSubscriptionDetail.ts b/hooks/stripeSubscription/useUserSubscriptionDetail.ts new file mode 100644 index 0000000000..b0ad9f8f93 --- /dev/null +++ b/hooks/stripeSubscription/useUserSubscriptionDetail.ts @@ -0,0 +1,52 @@ +import { useSupabaseClient } from '@supabase/auth-helpers-react'; +import { useQuery } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; + +import { UserSubscriptionDetail } from '@/types/user'; + +export const useUserSubscriptionDetail = ({ + isPaidUser, +}: { + isPaidUser: boolean; +}) => { + const { t } = useTranslation('common'); + const supabase = useSupabaseClient(); + + const fetchUserSubscriptionDetail = async (): Promise< + UserSubscriptionDetail | undefined + > => { + const accessToken = (await supabase.auth.getSession()).data.session + ?.access_token!; + + const response = await fetch('/api/stripe/user-subscription-detail', { + method: 'GET', + headers: { + 'access-token': accessToken, + }, + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + return (await response.json())?.data; + }; + + return useQuery( + ['userSubscriptionDetail', isPaidUser], + fetchUserSubscriptionDetail, + { + cacheTime: 0, + enabled: isPaidUser, + onError: (error) => { + console.error('Error fetching user subscription detail:', error); + toast.error( + t( + 'There was a problem fetching user subscription detail, please contact support team', + ), + ); + }, + }, + ); +}; From 81bdbdb2c8f055475314e57b8ebbb4a910a5455d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:57:45 +0800 Subject: [PATCH 093/134] refactor: Remove useChangeSubscriptionPlan hook and related code --- hooks/useChangeSubscriptionPlan.ts | 50 ------------------------------ 1 file changed, 50 deletions(-) delete mode 100644 hooks/useChangeSubscriptionPlan.ts diff --git a/hooks/useChangeSubscriptionPlan.ts b/hooks/useChangeSubscriptionPlan.ts deleted file mode 100644 index d7ffc6cc75..0000000000 --- a/hooks/useChangeSubscriptionPlan.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useSupabaseClient } from '@supabase/auth-helpers-react'; -import { useMutation } from '@tanstack/react-query'; -import { useContext } from 'react'; -import toast from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; - -import HomeContext from '@/components/home/home.context'; - -export const useChangeSubscriptionPlan = () => { - const supabase = useSupabaseClient(); - const { - state: { user }, - } = useContext(HomeContext); - const { t } = useTranslation('common'); - - const changeSubscriptionPlan = async (priceId: string) => { - if (!user) { - throw new Error('User is not authenticated'); - } - const accessToken = (await supabase.auth.getSession()).data.session - ?.access_token!; - const response = await fetch('/api/stripe/change-subscription-plan', { - method: 'POST', - headers: { - 'access-token': accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ priceId }), - }); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - const data = await response.json(); - console.log(data); - return data.subscription; - }; - - return useMutation(changeSubscriptionPlan, { - onError: (error) => { - if (error instanceof Error) { - console.log(error.message); - } - toast.error( - t( - 'There was a problem when changing subscription plan, please contact support team', - ), - ); - }, - }); -}; From eb7f1269f593ea45f6a15395df4653b0d60e10d0 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:57:54 +0800 Subject: [PATCH 094/134] refactor: Add user subscription detail fetching and error handling --- public/locales/zh-Hans/common.json | 3 ++- public/locales/zh-Hant/common.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/zh-Hans/common.json b/public/locales/zh-Hans/common.json index 27b4db61a6..967c353948 100644 --- a/public/locales/zh-Hans/common.json +++ b/public/locales/zh-Hans/common.json @@ -11,5 +11,6 @@ "Upload": "上传", "File uploaded successfully": "文件上传成功", "File upload failed": "文件上传失败", - "There was a problem when changing subscription plan, please contact support team": "当您更改订阅方案时,发生问题,请联系支援团队" + "There was a problem when changing subscription plan, please contact support team": "当您更改订阅方案时,发生问题,请联系支援团队", + "There was a problem fetching user subscription detail, please contact support team": "取得使用者订阅详情时,发生问题,请联系支援团队" } diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json index 3d04c90a38..84d3d4aca3 100644 --- a/public/locales/zh-Hant/common.json +++ b/public/locales/zh-Hant/common.json @@ -11,5 +11,6 @@ "Upload": "上傳", "File uploaded successfully": "檔案上傳成功", "File upload failed": "檔案上傳失敗", - "There was a problem when changing subscription plan, please contact support team": "當您更改訂閱方案時,發生問題,請聯絡支援團隊" + "There was a problem when changing subscription plan, please contact support team": "當您更改訂閱方案時,發生問題,請聯絡支援團隊", + "There was a problem fetching user subscription detail, please contact support team": "取得使用者訂閱詳細資訊時,發生問題,請聯絡支援團隊" } From 7ae32b2fa3e8386071ec67457a90bc051f92975a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:58:04 +0800 Subject: [PATCH 095/134] refactor: Update PlanComparison component to include isPaidUser prop --- components/User/Settings/Settings_Account.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/User/Settings/Settings_Account.tsx b/components/User/Settings/Settings_Account.tsx index da3aa75a59..1d8f445662 100644 --- a/components/User/Settings/Settings_Account.tsx +++ b/components/User/Settings/Settings_Account.tsx @@ -58,7 +58,7 @@ export default function Settings_Account() {

)} - {} + {} {displayReferralCodeEnterer && }
From e388ec6620d5f7b6fefe8bbe3f168f088f129e36 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:58:23 +0800 Subject: [PATCH 096/134] refactor: Remove change subscription plan API endpoint --- pages/api/stripe/change-subscription-plan.ts | 99 -------------------- 1 file changed, 99 deletions(-) delete mode 100644 pages/api/stripe/change-subscription-plan.ts diff --git a/pages/api/stripe/change-subscription-plan.ts b/pages/api/stripe/change-subscription-plan.ts deleted file mode 100644 index 84221761d1..0000000000 --- a/pages/api/stripe/change-subscription-plan.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { fetchUserProfileWithAccessToken } from '@/utils/server/auth'; -import { fetchSubscriptionIdByUserId } from '@/utils/server/stripe/strip_helper'; - -import Stripe from 'stripe'; -import { z } from 'zod'; - -export const config = { - runtime: 'edge', -}; - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: '2022-11-15', -}); - -const handler = async (req: Request) => { - if (req.method !== 'POST') { - return new Response('Method Not Allowed', { status: 405 }); - } - - try { - const requestBodySchema = z.object({ - priceId: z.string(), - }); - - const body = await req.json(); - const result = requestBodySchema.safeParse(body); - if (!result.success) { - return new Response('Invalid request body', { status: 400 }); - } - const { priceId } = result.data; - const newPaidPlanPriceId = priceId; - - // Step 1: Get User Profile and Subscription ID - const userProfile = await fetchUserProfileWithAccessToken(req); - if (userProfile.plan !== 'pro' && userProfile.plan !== 'ultra') { - throw new Error('User does not have a paid plan'); - } - const subscriptionId = await fetchSubscriptionIdByUserId(userProfile.id); - if (!subscriptionId) { - throw new Error('User does not have a valid subscription in Stripe'); - } - - // Step 2: Check if the new price id is valid - const newPricePlan = await stripe.prices.retrieve(newPaidPlanPriceId); - if (!newPricePlan) { - throw new Error('New price id is not valid'); - } - - // Step 3: Retrieve Current Subscription - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - - // Check if user has any active items - if (!(subscription.items.data?.[0].object === 'subscription_item')) { - throw new Error('Subscription has no active subscription plan'); - } - const currentPriceId = subscription.items.data?.[0].price; - if (!currentPriceId) { - throw new Error('Subscription has no active subscription plan'); - } - - // Step 4: Update Subscription - const updatedSubscription = await stripe.subscriptions.update( - subscriptionId, - { - items: [ - { - id: subscription.items.data[0].id, - price: newPaidPlanPriceId, - }, - ], - proration_behavior: 'always_invoice', - }, - ); - - // Step 5: Retrieve Invoice URL - const invoice = await stripe.invoices.retrieve( - updatedSubscription.latest_invoice as string, - ); - const invoiceUrl = invoice.hosted_invoice_url; - - return new Response( - JSON.stringify({ - invoiceUrl: invoiceUrl, - }), - { - status: 200, - }, - ); - } catch (error) { - console.error(error); - if (error instanceof Error) { - return new Response(error.message, { status: 500 }); - } - - return new Response('Internal Server Error', { status: 500 }); - } -}; - -export default handler; From 89c6c1ec3c2ed07c7c712db6f80ea99177f11bf8 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:58:30 +0800 Subject: [PATCH 097/134] refactor: Add user subscription detail API endpoint --- pages/api/stripe/user-subscription-detail.ts | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 pages/api/stripe/user-subscription-detail.ts diff --git a/pages/api/stripe/user-subscription-detail.ts b/pages/api/stripe/user-subscription-detail.ts new file mode 100644 index 0000000000..39c340698b --- /dev/null +++ b/pages/api/stripe/user-subscription-detail.ts @@ -0,0 +1,79 @@ +import { fetchUserProfileWithAccessToken } from '@/utils/server/auth'; +import StripeHelper, { + fetchSubscriptionIdByUserId, +} from '@/utils/server/stripe/strip_helper'; + +import Stripe from 'stripe'; + +export const config = { + runtime: 'edge', +}; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2022-11-15', +}); + +const handler = async (req: Request) => { + if (req.method !== 'GET') { + return new Response('Method Not Allowed', { status: 405 }); + } + + try { + // Step 1: Get User Profile and Subscription ID + const userProfile = await fetchUserProfileWithAccessToken(req); + if (userProfile.plan !== 'pro' && userProfile.plan !== 'ultra') { + throw new Error('User is not in Paid plan'); + } + const defaultResponse = () => { + return new Response( + JSON.stringify({ + data: undefined, + }), + { status: 200 }, + ); + }; + const subscriptionId = await fetchSubscriptionIdByUserId(userProfile.id); + if (!subscriptionId) { + return defaultResponse(); + } + + // Step 2: Retrieve Current Subscription + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + const productId = subscription.items.data[0].price.product; + if (!productId || typeof productId !== 'string') { + return defaultResponse(); + } + const product = await StripeHelper.product.getProductByProductId( + productId, + 'subscription', + ); + if (product.type !== 'paid_plan') { + return defaultResponse(); + } + + // Step 3: Extract User Plan and Currency + const userPlan = product.productValue; + const currency = subscription.currency; + + return new Response( + JSON.stringify({ + data: { + userPlan, + subscriptionCurrency: currency, + }, + }), + { + status: 200, + }, + ); + } catch (error) { + console.error(error); + if (error instanceof Error) { + return new Response(error.message, { status: 500 }); + } + + return new Response('Internal Server Error', { status: 500 }); + } +}; + +export default handler; From 39b1dcf2f19097d16221c315ba3a923bdaa17575 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:58:43 +0800 Subject: [PATCH 098/134] refactor: Update fetchSubscriptionIdByUserId to return null if no subscription ID is found --- utils/server/stripe/strip_helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts index e511f081ee..f90089ff6a 100644 --- a/utils/server/stripe/strip_helper.ts +++ b/utils/server/stripe/strip_helper.ts @@ -83,7 +83,7 @@ async function getProductIdByCheckoutSessionId( export async function fetchSubscriptionIdByUserId( userId: string, -): Promise { +): Promise { const { data: userProfile } = await supabase .from('profiles') .select('stripe_subscription_id') From 41cebf5f3db9b198c64df08c9528144a3f55f63f Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:58:49 +0800 Subject: [PATCH 099/134] refactor: Add UserSubscriptionDetail interface for user subscription details --- types/user.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/types/user.ts b/types/user.ts index 829cd0841f..c1003e50bd 100644 --- a/types/user.ts +++ b/types/user.ts @@ -1,6 +1,10 @@ import { ExportFormatV4 } from './export'; import { SubscriptionPlan } from './paid_plan'; import { PluginID } from './plugin'; +import { + PaidPlanCurrencyType, + StripeProductPaidPlanType, +} from './stripe-product'; import { SupabaseClient } from '@supabase/supabase-js'; @@ -55,3 +59,8 @@ export type UserProfileQuery = RequiredOne< UserProfileQueryProps, 'userId' | 'email' >; + +export interface UserSubscriptionDetail { + userPlan: StripeProductPaidPlanType | null; + subscriptionCurrency: PaidPlanCurrencyType; +} From 4dbcc4753fa97fb3da9cc0c5348490c30bb5f9f8 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 09:58:57 +0800 Subject: [PATCH 100/134] refactor: Update PaidPlanLinks type to use AvailablePaidPlanType --- types/stripe-product.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/stripe-product.ts b/types/stripe-product.ts index 4ca60e4755..ad9a1f8056 100644 --- a/types/stripe-product.ts +++ b/types/stripe-product.ts @@ -48,7 +48,7 @@ export interface StripeOneTimePaidPlanProduct extends BaseStripeProduct { export type NewStripeProduct = StripeTopUpProduct | StripePaidPlanProduct; export type PaidPlanCurrencyType = 'usd' | 'twd'; -export type PaidAvailablePlanType = +export type AvailablePaidPlanType = | 'ultra-yearly' | 'ultra-monthly' | 'pro-monthly'; @@ -60,4 +60,4 @@ export type PaidPlanLink = { }; }; -export type PaidPlanLinks = Record; +export type PaidPlanLinks = Record; From 7b0f60477e996265c159ac4fa4364d5db99c0480 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 10:18:52 +0800 Subject: [PATCH 101/134] refactor: Add ChangeSubscriptionButton component for managing user subscription changes --- .../Settings/ChangeSubscriptionButton.tsx | 116 ++++++++++++ components/User/Settings/PlanComparison.tsx | 167 ++++++++++++------ 2 files changed, 229 insertions(+), 54 deletions(-) create mode 100644 components/User/Settings/ChangeSubscriptionButton.tsx diff --git a/components/User/Settings/ChangeSubscriptionButton.tsx b/components/User/Settings/ChangeSubscriptionButton.tsx new file mode 100644 index 0000000000..0a8aa7f32e --- /dev/null +++ b/components/User/Settings/ChangeSubscriptionButton.tsx @@ -0,0 +1,116 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { OrderedSubscriptionPlans } from '@/utils/app/const'; + +import { + AvailablePaidPlanType, + StripeProductPaidPlanType, +} from '@/types/stripe-product'; +import { User, UserSubscriptionDetail } from '@/types/user'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; + +const ChangeSubscriptionButton: React.FC<{ + plan: StripeProductPaidPlanType; + user: User | null; + userSubscription: UserSubscriptionDetail | undefined; + interval: 'monthly' | 'yearly'; +}> = ({ plan, userSubscription, user, interval }) => { + const { t } = useTranslation('model'); + const availablePlan = useMemo(() => { + if (plan === 'pro') { + return 'pro-monthly'; + } else { + if (interval === 'monthly') { + return 'ultra-monthly'; + } else { + return 'ultra-yearly'; + } + } + }, [plan, interval]); + + const showUpgradeButton = useMemo(() => { + if (!userSubscription) return false; + if (!userSubscription.userPlan) return false; + return ( + OrderedSubscriptionPlans.indexOf(plan) > + OrderedSubscriptionPlans.indexOf(userSubscription.userPlan) + ); + }, [userSubscription, plan]); + + const showDowngradeButton = useMemo(() => { + if (!userSubscription) return false; + if (!userSubscription.userPlan) return false; + return ( + OrderedSubscriptionPlans.indexOf(plan) < + OrderedSubscriptionPlans.indexOf(userSubscription.userPlan) + ); + }, [userSubscription, plan]); + + // Only show upgrade/downgrade button if user has a valid subscription + if (!userSubscription) return null; + + return ( + + + {showDowngradeButton ? ( + + ) : showUpgradeButton ? ( + + ) : null} + + + + + {t('Need Assistance with Subscription Change?')} + + + {t( + 'If you wish to upgrade or downgrade your subscription, kindly contact our support team via email at', + )} + + jack@exploratorlabs.com + + . + + + + + ); + + return null; +}; + +export default ChangeSubscriptionButton; diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 1405e1ef89..f3e5a9e1c1 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { useChangeSubscriptionPlan } from '@/hooks/useChangeSubscriptionPlan'; +import { useUserSubscriptionDetail } from '@/hooks/stripeSubscription/useUserSubscriptionDetail'; import { OrderedSubscriptionPlans, @@ -11,13 +11,35 @@ import { import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; -import { User } from '@/types/user'; +import { User, UserSubscriptionDetail } from '@/types/user'; +import Spinner from '@/components/Spinner'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import ChangeSubscriptionButton from './ChangeSubscriptionButton'; + import dayjs from 'dayjs'; -const PlanComparison = ({ user }: { user: User | null }) => { +const PlanComparison = ({ + user, + isPaidUser, +}: { + user: User | null; + isPaidUser: boolean; +}) => { + const { data: userSubscriptionDetail, isFetched } = useUserSubscriptionDetail( + { + isPaidUser, + }, + ); + + if (isPaidUser && !isFetched) { + return ( +
+ +
+ ); + } return (
{/* Free Plan */} @@ -27,12 +49,15 @@ const PlanComparison = ({ user }: { user: User | null }) => { {/* Pro Plan */}
- +
{/* Ultra Plan */}
- +
); @@ -72,15 +97,21 @@ const FreePlanContent = ({ user }: { user: User | null }) => { ); }; -const ProPlanContent = ({ user }: { user: User | null }) => { +const ProPlanContent = ({ + user, + userSubscription, +}: { + user: User | null; + userSubscription: UserSubscriptionDetail | undefined; +}) => { const { t, i18n } = useTranslation('model'); const showUpgradeToPro = useMemo(() => { + if (userSubscription) return false; if (!user) return true; const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); const proPlanIndex = OrderedSubscriptionPlans.indexOf('pro'); return userPlanIndex < proPlanIndex; - }, [user]); - const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); + }, [user, userSubscription]); const upgradeLinkOnClick = () => { const paymentLink = @@ -119,7 +150,7 @@ const ProPlanContent = ({ user }: { user: User | null }) => { {user?.plan === 'pro' && }
- +
@@ -143,18 +174,13 @@ const ProPlanContent = ({ user }: { user: User | null }) => {

)} - {user?.plan === 'ultra' && user.proPlanExpirationDate && ( - - )} + + {user?.plan === 'pro' && user.proPlanExpirationDate && ( @@ -163,17 +189,22 @@ const ProPlanContent = ({ user }: { user: User | null }) => { ); }; -const UltraPlanContent = ({ user }: { user: User | null }) => { +const UltraPlanContent = ({ + user, + userSubscription, +}: { + user: User | null; + userSubscription: UserSubscriptionDetail | undefined; +}) => { const { t, i18n } = useTranslation('model'); const [priceType, setPriceType] = useState<'monthly' | 'yearly'>('monthly'); const showUpgradeToUltra = useMemo(() => { + if (userSubscription) return false; if (!user) return true; const userPlanIndex = OrderedSubscriptionPlans.indexOf(user.plan); const ultraPlanIndex = OrderedSubscriptionPlans.indexOf('ultra'); return userPlanIndex < ultraPlanIndex; - }, [user]); - - const { mutate: changeSubscriptionPlan } = useChangeSubscriptionPlan(); + }, [user, userSubscription]); const upgradeLinkOnClick = () => { let paymentLink = STRIPE_PAID_PLAN_LINKS['ultra-monthly'].usd.link; @@ -222,7 +253,12 @@ const UltraPlanContent = ({ user }: { user: User | null }) => { {user?.plan === 'ultra' && }
- {user?.plan !== 'ultra' && } + {user?.plan !== 'ultra' && ( + + )}
@@ -247,21 +283,14 @@ const UltraPlanContent = ({ user }: { user: User | null }) => {

)} + {user?.plan === 'pro' && user.proPlanExpirationDate && ( - - )} - - {user?.plan === 'ultra' && user.proPlanExpirationDate && ( )} @@ -276,9 +305,19 @@ const CurrentPlanTag = () => { ); }; -const ProPlanPrice = () => { +const ProPlanPrice = ({ + userSubscription, +}: { + userSubscription: UserSubscriptionDetail | undefined; +}) => { const { i18n } = useTranslation('model'); + if (userSubscription && userSubscription.subscriptionCurrency === 'twd') { + return {'TWD$249.99 / month'}; + } + if (userSubscription && userSubscription.subscriptionCurrency === 'usd') { + return {'USD$9.99 / month'}; + } switch (i18n.language) { case 'zh-Hant': case 'zh': @@ -290,11 +329,43 @@ const ProPlanPrice = () => { const UltraPlanPrice = ({ setPriceType, + userSubscription, }: { setPriceType: (type: 'monthly' | 'yearly') => void; + userSubscription: UserSubscriptionDetail | undefined; }) => { const { t, i18n } = useTranslation('model'); + const monthlyPriceComponent = useMemo(() => { + if (userSubscription && userSubscription.subscriptionCurrency === 'twd') { + return {'TWD$880 / month'}; + } + + if (userSubscription && userSubscription.subscriptionCurrency === 'usd') { + return {'USD$29.99 / month'}; + } + if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { + return {'TWD$880 / month'}; + } else { + return {'USD$29.99 / month'}; + } + }, [userSubscription, i18n.language]); + + const yearlyPriceComponent = useMemo(() => { + if (userSubscription && userSubscription.subscriptionCurrency === 'twd') { + return {'TWD$8800 / year'}; + } + if (userSubscription && userSubscription.subscriptionCurrency === 'usd') { + return {'USD$279.99 / year'}; + } + + if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { + return {'TWD$8800 / year'}; + } else { + return {'USD$279.99 / year'}; + } + }, [userSubscription, i18n.language]); + return ( @@ -317,20 +388,8 @@ const UltraPlanPrice = ({ {t('YEARLY')} - - {i18n.language === 'zh-Hant' || i18n.language === 'zh' ? ( - {'TWD$880 / month'} - ) : ( - {'USD$29.99 / month'} - )} - - - {i18n.language === 'zh-Hant' || i18n.language === 'zh' ? ( - {'TWD$8800 / year'} - ) : ( - {'USD$279.99 / year'} - )} - + {monthlyPriceComponent} + {yearlyPriceComponent} ); }; From 531c224787db4939338fb1ff2b2e49a49dd4e1de Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 10:18:57 +0800 Subject: [PATCH 102/134] refactor: Update localization for subscription change messages in Chinese (Simplified and Traditional) --- public/locales/zh-Hans/model.json | 4 +++- public/locales/zh-Hant/model.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/locales/zh-Hans/model.json b/public/locales/zh-Hans/model.json index 68d98116aa..ab7839c148 100644 --- a/public/locales/zh-Hans/model.json +++ b/public/locales/zh-Hans/model.json @@ -237,5 +237,7 @@ "Please sign-up before upgrading to paid plan": "请在注册后再升级到付费方案", "MONTHLY": "月费", "YEARLY": "年费", - "Chat with document": "文档对话" + "Chat with document": "文档对话", + "Need Assistance with Subscription Change?": "需要订阅变更的帮助?", + "If you wish to upgrade or downgrade your subscription, kindly contact our support team via email at": "如果您想升级或降级您的订阅,请通过电邮联系我们的支援团队" } diff --git a/public/locales/zh-Hant/model.json b/public/locales/zh-Hant/model.json index 8898cb812a..90ef1ed426 100644 --- a/public/locales/zh-Hant/model.json +++ b/public/locales/zh-Hant/model.json @@ -238,5 +238,7 @@ "Please sign-up before upgrading to paid plan": "請在註冊後再升級到付費方案", "MONTHLY": "月費", "YEARLY": "年費", - "Chat with document": "文檔對話" + "Chat with document": "文檔對話", + "Need Assistance with Subscription Change?": "需要訂閱變更的協助?", + "If you wish to upgrade or downgrade your subscription, kindly contact our support team via email at": "如果您想升級或降級您的訂閱,請通過電郵聯繫我們的支援團隊" } From 769842ab17aca7368e468ab1a96a0465eb9eeea4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 3 Jun 2024 10:55:21 +0800 Subject: [PATCH 103/134] refactor: Remove unused import in stripe_config.ts --- utils/app/stripe/stripe_config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/app/stripe/stripe_config.ts b/utils/app/stripe/stripe_config.ts index 41bef8c748..5e32916d8d 100644 --- a/utils/app/stripe/stripe_config.ts +++ b/utils/app/stripe/stripe_config.ts @@ -1,5 +1,3 @@ -import { PaidPlanLinks } from '@/types/stripe-product'; - import { STRIPE_PAID_PLAN_LINKS_PRODUCTION, STRIPE_PAID_PLAN_LINKS_STAGING, From f9e549bae4fe2721b1ac54e06f509ac87ef420b7 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 24 Jun 2024 18:11:16 +0800 Subject: [PATCH 104/134] chore: update package --- .../useUserSubscriptionDetail.ts | 1 + package-lock.json | 376 ++++++++++++++++++ 2 files changed, 377 insertions(+) diff --git a/hooks/stripeSubscription/useUserSubscriptionDetail.ts b/hooks/stripeSubscription/useUserSubscriptionDetail.ts index b0ad9f8f93..a8bb0f4f73 100644 --- a/hooks/stripeSubscription/useUserSubscriptionDetail.ts +++ b/hooks/stripeSubscription/useUserSubscriptionDetail.ts @@ -31,6 +31,7 @@ export const useUserSubscriptionDetail = ({ } return (await response.json())?.data; + return json?.data || undefined; }; return useQuery( diff --git a/package-lock.json b/package-lock.json index 39a3729f84..c18b81faf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@ramonak/react-progress-bar": "^5.0.3", "@sendgrid/eventwebhook": "^8.0.0", @@ -3696,6 +3697,261 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz", + "integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", @@ -20861,6 +21117,126 @@ "@radix-ui/react-use-size": "1.0.1" } }, + "@radix-ui/react-tabs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz", + "integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==", + "requires": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "requires": {} + }, + "@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "requires": {} + }, + "@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "requires": {} + }, + "@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.0" + } + }, + "@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + } + }, + "@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "requires": { + "@radix-ui/react-slot": "1.1.0" + } + }, + "@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "requires": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.0" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "requires": {} + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "requires": { + "@radix-ui/react-use-callback-ref": "1.1.0" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "requires": {} + } + } + }, "@radix-ui/react-tooltip": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", From 65ddaf81d669316cd1789cefd03627987a6b5376 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 24 Jun 2024 18:12:00 +0800 Subject: [PATCH 105/134] refactor: handle when no subscription data from stripe --- components/User/Settings/PlanComparison.tsx | 6 +-- .../useUserSubscriptionDetail.ts | 44 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index f3e5a9e1c1..b6d114801f 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -29,7 +29,8 @@ const PlanComparison = ({ }) => { const { data: userSubscriptionDetail, isFetched } = useUserSubscriptionDetail( { - isPaidUser, + isPaidUser: isPaidUser, + user, }, ); @@ -68,9 +69,6 @@ export default PlanComparison; const PlanExpirationDate: React.FC<{ expirationDate: string }> = ({ expirationDate, }) => { - console.log({ - expirationDate, - }); const { t } = useTranslation('model'); return (
diff --git a/hooks/stripeSubscription/useUserSubscriptionDetail.ts b/hooks/stripeSubscription/useUserSubscriptionDetail.ts index a8bb0f4f73..56ae5d65ea 100644 --- a/hooks/stripeSubscription/useUserSubscriptionDetail.ts +++ b/hooks/stripeSubscription/useUserSubscriptionDetail.ts @@ -3,36 +3,44 @@ import { useQuery } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { UserSubscriptionDetail } from '@/types/user'; +import { User, UserSubscriptionDetail } from '@/types/user'; export const useUserSubscriptionDetail = ({ isPaidUser, + user, }: { isPaidUser: boolean; + user: User | null; }) => { const { t } = useTranslation('common'); const supabase = useSupabaseClient(); - const fetchUserSubscriptionDetail = async (): Promise< - UserSubscriptionDetail | undefined - > => { - const accessToken = (await supabase.auth.getSession()).data.session - ?.access_token!; + const fetchUserSubscriptionDetail = + async (): Promise => { + const accessToken = (await supabase.auth.getSession()).data.session + ?.access_token!; - const response = await fetch('/api/stripe/user-subscription-detail', { - method: 'GET', - headers: { - 'access-token': accessToken, - }, - }); + const response = await fetch('/api/stripe/user-subscription-detail', { + method: 'GET', + headers: { + 'access-token': accessToken, + }, + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } - if (!response.ok) { - throw new Error('Network response was not ok'); - } + const data = (await response.json())?.data; - return (await response.json())?.data; - return json?.data || undefined; - }; + // NOTE: return undefined will be triggered as error in useQuery + return data + ? data + : { + userPlan: user?.plan || null, + subscriptionCurrency: 'usd', + }; + }; return useQuery( ['userSubscriptionDetail', isPaidUser], From 1562981b0b3c9f2612ecdb33ede3538399a5e7ce Mon Sep 17 00:00:00 2001 From: 1orzero Date: Tue, 9 Jul 2024 17:55:10 +0800 Subject: [PATCH 106/134] feat(tests): add account data and update pro user login --- cypress/e2e/account.ts | 23 +++++++++++++++++++++++ cypress/e2e/general.cy.ts | 8 ++++---- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 cypress/e2e/account.ts diff --git a/cypress/e2e/account.ts b/cypress/e2e/account.ts new file mode 100644 index 0000000000..c499a91322 --- /dev/null +++ b/cypress/e2e/account.ts @@ -0,0 +1,23 @@ + + +const FREE_USER = { + email: 'cypress@exploratorlabs.com', + password: 'chateverywhere', +}; + +const PRO_USER = { + email: 'cypress+pro@exploratorlabs.com', + password: 'chateverywhere', +}; + +const ULTRA_USER = { + email: 'cypress+ultra@exploratorlabs.com', + password: 'chateverywhere', +}; + +const TEACHER_USER = { + email: 'cypress+teacher@exploratorlabs.com', + password: 'chateverywhere', +}; + +export { FREE_USER, PRO_USER, ULTRA_USER, TEACHER_USER }; diff --git a/cypress/e2e/general.cy.ts b/cypress/e2e/general.cy.ts index 77d3d73071..41e204e69d 100644 --- a/cypress/e2e/general.cy.ts +++ b/cypress/e2e/general.cy.ts @@ -1,3 +1,5 @@ +import { PRO_USER } from "./account"; + describe('Free user usage', () => { const hostUrl = Cypress.env('HOST_URL') || 'http://localhost:3000'; @@ -19,13 +21,11 @@ describe('Free user usage', () => { describe('Pro user usage', () => { const hostUrl = Cypress.env('HOST_URL') || 'http://localhost:3000'; - const userEmail = 'test@exploratorlabs.com'; - const userPassword = 'chateverywhere'; beforeEach(() => { cy.session( 'user', () => { - cy.login(userEmail, userPassword); + cy.login(PRO_USER.email, PRO_USER.password); }, { validate: () => { @@ -58,4 +58,4 @@ describe('Pro user usage', () => { }); }); -export {}; +export { }; From 0a73f0235cdfdb21a40dfad6d69509b0d3610683 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 11 Jul 2024 23:48:30 +0800 Subject: [PATCH 107/134] feat(api): add Chinese prompt for TW users --- pages/api/chat-gpt4.ts | 11 +++++++++-- pages/api/chat.ts | 21 ++++++++++++++------- pages/api/gemini.ts | 24 +++++++++++++++--------- pages/api/v1/completion-api.ts | 8 +++++--- utils/app/const.ts | 2 +- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/pages/api/chat-gpt4.ts b/pages/api/chat-gpt4.ts index c9f620fdf6..e72b785f9a 100644 --- a/pages/api/chat-gpt4.ts +++ b/pages/api/chat-gpt4.ts @@ -1,4 +1,4 @@ -import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE } from '@/utils/app/const'; +import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE, RESPONSE_IN_CHINESE_PROMPT } from '@/utils/app/const'; import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { OpenAIError, OpenAIStream } from '@/utils/server'; import { @@ -14,6 +14,7 @@ import { ChatBody } from '@/types/chat'; import { type Message } from '@/types/chat'; import { OpenAIModelID, OpenAIModels } from '@/types/openai'; import { PluginID } from '@/types/plugin'; +import { geolocation } from '@vercel/edge'; const supabase = getAdminSupabaseClient(); @@ -45,6 +46,7 @@ const unauthorizedResponse = new Response('Unauthorized', { status: 401 }); const handler = async (req: Request): Promise => { const userToken = req.headers.get('user-token'); + const { country } = geolocation(req); const { data, error } = await supabase.auth.getUser(userToken || ''); if (!data || error) return unauthorizedResponse; @@ -76,8 +78,13 @@ const handler = async (req: Request): Promise => { promptToSend = prompt; if (!promptToSend) { - promptToSend = DEFAULT_SYSTEM_PROMPT; + promptToSend = DEFAULT_SYSTEM_PROMPT } + + if (country?.includes('TW')) { + promptToSend += RESPONSE_IN_CHINESE_PROMPT; + } + let temperatureToUse = temperature; if (temperatureToUse == null) { temperatureToUse = DEFAULT_TEMPERATURE; diff --git a/pages/api/chat.ts b/pages/api/chat.ts index 051449c894..b6e84d29b9 100644 --- a/pages/api/chat.ts +++ b/pages/api/chat.ts @@ -1,7 +1,7 @@ // This endpoint only allow GPT-3.5 and GPT-3.5 16K models import { Logger } from 'next-axiom'; -import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE } from '@/utils/app/const'; +import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE, RESPONSE_IN_CHINESE_PROMPT } from '@/utils/app/const'; import { ERROR_MESSAGES } from '@/utils/app/const'; import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { OpenAIError, OpenAIStream } from '@/utils/server'; @@ -45,7 +45,11 @@ export const config = { const handler = async (req: Request): Promise => { retrieveUserSessionAndLogUsages(req); - const { country } = geolocation(req); + const { country, city } = geolocation(req); + // TODO:Remove this + console.log("User country code is " + country); + console.log("User city is " + city); + const log = new Logger(); const userIdentifier = req.headers.get('user-browser-id'); @@ -61,9 +65,13 @@ const handler = async (req: Request): Promise => { promptToSend = prompt; if (!promptToSend) { - promptToSend = DEFAULT_SYSTEM_PROMPT; + promptToSend = DEFAULT_SYSTEM_PROMPT + } + if (country?.includes('TW')) { + promptToSend += RESPONSE_IN_CHINESE_PROMPT; } + let temperatureToUse = temperature; if (temperatureToUse == null) { temperatureToUse = DEFAULT_TEMPERATURE; @@ -75,8 +83,8 @@ const handler = async (req: Request): Promise => { const requireToUseLargerContextWindowModel = (await getMessagesTokenCount(messages)) + - (await getStringTokenCount(promptToSend)) + - 1000 > + (await getStringTokenCount(promptToSend)) + + 1000 > defaultTokenLimit; const isPaidUser = await isPaidUserByAuthToken( @@ -94,8 +102,7 @@ const handler = async (req: Request): Promise => { if (selectedOutputLanguage) { messagesToSend[ messagesToSend.length - 1 - ].content = `${selectedOutputLanguage} ${ - messagesToSend[messagesToSend.length - 1].content + ].content = `${selectedOutputLanguage} ${messagesToSend[messagesToSend.length - 1].content }`; } diff --git a/pages/api/gemini.ts b/pages/api/gemini.ts index e453a080e7..dcbfd86c2b 100644 --- a/pages/api/gemini.ts +++ b/pages/api/gemini.ts @@ -1,4 +1,4 @@ -import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE } from '@/utils/app/const'; +import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE, RESPONSE_IN_CHINESE_PROMPT } from '@/utils/app/const'; import { serverSideTrackEvent } from '@/utils/app/eventTracking'; import { unauthorizedResponse } from '@/utils/server/auth'; import { callGeminiAPI } from '@/utils/server/google'; @@ -11,6 +11,7 @@ import { ChatBody } from '@/types/chat'; import { type Message } from '@/types/chat'; import { Content, GenerationConfig } from '@google-cloud/vertexai'; +import { geolocation } from '@vercel/edge'; const supabase = getAdminSupabaseClient(); @@ -41,6 +42,7 @@ export const config = { const BUCKET_NAME = process.env.GCP_CHAT_WITH_DOCUMENTS_BUCKET_NAME as string; const handler = async (req: Request): Promise => { + const { country } = geolocation(req); const userToken = req.headers.get('user-token'); const { data, error } = await supabase.auth.getUser(userToken || ''); @@ -70,8 +72,13 @@ const handler = async (req: Request): Promise => { promptToSend = prompt; if (!promptToSend) { - promptToSend = DEFAULT_SYSTEM_PROMPT; + promptToSend = DEFAULT_SYSTEM_PROMPT } + + if (country?.includes('TW')) { + promptToSend += RESPONSE_IN_CHINESE_PROMPT; + } + let temperatureToUse = temperature; if (temperatureToUse == null) { temperatureToUse = DEFAULT_TEMPERATURE; @@ -82,8 +89,7 @@ const handler = async (req: Request): Promise => { if (selectedOutputLanguage) { messageToSend[ messageToSend.length - 1 - ].content = `${selectedOutputLanguage} ${ - messageToSend[messageToSend.length - 1].content + ].content = `${selectedOutputLanguage} ${messageToSend[messageToSend.length - 1].content }`; } @@ -100,11 +106,11 @@ const handler = async (req: Request): Promise => { const textParts = [{ text: message.content }]; const fileDataList = message.fileList ? message.fileList.map((file) => ({ - fileData: { - mimeType: file.filetype, - fileUri: `gs://${BUCKET_NAME}/${file.objectPath}`, - }, - })) + fileData: { + mimeType: file.filetype, + fileUri: `gs://${BUCKET_NAME}/${file.objectPath}`, + }, + })) : []; return { role, diff --git a/pages/api/v1/completion-api.ts b/pages/api/v1/completion-api.ts index efaf3a76bd..378992f65e 100644 --- a/pages/api/v1/completion-api.ts +++ b/pages/api/v1/completion-api.ts @@ -3,6 +3,7 @@ import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE, OPENAI_API_HOST, + RESPONSE_IN_CHINESE_PROMPT, } from '@/utils/app/const'; import { OpenAIError } from '@/utils/server'; import { getAdminSupabaseClient } from '@/utils/server/supabase'; @@ -11,6 +12,7 @@ import model from '@dqbd/tiktoken/encoders/cl100k_base.json'; import { Tiktoken, init } from '@dqbd/tiktoken/lite/init'; // @ts-expect-error import * as wasm from '@dqbd/tiktoken/lite/tiktoken_bg.wasm?module'; +import { geolocation } from '@vercel/edge'; import { ParsedEvent, ReconnectInterval, @@ -60,6 +62,7 @@ const addApiUsageEntry = async (tokenLength: number) => { }; const handler = async (req: Request): Promise => { + const { country } = geolocation(req); // check Authorization header const auth = req.headers.get('Authorization'); if (auth !== `Bearer ${process.env.API_ACCESS_KEY}`) { @@ -78,7 +81,7 @@ const handler = async (req: Request): Promise => { messages: [ { role: 'system', - content: DEFAULT_SYSTEM_PROMPT, + content: country?.includes('TW') ? DEFAULT_SYSTEM_PROMPT + RESPONSE_IN_CHINESE_PROMPT : DEFAULT_SYSTEM_PROMPT, }, { role: 'user', @@ -114,8 +117,7 @@ const handler = async (req: Request): Promise => { ); } else { throw new Error( - `OpenAI API returned an error: ${ - decoder.decode(result?.value) || result.statusText + `OpenAI API returned an error: ${decoder.decode(result?.value) || result.statusText }`, ); } diff --git a/utils/app/const.ts b/utils/app/const.ts index b3ac8815a0..fb9a23cb85 100644 --- a/utils/app/const.ts +++ b/utils/app/const.ts @@ -13,9 +13,9 @@ export { V2_CHAT_UPGRADE_LINK, } from './stripe/stripe_config'; +export const RESPONSE_IN_CHINESE_PROMPT = `Whenever you respond in Chinese, you must respond in Traditional Chinese (繁體中文).` export const DEFAULT_SYSTEM_PROMPT = "You are an AI language model named Chat Everywhere, designed to answer user questions as accurately and helpfully as possible. Always be aware of the current date and time, and make sure to generate responses in the exact same language as the user's query. Adapt your responses to match the user's input language and context, maintaining an informative and supportive communication style. Additionally, format all responses using Markdown syntax, regardless of the input format." + - 'Whenever you respond in Chinese, you must respond in Traditional Chinese (繁體中文).' + 'If the input includes text such as [lang=xxx], the response should not include this text.' + `The current date is ${new Date().toLocaleDateString()}.`; From fbee487228820ba444991a540710eaaf470366c5 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Thu, 11 Jul 2024 23:55:05 +0800 Subject: [PATCH 108/134] feat(llm): add Chinese response for Taiwan users --- utils/server/functionCalls/aiPainterllmHandler.ts | 7 +++++-- utils/server/functionCalls/llmHandler.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/utils/server/functionCalls/aiPainterllmHandler.ts b/utils/server/functionCalls/aiPainterllmHandler.ts index 493f8efa6d..b66e39bcff 100644 --- a/utils/server/functionCalls/aiPainterllmHandler.ts +++ b/utils/server/functionCalls/aiPainterllmHandler.ts @@ -1,6 +1,6 @@ // This is a handler to execute and return the result of a function call to LLM. // This would seat between the endpoint and LLM. -import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; +import { DEFAULT_SYSTEM_PROMPT, RESPONSE_IN_CHINESE_PROMPT } from '@/utils/app/const'; import { AIStream } from '@/utils/server/functionCalls/AIStream'; import { triggerHelperFunction } from '@/utils/server/functionCalls/llmHandlerHelpers'; @@ -18,7 +18,7 @@ type handlerType = { onEnd: () => void; }; -const llmHandlerPrompt = +let llmHandlerPrompt = DEFAULT_SYSTEM_PROMPT + `Your main task is to process image generation tasks, utilizing the generate-image function. Below is the pseudo-code for you to follow: def ImageGenerationTask(request): @@ -103,6 +103,9 @@ export const aiPainterLlmHandler = async ({ onErrorUpdate, onEnd, }: handlerType) => { + if (countryCode?.includes('TW')) { + llmHandlerPrompt += RESPONSE_IN_CHINESE_PROMPT + } const functionCallsToSend: FunctionCall[] = [ { name: 'generate-html-for-ai-painter-images', diff --git a/utils/server/functionCalls/llmHandler.ts b/utils/server/functionCalls/llmHandler.ts index 6a80b695bf..4266bb68f0 100644 --- a/utils/server/functionCalls/llmHandler.ts +++ b/utils/server/functionCalls/llmHandler.ts @@ -1,6 +1,6 @@ // This is a handler to execute and return the result of a function call to LLM. // This would seat between the endpoint and LLM. -import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; +import { DEFAULT_SYSTEM_PROMPT, RESPONSE_IN_CHINESE_PROMPT } from '@/utils/app/const'; import { AIStream } from '@/utils/server/functionCalls/AIStream'; import { getFunctionCallsFromMqttConnections, @@ -23,7 +23,7 @@ type handlerType = { onEnd: () => void; }; -const llmHandlerPrompt = +let llmHandlerPrompt = DEFAULT_SYSTEM_PROMPT + ` Remember. You now have the capability to control real world devices via MQTT connections via function calls (the function name starts with 'mqtt'). @@ -77,6 +77,10 @@ export const llmHandler = async ({ ...getHelperFunctionCalls(profile.line_access_token), ); + if (countryCode?.includes('TW')) { + llmHandlerPrompt += RESPONSE_IN_CHINESE_PROMPT + } + try { while (isFunctionCallRequired) { const requestedFunctionCalls = await AIStream({ From 8976179e67d21ea16d0336860a27bca6fc4b87d2 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 10:09:32 +0800 Subject: [PATCH 109/134] feat(cypress): add TEST_PAYMENT_USER constant --- cypress/e2e/account.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/account.ts b/cypress/e2e/account.ts index c499a91322..c1bb471fbd 100644 --- a/cypress/e2e/account.ts +++ b/cypress/e2e/account.ts @@ -20,4 +20,9 @@ const TEACHER_USER = { password: 'chateverywhere', }; -export { FREE_USER, PRO_USER, ULTRA_USER, TEACHER_USER }; +const TEST_PAYMENT_USER = { + email: 'cypress+stripe@exploratorlabs.com', + password: 'chateverywhere', +} + +export { FREE_USER, PRO_USER, ULTRA_USER, TEACHER_USER, TEST_PAYMENT_USER }; From 74cf7453bb9b3310fe11e8f95904e0036e5e5838 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 10:09:38 +0800 Subject: [PATCH 110/134] feat(cypress): add payment flow test skeleton --- cypress/e2e/payment.ts | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 cypress/e2e/payment.ts diff --git a/cypress/e2e/payment.ts b/cypress/e2e/payment.ts new file mode 100644 index 0000000000..97ab455576 --- /dev/null +++ b/cypress/e2e/payment.ts @@ -0,0 +1,51 @@ + +import { TEST_PAYMENT_USER } from "./account"; + +describe('Test Payment Flow', () => { + const hostUrl = Cypress.env('HOST_URL') || 'http://localhost:3000'; + + + beforeEach(() => { + cy.session( + 'user', + () => { + cy.login(TEST_PAYMENT_USER.email, TEST_PAYMENT_USER.password); + }, + { + validate: () => { + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).should( + 'be.visible', + ); + }, + }, + ); + + cy.visit(hostUrl); + }); + + // Reset payment by calling the /api/cypress/reset-user-subscription endpoint + after(() => { + // TODO: add the reset user subscription endpoint + // TODO: call the reset user subscription endpoint + }); + + // Logout + after(() => { + cy.get('[data-cy="settings-button"]').click(); + cy.get('[data-cy="chatbar-settings-modal"]') + .scrollIntoView() + .should('be.visible'); + cy.get('[data-cy="sign-out-confirmation-button"]').click(); + cy.get('[data-cy="sign-out-and-clear-button"]').click(); + cy.contains('You have been logged out'); + }); + it('upgrade to pro plan', () => { + // TODO: 1. Make sure the user is on Free plan. + // TODO: 2. Cypress calls the `/api/cypress/test-payment-event` endpoint to start the test. + // TODO: 3. The webhook will update the user's subscription plan to `pro-monthly`. + // TODO: 4. Cypress refreshes the page and checks if the user is on Pro plan. + }); + + // TODO: add test for Ultra plan +}); + From a699f300de0329156398f3662c149ceb2ebe8d67 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 11:53:01 +0800 Subject: [PATCH 111/134] feat(api): add test-payment-event endpoint --- pages/api/cypress/test-payment-event.ts | 156 ++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 pages/api/cypress/test-payment-event.ts diff --git a/pages/api/cypress/test-payment-event.ts b/pages/api/cypress/test-payment-event.ts new file mode 100644 index 0000000000..cdc7e9235a --- /dev/null +++ b/pages/api/cypress/test-payment-event.ts @@ -0,0 +1,156 @@ +import { getHomeUrl } from '@/utils/app/api'; + +export const config = { + runtime: 'edge', +}; + +const handler = async (req: Request): Promise => { + const isProd = process.env.VERCEL_ENV === 'production'; + try { + if (isProd) { + return new Response('Not allowed in production', { status: 403 }); + } + // read req body (plan: pro/ultra) + // const body = await req.json(); + // const plan = body.plan; + + const response = await fetch(`${getHomeUrl()}/api/webhooks/stripe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + // TODO: add the correct body for the plan + // body: JSON.stringify(plan === 'pro' ? fakeProPlanSubscriptionEvent : fakeUltraPlanSubscriptionEvent), + body: JSON.stringify({ testEvent: fakeProPlanSubscriptionEvent }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return new Response(JSON.stringify({ success: true }), { status: 200 }); + } catch (error) { + console.error(error); + return new Response('Error', { status: 500 }); + } +}; + +const fakeProPlanSubscriptionEvent = { + id: 'evt_1PbZayEEvfd1BzvuIWPC3rOO', + object: 'event', + api_version: '2020-08-27', + created: 1720752748, + data: { + object: { + id: 'cs_test_b1oa9hQjSmRAIY7VADODVvgEsT533JfITznflkeTCh60nBlfFlyvGr8MbJ', + object: 'checkout.session', + after_expiration: null, + allow_promotion_codes: true, + amount_subtotal: 999, + amount_total: 999, + automatic_tax: { + enabled: false, + liability: null, + status: null, + }, + billing_address_collection: 'auto', + cancel_url: 'https://stripe.com', + client_reference_id: '5ce53e0d-5298-4180-8f6f-1c5becb99d4c', + client_secret: null, + consent: null, + consent_collection: { + payment_method_reuse_agreement: null, + promotions: 'none', + terms_of_service: 'none', + }, + created: 1720752727, + currency: 'usd', + currency_conversion: null, + custom_fields: [], + custom_text: { + after_submit: null, + shipping_address: null, + submit: null, + terms_of_service_acceptance: null, + }, + customer: 'cus_QSUavMdAX9Zdp3', + customer_creation: 'if_required', + customer_details: { + address: { + city: null, + country: 'MO', + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: 'laokameng.6@gmail.com', + name: 'StripeCardTest', + phone: null, + tax_exempt: 'none', + tax_ids: [], + }, + customer_email: null, + expires_at: 1720839127, + invoice: 'in_1PbZaqEEvfd1Bzvut22eUx6c', + invoice_creation: null, + livemode: false, + locale: 'auto', + metadata: { + plan_code: 'MONTHLY_PRO_PLAN_SUBSCRIPTION', + }, + mode: 'subscription', + payment_intent: null, + payment_link: 'plink_1N09fvEEvfd1Bzvu8RHD78kb', + payment_method_collection: 'always', + payment_method_configuration_details: { + id: 'pmc_1MzuEJEEvfd1BzvuSIHOnamP', + parent: null, + }, + payment_method_options: { + card: { + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card', 'link'], + payment_status: 'paid', + phone_number_collection: { + enabled: false, + }, + recovered_from: null, + saved_payment_method_options: { + allow_redisplay_filters: ['always'], + payment_method_remove: null, + payment_method_save: null, + }, + setup_intent: null, + shipping: null, + shipping_address_collection: null, + shipping_options: [], + shipping_rate: null, + status: 'complete', + submit_type: 'auto', + subscription: 'sub_1PbZaqEEvfd1BzvunkVi2BBG', + success_url: 'https://stripe.com', + total_details: { + amount_discount: 0, + amount_shipping: 0, + amount_tax: 0, + }, + ui_mode: 'hosted', + url: null, + }, + }, + livemode: false, + pending_webhooks: 4, + request: { + id: null, + idempotency_key: null, + }, + type: 'checkout.session.completed', +}; + +// TODO: add fake ultra plan subscription event +const fakeUltraPlanSubscriptionEvent = fakeProPlanSubscriptionEvent; + +export default handler; From 686fba759540b41d36a7b53feaa5563ec6553ba9 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 11:53:13 +0800 Subject: [PATCH 112/134] feat(stripe): add test event handling for non-prod --- pages/api/webhooks/stripe.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index b823992ff0..184e84b7ce 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -15,11 +15,14 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { }); const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const isProd = process.env.VERCEL_ENV === 'production'; + if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); res.status(405).end('Method Not Allowed'); return; } + const sig = req.headers['stripe-signature'] as string; let event: Stripe.Event; @@ -33,11 +36,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { process.env.STRIPE_WEBHOOK_SECRET!, ); } catch (err) { - console.error( - `Webhook signature verification failed.`, - (err as any).message, - ); - return res.status(400).send(`Webhook signature verification failed.`); + const bodyData = JSON.parse(rawBody.toString()); + + // The testEvent is only available in the non-prod environment, its used by Cypress `api/cypress/test-payment-event` + if (!isProd && bodyData.testEvent) { + event = bodyData.testEvent; + } else { + console.error( + `Webhook signature verification failed.`, + (err as any).message, + ); + return res.status(400).send(`Webhook signature verification failed.`); + } + } try { From d332b395e12c31a8d1557859c56f0c4efd974d4e Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 12:25:23 +0800 Subject: [PATCH 113/134] feat(api): add reset test payment user subscription --- .../reset-test-payment-user-subscription.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 pages/api/cypress/reset-test-payment-user-subscription.ts diff --git a/pages/api/cypress/reset-test-payment-user-subscription.ts b/pages/api/cypress/reset-test-payment-user-subscription.ts new file mode 100644 index 0000000000..41ffe3e147 --- /dev/null +++ b/pages/api/cypress/reset-test-payment-user-subscription.ts @@ -0,0 +1,25 @@ +import { TEST_PAYMENT_USER } from "@/cypress/e2e/account"; +import { getAdminSupabaseClient } from "@/utils/server/supabase"; + + +export const config = { + runtime: 'edge', +}; + +const handler = async (): Promise => { + try { + const supabase = getAdminSupabaseClient(); + // Reset the test payment user's plan to free + await supabase + .from('profiles') + .update({ plan: 'free' }) + .eq('email', TEST_PAYMENT_USER.email); + + return new Response(JSON.stringify({ success: true }), { status: 200 }); + } catch (error) { + console.error(error); + return new Response('Error', { status: 500 }); + } +}; + +export default handler; From 395ac13fc8ec940b1356172f7979e16116a39f51 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 12:32:12 +0800 Subject: [PATCH 114/134] feat(api): add test payment event for ultra plan --- pages/api/cypress/test-payment-event.ts | 367 ++++++++++++++++-------- 1 file changed, 249 insertions(+), 118 deletions(-) diff --git a/pages/api/cypress/test-payment-event.ts b/pages/api/cypress/test-payment-event.ts index cdc7e9235a..69e56c691c 100644 --- a/pages/api/cypress/test-payment-event.ts +++ b/pages/api/cypress/test-payment-event.ts @@ -1,4 +1,6 @@ +import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { getHomeUrl } from '@/utils/app/api'; +import { getAdminSupabaseClient } from '@/utils/server/supabase'; export const config = { runtime: 'edge', @@ -10,6 +12,252 @@ const handler = async (req: Request): Promise => { if (isProd) { return new Response('Not allowed in production', { status: 403 }); } + // Get user id from supabase + const supabase = getAdminSupabaseClient(); + const { data: userProfile } = await supabase + .from('profiles') + .select('id') + .eq('email', TEST_PAYMENT_USER.email) + .single(); + + if (!userProfile) return new Response('Test User not found', { status: 404 }); + + const fakeProPlanSubscriptionEvent = { + id: 'evt_1PbZayEEvfd1BzvuIWPC3rOO', + object: 'event', + api_version: '2020-08-27', + created: 1720752748, + data: { + object: { + id: 'cs_test_b1oa9hQjSmRAIY7VADODVvgEsT533JfITznflkeTCh60nBlfFlyvGr8MbJ', + object: 'checkout.session', + after_expiration: null, + allow_promotion_codes: true, + amount_subtotal: 999, + amount_total: 999, + automatic_tax: { + enabled: false, + liability: null, + status: null, + }, + billing_address_collection: 'auto', + cancel_url: 'https://stripe.com', + client_reference_id: userProfile.id, + client_secret: null, + consent: null, + consent_collection: { + payment_method_reuse_agreement: null, + promotions: 'none', + terms_of_service: 'none', + }, + created: 1720752727, + currency: 'usd', + currency_conversion: null, + custom_fields: [], + custom_text: { + after_submit: null, + shipping_address: null, + submit: null, + terms_of_service_acceptance: null, + }, + customer: 'cus_QSUavMdAX9Zdp3', + customer_creation: 'if_required', + customer_details: { + address: { + city: null, + country: 'MO', + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: TEST_PAYMENT_USER.email, + name: 'StripeCardTest', + phone: null, + tax_exempt: 'none', + tax_ids: [], + }, + customer_email: null, + expires_at: 1720839127, + invoice: 'in_1PbZaqEEvfd1Bzvut22eUx6c', + invoice_creation: null, + livemode: false, + locale: 'auto', + metadata: { + plan_code: 'MONTHLY_PRO_PLAN_SUBSCRIPTION', + }, + mode: 'subscription', + payment_intent: null, + payment_link: 'plink_1N09fvEEvfd1Bzvu8RHD78kb', + payment_method_collection: 'always', + payment_method_configuration_details: { + id: 'pmc_1MzuEJEEvfd1BzvuSIHOnamP', + parent: null, + }, + payment_method_options: { + card: { + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card', 'link'], + payment_status: 'paid', + phone_number_collection: { + enabled: false, + }, + recovered_from: null, + saved_payment_method_options: { + allow_redisplay_filters: ['always'], + payment_method_remove: null, + payment_method_save: null, + }, + setup_intent: null, + shipping: null, + shipping_address_collection: null, + shipping_options: [], + shipping_rate: null, + status: 'complete', + submit_type: 'auto', + subscription: 'sub_1PbZaqEEvfd1BzvunkVi2BBG', + success_url: 'https://stripe.com', + total_details: { + amount_discount: 0, + amount_shipping: 0, + amount_tax: 0, + }, + ui_mode: 'hosted', + url: null, + }, + }, + livemode: false, + pending_webhooks: 4, + request: { + id: null, + idempotency_key: null, + }, + type: 'checkout.session.completed', + }; + + + const fakeUltraPlanSubscriptionEvent = { + "id": "evt_1Pbb50EEvfd1Bzvu6cO2Pg6O", + "object": "event", + "api_version": "2020-08-27", + "created": 1720758454, + "data": { + "object": { + "id": "cs_test_a1OykWFuJxe6mR80M6mmm73ENFnZ8VJ2cDyrpxEXIzp74uVQtaRwDRMr7n", + "object": "checkout.session", + "after_expiration": null, + "allow_promotion_codes": false, + "amount_subtotal": 2999, + "amount_total": 2999, + "automatic_tax": { + "enabled": false, + "liability": null, + "status": null + }, + "billing_address_collection": "auto", + "cancel_url": "https://stripe.com", + "client_reference_id": userProfile.id, + "client_secret": null, + "consent": null, + "consent_collection": { + "payment_method_reuse_agreement": null, + "promotions": "none", + "terms_of_service": "none" + }, + "created": 1720758430, + "currency": "usd", + "currency_conversion": null, + "custom_fields": [], + "custom_text": { + "after_submit": null, + "shipping_address": null, + "submit": null, + "terms_of_service_acceptance": null + }, + "customer": "cus_QSW7Jm6qEisEf2", + "customer_creation": "if_required", + "customer_details": { + "address": { + "city": null, + "country": "MO", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": TEST_PAYMENT_USER.email, + "name": "StripeCardTest", + "phone": null, + "tax_exempt": "none", + "tax_ids": [] + }, + "customer_email": null, + "expires_at": 1720844830, + "invoice": "in_1Pbb4tEEvfd1BzvueDlUtXzd", + "invoice_creation": null, + "livemode": false, + "locale": "auto", + "metadata": { + "plan_code": "monthly_ultra_plan_subscription" + }, + "mode": "subscription", + "payment_intent": null, + "payment_link": "plink_1PLhmVEEvfd1BzvuRYKzYTER", + "payment_method_collection": "always", + "payment_method_configuration_details": { + "id": "pmc_1MzuEJEEvfd1BzvuSIHOnamP", + "parent": null + }, + "payment_method_options": { + "card": { + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card", + "link" + ], + "payment_status": "paid", + "phone_number_collection": { + "enabled": false + }, + "recovered_from": null, + "saved_payment_method_options": { + "allow_redisplay_filters": [ + "always" + ], + "payment_method_remove": null, + "payment_method_save": null + }, + "setup_intent": null, + "shipping": null, + "shipping_address_collection": null, + "shipping_options": [], + "shipping_rate": null, + "status": "complete", + "submit_type": "auto", + "subscription": "sub_1Pbb4tEEvfd1BzvuNYGOFJLD", + "success_url": "https://stripe.com", + "total_details": { + "amount_discount": 0, + "amount_shipping": 0, + "amount_tax": 0 + }, + "ui_mode": "hosted", + "url": null + } + }, + "livemode": false, + "pending_webhooks": 4, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "checkout.session.completed" + } + // read req body (plan: pro/ultra) // const body = await req.json(); // const plan = body.plan; @@ -21,7 +269,7 @@ const handler = async (req: Request): Promise => { }, // TODO: add the correct body for the plan // body: JSON.stringify(plan === 'pro' ? fakeProPlanSubscriptionEvent : fakeUltraPlanSubscriptionEvent), - body: JSON.stringify({ testEvent: fakeProPlanSubscriptionEvent }), + body: JSON.stringify({ fakeEvent: fakeUltraPlanSubscriptionEvent }), }); if (!response.ok) { @@ -35,122 +283,5 @@ const handler = async (req: Request): Promise => { } }; -const fakeProPlanSubscriptionEvent = { - id: 'evt_1PbZayEEvfd1BzvuIWPC3rOO', - object: 'event', - api_version: '2020-08-27', - created: 1720752748, - data: { - object: { - id: 'cs_test_b1oa9hQjSmRAIY7VADODVvgEsT533JfITznflkeTCh60nBlfFlyvGr8MbJ', - object: 'checkout.session', - after_expiration: null, - allow_promotion_codes: true, - amount_subtotal: 999, - amount_total: 999, - automatic_tax: { - enabled: false, - liability: null, - status: null, - }, - billing_address_collection: 'auto', - cancel_url: 'https://stripe.com', - client_reference_id: '5ce53e0d-5298-4180-8f6f-1c5becb99d4c', - client_secret: null, - consent: null, - consent_collection: { - payment_method_reuse_agreement: null, - promotions: 'none', - terms_of_service: 'none', - }, - created: 1720752727, - currency: 'usd', - currency_conversion: null, - custom_fields: [], - custom_text: { - after_submit: null, - shipping_address: null, - submit: null, - terms_of_service_acceptance: null, - }, - customer: 'cus_QSUavMdAX9Zdp3', - customer_creation: 'if_required', - customer_details: { - address: { - city: null, - country: 'MO', - line1: null, - line2: null, - postal_code: null, - state: null, - }, - email: 'laokameng.6@gmail.com', - name: 'StripeCardTest', - phone: null, - tax_exempt: 'none', - tax_ids: [], - }, - customer_email: null, - expires_at: 1720839127, - invoice: 'in_1PbZaqEEvfd1Bzvut22eUx6c', - invoice_creation: null, - livemode: false, - locale: 'auto', - metadata: { - plan_code: 'MONTHLY_PRO_PLAN_SUBSCRIPTION', - }, - mode: 'subscription', - payment_intent: null, - payment_link: 'plink_1N09fvEEvfd1Bzvu8RHD78kb', - payment_method_collection: 'always', - payment_method_configuration_details: { - id: 'pmc_1MzuEJEEvfd1BzvuSIHOnamP', - parent: null, - }, - payment_method_options: { - card: { - request_three_d_secure: 'automatic', - }, - }, - payment_method_types: ['card', 'link'], - payment_status: 'paid', - phone_number_collection: { - enabled: false, - }, - recovered_from: null, - saved_payment_method_options: { - allow_redisplay_filters: ['always'], - payment_method_remove: null, - payment_method_save: null, - }, - setup_intent: null, - shipping: null, - shipping_address_collection: null, - shipping_options: [], - shipping_rate: null, - status: 'complete', - submit_type: 'auto', - subscription: 'sub_1PbZaqEEvfd1BzvunkVi2BBG', - success_url: 'https://stripe.com', - total_details: { - amount_discount: 0, - amount_shipping: 0, - amount_tax: 0, - }, - ui_mode: 'hosted', - url: null, - }, - }, - livemode: false, - pending_webhooks: 4, - request: { - id: null, - idempotency_key: null, - }, - type: 'checkout.session.completed', -}; - -// TODO: add fake ultra plan subscription event -const fakeUltraPlanSubscriptionEvent = fakeProPlanSubscriptionEvent; export default handler; From c96b326d75131bd049127362f86079135e118465 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 12:33:02 +0800 Subject: [PATCH 115/134] feat(stripe): add fake event handling for testing --- pages/api/webhooks/stripe.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 184e84b7ce..6c9b447c3e 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -15,6 +15,7 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { }); const handler = async (req: NextApiRequest, res: NextApiResponse) => { + let isFakeEvent = false; const isProd = process.env.VERCEL_ENV === 'production'; if (req.method !== 'POST') { @@ -38,9 +39,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } catch (err) { const bodyData = JSON.parse(rawBody.toString()); - // The testEvent is only available in the non-prod environment, its used by Cypress `api/cypress/test-payment-event` - if (!isProd && bodyData.testEvent) { - event = bodyData.testEvent; + // The fakeEvent is only available in the non-prod environment, its used by Cypress `api/cypress/test-payment-event` + if (!isProd && bodyData.fakeEvent) { + event = bodyData.fakeEvent; + isFakeEvent = true; } else { console.error( `Webhook signature verification failed.`, @@ -51,6 +53,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } + try { switch (event.type) { case 'checkout.session.completed': @@ -58,6 +61,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { console.log('✅ checkout.session.completed'); await handleCheckoutSessionCompleted( event.data.object as Stripe.Checkout.Session, + isFakeEvent, ); break; case 'customer.subscription.updated': From 36f8482b4ed82d22c346f71f5f6c6e708fe6989b Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 12:33:25 +0800 Subject: [PATCH 116/134] feat(stripe): add isFakeEvent param to handleSubscription --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 0164c3cea3..2ed1bb0a57 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -27,6 +27,7 @@ dayjs.extend(utc); export default async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, + isFakeEvent = false, ): Promise { const userId = session.client_reference_id; const email = session.customer_details?.email; @@ -58,6 +59,7 @@ export default async function handleCheckoutSessionCompleted( stripeSubscriptionId, userId || undefined, email || undefined, + isFakeEvent, ); } else if (session.mode === 'payment') { // One-time payment flow @@ -133,12 +135,14 @@ async function handleSubscription( stripeSubscriptionId: string, userId: string | undefined, email: string | undefined, + isFakeEvent = false, ) { const subscription = await StripeHelper.subscription.getSubscriptionById( stripeSubscriptionId, ); + // NOTE: Since the stripeSubscriptionId is fake, we use the current date to simulate the expiration date const currentPeriodEnd = dayjs - .unix(subscription.current_period_end) + .unix(isFakeEvent ? dayjs().add(2, 'month').unix() : subscription.current_period_end) .utc() .toDate(); From 5b75602b0a96a0085bc2142140614554174a2f7d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 12:57:41 +0800 Subject: [PATCH 117/134] feat(api): add plan selection to test payment event --- pages/api/cypress/test-payment-event.ts | 29 ++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pages/api/cypress/test-payment-event.ts b/pages/api/cypress/test-payment-event.ts index 69e56c691c..6e50f6d5b6 100644 --- a/pages/api/cypress/test-payment-event.ts +++ b/pages/api/cypress/test-payment-event.ts @@ -1,13 +1,23 @@ import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { getHomeUrl } from '@/utils/app/api'; import { getAdminSupabaseClient } from '@/utils/server/supabase'; +import { z } from 'zod'; export const config = { runtime: 'edge', }; +const requestSchema = z.object({ + plan: z.enum(['pro', 'ultra']) +}); + const handler = async (req: Request): Promise => { const isProd = process.env.VERCEL_ENV === 'production'; + + if (req.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + try { if (isProd) { return new Response('Not allowed in production', { status: 403 }); @@ -259,17 +269,26 @@ const handler = async (req: Request): Promise => { } // read req body (plan: pro/ultra) - // const body = await req.json(); - // const plan = body.plan; + const body = await req.json(); + const validationResult = requestSchema.safeParse(body); + if (!validationResult.success) { + return new Response('Invalid request body. Plan must be either "pro" or "ultra".', { status: 400 }); + } + + const { plan } = validationResult.data; + + if (!plan) { + throw new Error('Missing plan in the request body'); + } const response = await fetch(`${getHomeUrl()}/api/webhooks/stripe`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - // TODO: add the correct body for the plan - // body: JSON.stringify(plan === 'pro' ? fakeProPlanSubscriptionEvent : fakeUltraPlanSubscriptionEvent), - body: JSON.stringify({ fakeEvent: fakeUltraPlanSubscriptionEvent }), + body: JSON.stringify({ + fakeEvent: plan === 'pro' ? fakeProPlanSubscriptionEvent : fakeUltraPlanSubscriptionEvent, + }), }); if (!response.ok) { From b83817d5a1f56671c966ea7577e2656f897e79ff Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 12:58:08 +0800 Subject: [PATCH 118/134] feat(stripe): handle fake events in non-prod env --- pages/api/webhooks/stripe.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 6c9b447c3e..0e444e1304 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -38,7 +38,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { ); } catch (err) { const bodyData = JSON.parse(rawBody.toString()); - // The fakeEvent is only available in the non-prod environment, its used by Cypress `api/cypress/test-payment-event` if (!isProd && bodyData.fakeEvent) { event = bodyData.fakeEvent; From 8bd04dbfb2244685ff36ae6d9bce8d2ef009b46a Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 12 Jul 2024 13:23:51 +0800 Subject: [PATCH 119/134] feat(test): add payment flow e2e tests --- cypress/e2e/payment.cy.ts | 98 +++++++++++++++++++++++++++++++++++++++ cypress/e2e/payment.ts | 51 -------------------- 2 files changed, 98 insertions(+), 51 deletions(-) create mode 100644 cypress/e2e/payment.cy.ts delete mode 100644 cypress/e2e/payment.ts diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts new file mode 100644 index 0000000000..fb9ec38600 --- /dev/null +++ b/cypress/e2e/payment.cy.ts @@ -0,0 +1,98 @@ +import { TEST_PAYMENT_USER } from "./account"; + +describe('Test Payment Flow', () => { + const hostUrl = Cypress.env('HOST_URL') || 'http://localhost:3000'; + + + beforeEach(() => { + cy.session( + 'user', + () => { + cy.login(TEST_PAYMENT_USER.email, TEST_PAYMENT_USER.password); + }, + { + validate: () => { + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).should( + 'be.visible', + ); + }, + }, + ); + + cy.visit(hostUrl); + }); + + // Reset payment by calling API + afterEach(() => { + cy.request({ + method: 'POST', + url: '/api/cypress/reset-test-payment-user-subscription', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + // Logout + after(() => { + cy.get('[data-cy="settings-button"]').click(); + cy.get('[data-cy="chatbar-settings-modal"]') + .scrollIntoView() + .should('be.visible'); + cy.get('[data-cy="sign-out-confirmation-button"]').click(); + cy.get('[data-cy="sign-out-and-clear-button"]').click(); + cy.contains('You have been logged out'); + }); + it('upgrade to pro plan', () => { + // Make sure the user is on Free plan. + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { + expect($el).to.have.text('Free'); + }); + + // calls the `/api/cypress/test-payment-event` endpoint to test the payment flow. + cy.request({ + method: 'POST', + url: '/api/cypress/test-payment-event', + headers: { + 'Content-Type': 'application/json', + }, + body: { + plan: 'pro', + }, + }); + + // Refreshes the page and checks if the user is on Pro plan. + cy.reload(); + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { + expect($el).to.have.text('Pro'); + }); + }); + + // DOING: add test for Ultra plan + it('upgrade to ultra plan', () => { + // Make sure the user is on Free plan. + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { + expect($el).to.have.text('Free'); + }); + + // calls the `/api/cypress/test-payment-event` endpoint to test the payment flow. + cy.request({ + method: 'POST', + url: '/api/cypress/test-payment-event', + headers: { + 'Content-Type': 'application/json', + }, + body: { + plan: 'ultra', + }, + }); + + + // Refreshes the page and checks if the user is on Ultra plan. + cy.reload(); + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { + expect($el).to.have.text('Ultra'); + }); + }); +}); + diff --git a/cypress/e2e/payment.ts b/cypress/e2e/payment.ts deleted file mode 100644 index 97ab455576..0000000000 --- a/cypress/e2e/payment.ts +++ /dev/null @@ -1,51 +0,0 @@ - -import { TEST_PAYMENT_USER } from "./account"; - -describe('Test Payment Flow', () => { - const hostUrl = Cypress.env('HOST_URL') || 'http://localhost:3000'; - - - beforeEach(() => { - cy.session( - 'user', - () => { - cy.login(TEST_PAYMENT_USER.email, TEST_PAYMENT_USER.password); - }, - { - validate: () => { - cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).should( - 'be.visible', - ); - }, - }, - ); - - cy.visit(hostUrl); - }); - - // Reset payment by calling the /api/cypress/reset-user-subscription endpoint - after(() => { - // TODO: add the reset user subscription endpoint - // TODO: call the reset user subscription endpoint - }); - - // Logout - after(() => { - cy.get('[data-cy="settings-button"]').click(); - cy.get('[data-cy="chatbar-settings-modal"]') - .scrollIntoView() - .should('be.visible'); - cy.get('[data-cy="sign-out-confirmation-button"]').click(); - cy.get('[data-cy="sign-out-and-clear-button"]').click(); - cy.contains('You have been logged out'); - }); - it('upgrade to pro plan', () => { - // TODO: 1. Make sure the user is on Free plan. - // TODO: 2. Cypress calls the `/api/cypress/test-payment-event` endpoint to start the test. - // TODO: 3. The webhook will update the user's subscription plan to `pro-monthly`. - // TODO: 4. Cypress refreshes the page and checks if the user is on Pro plan. - }); - - // TODO: add test for Ultra plan -}); - From a4982af3a2a8147573b2f078cb9b80b4ad733602 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 13:55:39 +0800 Subject: [PATCH 120/134] fix(UX): correct plan type in expiration date display condition --- components/User/Settings/PlanComparison.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index b6d114801f..0e1682eb0f 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -288,7 +288,7 @@ const UltraPlanContent = ({ interval={priceType} /> - {user?.plan === 'pro' && user.proPlanExpirationDate && ( + {user?.plan === 'ultra' && user.proPlanExpirationDate && ( )} From 040ee13edea08b0b910170853b95267073894f71 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 13:56:00 +0800 Subject: [PATCH 121/134] fix(subscription): adjust fake event expiration period to one month --- utils/server/stripe/handleCheckoutSessionCompleted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts index 2ed1bb0a57..e5b755d908 100644 --- a/utils/server/stripe/handleCheckoutSessionCompleted.ts +++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts @@ -142,7 +142,7 @@ async function handleSubscription( ); // NOTE: Since the stripeSubscriptionId is fake, we use the current date to simulate the expiration date const currentPeriodEnd = dayjs - .unix(isFakeEvent ? dayjs().add(2, 'month').unix() : subscription.current_period_end) + .unix(isFakeEvent ? dayjs().add(1, 'month').unix() : subscription.current_period_end) .utc() .toDate(); From 1749f51774bd0554b126f122e2f854105accb2e9 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 13:56:18 +0800 Subject: [PATCH 122/134] test(e2e): add verification for plan expiration dates --- cypress/e2e/payment.cy.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index fb9ec38600..fd89cf1210 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -1,3 +1,4 @@ +import dayjs from "dayjs"; import { TEST_PAYMENT_USER } from "./account"; describe('Test Payment Flow', () => { @@ -66,9 +67,17 @@ describe('Test Payment Flow', () => { cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { expect($el).to.have.text('Pro'); }); + + cy.get('[data-cy="settings-button"]').click(); + cy.get('[data-cy="chatbar-settings-modal"]') + .scrollIntoView() + .should('be.visible'); + cy.get('[data-cy="chatbar-settings-modal"]').within(() => { + const newExpirationDate = dayjs().add(1, 'month').format('MMM DD, YYYY'); + cy.contains(`Expires on: ${newExpirationDate}`); + }); }); - // DOING: add test for Ultra plan it('upgrade to ultra plan', () => { // Make sure the user is on Free plan. cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { @@ -93,6 +102,20 @@ describe('Test Payment Flow', () => { cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { expect($el).to.have.text('Ultra'); }); + + cy.get('[data-cy="settings-button"]').click(); + cy.get('[data-cy="chatbar-settings-modal"]') + .scrollIntoView() + .should('be.visible'); + cy.get('[data-cy="chatbar-settings-modal"]').within(() => { + const newExpirationDate = dayjs().add(1, 'month').format('MMM DD, YYYY'); + cy.contains(`Expires on: ${newExpirationDate}`); + }); + }); + + + it('cancel subscription', () => { + }); }); From 72ae377ed6111fd8eacef16840b32d833c422006 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 14:14:52 +0800 Subject: [PATCH 123/134] refactor(api): rename test-payment-event to test-subscription-plan-payment --- cypress/e2e/payment.cy.ts | 8 ++++---- ...payment-event.ts => test-subscription-plan-payment.ts} | 1 + pages/api/webhooks/stripe.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) rename pages/api/cypress/{test-payment-event.ts => test-subscription-plan-payment.ts} (99%) diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index fd89cf1210..382a21c972 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -50,10 +50,10 @@ describe('Test Payment Flow', () => { expect($el).to.have.text('Free'); }); - // calls the `/api/cypress/test-payment-event` endpoint to test the payment flow. + // calls the `/api/cypress/test-subscription-plan-payment` endpoint to test the payment flow. cy.request({ method: 'POST', - url: '/api/cypress/test-payment-event', + url: '/api/cypress/test-subscription-plan-payment', headers: { 'Content-Type': 'application/json', }, @@ -84,10 +84,10 @@ describe('Test Payment Flow', () => { expect($el).to.have.text('Free'); }); - // calls the `/api/cypress/test-payment-event` endpoint to test the payment flow. + // calls the `/api/cypress/test-subscription-plan-payment` endpoint to test the payment flow. cy.request({ method: 'POST', - url: '/api/cypress/test-payment-event', + url: '/api/cypress/test-subscription-plan-payment', headers: { 'Content-Type': 'application/json', }, diff --git a/pages/api/cypress/test-payment-event.ts b/pages/api/cypress/test-subscription-plan-payment.ts similarity index 99% rename from pages/api/cypress/test-payment-event.ts rename to pages/api/cypress/test-subscription-plan-payment.ts index 6e50f6d5b6..192676fc10 100644 --- a/pages/api/cypress/test-payment-event.ts +++ b/pages/api/cypress/test-subscription-plan-payment.ts @@ -1,3 +1,4 @@ +// This file is intended for testing the subscription plan payment process. import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { getHomeUrl } from '@/utils/app/api'; import { getAdminSupabaseClient } from '@/utils/server/supabase'; diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 0e444e1304..13eca7f08b 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -38,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { ); } catch (err) { const bodyData = JSON.parse(rawBody.toString()); - // The fakeEvent is only available in the non-prod environment, its used by Cypress `api/cypress/test-payment-event` + // The fakeEvent is only available in the non-prod environment, its used by Cypress `api/cypress/test-subscription-plan-payment` if (!isProd && bodyData.fakeEvent) { event = bodyData.fakeEvent; isFakeEvent = true; From 4c9a6d2ba97698b39e2e4e3fe626288bf4bcccb6 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 14:48:30 +0800 Subject: [PATCH 124/134] docs(api): clarify purpose of test subscription payment file --- pages/api/cypress/test-subscription-plan-payment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/cypress/test-subscription-plan-payment.ts b/pages/api/cypress/test-subscription-plan-payment.ts index 192676fc10..051b426c09 100644 --- a/pages/api/cypress/test-subscription-plan-payment.ts +++ b/pages/api/cypress/test-subscription-plan-payment.ts @@ -1,4 +1,4 @@ -// This file is intended for testing the subscription plan payment process. +// NOTE: This file is intended for testing the subscription plan payment process. import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { getHomeUrl } from '@/utils/app/api'; import { getAdminSupabaseClient } from '@/utils/server/supabase'; From 8ec4c2d28feeea23c0d4bf6c565d7b06bebc4473 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 14:48:47 +0800 Subject: [PATCH 125/134] feat(stripe): add support for fake events in subscription deletion --- pages/api/webhooks/stripe.ts | 1 + utils/server/stripe/handleCustomerSubscriptionDeleted.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/pages/api/webhooks/stripe.ts b/pages/api/webhooks/stripe.ts index 13eca7f08b..293ec98ca8 100644 --- a/pages/api/webhooks/stripe.ts +++ b/pages/api/webhooks/stripe.ts @@ -75,6 +75,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { console.log('✅ customer.subscription.deleted'); await handleCustomerSubscriptionDeleted( event.data.object as Stripe.Subscription, + isFakeEvent, ); break; default: diff --git a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts index 6b74093543..88b730f3e7 100644 --- a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts +++ b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts @@ -4,10 +4,18 @@ import { } from './strip_helper'; import Stripe from 'stripe'; +import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; export default async function handleCustomerSubscriptionDeleted( session: Stripe.Subscription, + isFakeEvent = false, ): Promise { + if (isFakeEvent) { + await downgradeUserAccount({ + email: TEST_PAYMENT_USER.email, + }); + return + } const stripeSubscriptionId = session.id; if (!stripeSubscriptionId) { From 208a7b6e61d25f6c46a1a85b7e2298fe1a99e8eb Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 14:48:59 +0800 Subject: [PATCH 126/134] feat(api): add test endpoint for subscription cancellation --- pages/api/cypress/test-cancel-subscription.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 pages/api/cypress/test-cancel-subscription.ts diff --git a/pages/api/cypress/test-cancel-subscription.ts b/pages/api/cypress/test-cancel-subscription.ts new file mode 100644 index 0000000000..54a23aa40a --- /dev/null +++ b/pages/api/cypress/test-cancel-subscription.ts @@ -0,0 +1,62 @@ +// NOTE: This file is intended for testing the cancel subscription process. +import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; +import { getHomeUrl } from '@/utils/app/api'; +import { getAdminSupabaseClient } from '@/utils/server/supabase'; + +export const config = { + runtime: 'edge', +}; + + +const handler = async (req: Request): Promise => { + const isProd = process.env.VERCEL_ENV === 'production'; + + if (req.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + if (isProd) { + return new Response('Not allowed in production', { status: 403 }); + } + // Get user id from supabase + const supabase = getAdminSupabaseClient(); + const { data: userProfile } = await supabase + .from('profiles') + .select('id') + .eq('email', TEST_PAYMENT_USER.email) + .single(); + + if (!userProfile) return new Response('Test User not found', { status: 404 }); + + const fakeCancelProPlanSubscriptionEvent = { + "data": { + "object": {} + }, + "type": "customer.subscription.deleted" + } + + + const response = await fetch(`${getHomeUrl()}/api/webhooks/stripe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fakeEvent: fakeCancelProPlanSubscriptionEvent, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return new Response(JSON.stringify({ success: true }), { status: 200 }); + } catch (error) { + console.error(error); + return new Response('Error', { status: 500 }); + } +}; + + +export default handler; From a28cc973eed9bdfc1cc680081698d72a4a6a79e5 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 15 Jul 2024 14:50:17 +0800 Subject: [PATCH 127/134] test(e2e): implement downgrade flow verification --- cypress/e2e/payment.cy.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index 382a21c972..a95f3c5086 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -114,8 +114,38 @@ describe('Test Payment Flow', () => { }); - it('cancel subscription', () => { + it('Make sure downgrade event is working', () => { + // Make the user to Pro plan by calling the /api/cypress/test-subscription-plan-payment endpoint + cy.request({ + method: 'POST', + url: '/api/cypress/test-subscription-plan-payment', + headers: { + 'Content-Type': 'application/json', + }, + body: { + plan: 'pro', + }, + }); + + // Make sure the user is on Pro plan + cy.reload(); + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { + expect($el).to.have.text('Pro'); + }); + // Call the /api/cypress/test-cancel-subscription endpoint + cy.request({ + method: 'POST', + url: '/api/cypress/test-cancel-subscription', + headers: { + 'Content-Type': 'application/json', + }, + }); + + cy.reload(); + cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { + expect($el).to.have.text('Free'); + }); }); }); From aa54ef4c63d9ffb3d4e29499971dcfad646b3304 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 19 Jul 2024 16:10:26 +0800 Subject: [PATCH 128/134] fix(lint): expand firebase directory scope in .eslintignore --- .eslintignore | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index 408438c991..143929bb6a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -firebase/ firebase/functions/.eslintrc.js +firebase/* diff --git a/package.json b/package.json index 4218accbea..1f8d103bf5 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,8 @@ }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ - "prettier --write" + "prettier --write", + "eslint --fix" ] } } From ac7fd20f13078af442309511af50e1b467117edd Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 19 Jul 2024 16:13:22 +0800 Subject: [PATCH 129/134] fix(UserSettings): remove unused user prop from ChangeSubscriptionButton --- .../Settings/ChangeSubscriptionButton.tsx | 36 +-- components/User/Settings/PlanComparison.tsx | 66 +++-- components/ui/tabs.tsx | 34 +-- cypress/e2e/payment.cy.ts | 11 +- .../useUserSubscriptionDetail.ts | 2 +- .../reset-test-payment-user-subscription.ts | 4 +- pages/api/cypress/test-cancel-subscription.ts | 17 +- .../cypress/test-subscription-plan-payment.ts | 225 +++++++++--------- types/stripe-product.ts | 4 +- .../stripe/stripe_paid_plan_links_config.ts | 2 +- .../app/stripe/stripe_product_list_config.ts | 2 +- 11 files changed, 189 insertions(+), 214 deletions(-) diff --git a/components/User/Settings/ChangeSubscriptionButton.tsx b/components/User/Settings/ChangeSubscriptionButton.tsx index 0a8aa7f32e..0f72235dc7 100644 --- a/components/User/Settings/ChangeSubscriptionButton.tsx +++ b/components/User/Settings/ChangeSubscriptionButton.tsx @@ -3,11 +3,8 @@ import { useTranslation } from 'react-i18next'; import { OrderedSubscriptionPlans } from '@/utils/app/const'; -import { - AvailablePaidPlanType, - StripeProductPaidPlanType, -} from '@/types/stripe-product'; -import { User, UserSubscriptionDetail } from '@/types/user'; +import type { StripeProductPaidPlanType } from '@/types/stripe-product'; +import type { UserSubscriptionDetail } from '@/types/user'; import { Dialog, @@ -20,22 +17,9 @@ import { const ChangeSubscriptionButton: React.FC<{ plan: StripeProductPaidPlanType; - user: User | null; userSubscription: UserSubscriptionDetail | undefined; - interval: 'monthly' | 'yearly'; -}> = ({ plan, userSubscription, user, interval }) => { +}> = ({ plan, userSubscription }) => { const { t } = useTranslation('model'); - const availablePlan = useMemo(() => { - if (plan === 'pro') { - return 'pro-monthly'; - } else { - if (interval === 'monthly') { - return 'ultra-monthly'; - } else { - return 'ultra-yearly'; - } - } - }, [plan, interval]); const showUpgradeButton = useMemo(() => { if (!userSubscription) return false; @@ -62,21 +46,21 @@ const ChangeSubscriptionButton: React.FC<{ {showDowngradeButton ? ( -
+ ) : showUpgradeButton ? ( -
+
{t(`Upgrade to ${plan === 'pro' ? 'Pro' : 'Ultra'} Plan`)} @@ -97,8 +81,8 @@ const ChangeSubscriptionButton: React.FC<{ showDowngradeButton ? 'Required%20downgrade%20subscription' : showUpgradeButton - ? 'Required%20upgrade%20subscription' - : 'Subscription%20Inquiry' + ? 'Required%20upgrade%20subscription' + : 'Subscription%20Inquiry' }`} > jack@exploratorlabs.com @@ -109,8 +93,6 @@ const ChangeSubscriptionButton: React.FC<{
); - - return null; }; export default ChangeSubscriptionButton; diff --git a/components/User/Settings/PlanComparison.tsx b/components/User/Settings/PlanComparison.tsx index 0e1682eb0f..397acb511f 100644 --- a/components/User/Settings/PlanComparison.tsx +++ b/components/User/Settings/PlanComparison.tsx @@ -11,7 +11,7 @@ import { import { trackEvent } from '@/utils/app/eventTracking'; import { FeatureItem, PlanDetail } from '@/utils/app/ui'; -import { User, UserSubscriptionDetail } from '@/types/user'; +import type { User, UserSubscriptionDetail } from '@/types/user'; import Spinner from '@/components/Spinner'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -36,25 +36,25 @@ const PlanComparison = ({ if (isPaidUser && !isFetched) { return ( -
+
); } return ( -
+
{/* Free Plan */} -
+
{/* Pro Plan */} -
+
{/* Ultra Plan */} -
+
= ({ }) => { const { t } = useTranslation('model'); return ( -
-
+
+
{`${t('Expires on')}: ${dayjs(expirationDate).format('ll')}`}
@@ -84,7 +84,7 @@ const FreePlanContent = ({ user }: { user: User | null }) => { return ( <>
- Free + Free {(user?.plan === 'free' || !user) && }
@@ -136,7 +136,7 @@ const ProPlanContent = ({ <>
{/* Upgrade button */} {showUpgradeToPro && ( -
+
{t('Upgrade')} -

+

{t('No Strings Attached - Cancel Anytime!')}

@@ -175,9 +175,7 @@ const ProPlanContent = ({ {user?.plan === 'pro' && user.proPlanExpirationDate && ( @@ -239,7 +237,7 @@ const UltraPlanContent = ({ <>
{/* Upgrade button */} {showUpgradeToUltra && ( -
+
{t('Upgrade to Ultra')} -

+

{t('No Strings Attached - Cancel Anytime!')}

)} {user?.plan === 'ultra' && user.proPlanExpirationDate && ( @@ -297,7 +293,7 @@ const UltraPlanContent = ({ const CurrentPlanTag = () => { return ( - + CURRENT PLAN ); @@ -311,17 +307,17 @@ const ProPlanPrice = ({ const { i18n } = useTranslation('model'); if (userSubscription && userSubscription.subscriptionCurrency === 'twd') { - return {'TWD$249.99 / month'}; + return {'TWD$249.99 / month'}; } if (userSubscription && userSubscription.subscriptionCurrency === 'usd') { - return {'USD$9.99 / month'}; + return {'USD$9.99 / month'}; } switch (i18n.language) { case 'zh-Hant': case 'zh': - return {'TWD$249.99 / month'}; + return {'TWD$249.99 / month'}; default: - return {'USD$9.99 / month'}; + return {'USD$9.99 / month'}; } }; @@ -336,36 +332,36 @@ const UltraPlanPrice = ({ const monthlyPriceComponent = useMemo(() => { if (userSubscription && userSubscription.subscriptionCurrency === 'twd') { - return {'TWD$880 / month'}; + return {'TWD$880 / month'}; } if (userSubscription && userSubscription.subscriptionCurrency === 'usd') { - return {'USD$29.99 / month'}; + return {'USD$29.99 / month'}; } if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { - return {'TWD$880 / month'}; + return {'TWD$880 / month'}; } else { - return {'USD$29.99 / month'}; + return {'USD$29.99 / month'}; } }, [userSubscription, i18n.language]); const yearlyPriceComponent = useMemo(() => { if (userSubscription && userSubscription.subscriptionCurrency === 'twd') { - return {'TWD$8800 / year'}; + return {'TWD$8800 / year'}; } if (userSubscription && userSubscription.subscriptionCurrency === 'usd') { - return {'USD$279.99 / year'}; + return {'USD$279.99 / year'}; } if (i18n.language === 'zh-Hant' || i18n.language === 'zh') { - return {'TWD$8800 / year'}; + return {'TWD$8800 / year'}; } else { - return {'USD$279.99 / year'}; + return {'USD$279.99 / year'}; } }, [userSubscription, i18n.language]); return ( - + , @@ -12,13 +12,13 @@ const TabsList = React.forwardRef< -)) -TabsList.displayName = TabsPrimitive.List.displayName +)); +TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, @@ -27,13 +27,13 @@ const TabsTrigger = React.forwardRef< -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, @@ -42,12 +42,12 @@ const TabsContent = React.forwardRef< -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index a95f3c5086..9ebb311fe0 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -1,10 +1,10 @@ -import dayjs from "dayjs"; -import { TEST_PAYMENT_USER } from "./account"; +import { TEST_PAYMENT_USER } from './account'; + +import dayjs from 'dayjs'; describe('Test Payment Flow', () => { const hostUrl = Cypress.env('HOST_URL') || 'http://localhost:3000'; - beforeEach(() => { cy.session( 'user', @@ -34,7 +34,7 @@ describe('Test Payment Flow', () => { }); }); - // Logout + // Logout after(() => { cy.get('[data-cy="settings-button"]').click(); cy.get('[data-cy="chatbar-settings-modal"]') @@ -96,7 +96,6 @@ describe('Test Payment Flow', () => { }, }); - // Refreshes the page and checks if the user is on Ultra plan. cy.reload(); cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { @@ -113,7 +112,6 @@ describe('Test Payment Flow', () => { }); }); - it('Make sure downgrade event is working', () => { // Make the user to Pro plan by calling the /api/cypress/test-subscription-plan-payment endpoint cy.request({ @@ -148,4 +146,3 @@ describe('Test Payment Flow', () => { }); }); }); - diff --git a/hooks/stripeSubscription/useUserSubscriptionDetail.ts b/hooks/stripeSubscription/useUserSubscriptionDetail.ts index 56ae5d65ea..ad1e8fc5a5 100644 --- a/hooks/stripeSubscription/useUserSubscriptionDetail.ts +++ b/hooks/stripeSubscription/useUserSubscriptionDetail.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { User, UserSubscriptionDetail } from '@/types/user'; +import type { User, UserSubscriptionDetail } from '@/types/user'; export const useUserSubscriptionDetail = ({ isPaidUser, diff --git a/pages/api/cypress/reset-test-payment-user-subscription.ts b/pages/api/cypress/reset-test-payment-user-subscription.ts index 41ffe3e147..64bf306c23 100644 --- a/pages/api/cypress/reset-test-payment-user-subscription.ts +++ b/pages/api/cypress/reset-test-payment-user-subscription.ts @@ -1,6 +1,6 @@ -import { TEST_PAYMENT_USER } from "@/cypress/e2e/account"; -import { getAdminSupabaseClient } from "@/utils/server/supabase"; +import { getAdminSupabaseClient } from '@/utils/server/supabase'; +import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; export const config = { runtime: 'edge', diff --git a/pages/api/cypress/test-cancel-subscription.ts b/pages/api/cypress/test-cancel-subscription.ts index 54a23aa40a..ba4b46efc8 100644 --- a/pages/api/cypress/test-cancel-subscription.ts +++ b/pages/api/cypress/test-cancel-subscription.ts @@ -1,13 +1,13 @@ // NOTE: This file is intended for testing the cancel subscription process. -import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { getHomeUrl } from '@/utils/app/api'; import { getAdminSupabaseClient } from '@/utils/server/supabase'; +import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; + export const config = { runtime: 'edge', }; - const handler = async (req: Request): Promise => { const isProd = process.env.VERCEL_ENV === 'production'; @@ -27,15 +27,15 @@ const handler = async (req: Request): Promise => { .eq('email', TEST_PAYMENT_USER.email) .single(); - if (!userProfile) return new Response('Test User not found', { status: 404 }); + if (!userProfile) + return new Response('Test User not found', { status: 404 }); const fakeCancelProPlanSubscriptionEvent = { - "data": { - "object": {} + data: { + object: {}, }, - "type": "customer.subscription.deleted" - } - + type: 'customer.subscription.deleted', + }; const response = await fetch(`${getHomeUrl()}/api/webhooks/stripe`, { method: 'POST', @@ -58,5 +58,4 @@ const handler = async (req: Request): Promise => { } }; - export default handler; diff --git a/pages/api/cypress/test-subscription-plan-payment.ts b/pages/api/cypress/test-subscription-plan-payment.ts index 051b426c09..abbe605dfa 100644 --- a/pages/api/cypress/test-subscription-plan-payment.ts +++ b/pages/api/cypress/test-subscription-plan-payment.ts @@ -1,7 +1,8 @@ // NOTE: This file is intended for testing the subscription plan payment process. -import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { getHomeUrl } from '@/utils/app/api'; import { getAdminSupabaseClient } from '@/utils/server/supabase'; + +import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; import { z } from 'zod'; export const config = { @@ -9,7 +10,7 @@ export const config = { }; const requestSchema = z.object({ - plan: z.enum(['pro', 'ultra']) + plan: z.enum(['pro', 'ultra']), }); const handler = async (req: Request): Promise => { @@ -31,7 +32,8 @@ const handler = async (req: Request): Promise => { .eq('email', TEST_PAYMENT_USER.email) .single(); - if (!userProfile) return new Response('Test User not found', { status: 404 }); + if (!userProfile) + return new Response('Test User not found', { status: 404 }); const fakeProPlanSubscriptionEvent = { id: 'evt_1PbZayEEvfd1BzvuIWPC3rOO', @@ -148,133 +150,130 @@ const handler = async (req: Request): Promise => { type: 'checkout.session.completed', }; - const fakeUltraPlanSubscriptionEvent = { - "id": "evt_1Pbb50EEvfd1Bzvu6cO2Pg6O", - "object": "event", - "api_version": "2020-08-27", - "created": 1720758454, - "data": { - "object": { - "id": "cs_test_a1OykWFuJxe6mR80M6mmm73ENFnZ8VJ2cDyrpxEXIzp74uVQtaRwDRMr7n", - "object": "checkout.session", - "after_expiration": null, - "allow_promotion_codes": false, - "amount_subtotal": 2999, - "amount_total": 2999, - "automatic_tax": { - "enabled": false, - "liability": null, - "status": null + id: 'evt_1Pbb50EEvfd1Bzvu6cO2Pg6O', + object: 'event', + api_version: '2020-08-27', + created: 1720758454, + data: { + object: { + id: 'cs_test_a1OykWFuJxe6mR80M6mmm73ENFnZ8VJ2cDyrpxEXIzp74uVQtaRwDRMr7n', + object: 'checkout.session', + after_expiration: null, + allow_promotion_codes: false, + amount_subtotal: 2999, + amount_total: 2999, + automatic_tax: { + enabled: false, + liability: null, + status: null, }, - "billing_address_collection": "auto", - "cancel_url": "https://stripe.com", - "client_reference_id": userProfile.id, - "client_secret": null, - "consent": null, - "consent_collection": { - "payment_method_reuse_agreement": null, - "promotions": "none", - "terms_of_service": "none" + billing_address_collection: 'auto', + cancel_url: 'https://stripe.com', + client_reference_id: userProfile.id, + client_secret: null, + consent: null, + consent_collection: { + payment_method_reuse_agreement: null, + promotions: 'none', + terms_of_service: 'none', }, - "created": 1720758430, - "currency": "usd", - "currency_conversion": null, - "custom_fields": [], - "custom_text": { - "after_submit": null, - "shipping_address": null, - "submit": null, - "terms_of_service_acceptance": null + created: 1720758430, + currency: 'usd', + currency_conversion: null, + custom_fields: [], + custom_text: { + after_submit: null, + shipping_address: null, + submit: null, + terms_of_service_acceptance: null, }, - "customer": "cus_QSW7Jm6qEisEf2", - "customer_creation": "if_required", - "customer_details": { - "address": { - "city": null, - "country": "MO", - "line1": null, - "line2": null, - "postal_code": null, - "state": null + customer: 'cus_QSW7Jm6qEisEf2', + customer_creation: 'if_required', + customer_details: { + address: { + city: null, + country: 'MO', + line1: null, + line2: null, + postal_code: null, + state: null, }, - "email": TEST_PAYMENT_USER.email, - "name": "StripeCardTest", - "phone": null, - "tax_exempt": "none", - "tax_ids": [] + email: TEST_PAYMENT_USER.email, + name: 'StripeCardTest', + phone: null, + tax_exempt: 'none', + tax_ids: [], }, - "customer_email": null, - "expires_at": 1720844830, - "invoice": "in_1Pbb4tEEvfd1BzvueDlUtXzd", - "invoice_creation": null, - "livemode": false, - "locale": "auto", - "metadata": { - "plan_code": "monthly_ultra_plan_subscription" + customer_email: null, + expires_at: 1720844830, + invoice: 'in_1Pbb4tEEvfd1BzvueDlUtXzd', + invoice_creation: null, + livemode: false, + locale: 'auto', + metadata: { + plan_code: 'monthly_ultra_plan_subscription', }, - "mode": "subscription", - "payment_intent": null, - "payment_link": "plink_1PLhmVEEvfd1BzvuRYKzYTER", - "payment_method_collection": "always", - "payment_method_configuration_details": { - "id": "pmc_1MzuEJEEvfd1BzvuSIHOnamP", - "parent": null + mode: 'subscription', + payment_intent: null, + payment_link: 'plink_1PLhmVEEvfd1BzvuRYKzYTER', + payment_method_collection: 'always', + payment_method_configuration_details: { + id: 'pmc_1MzuEJEEvfd1BzvuSIHOnamP', + parent: null, }, - "payment_method_options": { - "card": { - "request_three_d_secure": "automatic" - } + payment_method_options: { + card: { + request_three_d_secure: 'automatic', + }, }, - "payment_method_types": [ - "card", - "link" - ], - "payment_status": "paid", - "phone_number_collection": { - "enabled": false + payment_method_types: ['card', 'link'], + payment_status: 'paid', + phone_number_collection: { + enabled: false, }, - "recovered_from": null, - "saved_payment_method_options": { - "allow_redisplay_filters": [ - "always" - ], - "payment_method_remove": null, - "payment_method_save": null + recovered_from: null, + saved_payment_method_options: { + allow_redisplay_filters: ['always'], + payment_method_remove: null, + payment_method_save: null, }, - "setup_intent": null, - "shipping": null, - "shipping_address_collection": null, - "shipping_options": [], - "shipping_rate": null, - "status": "complete", - "submit_type": "auto", - "subscription": "sub_1Pbb4tEEvfd1BzvuNYGOFJLD", - "success_url": "https://stripe.com", - "total_details": { - "amount_discount": 0, - "amount_shipping": 0, - "amount_tax": 0 + setup_intent: null, + shipping: null, + shipping_address_collection: null, + shipping_options: [], + shipping_rate: null, + status: 'complete', + submit_type: 'auto', + subscription: 'sub_1Pbb4tEEvfd1BzvuNYGOFJLD', + success_url: 'https://stripe.com', + total_details: { + amount_discount: 0, + amount_shipping: 0, + amount_tax: 0, }, - "ui_mode": "hosted", - "url": null - } + ui_mode: 'hosted', + url: null, + }, }, - "livemode": false, - "pending_webhooks": 4, - "request": { - "id": null, - "idempotency_key": null + livemode: false, + pending_webhooks: 4, + request: { + id: null, + idempotency_key: null, }, - "type": "checkout.session.completed" - } + type: 'checkout.session.completed', + }; // read req body (plan: pro/ultra) const body = await req.json(); const validationResult = requestSchema.safeParse(body); if (!validationResult.success) { - return new Response('Invalid request body. Plan must be either "pro" or "ultra".', { status: 400 }); + return new Response( + 'Invalid request body. Plan must be either "pro" or "ultra".', + { status: 400 }, + ); } const { plan } = validationResult.data; @@ -288,7 +287,10 @@ const handler = async (req: Request): Promise => { 'Content-Type': 'application/json', }, body: JSON.stringify({ - fakeEvent: plan === 'pro' ? fakeProPlanSubscriptionEvent : fakeUltraPlanSubscriptionEvent, + fakeEvent: + plan === 'pro' + ? fakeProPlanSubscriptionEvent + : fakeUltraPlanSubscriptionEvent, }), }); @@ -303,5 +305,4 @@ const handler = async (req: Request): Promise => { } }; - export default handler; diff --git a/types/stripe-product.ts b/types/stripe-product.ts index ad9a1f8056..f61d3b9639 100644 --- a/types/stripe-product.ts +++ b/types/stripe-product.ts @@ -1,6 +1,6 @@ -import { SubscriptionPlan } from './paid_plan'; +import type { SubscriptionPlan } from './paid_plan'; -import Stripe from 'stripe'; +import type Stripe from 'stripe'; export type StripeProductType = 'top_up' | 'paid_plan'; diff --git a/utils/app/stripe/stripe_paid_plan_links_config.ts b/utils/app/stripe/stripe_paid_plan_links_config.ts index 2a65fada4a..de6297f0cc 100644 --- a/utils/app/stripe/stripe_paid_plan_links_config.ts +++ b/utils/app/stripe/stripe_paid_plan_links_config.ts @@ -1,4 +1,4 @@ -import { PaidPlanLinks } from '@/types/stripe-product'; +import type { PaidPlanLinks } from '@/types/stripe-product'; export const STRIPE_PAID_PLAN_LINKS_PRODUCTION: PaidPlanLinks = { 'ultra-yearly': { diff --git a/utils/app/stripe/stripe_product_list_config.ts b/utils/app/stripe/stripe_product_list_config.ts index 114b58724b..6fc6987e78 100644 --- a/utils/app/stripe/stripe_product_list_config.ts +++ b/utils/app/stripe/stripe_product_list_config.ts @@ -1,4 +1,4 @@ -import { NewStripeProduct } from '@/types/stripe-product'; +import type { NewStripeProduct } from '@/types/stripe-product'; export const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [ { From 63cb57499adf9f8a7e818fb1506a60ca9426606d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 19 Jul 2024 16:19:59 +0800 Subject: [PATCH 130/134] fix(config): correct environment variable reference in Settings_Account.tsx --- components/User/Settings/Settings_Account.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/User/Settings/Settings_Account.tsx b/components/User/Settings/Settings_Account.tsx index dbe2aea236..66c59be780 100644 --- a/components/User/Settings/Settings_Account.tsx +++ b/components/User/Settings/Settings_Account.tsx @@ -28,7 +28,7 @@ export default function Settings_Account() { }; const subscriptionManagementLink = () => - processenv.NEXT_PUBLIC_ENV === 'production' + process.env.NEXT_PUBLIC_ENV === 'production' ? 'https://billing.stripe.com/p/login/5kAbMj0wt5VF6AwaEE' : 'https://billing.stripe.com/p/login/test_28o4jFe6GaqK1UY5kk'; From 96b3d194c39bc4176a4ba0894147ac3c4f5a5fa5 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 19 Jul 2024 23:48:25 +0800 Subject: [PATCH 131/134] refactor(subscriptions): adjust expiration date to include an extra day for timezone safety --- .../handleCustomerSubscriptionDeleted.ts | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts index ac51917ed4..c977e66fbc 100644 --- a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts +++ b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts @@ -1,32 +1,59 @@ -import { - downgradeUserAccount, - getCustomerEmailByCustomerID, -} from './strip_helper'; +import { getAdminSupabaseClient } from '../supabase'; +import { getCustomerEmailByCustomerID } from './strip_helper'; import { TEST_PAYMENT_USER } from '@/cypress/e2e/account'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import type Stripe from 'stripe'; +const supabase = getAdminSupabaseClient(); +dayjs.extend(utc); + export default async function handleCustomerSubscriptionDeleted( session: Stripe.Subscription, isFakeEvent = false, ): Promise { + // Add 1 day to the current date to avoid time zone differences + if (isFakeEvent) { - await downgradeUserAccount({ - email: TEST_PAYMENT_USER.email, - }); + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + pro_plan_expiration_date: dayjs() + .add(1, 'month') + .add(1, 'day') + .utc() + .toDate(), + }) + .eq('email', TEST_PAYMENT_USER.email); + if (updatedUserError) throw updatedUserError; return; } + + const newExpirationDate = dayjs + .unix(session.current_period_end) + .add(1, 'day') + .utc() + .toDate(); const stripeSubscriptionId = session.id; if (!stripeSubscriptionId) { const customerId = session.customer as string; const email = await getCustomerEmailByCustomerID(customerId); - await downgradeUserAccount({ - email, - }); + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + pro_plan_expiration_date: newExpirationDate, + }) + .eq('email', email); + if (updatedUserError) throw updatedUserError; } else { - await downgradeUserAccount({ - stripeSubscriptionId, - }); + const { error: updatedUserError } = await supabase + .from('profiles') + .update({ + pro_plan_expiration_date: newExpirationDate, + }) + .eq('stripe_subscription_id', stripeSubscriptionId); + if (updatedUserError) throw updatedUserError; } } From f10d86ed68136bbb33aa0986617668a8f7441a8d Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 19 Jul 2024 23:57:11 +0800 Subject: [PATCH 132/134] test(payment): extend expiration date by an extra day for Test Downgrade --- cypress/e2e/payment.cy.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index 9ebb311fe0..4c09e1094b 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -36,6 +36,7 @@ describe('Test Payment Flow', () => { // Logout after(() => { + cy.reload(); cy.get('[data-cy="settings-button"]').click(); cy.get('[data-cy="chatbar-settings-modal"]') .scrollIntoView() @@ -141,8 +142,20 @@ describe('Test Payment Flow', () => { }); cy.reload(); + // Make sure the user is on Pro plan but with a extra day from the expiration date cy.get('[data-cy="user-account-badge"]', { timeout: 10000 }).then(($el) => { - expect($el).to.have.text('Free'); + expect($el).to.have.text('Pro'); + }); + cy.get('[data-cy="settings-button"]').click(); + cy.get('[data-cy="chatbar-settings-modal"]') + .scrollIntoView() + .should('be.visible'); + cy.get('[data-cy="chatbar-settings-modal"]').within(() => { + const newExpirationDate = dayjs() + .add(1, 'month') + .add(1, 'day') + .format('MMM DD, YYYY'); + cy.contains(`Expires on: ${newExpirationDate}`); }); }); }); From a54e7ba0ef7682c8c6d79d0aaf9e6be7803c3f8f Mon Sep 17 00:00:00 2001 From: 1orzero Date: Fri, 19 Jul 2024 23:58:06 +0800 Subject: [PATCH 133/134] refactor(tests): rename test for subscription removal event accuracy --- cypress/e2e/payment.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index 4c09e1094b..26c004421a 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -113,7 +113,7 @@ describe('Test Payment Flow', () => { }); }); - it('Make sure downgrade event is working', () => { + it('Make sure custom remove subscription event is working', () => { // Make the user to Pro plan by calling the /api/cypress/test-subscription-plan-payment endpoint cy.request({ method: 'POST', From 5cd9451d66d4447d5315636164f109e8fe02c8b4 Mon Sep 17 00:00:00 2001 From: 1orzero Date: Mon, 22 Jul 2024 10:37:59 +0800 Subject: [PATCH 134/134] fix(payment): update description for subscription cancellation test --- cypress/e2e/payment.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/payment.cy.ts b/cypress/e2e/payment.cy.ts index 26c004421a..0d08742afb 100644 --- a/cypress/e2e/payment.cy.ts +++ b/cypress/e2e/payment.cy.ts @@ -113,7 +113,7 @@ describe('Test Payment Flow', () => { }); }); - it('Make sure custom remove subscription event is working', () => { + it('cancel subscription event should not cancel the plan immediately', () => { // Make the user to Pro plan by calling the /api/cypress/test-subscription-plan-payment endpoint cy.request({ method: 'POST',