diff --git a/apps/studio/electron/main/events/payments.ts b/apps/studio/electron/main/events/payments.ts index cc86930f1..035bef4db 100644 --- a/apps/studio/electron/main/events/payments.ts +++ b/apps/studio/electron/main/events/payments.ts @@ -1,12 +1,13 @@ import { MainChannels } from '@onlook/models/constants'; +import { UsagePlanType } from '@onlook/models/usage'; import { ipcMain } from 'electron'; import { checkoutWithStripe, checkSubscription, manageSubscription } from '../payment'; export function listenForPaymentMessages() { ipcMain.handle( MainChannels.CREATE_STRIPE_CHECKOUT, - async (e: Electron.IpcMainInvokeEvent, args) => { - return await checkoutWithStripe(); + async (e: Electron.IpcMainInvokeEvent, plan: UsagePlanType = UsagePlanType.PRO) => { + return await checkoutWithStripe(plan); }, ); diff --git a/apps/studio/electron/main/payment/index.ts b/apps/studio/electron/main/payment/index.ts index d6016ab12..cbc80a45b 100644 --- a/apps/studio/electron/main/payment/index.ts +++ b/apps/studio/electron/main/payment/index.ts @@ -1,14 +1,17 @@ import { ApiRoutes, BASE_API_ROUTE, FUNCTIONS_ROUTE } from '@onlook/models/constants'; import type { AuthTokens } from '@onlook/models/settings'; +import { UsagePlanType } from '@onlook/models/usage'; import { shell } from 'electron'; import { getRefreshedAuthTokens } from '../auth'; -export const checkoutWithStripe = async (): Promise<{ +export const checkoutWithStripe = async ( + plan: UsagePlanType = UsagePlanType.PRO, +): Promise<{ success: boolean; error?: string; }> => { try { - const checkoutUrl = await createCheckoutSession(); + const checkoutUrl = await createCheckoutSession(plan); shell.openExternal(checkoutUrl); return { success: true }; } catch (error) { @@ -16,7 +19,7 @@ export const checkoutWithStripe = async (): Promise<{ } }; -const createCheckoutSession = async () => { +const createCheckoutSession = async (plan: UsagePlanType = UsagePlanType.PRO) => { const authTokens: AuthTokens = await getRefreshedAuthTokens(); if (!authTokens) { throw new Error('No auth tokens found'); @@ -30,6 +33,7 @@ const createCheckoutSession = async () => { 'Content-Type': 'application/json', Authorization: `Bearer ${authTokens.accessToken}`, }, + body: JSON.stringify({ plan }), }, ); diff --git a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx index ae8801bf4..99fb46f49 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx @@ -4,6 +4,22 @@ import { MotionCard } from '@onlook/ui/motion-card'; import { motion } from 'framer-motion'; import { useTranslation } from 'react-i18next'; +interface PricingCardProps { + plan: string; + price: string; + description: string; + features: string[]; + buttonText: string; + buttonProps: React.ButtonHTMLAttributes; + delay: number; + isLoading?: boolean; + className?: string; + showFeaturesPrefix?: boolean; + featuresPrefixText?: string; + isRecommended?: boolean; + isCurrentPlan?: boolean; +} + export const PricingCard = ({ plan, price, @@ -13,59 +29,88 @@ export const PricingCard = ({ buttonProps, delay, isLoading, -}: { - plan: string; - price: string; - description: string; - features: string[]; - buttonText: string; - buttonProps: React.ButtonHTMLAttributes; - delay: number; - isLoading?: boolean; -}) => { + className, + showFeaturesPrefix = false, + featuresPrefixText = 'Everything in Pro plus:', + isRecommended = false, + isCurrentPlan = false, +}: PricingCardProps) => { const { t } = useTranslation(); return ( - - -
-

{plan}

-

{price}

-
-
-

{description}

-
-
- {features.map((feature, i) => ( +
+ + {isRecommended && ( +
- - {feature} + Recommended
- ))} -
- +
+
+ {showFeaturesPrefix && ( +

{featuresPrefixText}

+ )} + {features.map((feature, i) => ( +
+ + {feature} +
+ ))}
- ) : ( - buttonText - )} - + + - +
); }; diff --git a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx index 7678f9a15..82aa374f2 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx @@ -45,7 +45,11 @@ export const SubscriptionModal = observer(() => { const getPlan = async () => { const plan = await userManager.subscription.getPlanFromServer(); - if (plan === UsagePlanType.PRO) { + if ( + plan === UsagePlanType.PRO || + plan === UsagePlanType.LAUNCH || + plan === UsagePlanType.SCALE + ) { editorEngine.chat.stream.clearRateLimited(); editorEngine.chat.stream.clearErrorMessage(); } @@ -74,7 +78,78 @@ export const SubscriptionModal = observer(() => { success: boolean; error?: string; } - | undefined = await invokeMainChannel(MainChannels.CREATE_STRIPE_CHECKOUT); + | undefined = await invokeMainChannel( + MainChannels.CREATE_STRIPE_CHECKOUT, + UsagePlanType.PRO, + ); + if (res?.success) { + toast({ + variant: 'default', + title: t('pricing.toasts.checkingOut.title'), + description: t('pricing.toasts.checkingOut.description'), + }); + } else { + throw new Error('No checkout URL received'); + } + setIsCheckingOut(null); + } catch (error) { + toast({ + variant: 'destructive', + title: t('pricing.toasts.error.title'), + description: t('pricing.toasts.error.description'), + }); + console.error('Payment error:', error); + setIsCheckingOut(null); + } + }; + + const startLaunchCheckout = async () => { + sendAnalytics('start launch checkout'); + try { + setIsCheckingOut(UsagePlanType.LAUNCH); + const res: + | { + success: boolean; + error?: string; + } + | undefined = await invokeMainChannel( + MainChannels.CREATE_STRIPE_CHECKOUT, + UsagePlanType.LAUNCH, + ); + if (res?.success) { + toast({ + variant: 'default', + title: t('pricing.toasts.checkingOut.title'), + description: t('pricing.toasts.checkingOut.description'), + }); + } else { + throw new Error('No checkout URL received'); + } + setIsCheckingOut(null); + } catch (error) { + toast({ + variant: 'destructive', + title: t('pricing.toasts.error.title'), + description: t('pricing.toasts.error.description'), + }); + console.error('Payment error:', error); + setIsCheckingOut(null); + } + }; + + const startScaleCheckout = async () => { + sendAnalytics('start scale checkout'); + try { + setIsCheckingOut(UsagePlanType.SCALE); + const res: + | { + success: boolean; + error?: string; + } + | undefined = await invokeMainChannel( + MainChannels.CREATE_STRIPE_CHECKOUT, + UsagePlanType.SCALE, + ); if (res?.success) { toast({ variant: 'default', @@ -98,7 +173,7 @@ export const SubscriptionModal = observer(() => { const manageSubscription = async () => { try { - setIsCheckingOut(UsagePlanType.BASIC); + setIsCheckingOut(UsagePlanType.FREE); const res: | { success: boolean; @@ -131,112 +206,211 @@ export const SubscriptionModal = observer(() => { {editorEngine.isPlansOpen && (
-
- -
- - - -
-

- {userManager.subscription.plan === UsagePlanType.PRO - ? t('pricing.titles.proMember') - : t('pricing.titles.choosePlan')} -

-
-
-
- { - manageSubscription(); - }, - disabled: +
+
+ +
+ +
+
+ + + +
+

+ {(() => { + switch (userManager.subscription.plan) { + case UsagePlanType.PRO: + return t( + 'pricing.titles.proMember', + ); + case UsagePlanType.LAUNCH: + return t( + 'pricing.titles.launchMember', + ); + case UsagePlanType.SCALE: + return t( + 'pricing.titles.scaleMember', + ); + default: + return t( + 'pricing.titles.choosePlan', + ); + } + })()} +

+
+
+
+ { + manageSubscription(); + }, + disabled: + userManager.subscription.plan === + UsagePlanType.FREE || + isCheckingOut === UsagePlanType.FREE, + }} + delay={0.1} + isLoading={isCheckingOut === UsagePlanType.FREE} + /> + - + -
- -

- {t('pricing.footer.unusedMessages')} -

+ UsagePlanType.LAUNCH + ? `You are on ${t('pricing.plans.launch.name')}` + : userManager.subscription.plan === + UsagePlanType.SCALE + ? `Downgrade to ${t('pricing.plans.launch.name')}` + : 'Get Launch' + } + buttonProps={{ + onClick: startLaunchCheckout, + disabled: + userManager.subscription.plan === + UsagePlanType.LAUNCH || + isCheckingOut === UsagePlanType.LAUNCH, + }} + delay={0.15} + isLoading={isCheckingOut === UsagePlanType.LAUNCH} + showFeaturesPrefix={true} + featuresPrefixText="Everything in Pro plus:" + isCurrentPlan={ + userManager.subscription.plan === + UsagePlanType.LAUNCH + } + /> + +
+ +

+ {t('pricing.footer.unusedMessages')} +

+
- - + +
diff --git a/apps/studio/src/lib/user/subscription.ts b/apps/studio/src/lib/user/subscription.ts index 2cfbe3b8d..fd4529dee 100644 --- a/apps/studio/src/lib/user/subscription.ts +++ b/apps/studio/src/lib/user/subscription.ts @@ -4,7 +4,7 @@ import { makeAutoObservable } from 'mobx'; import { invokeMainChannel } from '../utils'; export class SubscriptionManager { - plan: UsagePlanType = UsagePlanType.BASIC; + plan: UsagePlanType = UsagePlanType.PRO; constructor() { makeAutoObservable(this); @@ -14,7 +14,7 @@ export class SubscriptionManager { private restoreCachedPlan() { const cachedPlan = localStorage.getItem('currentPlan'); - this.plan = (cachedPlan as UsagePlanType) || UsagePlanType.BASIC; + this.plan = UsagePlanType.PRO; } async updatePlan(plan: UsagePlanType) { @@ -35,12 +35,31 @@ export class SubscriptionManager { if (!res?.success) { throw new Error(res?.error || 'Error checking premium status'); } - const newPlan = res.data.name === 'pro' ? UsagePlanType.PRO : UsagePlanType.BASIC; + + // Determine plan type based on API response + let newPlan = UsagePlanType.FREE; + + if (res.data && res.data.name) { + switch (res.data.name) { + case 'pro': + newPlan = UsagePlanType.PRO; + break; + case 'launch': + newPlan = UsagePlanType.LAUNCH; + break; + case 'scale': + newPlan = UsagePlanType.SCALE; + break; + default: + newPlan = UsagePlanType.FREE; + } + } + await this.updatePlan(newPlan); return newPlan; } catch (error) { console.error('Error checking premium status:', error); - return UsagePlanType.BASIC; + return UsagePlanType.FREE; } } } diff --git a/apps/studio/src/locales/en/translation.json b/apps/studio/src/locales/en/translation.json index 0f153e79d..840ac8e40 100644 --- a/apps/studio/src/locales/en/translation.json +++ b/apps/studio/src/locales/en/translation.json @@ -96,6 +96,18 @@ }, "pricing": { "plans": { + "free": { + "name": "Free", + "price": "$0/month", + "description": "Prototype and experiment in code with ease.", + "features": [ + "Visual code editor access", + "Unlimited projects", + "{{dailyMessages}} AI chat messages a day", + "{{monthlyMessages}} AI messages a month", + "Limited to 1 screenshot per chat" + ] + }, "basic": { "name": "Onlook Basic", "price": "$0/month", @@ -109,7 +121,7 @@ ] }, "pro": { - "name": "Onlook Pro", + "name": "Pro", "price": "$20/month", "description": "Creativity – unconstrained. Build stunning sites with AI.", "features": [ @@ -121,16 +133,44 @@ "1 free custom domain hosted with Onlook", "Priority support" ] + }, + "launch": { + "name": "Launch", + "price": "$50/month", + "description": "Perfect for startups and growing teams", + "features": [ + "Unlimited daily messages", + "Priority support", + "Advanced integrations", + "Team collaboration features" + ] + }, + "scale": { + "name": "Scale", + "price": "$100/month", + "description": "Ready to grow features for large teams", + "features": [ + "Everything in Launch plan", + "Dedicated account manager", + "Custom integrations", + "Advanced analytics", + "24/7 premium support" + ] } }, "titles": { "choosePlan": "Choose your plan", - "proMember": "Thanks for being a Pro member!" + "proMember": "Thanks for being a Pro member!", + "launchMember": "Thanks for being a Launch member!", + "scaleMember": "Thanks for being a Scale member!" }, "buttons": { "currentPlan": "Current Plan", "getPro": "Get Pro", - "manageSubscription": "Manage Subscription" + "manageSubscription": "Manage Subscription", + "downgradeToFree": "Downgrade to Free", + "downgradeToLaunch": "Downgrade to Launch", + "downgradeToPro": "Downgrade to Pro" }, "loading": { "checkingPayment": "Checking for payment..." diff --git a/apps/studio/src/locales/zh/translation.json b/apps/studio/src/locales/zh/translation.json index 31474ccc6..10efb191f 100644 --- a/apps/studio/src/locales/zh/translation.json +++ b/apps/studio/src/locales/zh/translation.json @@ -96,6 +96,18 @@ }, "pricing": { "plans": { + "free": { + "name": "免费版", + "price": "$0/月", + "description": "轻松在代码中进行原型设计和实验。", + "features": [ + "可视化代码编辑器访问", + "无限制项目", + "每天 {{dailyMessages}} 条 AI 聊天消息", + "每月 {{monthlyMessages}} 条 AI 消息", + "每次聊天限 1 张截图" + ] + }, "basic": { "name": "Onlook Basic", "price": "$0/月", @@ -130,7 +142,8 @@ "buttons": { "currentPlan": "当前方案", "getPro": "升级到 Pro", - "manageSubscription": "管理订阅" + "manageSubscription": "管理订阅", + "downgradeToFree": "降级到免费版" }, "loading": { "checkingPayment": "正在检查付款..." diff --git a/packages/models/src/supabase/db.ts b/packages/models/src/supabase/db.ts index b36efb665..43dbb7b78 100644 --- a/packages/models/src/supabase/db.ts +++ b/packages/models/src/supabase/db.ts @@ -717,7 +717,7 @@ export type Database = { }; Enums: { usage_limit_reason: 'none' | 'daily' | 'monthly'; - usage_plan_values: 'basic' | 'pro'; + usage_plan_values: 'free' | 'pro'; }; CompositeTypes: { [_ in never]: never; diff --git a/packages/models/src/usage/index.ts b/packages/models/src/usage/index.ts index e140a3908..e8a3a2f36 100644 --- a/packages/models/src/usage/index.ts +++ b/packages/models/src/usage/index.ts @@ -1,5 +1,7 @@ export enum UsagePlanType { - BASIC = 'basic', + FREE = 'free', + LAUNCH = 'launch', + SCALE = 'scale', PRO = 'pro', }