Skip to content

Tys paywall #1721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/studio/electron/main/events/payments.ts
Original file line number Diff line number Diff line change
@@ -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);
},
);

Expand Down
10 changes: 7 additions & 3 deletions apps/studio/electron/main/payment/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
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) {
return { success: false, error: (error as Error).message };
}
};

const createCheckoutSession = async () => {
const createCheckoutSession = async (plan: UsagePlanType = UsagePlanType.PRO) => {
const authTokens: AuthTokens = await getRefreshedAuthTokens();
if (!authTokens) {
throw new Error('No auth tokens found');
Expand All @@ -30,6 +33,7 @@ const createCheckoutSession = async () => {
'Content-Type': 'application/json',
Authorization: `Bearer ${authTokens.accessToken}`,
},
body: JSON.stringify({ plan }),
},
);

Expand Down
135 changes: 90 additions & 45 deletions apps/studio/src/components/Modals/Subscription/PricingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement>;
delay: number;
isLoading?: boolean;
className?: string;
showFeaturesPrefix?: boolean;
featuresPrefixText?: string;
isRecommended?: boolean;
isCurrentPlan?: boolean;
}

export const PricingCard = ({
plan,
price,
Expand All @@ -13,59 +29,88 @@ export const PricingCard = ({
buttonProps,
delay,
isLoading,
}: {
plan: string;
price: string;
description: string;
features: string[];
buttonText: string;
buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
delay: number;
isLoading?: boolean;
}) => {
className,
showFeaturesPrefix = false,
featuresPrefixText = 'Everything in Pro plus:',
isRecommended = false,
isCurrentPlan = false,
}: PricingCardProps) => {
const { t } = useTranslation();

return (
<MotionCard
className="w-[360px]"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
>
<motion.div className="p-6 flex flex-col h-full">
<div className="space-y-1">
<h2 className="text-title2">{plan}</h2>
<p className="text-foreground-onlook text-largePlus">{price}</p>
</div>
<div className="border-[0.5px] border-border-primary -mx-6 my-6" />
<p className="text-foreground-primary text-title3 text-balance">{description}</p>
<div className="border-[0.5px] border-border-primary -mx-6 my-6" />
<div className="space-y-4 mb-6">
{features.map((feature, i) => (
<div className="relative h-full">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
className="h-full"
>
{isRecommended && (
<div className="absolute -top-8 left-5 right-5">
<div
key={feature}
className="flex items-center gap-3 text-sm text-foreground-secondary/80"
className="w-full py-1.5 text-center text-sm font-medium rounded-t-lg relative"
style={{
backdropFilter: 'blur(12px)',
backgroundColor: 'hsl(var(--background) /0.6)',
boxShadow: '0px 0px 0px 0.5px hsl(var(--foreground) /0.2)',
color: 'var(--card-foreground)',
}}
>
<Icons.Check className="w-5 h-5 text-foreground-secondary/80" />
<span>{feature}</span>
Recommended
</div>
))}
</div>
<Button
className="mt-auto w-full"
{...buttonProps}
disabled={isLoading || buttonProps.disabled}
</div>
)}
<MotionCard
className={`max-w-[420px] h-[680px] flex-shrink-0 flex ${className || ''}`}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Icons.Shadow className="w-4 h-4 animate-spin" />
<span>{t('pricing.loading.checkingPayment')}</span>
<motion.div className="p-5 pb-8 flex flex-col w-full h-full">
<div className="flex-shrink-0">
<h2 className="text-[18px] font-medium">{plan}</h2>
<p className="text-[40px] font-medium flex items-baseline">
<span>{price.split('/')[0]}</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using price.split('/') to extract the amount and hardcoding /month could cause issues if the price string format changes. Consider parameterizing the billing period.

<span className="text-[18px] font-normal ml-1 text-muted-foreground">
/month
</span>
</p>
</div>
<p className="text-title3 font-normal text-balance text-muted-foreground mt-2 flex-shrink-0">
{description}
</p>
<Button
className={`w-full text-base font-medium mt-6 mb-2 h-12 flex-shrink-0 ${
isCurrentPlan ? 'bg-white/75' : ''
}`}
size="default"
{...buttonProps}
disabled={isLoading || buttonProps.disabled}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Icons.Shadow className="w-4 h-4 animate-spin" />
<span>{t('pricing.loading.checkingPayment')}</span>
</div>
) : (
buttonText
)}
</Button>
<div className="h-[0.5px] bg-white/20 -mx-5 my-5 flex-shrink-0" />
<div className="space-y-3 mt-1 flex-grow overflow-y-auto">
{showFeaturesPrefix && (
<p className="text-base font-medium mb-2">{featuresPrefixText}</p>
)}
{features.map((feature, i) => (
<div
key={feature}
className="flex items-start gap-2 text-base text-foreground-secondary/80"
>
<Icons.Check className="w-4 h-4 text-foreground-secondary/80 flex-shrink-0 mt-0.5" />
<span className="text-balance">{feature}</span>
</div>
))}
</div>
) : (
buttonText
)}
</Button>
</motion.div>
</MotionCard>
</motion.div>
</MotionCard>
</div>
);
};
Loading