From 47c0571aa59f65e35ddf23d7dfa1490b54f9178b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 23 Aug 2025 21:13:31 +0000 Subject: [PATCH 1/3] Migrate ad preference management from local storage to Convex Co-authored-by: tannerlinsley --- convex/schema.ts | 1 + convex/users.ts | 68 +++++++++++++++++++++++++++++++ src/hooks/useAdPreference.ts | 57 ++++++++++++++++++++++++++ src/routes/_libraries/account.tsx | 21 ++++++---- src/stores/userSettings.ts | 63 +++++++++------------------- 5 files changed, 160 insertions(+), 50 deletions(-) create mode 100644 src/hooks/useAdPreference.ts diff --git a/convex/schema.ts b/convex/schema.ts index 4af181b3b..a885fdcb7 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -31,6 +31,7 @@ const schema = defineSchema({ capabilities: v.array( v.union(...validCapabilities.map((cap) => v.literal(cap))) ), + adsDisabled: v.optional(v.boolean()), }), }) diff --git a/convex/users.ts b/convex/users.ts index eae24fff4..00f78cdc7 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -80,3 +80,71 @@ async function requireCapability(ctx: QueryCtx, capability: Capability) { return { currentUser } } + +// Get current user's ad preference +export const getUserAdPreference = query({ + args: {}, + handler: async (ctx) => { + const currentUser = await getCurrentUserConvex(ctx) + if (!currentUser) { + throw new Error('Not authenticated') + } + + return { + adsDisabled: currentUser.adsDisabled ?? false, + canDisableAds: currentUser.capabilities.includes('disableAds'), + } + }, +}) + +// Toggle ad preference (only for users with disableAds capability) +export const toggleAdPreference = mutation({ + args: {}, + handler: async (ctx) => { + const currentUser = await getCurrentUserConvex(ctx) + if (!currentUser) { + throw new Error('Not authenticated') + } + + // Check if user has capability to disable ads + if (!currentUser.capabilities.includes('disableAds')) { + throw new Error('User does not have permission to disable ads') + } + + const currentAdsDisabled = currentUser.adsDisabled ?? false + + await ctx.db.patch(currentUser._id, { + adsDisabled: !currentAdsDisabled, + }) + + return { + adsDisabled: !currentAdsDisabled, + } + }, +}) + +// Set ad preference (only for users with disableAds capability) +export const setAdPreference = mutation({ + args: { + adsDisabled: v.boolean(), + }, + handler: async (ctx, args) => { + const currentUser = await getCurrentUserConvex(ctx) + if (!currentUser) { + throw new Error('Not authenticated') + } + + // Check if user has capability to disable ads + if (!currentUser.capabilities.includes('disableAds')) { + throw new Error('User does not have permission to disable ads') + } + + await ctx.db.patch(currentUser._id, { + adsDisabled: args.adsDisabled, + }) + + return { + adsDisabled: args.adsDisabled, + } + }, +}) diff --git a/src/hooks/useAdPreference.ts b/src/hooks/useAdPreference.ts new file mode 100644 index 000000000..1e040c66f --- /dev/null +++ b/src/hooks/useAdPreference.ts @@ -0,0 +1,57 @@ +import { convexQuery, useConvexMutation } from '@convex-dev/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { api } from 'convex/_generated/api' + +export function useAdPreferenceQuery() { + return useQuery(convexQuery(api.users.getUserAdPreference, {})) +} + +export function useToggleAdPreference() { + const queryClient = useQueryClient() + + return useConvexMutation(api.users.toggleAdPreference, { + onSuccess: () => { + // Invalidate and refetch ad preference query + queryClient.invalidateQueries({ + queryKey: ['convex', api.users.getUserAdPreference, {}], + }) + // Also invalidate current user query since it might contain ad preferences + queryClient.invalidateQueries({ + queryKey: ['convex', api.auth.getCurrentUser, {}], + }) + }, + }) +} + +export function useSetAdPreference() { + const queryClient = useQueryClient() + + return useConvexMutation(api.users.setAdPreference, { + onSuccess: () => { + // Invalidate and refetch ad preference query + queryClient.invalidateQueries({ + queryKey: ['convex', api.users.getUserAdPreference, {}], + }) + // Also invalidate current user query + queryClient.invalidateQueries({ + queryKey: ['convex', api.auth.getCurrentUser, {}], + }) + }, + }) +} + +// Legacy hook for backward compatibility - replace the useAdsPreference function +export function useAdsPreference() { + const adPreferenceQuery = useAdPreferenceQuery() + + if (adPreferenceQuery.isLoading || !adPreferenceQuery.data) { + return { adsEnabled: true } // Default to showing ads while loading + } + + const { adsDisabled, canDisableAds } = adPreferenceQuery.data + + // Ads are enabled if user can't disable them OR if they haven't disabled them + const adsEnabled = !canDisableAds || !adsDisabled + + return { adsEnabled } +} \ No newline at end of file diff --git a/src/routes/_libraries/account.tsx b/src/routes/_libraries/account.tsx index 1dbccd718..0e342da6a 100644 --- a/src/routes/_libraries/account.tsx +++ b/src/routes/_libraries/account.tsx @@ -1,4 +1,4 @@ -import { useUserSettingsStore } from '~/stores/userSettings' +import { useAdPreferenceQuery, useToggleAdPreference } from '~/hooks/useAdPreference' import { FaSignOutAlt } from 'react-icons/fa' import { Authenticated, Unauthenticated } from 'convex/react' import { Link, redirect } from '@tanstack/react-router' @@ -11,10 +11,17 @@ export const Route = createFileRoute({ function UserSettings() { const userQuery = useCurrentUserQuery() - const adsDisabled = useUserSettingsStore((s) => s.settings.adsDisabled) - const toggleAds = useUserSettingsStore((s) => s.toggleAds) - - const canDisableAds = userQuery.data?.capabilities.includes('disableAds') + // Replace local storage-based state with Convex-based queries + const adPreferenceQuery = useAdPreferenceQuery() + const toggleAdPreferenceMutation = useToggleAdPreference() + + // Get values from the new queries + const adsDisabled = adPreferenceQuery.data?.adsDisabled ?? false + const canDisableAds = adPreferenceQuery.data?.canDisableAds ?? false + + const handleToggleAds = () => { + toggleAdPreferenceMutation.mutate() + } const signOut = async () => { await authClient.signOut() @@ -53,8 +60,8 @@ function UserSettings() { type="checkbox" className="h-4 w-4 accent-blue-600 my-1" checked={adsDisabled} - onChange={toggleAds} - disabled={userQuery.isLoading} + onChange={handleToggleAds} + disabled={adPreferenceQuery.isLoading || toggleAdPreferenceMutation.isPending} aria-label="Disable Ads" />
diff --git a/src/stores/userSettings.ts b/src/stores/userSettings.ts index d2238a997..ae5533a85 100644 --- a/src/stores/userSettings.ts +++ b/src/stores/userSettings.ts @@ -1,55 +1,32 @@ import { create } from 'zustand' -import { persist, createJSONStorage } from 'zustand/middleware' -import { useCurrentUserQuery } from '~/hooks/useCurrentUser' +// Remove persist and createJSONStorage imports since we no longer use localStorage +// import { persist, createJSONStorage } from 'zustand/middleware' +// Remove the useCurrentUserQuery import since we'll use the new hook +// import { useCurrentUserQuery } from '~/hooks/useCurrentUser' + +// Update the export to use the new hook +export { useAdsPreference } from '~/hooks/useAdPreference' export type UserSettings = { - adsDisabled: boolean + // Remove adsDisabled since it's now handled by Convex + // Other settings can be added here in the future } type UserSettingsState = { settings: UserSettings hasHydrated: boolean setHasHydrated: (value: boolean) => void - toggleAds: () => void + // Remove toggleAds since it's now handled by the new hooks } -export const useUserSettingsStore = create()( - persist( - (set, get) => ({ - settings: { - adsDisabled: false, - }, - hasHydrated: false, - setHasHydrated: (value) => set({ hasHydrated: value }), - toggleAds: () => - set({ - settings: { - ...get().settings, - adsDisabled: !get().settings.adsDisabled, - }, - }), - }), - { - name: 'user_settings_v1', - storage: createJSONStorage(() => localStorage), - onRehydrateStorage: () => (state, error) => { - if (!state || error) return - state.setHasHydrated(true) - }, - partialize: (state) => ({ settings: state.settings }), - } - ) -) - -export function useAdsPreference() { - const userQuery = useCurrentUserQuery() - const { settings } = useUserSettingsStore((s) => ({ - settings: s.settings, - })) +export const useUserSettingsStore = create()((set, get) => ({ + settings: { + // Remove adsDisabled initialization + }, + hasHydrated: true, // No need for hydration since we're not using persistence + setHasHydrated: (value) => set({ hasHydrated: value }), + // Remove toggleAds function +})) - const adsEnabled = userQuery.data - ? userQuery.data.capabilities.includes('disableAds') && - !settings.adsDisabled - : true - return { adsEnabled } -} +// Remove the persist wrapper and localStorage configuration +// The useAdsPreference function is now exported from useAdPreference.ts From ccc67595ab667b28f4d53157c66d9efa38c82e31 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 23 Aug 2025 22:14:00 +0000 Subject: [PATCH 2/3] Remove ad preference query and use current user data directly Co-authored-by: tannerlinsley --- convex/users.ts | 16 --------------- src/hooks/useAdPreference.ts | 33 +++++++++++-------------------- src/routes/_libraries/account.tsx | 13 ++++++------ 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/convex/users.ts b/convex/users.ts index 00f78cdc7..27d74141e 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -81,22 +81,6 @@ async function requireCapability(ctx: QueryCtx, capability: Capability) { return { currentUser } } -// Get current user's ad preference -export const getUserAdPreference = query({ - args: {}, - handler: async (ctx) => { - const currentUser = await getCurrentUserConvex(ctx) - if (!currentUser) { - throw new Error('Not authenticated') - } - - return { - adsDisabled: currentUser.adsDisabled ?? false, - canDisableAds: currentUser.capabilities.includes('disableAds'), - } - }, -}) - // Toggle ad preference (only for users with disableAds capability) export const toggleAdPreference = mutation({ args: {}, diff --git a/src/hooks/useAdPreference.ts b/src/hooks/useAdPreference.ts index 1e040c66f..f0673e015 100644 --- a/src/hooks/useAdPreference.ts +++ b/src/hooks/useAdPreference.ts @@ -1,21 +1,14 @@ -import { convexQuery, useConvexMutation } from '@convex-dev/react-query' -import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useConvexMutation } from '@convex-dev/react-query' +import { useQueryClient } from '@tanstack/react-query' import { api } from 'convex/_generated/api' - -export function useAdPreferenceQuery() { - return useQuery(convexQuery(api.users.getUserAdPreference, {})) -} +import { useCurrentUserQuery } from './useCurrentUser' export function useToggleAdPreference() { const queryClient = useQueryClient() return useConvexMutation(api.users.toggleAdPreference, { onSuccess: () => { - // Invalidate and refetch ad preference query - queryClient.invalidateQueries({ - queryKey: ['convex', api.users.getUserAdPreference, {}], - }) - // Also invalidate current user query since it might contain ad preferences + // Invalidate current user query to refresh the ad preference queryClient.invalidateQueries({ queryKey: ['convex', api.auth.getCurrentUser, {}], }) @@ -28,11 +21,7 @@ export function useSetAdPreference() { return useConvexMutation(api.users.setAdPreference, { onSuccess: () => { - // Invalidate and refetch ad preference query - queryClient.invalidateQueries({ - queryKey: ['convex', api.users.getUserAdPreference, {}], - }) - // Also invalidate current user query + // Invalidate current user query to refresh the ad preference queryClient.invalidateQueries({ queryKey: ['convex', api.auth.getCurrentUser, {}], }) @@ -40,15 +29,17 @@ export function useSetAdPreference() { }) } -// Legacy hook for backward compatibility - replace the useAdsPreference function +// Legacy hook for backward compatibility - now uses current user query export function useAdsPreference() { - const adPreferenceQuery = useAdPreferenceQuery() + const userQuery = useCurrentUserQuery() - if (adPreferenceQuery.isLoading || !adPreferenceQuery.data) { - return { adsEnabled: true } // Default to showing ads while loading + if (userQuery.isLoading || !userQuery.data) { + return { adsEnabled: true } // Default to showing ads while loading or not authenticated } - const { adsDisabled, canDisableAds } = adPreferenceQuery.data + const user = userQuery.data + const adsDisabled = user.adsDisabled ?? false + const canDisableAds = user.capabilities.includes('disableAds') // Ads are enabled if user can't disable them OR if they haven't disabled them const adsEnabled = !canDisableAds || !adsDisabled diff --git a/src/routes/_libraries/account.tsx b/src/routes/_libraries/account.tsx index 0e342da6a..e51bca374 100644 --- a/src/routes/_libraries/account.tsx +++ b/src/routes/_libraries/account.tsx @@ -1,4 +1,4 @@ -import { useAdPreferenceQuery, useToggleAdPreference } from '~/hooks/useAdPreference' +import { useToggleAdPreference } from '~/hooks/useAdPreference' import { FaSignOutAlt } from 'react-icons/fa' import { Authenticated, Unauthenticated } from 'convex/react' import { Link, redirect } from '@tanstack/react-router' @@ -11,13 +11,12 @@ export const Route = createFileRoute({ function UserSettings() { const userQuery = useCurrentUserQuery() - // Replace local storage-based state with Convex-based queries - const adPreferenceQuery = useAdPreferenceQuery() + // Use current user query directly instead of separate ad preference query const toggleAdPreferenceMutation = useToggleAdPreference() - // Get values from the new queries - const adsDisabled = adPreferenceQuery.data?.adsDisabled ?? false - const canDisableAds = adPreferenceQuery.data?.canDisableAds ?? false + // Get values directly from the current user data + const adsDisabled = userQuery.data?.adsDisabled ?? false + const canDisableAds = userQuery.data?.capabilities.includes('disableAds') ?? false const handleToggleAds = () => { toggleAdPreferenceMutation.mutate() @@ -61,7 +60,7 @@ function UserSettings() { className="h-4 w-4 accent-blue-600 my-1" checked={adsDisabled} onChange={handleToggleAds} - disabled={adPreferenceQuery.isLoading || toggleAdPreferenceMutation.isPending} + disabled={userQuery.isLoading || toggleAdPreferenceMutation.isPending} aria-label="Disable Ads" />
From e09b019620a2e22f588a91014e478e3da2622ec2 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 25 Aug 2025 10:01:47 -0600 Subject: [PATCH 3/3] Store disableAds in convex --- convex/users.ts | 47 +++++-------------------------- src/hooks/useAdPreference.ts | 39 ++++--------------------- src/routes/_libraries/account.tsx | 32 +++++++++++++++------ 3 files changed, 35 insertions(+), 83 deletions(-) diff --git a/convex/users.ts b/convex/users.ts index 27d74141e..51f46173b 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -2,6 +2,7 @@ import { v } from 'convex/values' import { mutation, query, QueryCtx } from './_generated/server' import { Capability, CapabilitySchema } from './schema' import { getCurrentUserConvex } from './auth' +import { Id } from './_generated/dataModel' export const updateUserCapabilities = mutation({ args: { @@ -82,53 +83,19 @@ async function requireCapability(ctx: QueryCtx, capability: Capability) { } // Toggle ad preference (only for users with disableAds capability) -export const toggleAdPreference = mutation({ - args: {}, - handler: async (ctx) => { - const currentUser = await getCurrentUserConvex(ctx) - if (!currentUser) { - throw new Error('Not authenticated') - } - - // Check if user has capability to disable ads - if (!currentUser.capabilities.includes('disableAds')) { - throw new Error('User does not have permission to disable ads') - } - - const currentAdsDisabled = currentUser.adsDisabled ?? false - - await ctx.db.patch(currentUser._id, { - adsDisabled: !currentAdsDisabled, - }) - - return { - adsDisabled: !currentAdsDisabled, - } - }, -}) - -// Set ad preference (only for users with disableAds capability) -export const setAdPreference = mutation({ +export const updateAdPreference = mutation({ args: { adsDisabled: v.boolean(), }, handler: async (ctx, args) => { - const currentUser = await getCurrentUserConvex(ctx) - if (!currentUser) { - throw new Error('Not authenticated') - } - - // Check if user has capability to disable ads - if (!currentUser.capabilities.includes('disableAds')) { - throw new Error('User does not have permission to disable ads') - } + // Validate admin capability + const { currentUser } = await requireCapability(ctx, 'disableAds') - await ctx.db.patch(currentUser._id, { + // Update target user's capabilities + await ctx.db.patch(currentUser.userId as Id<'users'>, { adsDisabled: args.adsDisabled, }) - return { - adsDisabled: args.adsDisabled, - } + return { success: true } }, }) diff --git a/src/hooks/useAdPreference.ts b/src/hooks/useAdPreference.ts index f0673e015..a14d60d19 100644 --- a/src/hooks/useAdPreference.ts +++ b/src/hooks/useAdPreference.ts @@ -1,48 +1,19 @@ -import { useConvexMutation } from '@convex-dev/react-query' -import { useQueryClient } from '@tanstack/react-query' -import { api } from 'convex/_generated/api' import { useCurrentUserQuery } from './useCurrentUser' -export function useToggleAdPreference() { - const queryClient = useQueryClient() - - return useConvexMutation(api.users.toggleAdPreference, { - onSuccess: () => { - // Invalidate current user query to refresh the ad preference - queryClient.invalidateQueries({ - queryKey: ['convex', api.auth.getCurrentUser, {}], - }) - }, - }) -} - -export function useSetAdPreference() { - const queryClient = useQueryClient() - - return useConvexMutation(api.users.setAdPreference, { - onSuccess: () => { - // Invalidate current user query to refresh the ad preference - queryClient.invalidateQueries({ - queryKey: ['convex', api.auth.getCurrentUser, {}], - }) - }, - }) -} - // Legacy hook for backward compatibility - now uses current user query export function useAdsPreference() { const userQuery = useCurrentUserQuery() - + if (userQuery.isLoading || !userQuery.data) { return { adsEnabled: true } // Default to showing ads while loading or not authenticated } - + const user = userQuery.data const adsDisabled = user.adsDisabled ?? false const canDisableAds = user.capabilities.includes('disableAds') - + // Ads are enabled if user can't disable them OR if they haven't disabled them const adsEnabled = !canDisableAds || !adsDisabled - + return { adsEnabled } -} \ No newline at end of file +} diff --git a/src/routes/_libraries/account.tsx b/src/routes/_libraries/account.tsx index e51bca374..fff80be54 100644 --- a/src/routes/_libraries/account.tsx +++ b/src/routes/_libraries/account.tsx @@ -1,9 +1,9 @@ -import { useToggleAdPreference } from '~/hooks/useAdPreference' import { FaSignOutAlt } from 'react-icons/fa' -import { Authenticated, Unauthenticated } from 'convex/react' +import { Authenticated, Unauthenticated, useMutation } from 'convex/react' import { Link, redirect } from '@tanstack/react-router' import { authClient } from '~/utils/auth.client' import { useCurrentUserQuery } from '~/hooks/useCurrentUser' +import { api } from 'convex/_generated/api' export const Route = createFileRoute({ component: AccountPage, @@ -12,14 +12,28 @@ export const Route = createFileRoute({ function UserSettings() { const userQuery = useCurrentUserQuery() // Use current user query directly instead of separate ad preference query - const toggleAdPreferenceMutation = useToggleAdPreference() - + const updateAdPreferenceMutation = useMutation( + api.users.updateAdPreference + ).withOptimisticUpdate((localStore, args) => { + const { adsDisabled } = args + const currentValue = localStore.getQuery(api.auth.getCurrentUser) + if (currentValue !== undefined) { + localStore.setQuery(api.auth.getCurrentUser, {}, { + ...currentValue, + adsDisabled: adsDisabled, + } as any) + } + }) + // Get values directly from the current user data const adsDisabled = userQuery.data?.adsDisabled ?? false - const canDisableAds = userQuery.data?.capabilities.includes('disableAds') ?? false - - const handleToggleAds = () => { - toggleAdPreferenceMutation.mutate() + const canDisableAds = + userQuery.data?.capabilities.includes('disableAds') ?? false + + const handleToggleAds = (e: React.ChangeEvent) => { + updateAdPreferenceMutation({ + adsDisabled: e.target.checked, + }) } const signOut = async () => { @@ -60,7 +74,7 @@ function UserSettings() { className="h-4 w-4 accent-blue-600 my-1" checked={adsDisabled} onChange={handleToggleAds} - disabled={userQuery.isLoading || toggleAdPreferenceMutation.isPending} + disabled={userQuery.isLoading} aria-label="Disable Ads" />