-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: add subscription management to settings panel #2868
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
Changes from 11 commits
a73b440
5f9a038
be0b674
f0b642d
19f437a
e69617c
303af09
231d873
df4daab
2bf01f1
291233a
3de3840
ec39c3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align dev login error handling with OAuth; avoid rethrow and hardcoded text
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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(() => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainConfirm 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 || trueLength 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
doneLength 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):
🤖 Prompt for AI Agents |
||
| 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> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
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).