chahinebrini db377da7ce 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>
2026-05-15 21:48:54 +02:00

128 lines
3.6 KiB
TypeScript

import { useEffect } from "react";
import { supabase } from "../lib/supabase";
import type { RealtimeChannel } from "@supabase/supabase-js";
/**
* Realtime-Subscription für DM-Konversation:
* Lauscht auf INSERT in rebreak.direct_messages mit sender_id=eq.{partnerId}.
* Filter: Wir bekommen nur Nachrichten DES Partners (eigene werden lokal optimistisch
* hinzugefügt). callback erhält die rohe Postgres-Row.
*/
export function useDmRealtime(
partnerId: string | undefined,
onInsert: (row: any) => void,
enabled: boolean = true,
) {
useEffect(() => {
if (!enabled || !partnerId) return;
let channel: RealtimeChannel | null = null;
let cancelled = false;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
async function subscribe() {
const { data } = await supabase.auth.getSession();
if (cancelled || !data.session?.access_token) return;
channel = supabase
.channel(`dm:${partnerId}:${Date.now()}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "rebreak",
table: "direct_messages",
filter: `sender_id=eq.${partnerId}`,
},
(payload: any) => {
onInsert(payload.new);
},
)
.subscribe((status) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
cleanup();
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
if (!cancelled) subscribe();
}, 3000);
}
});
}
function cleanup() {
if (channel) {
supabase.removeChannel(channel);
channel = null;
}
}
subscribe();
return () => {
cancelled = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
cleanup();
};
}, [partnerId, enabled, onInsert]);
}
/**
* Realtime für Gruppen-Chat: lauscht auf INSERT in rebreak.chat_messages mit room_id=eq.{roomId}.
*/
export function useRoomRealtime(
roomId: string | undefined,
myUserId: string | undefined,
onInsert: (row: any) => void,
enabled: boolean = true,
) {
useEffect(() => {
if (!enabled || !roomId) return;
let channel: RealtimeChannel | null = null;
let cancelled = false;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
async function subscribe() {
const { data } = await supabase.auth.getSession();
if (cancelled || !data.session?.access_token) return;
channel = supabase
.channel(`room:${roomId}:${Date.now()}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "rebreak",
table: "chat_messages",
filter: `room_id=eq.${roomId}`,
},
(payload: any) => {
// Eigene Nachrichten überspringen (lokal optimistisch hinzugefügt)
if (payload.new?.user_id === myUserId) return;
onInsert(payload.new);
},
)
.subscribe((status) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
cleanup();
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
if (!cancelled) subscribe();
}, 3000);
}
});
}
function cleanup() {
if (channel) {
supabase.removeChannel(channel);
channel = null;
}
}
subscribe();
return () => {
cancelled = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
cleanup();
};
}, [roomId, myUserId, enabled, onInsert]);
}