Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion apps/web/client/src/app/_components/top-bar/github.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Icons } from '@onlook/ui/icons';
import { useEffect, useState } from 'react';

const DEFAULT_STAR_COUNT = 20550;
const DEFAULT_STAR_COUNT = 22000;
const DEFAULT_CONTRIBUTORS_COUNT = 90;

const formatStarCount = (count: number): string => {
Expand Down
43 changes: 28 additions & 15 deletions apps/web/client/src/app/auth/auth-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { LocalForageKeys } from '@/utils/constants';
import { SignInMethod } from '@onlook/models/auth';
import { toast } from '@onlook/ui/sonner';
import localforage from 'localforage';
import type { ReactNode } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';
Expand Down Expand Up @@ -34,26 +35,38 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}, []);

const handleLogin = async (method: SignInMethod.GITHUB | SignInMethod.GOOGLE, returnUrl: string | null) => {
setSigningInMethod(method);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await localforage.setItem(LAST_SIGN_IN_METHOD_KEY, method);
await login(method);
setTimeout(() => {
try {
setSigningInMethod(method);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await localforage.setItem(LAST_SIGN_IN_METHOD_KEY, method);
await login(method);
} catch (error) {
toast.error('Error signing in with password');
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using a generic error message instead of 'Error signing in with password' since this flow may not always involve a password (e.g., OAuth).

Suggested change
toast.error('Error signing in with password');
toast.error('Error signing in');

console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
} finally {
setSigningInMethod(null);
}, 5000);
}
};

const handleDevLogin = async (returnUrl: string | null) => {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
setTimeout(() => {
try {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
} catch (error) {
toast.error('Error signing in with password', {
description: error instanceof Error ? error.message : 'Please try again.',
});
console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
} finally {
setSigningInMethod(null);
}, 5000);
}
}
Comment on lines 54 to 70
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Align dev login error handling with OAuth; avoid rethrow and hardcoded text

  • Don’t rethrow after toasting; return instead.
  • Use next-intl; keep the descriptive message.
  • Unify console tags.

Apply within this block:

 } catch (error) {
-    toast.error('Error signing in with password', {
-        description: error instanceof Error ? error.message : 'Please try again.',
-    });
-    console.error('Error signing in with password:', error);
-    throw new Error('Error signing in with password');
+    toast.error(t('auth.signIn.passwordErrorTitle'), {
+        description: error instanceof Error ? error.message : t('common.tryAgain'),
+    });
+    console.error('auth.signIn.devError', error);
+    return;
 } finally {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDevLogin = async (returnUrl: string | null) => {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
setTimeout(() => {
try {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
} catch (error) {
toast.error('Error signing in with password', {
description: error instanceof Error ? error.message : 'Please try again.',
});
console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
} finally {
setSigningInMethod(null);
}, 5000);
}
}
const handleDevLogin = async (returnUrl: string | null) => {
try {
setSigningInMethod(SignInMethod.DEV);
if (returnUrl) {
await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
}
await devLogin();
} catch (error) {
toast.error(t('auth.signIn.passwordErrorTitle'), {
description: error instanceof Error ? error.message : t('common.tryAgain'),
});
console.error('auth.signIn.devError', error);
return;
} finally {
setSigningInMethod(null);
}
}
🤖 Prompt for AI Agents
In apps/web/client/src/app/auth/auth-context.tsx around lines 54 to 70, the dev
login catch block currently rethrows, uses a hardcoded English error title, and
prints a console message inconsistent with OAuth handlers; update it to mirror
the OAuth error handling by (1) replacing the hardcoded toast title with
next-intl usage (use intl.formatMessage for the title) while keeping the
descriptive message in the description, (2) remove the throw and simply return
after showing the toast, and (3) standardize the console error tag to match the
OAuth handlers (e.g., use the same prefix/string used elsewhere); preserve the
finally block that clears signingInMethod.


return (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/client/src/app/login/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function devLogin() {

if (error) {
console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
throw new Error(error.message);
}
redirect(Routes.AUTH_REDIRECT);
}
5 changes: 4 additions & 1 deletion apps/web/client/src/components/ui/avatar-dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getInitials } from '@onlook/utility';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { UsageSection } from './plans';
import { SettingsTabValue } from '../settings-modal/helpers';

export const CurrentUserAvatar = ({ className }: { className?: string }) => {
const stateManager = useStateManager();
Expand All @@ -40,11 +41,13 @@ export const CurrentUserAvatar = ({ className }: { className?: string }) => {
};

const handleOpenSubscription = () => {
stateManager.isSubscriptionModalOpen = true;
stateManager.settingsTab = SettingsTabValue.SUBSCRIPTION;
stateManager.isSettingsModalOpen = true;
setOpen(false);
};

const handleOpenSettings = () => {
stateManager.settingsTab = SettingsTabValue.PREFERENCES;
stateManager.isSettingsModalOpen = true;
setOpen(false);
};
Expand Down
3 changes: 2 additions & 1 deletion apps/web/client/src/components/ui/settings-modal/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export enum SettingsTabValue {
DOMAIN = 'domain',
PROJECT = 'project',
PREFERENCES = 'preferences',
PREFERENCES = 'account',
SUBSCRIPTION = 'subscription',
VERSIONS = 'versions',
ADVANCED = 'advanced',
SITE = 'site',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import { observer } from 'mobx-react-lite';
import { SettingsTabValue, type SettingTab } from './helpers';
import { PreferencesTab } from './preferences-tab';
import { SubscriptionTab } from './subscription-tab';

export const NonProjectSettingsModal = observer(() => {
const stateManager = useStateManager();
Expand All @@ -18,6 +19,11 @@ export const NonProjectSettingsModal = observer(() => {
icon: <Icons.Person className="mr-2 h-4 w-4" />,
component: <PreferencesTab />,
},
{
label: SettingsTabValue.SUBSCRIPTION,
icon: <Icons.CreditCard className="mr-2 h-4 w-4" />,
component: <SubscriptionTab />,
},
]

return (
Expand Down
116 changes: 2 additions & 114 deletions apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,122 +1,10 @@
import { api } from '@/trpc/react';
import { Button } from '@onlook/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@onlook/ui/dropdown-menu';
import { Icons } from '@onlook/ui/icons';
import { observer } from 'mobx-react-lite';
import { UserDeleteSection } from './user-delete-section';

export const PreferencesTab = observer(() => {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Confirm client boundary exists for this tab

tRPC React hooks (and MobX observer if retained) require a client component boundary above. Verify the parent settings modal or entry file has 'use client'.

Run:


🏁 Script executed:

#!/bin/bash
# Verify a client boundary exists in settings modal tree
rg -n --type=ts,tsx -C1 $'^[ \t]*[\'"]use client[\'"]' apps/web/client/src/components/ui/settings-modal
# Also check modal entry points mentioned in the PR
rg -n --type=ts,tsx -C1 $'^[ \t]*[\'"]use client[\'"]' apps/web/client/src/components/ui/settings-modal/{non-project.tsx,with-project.tsx} 2>/dev/null || true

Length of output: 30


🏁 Script executed:

#!/bin/bash
# Verify client boundary exists in settings modal tree (search for 'use client' in ts/tsx)
dir="apps/web/client/src/components/ui/settings-modal"
echo "Checking directory: $dir"
if [ ! -d "$dir" ]; then
  echo "DIR_NOT_FOUND"
  exit 0
fi
echo "Listing files in $dir"
ls -la "$dir"
echo "Searching for 'use client' in .ts/.tsx under $dir"
grep -nR --include='*.tsx' --include='*.ts' -E "^\s*['\"]use client['\"]" "$dir" -n -H -C1 || true
echo "Also checking specific entry files (non-project.tsx, with-project.tsx)"
for f in non-project.tsx with-project.tsx; do
  p="$dir/$f"
  if [ -f "$p" ]; then
    echo "---- $p (first 120 lines) ----"
    sed -n '1,120p' "$p"
    echo "grep result:"
    grep -nE "^\s*['\"]use client['\"]" "$p" || true
  else
    echo "MISSING: $p"
  fi
done

Length of output: 11899


Add 'use client' to the settings modal entry components

PreferencesTab is a MobX observer (and may use tRPC hooks); the modal entry files lack a client boundary—add a top-line "use client" to one of these entry components (or otherwise ensure a higher client boundary):

  • apps/web/client/src/components/ui/settings-modal/non-project.tsx
  • apps/web/client/src/components/ui/settings-modal/with-project.tsx
🤖 Prompt for AI Agents
In apps/web/client/src/components/ui/settings-modal/preferences-tab.tsx around
line 4, the PreferencesTab is a MobX observer and may use client-only hooks but
the modal entry components lack a client boundary; add a top-line "use client"
directive to one of the entry components (preferably both) —
apps/web/client/src/components/ui/settings-modal/non-project.tsx and
apps/web/client/src/components/ui/settings-modal/with-project.tsx — or ensure a
higher-level parent is already a client component, so the observer and any
tRPC/hooks run in a client boundary.

const { data: settings } = api.user.settings.get.useQuery();
const { mutate: updateSettings } = api.user.settings.upsert.useMutation();
const shouldWarnDelete = settings?.editor?.shouldWarnDelete ?? true;

async function updateDeleteWarning(enabled: boolean) {
await updateSettings({ shouldWarnDelete: enabled });
}

return (
<div className="flex flex-col gap-8 p-6">
{/* <div className="flex justify-between items-center">
<div className="flex flex-col gap-2">
<p className="text-largePlus">Language</p>
<p className="text-foreground-onlook text-small">
Choose your preferred language
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="text-smallPlus min-w-[150px]">
{LANGUAGE_DISPLAY_NAMES[
locale as keyof typeof LANGUAGE_DISPLAY_NAMES
] ?? 'English'}
<Icons.ChevronDown className="ml-auto" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[150px]">
{Object.entries(LANGUAGE_DISPLAY_NAMES).map(([code, name]) => (
<DropdownMenuItem
key={code}
onClick={() => userManager.language.update(code as Language)}
>
<span>{name}</span>
{locale === code && <Icons.CheckCircled className="ml-auto" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex justify-between items-center">
<div className="flex flex-col gap-2">
<p className="text-largePlus">Theme</p>
<p className="text-foreground-onlook text-small">
Choose your preferred appearance
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="text-smallPlus min-w-[150px]">
{theme === SystemTheme.DARK && <Icons.Moon className="mr-2 h-4 w-4" />}
{theme === SystemTheme.LIGHT && <Icons.Sun className="mr-2 h-4 w-4" />}
{theme === SystemTheme.SYSTEM && (
<Icons.Laptop className="mr-2 h-4 w-4" />
)}
<span className="capitalize">{theme}</span>
<Icons.ChevronDown className="ml-auto" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[150px]">
<DropdownMenuItem onClick={() => setTheme(SystemTheme.LIGHT)}>
<Icons.Sun className="mr-2 h-4 w-4" />
<span>Light</span>
{theme === SystemTheme.LIGHT && (
<Icons.CheckCircled className="ml-auto" />
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme(SystemTheme.DARK)}>
<Icons.Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
{theme === SystemTheme.DARK && (
<Icons.CheckCircled className="ml-auto" />
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme(SystemTheme.SYSTEM)}>
<Icons.Laptop className="mr-2 h-4 w-4" />
<span>System</span>
{theme === SystemTheme.SYSTEM && (
<Icons.CheckCircled className="ml-auto" />
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> */}
<div className=" flex justify-between items-center gap-4">
<div className=" flex flex-col gap-2">
<p className="text-largePlus">{'Warn before delete'}</p>
<p className="text-foreground-onlook text-small">
{'This adds a warning before deleting elements in the editor'}
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="text-smallPlus min-w-[150px]">
{shouldWarnDelete ? 'On' : 'Off'}
<Icons.ChevronDown className="ml-auto" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="text-smallPlus min-w-[150px]">
<DropdownMenuItem onClick={() => updateDeleteWarning(true)}>
{'Warning On'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => updateDeleteWarning(false)}>
{'Warning Off'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<UserDeleteSection />
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { Button } from '@onlook/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@onlook/ui/dialog';

interface SubscriptionCancelModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirmCancel: () => void;
}

export const SubscriptionCancelModal = ({ open, onOpenChange, onConfirmCancel }: SubscriptionCancelModalProps) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Cancel Subscription</DialogTitle>
<DialogDescription className="pt-2">
Are you sure you want to cancel your subscription? You'll lose access to all premium features at the end of your current billing period.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex-col sm:flex-row gap-3 sm:gap-2">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="order-2 sm:order-1"
>
Keep Subscription
</Button>
<Button
variant="outline"
onClick={onConfirmCancel}
className="order-1 sm:order-2 text-red-200 hover:text-red-100 hover:bg-red-500/10 border-red-200 hover:border-red-100"
>
Cancel Subscription
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Loading