From db377da7ceb497936f7b8bcf068a57e9961374ac Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Fri, 15 May 2026 21:48:54 +0200 Subject: [PATCH] =?UTF-8?q?fix(native):=20realtime=20disconnect=20bug=20?= =?UTF-8?q?=E2=80=94=20accessToken=20callback=20+=20AppState=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug (diagnosed by backyard, see project_session_2026-05-15_push.md): - Manual `supabase.realtime.setAuth()` calls in subscribe-hooks set `_manuallySetToken=true` internally, blocking the automatic token-refresh on heartbeat. After ~1h the cached access_token expires → Postgres-Changes silently stop arriving (channel still shows "joined" but no events). - Plus: no AppState handler → no Foreground-Reconnect trigger after Background-kill of WebSocket. Fix A — lib/supabase.ts: createClient now passes a `realtime.accessToken` async callback that returns the current session token. Heartbeat picks fresh tokens automatically, no manual setAuth needed. Fix A — all 5 manual `supabase.realtime.setAuth()` calls removed from useChatRealtime, useCommunityRealtime, useDomainSubmissionRealtime, stores/notifications. Token is handled by the callback now. Fix B — _layout.tsx: AppState listener calls supabase.auth.startAutoRefresh()/stopAutoRefresh() — official Supabase RN pattern. On Foreground-Return, onAuthStateChange fires TOKEN_REFRESHED → realtime.setAuth gets called internally. Required for upcoming Auto-Detect protected-device handshake (Realtime channel listens on protected_devices status transitions pending→active). Co-Authored-By: Claude Opus 4.7 --- apps/rebreak-native/app/_layout.tsx | 17 +++++++++++++++++ apps/rebreak-native/hooks/useChatRealtime.ts | 2 -- .../hooks/useCommunityRealtime.ts | 1 - .../hooks/useDomainSubmissionRealtime.ts | 1 - apps/rebreak-native/lib/supabase.ts | 9 +++++++++ apps/rebreak-native/stores/notifications.ts | 1 - 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/rebreak-native/app/_layout.tsx b/apps/rebreak-native/app/_layout.tsx index 357a391..4be6785 100644 --- a/apps/rebreak-native/app/_layout.tsx +++ b/apps/rebreak-native/app/_layout.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import { AppState } from 'react-native'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import * as Notifications from 'expo-notifications'; @@ -15,6 +16,7 @@ import { Nunito_700Bold, Nunito_800ExtraBold, } from '@expo-google-fonts/nunito'; +import { supabase } from '../lib/supabase'; import { useAuthStore } from '../stores/auth'; import { useThemeStore } from '../stores/theme'; import { useRealtimeDebugStore } from '../stores/realtimeDebug'; @@ -71,6 +73,21 @@ function RootLayoutInner() { if (__DEV__) initRealtimeDebug(); }, []); + // Supabase-Doku-Pattern für RN: Token-Auto-Refresh nur wenn App aktiv ist. + // Plus Foreground-Reconnect via onAuthStateChange (TOKEN_REFRESHED → + // realtime.setAuth wird intern getriggert). Fixt den Realtime-Disconnect-Bug + // bei lange eingeloggten Usern (siehe `project_session_2026-05-15_push.md`). + useEffect(() => { + const sub = AppState.addEventListener('change', (state) => { + if (state === 'active') { + supabase.auth.startAutoRefresh(); + } else { + supabase.auth.stopAutoRefresh(); + } + }); + return () => sub.remove(); + }, []); + useEffect(() => { if (fontsLoaded && !loading && appLockReady) { SplashScreen.hideAsync(); diff --git a/apps/rebreak-native/hooks/useChatRealtime.ts b/apps/rebreak-native/hooks/useChatRealtime.ts index e0b9934..bea6d42 100644 --- a/apps/rebreak-native/hooks/useChatRealtime.ts +++ b/apps/rebreak-native/hooks/useChatRealtime.ts @@ -22,7 +22,6 @@ export function useDmRealtime( async function subscribe() { const { data } = await supabase.auth.getSession(); if (cancelled || !data.session?.access_token) return; - supabase.realtime.setAuth(data.session.access_token); channel = supabase .channel(`dm:${partnerId}:${Date.now()}`) @@ -83,7 +82,6 @@ export function useRoomRealtime( async function subscribe() { const { data } = await supabase.auth.getSession(); if (cancelled || !data.session?.access_token) return; - supabase.realtime.setAuth(data.session.access_token); channel = supabase .channel(`room:${roomId}:${Date.now()}`) diff --git a/apps/rebreak-native/hooks/useCommunityRealtime.ts b/apps/rebreak-native/hooks/useCommunityRealtime.ts index 4238cda..3901625 100644 --- a/apps/rebreak-native/hooks/useCommunityRealtime.ts +++ b/apps/rebreak-native/hooks/useCommunityRealtime.ts @@ -29,7 +29,6 @@ export function useCommunityRealtime(enabled: boolean = true) { if (!session?.access_token) return; if (cancelled) return; - supabase.realtime.setAuth(session.access_token); const myId = session.user.id; channel = supabase diff --git a/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts b/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts index 62d2adb..79695a1 100644 --- a/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts +++ b/apps/rebreak-native/hooks/useDomainSubmissionRealtime.ts @@ -26,7 +26,6 @@ export function useDomainSubmissionRealtime( if (!session?.access_token) return; if (cancelled) return; - supabase.realtime.setAuth(session.access_token); const myId = session.user.id; channel = supabase diff --git a/apps/rebreak-native/lib/supabase.ts b/apps/rebreak-native/lib/supabase.ts index 3304a52..16ba16b 100644 --- a/apps/rebreak-native/lib/supabase.ts +++ b/apps/rebreak-native/lib/supabase.ts @@ -24,5 +24,14 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, { params: { apikey: supabaseAnonKey, }, + // Auto-refresh Token on every heartbeat so Realtime keeps working across + // long sessions. Without this, manual setAuth() calls in subscribe-hooks + // set _manuallySetToken=true which blocks the internal token-refresh — after + // 1h the cached access_token expires and Postgres-Changes silently stop + // arriving. See `project_session_2026-05-15_push.md` for the full root-cause. + accessToken: async () => { + const { data } = await supabase.auth.getSession(); + return data.session?.access_token ?? null; + }, }, }); diff --git a/apps/rebreak-native/stores/notifications.ts b/apps/rebreak-native/stores/notifications.ts index 285e9a7..e2457b2 100644 --- a/apps/rebreak-native/stores/notifications.ts +++ b/apps/rebreak-native/stores/notifications.ts @@ -74,7 +74,6 @@ export const useNotificationStore = create((set, get) => ({ const session = data.session; if (!session?.user?.id) return; - supabase.realtime.setAuth(session.access_token); const myId = session.user.id; realtimeSub = supabase