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:
parent
cd5efab6e1
commit
db377da7ce
@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { AppState } from 'react-native';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
@ -15,6 +16,7 @@ import {
|
|||||||
Nunito_700Bold,
|
Nunito_700Bold,
|
||||||
Nunito_800ExtraBold,
|
Nunito_800ExtraBold,
|
||||||
} from '@expo-google-fonts/nunito';
|
} from '@expo-google-fonts/nunito';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
import { useThemeStore } from '../stores/theme';
|
import { useThemeStore } from '../stores/theme';
|
||||||
import { useRealtimeDebugStore } from '../stores/realtimeDebug';
|
import { useRealtimeDebugStore } from '../stores/realtimeDebug';
|
||||||
@ -71,6 +73,21 @@ function RootLayoutInner() {
|
|||||||
if (__DEV__) initRealtimeDebug();
|
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(() => {
|
useEffect(() => {
|
||||||
if (fontsLoaded && !loading && appLockReady) {
|
if (fontsLoaded && !loading && appLockReady) {
|
||||||
SplashScreen.hideAsync();
|
SplashScreen.hideAsync();
|
||||||
|
|||||||
@ -22,7 +22,6 @@ export function useDmRealtime(
|
|||||||
async function subscribe() {
|
async function subscribe() {
|
||||||
const { data } = await supabase.auth.getSession();
|
const { data } = await supabase.auth.getSession();
|
||||||
if (cancelled || !data.session?.access_token) return;
|
if (cancelled || !data.session?.access_token) return;
|
||||||
supabase.realtime.setAuth(data.session.access_token);
|
|
||||||
|
|
||||||
channel = supabase
|
channel = supabase
|
||||||
.channel(`dm:${partnerId}:${Date.now()}`)
|
.channel(`dm:${partnerId}:${Date.now()}`)
|
||||||
@ -83,7 +82,6 @@ export function useRoomRealtime(
|
|||||||
async function subscribe() {
|
async function subscribe() {
|
||||||
const { data } = await supabase.auth.getSession();
|
const { data } = await supabase.auth.getSession();
|
||||||
if (cancelled || !data.session?.access_token) return;
|
if (cancelled || !data.session?.access_token) return;
|
||||||
supabase.realtime.setAuth(data.session.access_token);
|
|
||||||
|
|
||||||
channel = supabase
|
channel = supabase
|
||||||
.channel(`room:${roomId}:${Date.now()}`)
|
.channel(`room:${roomId}:${Date.now()}`)
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export function useCommunityRealtime(enabled: boolean = true) {
|
|||||||
if (!session?.access_token) return;
|
if (!session?.access_token) return;
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
supabase.realtime.setAuth(session.access_token);
|
|
||||||
const myId = session.user.id;
|
const myId = session.user.id;
|
||||||
|
|
||||||
channel = supabase
|
channel = supabase
|
||||||
|
|||||||
@ -26,7 +26,6 @@ export function useDomainSubmissionRealtime(
|
|||||||
if (!session?.access_token) return;
|
if (!session?.access_token) return;
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
supabase.realtime.setAuth(session.access_token);
|
|
||||||
const myId = session.user.id;
|
const myId = session.user.id;
|
||||||
|
|
||||||
channel = supabase
|
channel = supabase
|
||||||
|
|||||||
@ -24,5 +24,14 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|||||||
params: {
|
params: {
|
||||||
apikey: supabaseAnonKey,
|
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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -74,7 +74,6 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
|
|||||||
const session = data.session;
|
const session = data.session;
|
||||||
if (!session?.user?.id) return;
|
if (!session?.user?.id) return;
|
||||||
|
|
||||||
supabase.realtime.setAuth(session.access_token);
|
|
||||||
const myId = session.user.id;
|
const myId = session.user.id;
|
||||||
|
|
||||||
realtimeSub = supabase
|
realtimeSub = supabase
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user