rebreak-monorepo/apps/rebreak-native/hooks/useDeviceApprovalRealtime.ts
chahinebrini 63fae25531 fix(android-protection): explicit specialUse FGS type — Samsung/Android 16 crash loop
RebreakVpnService.onStartCommand crashed with SecurityException because Android 16's validateForegroundServiceType rejects the implicit 2-arg startForeground(). Now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE explicitly (Google's documented best practice) and guards the call so a failed foreground promotion stops the service cleanly instead of crashing the app. Verified vs reported Galaxy A54 / Android 16 signature (97% of crash events, 1-user crash loop).

Bundles pending working-tree work across native/marketing/locales/mac + graphify-out rebuild. gitignore: google-services.json + /screenshots/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:33:28 +02:00

89 lines
2.7 KiB
TypeScript

import { useEffect } from "react";
import { supabase } from "../lib/supabase";
import { isRealtimeErrorReal } from "../lib/realtimeStatus";
import { useDeviceApprovalStore } from "../stores/deviceApproval";
import type { RealtimeChannel } from "@supabase/supabase-js";
/**
* Realtime-Subscription für Device-Approval-Requests.
*
* Lauscht auf INSERT/UPDATE auf rebreak.device_approval_requests gefiltert
* nach user_id. Wenn eine pending Row hereinkommt → triggert
* refreshIncomingFromServer() (holt die newest pending Row und zeigt das
* Approval-Sheet auf diesem existierenden Gerät).
*
* Wird global im _layout.tsx aufgerufen — IMMER aktiv für eingeloggte User.
*/
export function useDeviceApprovalRealtime(enabled: boolean = true) {
const refreshIncoming = useDeviceApprovalStore(
(s) => s.refreshIncomingFromServer,
);
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) return;
if (cancelled) return;
const myId = session.user.id;
// Initial pull (in case we missed something while offline)
refreshIncoming();
channel = supabase
.channel(`device_approvals:${myId}:${Date.now()}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "rebreak",
table: "device_approval_requests",
filter: `user_id=eq.${myId}`,
},
() => refreshIncoming(),
)
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "rebreak",
table: "device_approval_requests",
filter: `user_id=eq.${myId}`,
},
() => refreshIncoming(),
)
.subscribe((status, err) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
if (isRealtimeErrorReal()) console.warn("[approvalRealtime] error:", status, err ?? "");
cleanup();
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
if (!cancelled) subscribe();
}, 3000);
}
});
}
function cleanup() {
if (channel) {
supabase.removeChannel(channel).catch(() => {});
channel = null;
}
}
subscribe();
return () => {
cancelled = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
cleanup();
};
}, [enabled, refreshIncoming]);
}