rebreak-monorepo/apps/rebreak-native/hooks/useProtectedDevicesRealtime.ts
chahinebrini 42a8223bfc feat(native): auto-detect Mac activation via Supabase Realtime
Replaces the manual "I've installed it" button in AddMacSheet with an
auto-advancing waiting-pill. As soon as the backend flips status from
pending → active (triggered by the DoH handshake from the AdGuard
watcher), the sheet jumps to the success step automatically.

- useProtectedDevicesRealtime hook subscribes to rebreak.protected_devices
  UPDATE events for the current user, with auto-reconnect on CHANNEL_ERROR
- AddMacSheet listens only while in step 2 (download/install)
- devices.tsx keeps a list-level subscription so the table refreshes even
  if the user dismissed the sheet before activation
- i18n: waiting_install / waiting_hint / activated_toast (DE + EN)
2026-05-15 22:41:25 +02:00

73 lines
2.1 KiB
TypeScript

import { useEffect, useRef } from "react";
import { supabase } from "../lib/supabase";
import type { RealtimeChannel } from "@supabase/supabase-js";
import { useProtectedDevicesStore } from "../stores/protectedDevices";
type OnActivated = (deviceId: string) => void;
export function useProtectedDevicesRealtime(
onActivated?: OnActivated,
enabled: boolean = true,
) {
const onActivatedRef = useRef<OnActivated | undefined>(onActivated);
onActivatedRef.current = onActivated;
useEffect(() => {
if (!enabled) 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();
const session = data.session;
if (!session?.access_token || cancelled) return;
const userId = session.user.id;
channel = supabase
.channel(`protected:${userId}:${Date.now()}`)
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "rebreak",
table: "protected_devices",
filter: `user_id=eq.${userId}`,
},
(payload: any) => {
const updated = payload.new;
useProtectedDevicesStore.getState().load();
if (updated?.status === "active" && payload.old?.status === "pending") {
onActivatedRef.current?.(updated.id);
}
},
)
.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();
};
}, [enabled]);
}