From abfccd0e36d5c75f7bd219c5fedd5e0060449779 Mon Sep 17 00:00:00 2001 From: tapframe Date: Thu, 18 Sep 2025 15:32:05 +0530 Subject: [PATCH] Sync behaviour improvments --- src/contexts/AccountContext.tsx | 35 +++++++++++++++++++++++---------- src/screens/SettingsScreen.tsx | 6 ++++-- src/services/SyncService.ts | 30 ++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index 5eab848..67b6d56 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -10,6 +10,7 @@ type AccountContextValue = { signIn: (email: string, password: string) => Promise; signUp: (email: string, password: string) => Promise; signOut: () => Promise; + refreshCurrentUser: () => Promise; updateProfile: (partial: { avatarUrl?: string; displayName?: string }) => Promise; }; @@ -47,16 +48,21 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child // Auth state listener const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => { - const fullUser = session?.user ? await accountService.getCurrentUser() : null; - setUser(fullUser); - if (fullUser) { - await syncService.migrateLocalScopeToUser(); - await syncService.subscribeRealtime(); - // Pull first to hydrate local state, then push to avoid wiping server with empty local - await syncService.fullPull(); - await syncService.fullPush(); - } else { - syncService.unsubscribeRealtime(); + setLoading(true); + try { + const fullUser = session?.user ? await accountService.getCurrentUser() : null; + setUser(fullUser); + if (fullUser) { + await syncService.migrateLocalScopeToUser(); + await syncService.subscribeRealtime(); + // Pull first to hydrate local state, then push to avoid wiping server with empty local + await syncService.fullPull(); + await syncService.fullPush(); + } else { + syncService.unsubscribeRealtime(); + } + } finally { + setLoading(false); } }); @@ -81,6 +87,15 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child await accountService.signOut(); setUser(null); }, + refreshCurrentUser: async () => { + setLoading(true); + try { + const u = await accountService.getCurrentUser(); + setUser(u); + } finally { + setLoading(false); + } + }, updateProfile: async (partial) => { const err = await accountService.updateProfile(partial); if (!err) { diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 02fe8d1..f217a99 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -231,7 +231,7 @@ const SettingsScreen: React.FC = () => { const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - const { user, signOut, loading: accountLoading } = useAccount(); + const { user, signOut, loading: accountLoading, refreshCurrentUser } = useAccount(); // Tablet-specific state const [selectedCategory, setSelectedCategory] = useState('account'); @@ -248,10 +248,12 @@ const SettingsScreen: React.FC = () => { if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username }); } refreshAuthStatus(); + // Also refresh account user in case we returned from auth flow + refreshCurrentUser(); }); return unsubscribe; - }, [navigation, isAuthenticated, userProfile, refreshAuthStatus]); + }, [navigation, isAuthenticated, userProfile, refreshAuthStatus, refreshCurrentUser]); // States for dynamic content const [addonCount, setAddonCount] = useState(0); diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index 53d403b..3aca9e2 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -693,6 +693,8 @@ class SyncService { (stremioService as any).addonOrder = order; await (stremioService as any).saveInstalledAddons(); await (stremioService as any).saveAddonOrder(); + // Mark addons initialized for this user to prevent destructive merges on first push + try { await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true'); } catch {} // Push merged order to server to preserve across devices try { const rows = order.map((addonId: string, idx: number) => ({ @@ -848,9 +850,27 @@ class SyncService { const user = await accountService.getCurrentUser(); if (!user) return; const userId = user.id; - const addons = await stremioService.getInstalledAddonsAsync(); + let addons = await stremioService.getInstalledAddonsAsync(); logger.log(`[Sync] push installed_addons count=${addons.length}`); - const order = (stremioService as any).addonOrder as string[]; + let order = (stremioService as any).addonOrder as string[]; + + // Safety: if this is a first-time push and local addons are fewer than remote, pull before pushing + try { + const initialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true'; + const { data: remoteBefore } = await supabase + .from('installed_addons') + .select('addon_id') + .eq('user_id', userId); + const remoteCount = (remoteBefore || []).length; + if (!initialized && remoteCount > addons.length) { + logger.log('[Sync] addons not initialized and local smaller than remote → pulling before push'); + await this.pullAddonsSnapshot(userId); + // refresh local state after pull + addons = await stremioService.getInstalledAddonsAsync(); + order = (stremioService as any).addonOrder as string[]; + } + } catch {} + const rows = addons.map((a: any) => ({ user_id: userId, addon_id: a.id, @@ -863,12 +883,17 @@ class SyncService { manifest_data: a, })); // Delete remote addons that no longer exist locally (excluding pre-installed to be safe) + // Guard: do not perform deletions on first-time merge when remote has more addons try { const { data: remote, error: rErr } = await supabase .from('installed_addons') .select('addon_id') .eq('user_id', userId); if (!rErr && remote) { + const initialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true'; + if (!initialized && (remote as any[]).length > addons.length) { + logger.log('[Sync] skipping deletions during first-time addon merge'); + } else { const localIds = new Set(addons.map((a: any) => a.id)); const toDeletePromises = (remote as any[]) .map(r => r.addon_id as string) @@ -894,6 +919,7 @@ class SyncService { .in('addon_id', toDelete); if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error); } + } } } catch (e) { if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e);