fix(native): realtime disconnect bug — accessToken callback + AppState handler

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 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-15 21:48:54 +02:00
parent cd5efab6e1
commit db377da7ce
6 changed files with 26 additions and 5 deletions

View File

@ -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();

View File

@ -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()}`)

View File

@ -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

View File

@ -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

View File

@ -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;
},
},
});

View File

@ -74,7 +74,6 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
const session = data.session;
if (!session?.user?.id) return;
supabase.realtime.setAuth(session.access_token);
const myId = session.user.id;
realtimeSub = supabase