Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
81 changes: 67 additions & 14 deletions apps/mail/app/(routes)/settings/security/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { m } from '@/paraglide/messages';
import { useForm } from 'react-hook-form';

import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { toast } from 'sonner';
import { useEffect } from 'react';
import * as z from 'zod';

const formSchema = z.object({
Expand All @@ -22,7 +24,19 @@ const formSchema = z.object({
});

export default function SecurityPage() {
const [isSaving, setIsSaving] = useState(false);
const trpc = useTRPC();
const queryClient = useQueryClient();

// Fetch current user settings
const { data: settingsData, isLoading } = useQuery({
...trpc.settings.get.queryOptions(),
select: (data: any) => data?.settings,
});

// Save settings mutation
const { mutateAsync: saveUserSettings, isPending: isSaving } = useMutation(
trpc.settings.save.mutationOptions(),
);

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
Expand All @@ -32,14 +46,45 @@ export default function SecurityPage() {
},
});

function onSubmit(values: z.infer<typeof formSchema>) {
setIsSaving(true);
// Update form when settings are loaded
useEffect(() => {
if (settingsData) {
form.reset({
twoFactorAuth: settingsData.twoFactorAuth ?? false,
loginNotifications: settingsData.loginNotifications ?? true,
});
}
}, [settingsData, form]);

async function onSubmit(values: z.infer<typeof formSchema>) {
const saved = settingsData? { ...settingsData } : undefined;

try {
// Optimistically update the UI
queryClient.setQueryData(trpc.settings.get.queryKey(), (updater: any) => {
if (!updater) return;
return { settings: { ...updater.settings, ...values } };
});

// TODO: Save settings in user's account
setTimeout(() => {
console.log(values);
setIsSaving(false);
}, 1000);
await saveUserSettings({
twoFactorAuth: values.twoFactorAuth,
loginNotifications: values.loginNotifications,
});

toast.success(m['common.settings.saved']());
} catch (error) {
console.error('Failed to save security settings:', error);
toast.error(m['common.settings.failedToSave']());

// Revert optimistic update on error
queryClient.setQueryData(trpc.settings.get.queryKey(), (prev: any) => {
if (!prev || !saved) return;
return {
...prev,
settings: { ...(prev?.settings ?? {}), ...saved },
};
});
}
}

return (
Expand All @@ -50,7 +95,7 @@ export default function SecurityPage() {
footer={
<div className="flex gap-4">
<Button variant="destructive">{m['pages.settings.security.deleteAccount']()}</Button>
<Button type="submit" form="security-form" disabled={isSaving}>
<Button type="submit" form="security-form" disabled={isSaving || isLoading}>
{isSaving ? m['common.actions.saving']() : m['common.actions.saveChanges']()}
</Button>
</div>
Expand All @@ -73,7 +118,11 @@ export default function SecurityPage() {
</FormDescription>
</div>
<FormControl className="ml-4">
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isLoading}
/>
</FormControl>
</FormItem>
)}
Expand All @@ -92,7 +141,11 @@ export default function SecurityPage() {
</FormDescription>
</div>
<FormControl className="ml-4">
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isLoading}
/>
</FormControl>
</FormItem>
)}
Expand All @@ -103,4 +156,4 @@ export default function SecurityPage() {
</SettingsCard>
</div>
);
}
}
7 changes: 6 additions & 1 deletion apps/server/src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export const userSettingsSchema = z.object({
imageCompression: z.enum(['low', 'medium', 'original']).default('medium'),
autoRead: z.boolean().default(true),
animations: z.boolean().default(false),
// Security settings
twoFactorAuth: z.boolean().default(false),
loginNotifications: z.boolean().default(true),
});

export type UserSettings = z.infer<typeof userSettingsSchema>;
Expand All @@ -133,4 +136,6 @@ export const defaultUserSettings: UserSettings = {
undoSendEnabled: false,
imageCompression: 'medium',
animations: false,
};
twoFactorAuth: false,
loginNotifications: true,
};
Loading