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>
97 lines
2.7 KiB
TypeScript
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]);
|
|
}
|