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