From ee6c996f3d5b423056ad3f95dbc5f051d82ee494 Mon Sep 17 00:00:00 2001 From: tycreated Date: Wed, 2 Apr 2025 13:11:14 -0700 Subject: [PATCH 1/6] Pricing cards style base --- apps/studio/electron/main/events/payments.ts | 5 +- apps/studio/electron/main/payment/index.ts | 10 +- .../Modals/Subscription/PricingCard.tsx | 137 +++++---- .../Modals/Subscription/PricingPage.tsx | 279 +++++++++++++----- apps/studio/src/lib/user/subscription.ts | 21 +- apps/studio/src/locales/en/translation.json | 2 +- packages/models/src/usage/index.ts | 2 + 7 files changed, 332 insertions(+), 124 deletions(-) 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..71144eb41 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx @@ -4,6 +4,21 @@ 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; +} + export const PricingCard = ({ plan, price, @@ -13,59 +28,81 @@ 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, +}: PricingCardProps) => { const { t } = useTranslation(); return ( - - -
-

{plan}

-

{price}

-
-
-

{description}

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

{plan}

+

+ {price.split('/')[0]} + + /month + +

+
+

+ {description} +

+ +
+
+ {showFeaturesPrefix && ( +

{featuresPrefixText}

+ )} + {features.map((feature, i) => ( +
+ + {feature} +
+ ))} +
+ + +
); }; diff --git a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx index 7678f9a15..12832c75c 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', @@ -131,46 +206,139 @@ export const SubscriptionModal = observer(() => { {editorEngine.isPlansOpen && (
-
- -
- - - -
-

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

+
+
+ +
+ +
+
+ + + +
+

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

+
+
+
+ + +
-
-
{ disabled: userManager.subscription.plan === UsagePlanType.BASIC || - isCheckingOut === 'basic', + isCheckingOut === UsagePlanType.BASIC, }} delay={0.1} - isLoading={isCheckingOut === 'basic'} - /> - -
- -

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

+ +

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

+
- -
+ +
diff --git a/apps/studio/src/lib/user/subscription.ts b/apps/studio/src/lib/user/subscription.ts index 2cfbe3b8d..6095e4d9f 100644 --- a/apps/studio/src/lib/user/subscription.ts +++ b/apps/studio/src/lib/user/subscription.ts @@ -35,7 +35,26 @@ 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.BASIC; + + 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.BASIC; + } + } + await this.updatePlan(newPlan); return newPlan; } catch (error) { diff --git a/apps/studio/src/locales/en/translation.json b/apps/studio/src/locales/en/translation.json index 0f153e79d..502bea573 100644 --- a/apps/studio/src/locales/en/translation.json +++ b/apps/studio/src/locales/en/translation.json @@ -109,7 +109,7 @@ ] }, "pro": { - "name": "Onlook Pro", + "name": "Pro", "price": "$20/month", "description": "Creativity – unconstrained. Build stunning sites with AI.", "features": [ diff --git a/packages/models/src/usage/index.ts b/packages/models/src/usage/index.ts index e140a3908..112e3c70d 100644 --- a/packages/models/src/usage/index.ts +++ b/packages/models/src/usage/index.ts @@ -1,5 +1,7 @@ export enum UsagePlanType { BASIC = 'basic', + LAUNCH = 'launch', + SCALE = 'scale', PRO = 'pro', } From 6bd88b67080b76988145f9c746baca6d83067c85 Mon Sep 17 00:00:00 2001 From: tycreated Date: Wed, 2 Apr 2025 13:27:40 -0700 Subject: [PATCH 2/6] Updated pricing card styling --- .../Modals/Subscription/PricingCard.tsx | 128 +++++++++--------- .../Modals/Subscription/PricingPage.tsx | 8 +- 2 files changed, 70 insertions(+), 66 deletions(-) diff --git a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx index 71144eb41..fa35e7c9e 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx @@ -36,73 +36,77 @@ export const PricingCard = ({ const { t } = useTranslation(); return ( -
- {isRecommended && ( -
-
- Recommended -
-
- )} - + - -
-

{plan}

-

- {price.split('/')[0]} - - /month - -

+ {isRecommended && ( +
+
+ Recommended +
-

- {description} -

- -
-
- {showFeaturesPrefix && ( -

{featuresPrefixText}

- )} - {features.map((feature, i) => ( -
- - {feature} -
- ))} -
- - + )} + + +
+

{plan}

+

+ {price.split('/')[0]} + + /month + +

+
+

+ {description} +

+ +
+
+ {showFeaturesPrefix && ( +

{featuresPrefixText}

+ )} + {features.map((feature, i) => ( +
+ + {feature} +
+ ))} +
+ + +
); }; diff --git a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx index 12832c75c..a39cb1380 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx @@ -231,12 +231,12 @@ export const SubscriptionModal = observer(() => {
-
-
+
+
- + {
-
+
Date: Wed, 2 Apr 2025 13:36:18 -0700 Subject: [PATCH 3/6] Updated plan information Updated where the plan info was being pulled from to match the current setup with the pro and basic plan. --- .../Modals/Subscription/PricingPage.tsx | 35 +++++++++---------- apps/studio/src/locales/en/translation.json | 23 ++++++++++++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx index a39cb1380..dcf7dbd58 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx @@ -281,15 +281,14 @@ export const SubscriptionModal = observer(() => { featuresPrefixText="Everything in Free plus:" /> { isRecommended={true} /> Date: Wed, 2 Apr 2025 13:40:49 -0700 Subject: [PATCH 4/6] Minor styling --- apps/studio/src/components/Modals/Subscription/PricingPage.tsx | 2 +- apps/studio/src/locales/en/translation.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx index dcf7dbd58..f5117e458 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingPage.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingPage.tsx @@ -244,7 +244,7 @@ export const SubscriptionModal = observer(() => { transition={{ delay: 0.05 }} >
-

+

{userManager.subscription.plan === UsagePlanType.PRO ? t('pricing.titles.proMember') diff --git a/apps/studio/src/locales/en/translation.json b/apps/studio/src/locales/en/translation.json index 062f9056f..a69ade936 100644 --- a/apps/studio/src/locales/en/translation.json +++ b/apps/studio/src/locales/en/translation.json @@ -136,7 +136,7 @@ "scale": { "name": "Scale", "price": "$100/month", - "description": "Enterprise-grade features for large teams", + "description": "Ready to grow features for large teams", "features": [ "Everything in Launch plan", "Dedicated account manager", From 46239a10945405c238dbb7ce67555e3dc5c2dd0b Mon Sep 17 00:00:00 2001 From: tycreated Date: Wed, 2 Apr 2025 18:20:14 -0700 Subject: [PATCH 5/6] Updated logic Added logic for the title and button text of the pricing card for the current plan. --- .../Modals/Subscription/PricingCard.tsx | 6 +- .../Modals/Subscription/PricingPage.tsx | 55 +++++++++++++------ apps/studio/src/lib/user/subscription.ts | 4 +- apps/studio/src/locales/en/translation.json | 9 ++- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx index fa35e7c9e..9e321de67 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx @@ -17,6 +17,7 @@ interface PricingCardProps { showFeaturesPrefix?: boolean; featuresPrefixText?: string; isRecommended?: boolean; + isCurrentPlan?: boolean; } export const PricingCard = ({ @@ -32,6 +33,7 @@ export const PricingCard = ({ showFeaturesPrefix = false, featuresPrefixText = 'Everything in Pro plus:', isRecommended = false, + isCurrentPlan = false, }: PricingCardProps) => { const { t } = useTranslation(); @@ -75,7 +77,9 @@ export const PricingCard = ({ {description}

Date: Thu, 3 Apr 2025 11:52:44 -0700 Subject: [PATCH 6/6] Updated with free card and button logic --- .../Modals/Subscription/PricingCard.tsx | 12 +-- .../Modals/Subscription/PricingPage.tsx | 93 +++++++++++-------- apps/studio/src/lib/user/subscription.ts | 6 +- apps/studio/src/locales/en/translation.json | 14 ++- apps/studio/src/locales/zh/translation.json | 15 ++- packages/models/src/supabase/db.ts | 2 +- packages/models/src/usage/index.ts | 2 +- 7 files changed, 91 insertions(+), 53 deletions(-) diff --git a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx index 9e321de67..99fb46f49 100644 --- a/apps/studio/src/components/Modals/Subscription/PricingCard.tsx +++ b/apps/studio/src/components/Modals/Subscription/PricingCard.tsx @@ -61,10 +61,10 @@ export const PricingCard = ({
)} -
+

{plan}

{price.split('/')[0]} @@ -73,11 +73,11 @@ export const PricingCard = ({

-

+

{description}