rebreak-monorepo/apps/rebreak-native/hooks/useDomainSubmissionRealtime.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

97 lines
2.7 KiB
TypeScript

import { useEffect } from "react";
import { supabase } from "../lib/supabase";
import { isRealtimeErrorReal } from "../lib/realtimeStatus";
import type { RealtimeChannel } from "@supabase/supabase-js";
/**
* Realtime-Subscription für die Blocker-Page.
* Lauscht auf:
* - UPDATE auf rebreak.domain_submissions → ruft `onChange()` (refetch)
* - INSERT auf rebreak.notifications mit type=domain_accepted für eigene recipient_id → refetch
*
* Pendant zum Nuxt-Code in apps/rebreak/app/pages/app/blocker/index.vue.
*/
export function useDomainSubmissionRealtime(
onChange: () => void,
enabled: boolean = true,
) {
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;
channel = supabase
.channel(`blocker:domains:${myId}:${Date.now()}`)
.on(
"postgres_changes",
{
event: "*",
schema: "rebreak",
table: "domain_submissions",
filter: `user_id=eq.${myId}`,
},
() => onChange(),
)
.on(
"postgres_changes",
{
event: "*",
schema: "rebreak",
table: "user_custom_domains",
filter: `user_id=eq.${myId}`,
},
() => onChange(),
)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "rebreak",
table: "notifications",
filter: `recipient_id=eq.${myId}`,
},
(payload: any) => {
const t = payload.new?.type;
if (t === "domain_accepted" || t === "domain_rejected") {
onChange();
}
},
)
.subscribe((status, err) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
if (isRealtimeErrorReal()) console.warn("[domainRealtime] error:", status, err ?? "");
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, onChange]);
}