785 lines
22 KiB
TypeScript
785 lines
22 KiB
TypeScript
import { App as CapacitorApp } from "@capacitor/app";
|
||
import { Capacitor } from "@capacitor/core";
|
||
import { Device } from "@capacitor/device";
|
||
import { Preferences } from "@capacitor/preferences";
|
||
import { PushNotifications, type Token } from "@capacitor/push-notifications";
|
||
|
||
type PushSubscriptionsResponse = {
|
||
items: Array<{
|
||
id: string;
|
||
active: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
endpoint?: string;
|
||
platform?: string;
|
||
}>;
|
||
hasActiveSubscription: boolean;
|
||
};
|
||
|
||
type BeforeInstallPromptEvent = Event & {
|
||
prompt: () => Promise<void>;
|
||
userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
|
||
};
|
||
|
||
type NotificationPermissionState = "default" | "denied" | "granted" | "prompt" | "unsupported";
|
||
|
||
const ANON_PUSH_CLIENT_ID_KEY = "anonymous-push-client-id";
|
||
const PENDING_PUSH_ROUTE_KEY = "pending-push-route";
|
||
|
||
export function usePush() {
|
||
const { user, token } = useAuth();
|
||
const config = useRuntimeConfig();
|
||
const installPromptEvent = useState<BeforeInstallPromptEvent | null>("pwa-install-prompt-event", () => null);
|
||
const anonymousClientId = useState<string | null>("anonymous-push-client-id", () => null);
|
||
const nativePushToken = useState<string | null>("native-push-token", () => null);
|
||
const nativePushDeviceId = useState<string | null>("native-push-device-id", () => null);
|
||
const nativePushInitialized = useState<boolean>("native-push-initialized", () => false);
|
||
const webPushClickInitialized = useState<boolean>("web-push-click-initialized", () => false);
|
||
const pendingPushRoute = useState<string | null>("pending-push-route", () => null);
|
||
|
||
const isNativeApp = () => process.client && Capacitor.isNativePlatform();
|
||
|
||
const getNativePlatform = () => {
|
||
if (!isNativeApp()) {
|
||
return null;
|
||
}
|
||
|
||
const platform = Capacitor.getPlatform();
|
||
if (platform === "android" || platform === "ios") {
|
||
return platform;
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
const readStoredAnonymousClientId = async () => {
|
||
if (!process.client) {
|
||
return null;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
const result = await Preferences.get({ key: ANON_PUSH_CLIENT_ID_KEY });
|
||
return result.value;
|
||
}
|
||
|
||
return localStorage.getItem(ANON_PUSH_CLIENT_ID_KEY);
|
||
};
|
||
|
||
const persistAnonymousClientId = async (value: string) => {
|
||
if (!process.client) {
|
||
return;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
await Preferences.set({ key: ANON_PUSH_CLIENT_ID_KEY, value });
|
||
return;
|
||
}
|
||
|
||
localStorage.setItem(ANON_PUSH_CLIENT_ID_KEY, value);
|
||
};
|
||
|
||
const clearAnonymousClientId = async () => {
|
||
if (!process.client) {
|
||
return;
|
||
}
|
||
|
||
anonymousClientId.value = null;
|
||
|
||
if (isNativeApp()) {
|
||
await Preferences.remove({ key: ANON_PUSH_CLIENT_ID_KEY });
|
||
return;
|
||
}
|
||
|
||
localStorage.removeItem(ANON_PUSH_CLIENT_ID_KEY);
|
||
};
|
||
|
||
const readStoredPendingPushRoute = async () => {
|
||
if (!process.client) {
|
||
return null;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
const result = await Preferences.get({ key: PENDING_PUSH_ROUTE_KEY });
|
||
return result.value;
|
||
}
|
||
|
||
return localStorage.getItem(PENDING_PUSH_ROUTE_KEY);
|
||
};
|
||
|
||
const persistPendingPushRoute = async (value: string | null) => {
|
||
if (!process.client) {
|
||
return;
|
||
}
|
||
|
||
pendingPushRoute.value = value;
|
||
|
||
if (isNativeApp()) {
|
||
if (value) {
|
||
await Preferences.set({ key: PENDING_PUSH_ROUTE_KEY, value });
|
||
} else {
|
||
await Preferences.remove({ key: PENDING_PUSH_ROUTE_KEY });
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (value) {
|
||
localStorage.setItem(PENDING_PUSH_ROUTE_KEY, value);
|
||
} else {
|
||
localStorage.removeItem(PENDING_PUSH_ROUTE_KEY);
|
||
}
|
||
};
|
||
|
||
const normalizeTargetRoute = (targetUrl: unknown) => {
|
||
if (typeof targetUrl !== "string" || targetUrl.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
if (targetUrl.startsWith("/")) {
|
||
return targetUrl;
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(targetUrl);
|
||
return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const openPushTarget = async (targetUrl: unknown) => {
|
||
const route = normalizeTargetRoute(targetUrl);
|
||
if (!route) {
|
||
return;
|
||
}
|
||
|
||
await persistPendingPushRoute(route);
|
||
|
||
if (user.value) {
|
||
await navigateTo(route);
|
||
await persistPendingPushRoute(null);
|
||
return;
|
||
}
|
||
|
||
await navigateTo(`/login?redirect=${encodeURIComponent(route)}`);
|
||
};
|
||
|
||
const consumePendingPushRoute = async () => {
|
||
if (!process.client) {
|
||
return null;
|
||
}
|
||
|
||
if (!pendingPushRoute.value) {
|
||
pendingPushRoute.value = await readStoredPendingPushRoute();
|
||
}
|
||
|
||
const route = pendingPushRoute.value;
|
||
if (!route || !user.value) {
|
||
return null;
|
||
}
|
||
|
||
await navigateTo(route);
|
||
await persistPendingPushRoute(null);
|
||
return route;
|
||
};
|
||
|
||
const ensureAnonymousClientId = async () => {
|
||
if (!process.client) {
|
||
return "server-client";
|
||
}
|
||
|
||
if (!anonymousClientId.value) {
|
||
anonymousClientId.value = await readStoredAnonymousClientId();
|
||
}
|
||
|
||
if (!anonymousClientId.value) {
|
||
anonymousClientId.value = crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "");
|
||
await persistAnonymousClientId(anonymousClientId.value);
|
||
}
|
||
|
||
return anonymousClientId.value;
|
||
};
|
||
|
||
const registerServiceWorker = async () => {
|
||
if (!process.client || isNativeApp() || !("serviceWorker" in navigator)) {
|
||
return null;
|
||
}
|
||
|
||
return navigator.serviceWorker.register("/sw.js");
|
||
};
|
||
|
||
const syncServiceWorkerContext = async () => {
|
||
if (!process.client || isNativeApp() || !("serviceWorker" in navigator)) {
|
||
return;
|
||
}
|
||
|
||
const registration = await registerServiceWorker();
|
||
const readyRegistration = registration ?? (await navigator.serviceWorker.ready);
|
||
const worker = readyRegistration.active ?? navigator.serviceWorker.controller;
|
||
if (!worker) {
|
||
return;
|
||
}
|
||
|
||
worker.postMessage({
|
||
type: "push-context-sync",
|
||
payload: {
|
||
apiBase: config.public.apiBase,
|
||
isAuthenticated: Boolean(user.value),
|
||
anonymousClientId: await ensureAnonymousClientId()
|
||
}
|
||
});
|
||
};
|
||
|
||
const urlBase64ToUint8Array = (base64String: string) => {
|
||
const normalizedInput = base64String.trim();
|
||
if (!normalizedInput || normalizedInput.startsWith("replace_")) {
|
||
throw new Error("Push-уведомления еще не настроены на сервере");
|
||
}
|
||
|
||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||
const base64 = (normalizedInput + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||
|
||
try {
|
||
const rawData = window.atob(base64);
|
||
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
|
||
} catch {
|
||
throw new Error("На сервере задан некорректный VAPID public key");
|
||
}
|
||
};
|
||
|
||
const isMobileDevice = () => {
|
||
if (!process.client) {
|
||
return false;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
return true;
|
||
}
|
||
|
||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||
};
|
||
|
||
const isIosDevice = () => {
|
||
if (!process.client) {
|
||
return false;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
return Capacitor.getPlatform() === "ios";
|
||
}
|
||
|
||
return /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||
};
|
||
|
||
const isStandaloneMode = () => {
|
||
if (!process.client) {
|
||
return false;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
return true;
|
||
}
|
||
|
||
const standaloneMedia = window.matchMedia?.("(display-mode: standalone)")?.matches ?? false;
|
||
const standaloneNavigator =
|
||
"standalone" in navigator ? Boolean((navigator as Navigator & { standalone?: boolean }).standalone) : false;
|
||
return standaloneMedia || standaloneNavigator;
|
||
};
|
||
|
||
const setInstallPromptEvent = (event: BeforeInstallPromptEvent | null) => {
|
||
installPromptEvent.value = event;
|
||
};
|
||
|
||
const canTriggerInstallPrompt = () => !isNativeApp() && Boolean(installPromptEvent.value);
|
||
|
||
const triggerInstallPrompt = async () => {
|
||
if (!installPromptEvent.value) {
|
||
return false;
|
||
}
|
||
|
||
await installPromptEvent.value.prompt();
|
||
const choice = await installPromptEvent.value.userChoice;
|
||
installPromptEvent.value = null;
|
||
return choice.outcome === "accepted";
|
||
};
|
||
|
||
const getBrowserPermissionState = (): NotificationPermissionState => {
|
||
if (!process.client || !("Notification" in window)) {
|
||
return "unsupported";
|
||
}
|
||
|
||
return Notification.permission;
|
||
};
|
||
|
||
const getNativePermissionState = async (): Promise<NotificationPermissionState> => {
|
||
if (!isNativeApp()) {
|
||
return "unsupported";
|
||
}
|
||
|
||
const permissions = await PushNotifications.checkPermissions();
|
||
if (permissions.receive === "prompt") {
|
||
return "default";
|
||
}
|
||
|
||
return permissions.receive;
|
||
};
|
||
|
||
const getPermissionState = () => {
|
||
if (isNativeApp()) {
|
||
return getNativePermissionState();
|
||
}
|
||
|
||
return Promise.resolve(getBrowserPermissionState());
|
||
};
|
||
|
||
const requestBrowserPermission = async (): Promise<NotificationPermissionState> => {
|
||
if (!process.client || !("Notification" in window)) {
|
||
return "unsupported";
|
||
}
|
||
|
||
return Notification.requestPermission();
|
||
};
|
||
|
||
const requestNativePermission = async (): Promise<NotificationPermissionState> => {
|
||
if (!isNativeApp()) {
|
||
return "unsupported";
|
||
}
|
||
|
||
const permissions = await PushNotifications.requestPermissions();
|
||
if (permissions.receive === "prompt") {
|
||
return "default";
|
||
}
|
||
|
||
return permissions.receive;
|
||
};
|
||
|
||
const requestPermission = () => {
|
||
if (isNativeApp()) {
|
||
return requestNativePermission();
|
||
}
|
||
|
||
return requestBrowserPermission();
|
||
};
|
||
|
||
const syncWebSubscription = async (subscription: PushSubscription) => {
|
||
const serializedSubscription = subscription.toJSON();
|
||
if (!serializedSubscription.endpoint || !serializedSubscription.keys?.p256dh || !serializedSubscription.keys?.auth) {
|
||
throw new Error("Браузер вернул неполную push-подписку");
|
||
}
|
||
|
||
if (user.value) {
|
||
await useApi("/me/push-subscriptions", {
|
||
method: "POST",
|
||
body: serializedSubscription
|
||
});
|
||
return;
|
||
}
|
||
|
||
await useApi("/public/push-subscriptions", {
|
||
method: "POST",
|
||
body: {
|
||
clientId: await ensureAnonymousClientId(),
|
||
...serializedSubscription
|
||
}
|
||
});
|
||
};
|
||
|
||
const syncNativeSubscription = async (tokenValue = nativePushToken.value) => {
|
||
const platform = getNativePlatform();
|
||
if (!platform || !tokenValue) {
|
||
return;
|
||
}
|
||
|
||
if (!nativePushDeviceId.value) {
|
||
const device = await Device.getId();
|
||
nativePushDeviceId.value = device.identifier;
|
||
}
|
||
|
||
const body = {
|
||
token: tokenValue,
|
||
platform,
|
||
deviceId: nativePushDeviceId.value ?? undefined
|
||
};
|
||
|
||
if (user.value) {
|
||
await useApi("/me/native-push-subscriptions", {
|
||
method: "POST",
|
||
body
|
||
});
|
||
return;
|
||
}
|
||
|
||
await useApi("/public/native-push-subscriptions", {
|
||
method: "POST",
|
||
body: {
|
||
clientId: await ensureAnonymousClientId(),
|
||
...body
|
||
}
|
||
});
|
||
};
|
||
|
||
const initializeNativePush = async () => {
|
||
if (!isNativeApp() || nativePushInitialized.value) {
|
||
return;
|
||
}
|
||
|
||
nativePushInitialized.value = true;
|
||
|
||
await PushNotifications.createChannel({
|
||
id: "signals",
|
||
name: "Signals",
|
||
description: "Уведомления о новых сигналах",
|
||
importance: 5,
|
||
visibility: 1,
|
||
sound: "default"
|
||
}).catch(() => undefined);
|
||
|
||
await PushNotifications.addListener("registration", async (registrationToken: Token) => {
|
||
nativePushToken.value = registrationToken.value;
|
||
await syncNativeSubscription(registrationToken.value);
|
||
});
|
||
|
||
await PushNotifications.addListener("registrationError", (error) => {
|
||
console.error("Native push registration failed", error);
|
||
});
|
||
|
||
await PushNotifications.addListener("pushNotificationActionPerformed", (notification) => {
|
||
const targetUrl =
|
||
notification.notification.data?.url ||
|
||
notification.notification.data?.link ||
|
||
notification.notification.data?.path;
|
||
|
||
void openPushTarget(targetUrl);
|
||
});
|
||
|
||
await CapacitorApp.addListener("appUrlOpen", ({ url }) => {
|
||
if (!url) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(url);
|
||
const route = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
||
void openPushTarget(route || "/");
|
||
} catch {
|
||
// Ignore invalid deep links.
|
||
}
|
||
});
|
||
};
|
||
|
||
const initializeWebPushRouting = () => {
|
||
if (!process.client || isNativeApp() || webPushClickInitialized.value || !("serviceWorker" in navigator)) {
|
||
return;
|
||
}
|
||
|
||
webPushClickInitialized.value = true;
|
||
|
||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||
if (event.data?.type !== "push-notification-click") {
|
||
return;
|
||
}
|
||
|
||
void openPushTarget(event.data.url);
|
||
});
|
||
};
|
||
|
||
const getCurrentBrowserSubscription = async () => {
|
||
const registration = await registerServiceWorker();
|
||
if (!registration || !("PushManager" in window)) {
|
||
return null;
|
||
}
|
||
|
||
return registration.pushManager.getSubscription();
|
||
};
|
||
|
||
const claimAnonymousPushSubscriptions = async () => {
|
||
if (!process.client || !user.value) {
|
||
return;
|
||
}
|
||
|
||
const existingClientId = anonymousClientId.value ?? (await readStoredAnonymousClientId());
|
||
if (!existingClientId) {
|
||
await syncServiceWorkerContext();
|
||
return;
|
||
}
|
||
|
||
anonymousClientId.value = existingClientId;
|
||
|
||
if (isNativeApp()) {
|
||
if (nativePushToken.value) {
|
||
await syncNativeSubscription(nativePushToken.value);
|
||
}
|
||
|
||
const anonymousSubscriptions = await useApi<PushSubscriptionsResponse>(
|
||
`/public/native-push-subscriptions/${existingClientId}`
|
||
).catch(() => null);
|
||
|
||
for (const subscription of anonymousSubscriptions?.items ?? []) {
|
||
await useApi(`/public/native-push-subscriptions/${existingClientId}/${subscription.id}`, {
|
||
method: "DELETE"
|
||
}).catch(() => undefined);
|
||
}
|
||
} else {
|
||
const currentSubscription = await getCurrentBrowserSubscription();
|
||
if (currentSubscription) {
|
||
await syncWebSubscription(currentSubscription);
|
||
}
|
||
|
||
const anonymousSubscriptions = await useApi<PushSubscriptionsResponse>(
|
||
`/public/push-subscriptions/${existingClientId}`
|
||
).catch(() => null);
|
||
|
||
for (const subscription of anonymousSubscriptions?.items ?? []) {
|
||
await useApi(`/public/push-subscriptions/${existingClientId}/${subscription.id}`, {
|
||
method: "DELETE"
|
||
}).catch(() => undefined);
|
||
}
|
||
}
|
||
|
||
await clearAnonymousClientId();
|
||
await syncServiceWorkerContext();
|
||
};
|
||
|
||
const getCurrentSubscription = async () => {
|
||
if (isNativeApp()) {
|
||
return nativePushToken.value;
|
||
}
|
||
|
||
return getCurrentBrowserSubscription();
|
||
};
|
||
|
||
const deactivateCurrentPushSubscription = async () => {
|
||
if (!process.client || !user.value) {
|
||
return;
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
if (!nativePushToken.value) {
|
||
return;
|
||
}
|
||
|
||
await useApi("/me/native-push-subscriptions/deactivate", {
|
||
method: "POST",
|
||
body: {
|
||
token: nativePushToken.value
|
||
}
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
const currentSubscription = await getCurrentBrowserSubscription();
|
||
if (!currentSubscription) {
|
||
return;
|
||
}
|
||
|
||
const serializedSubscription = currentSubscription.toJSON();
|
||
if (!serializedSubscription.endpoint) {
|
||
return;
|
||
}
|
||
|
||
await useApi("/me/push-subscriptions/deactivate", {
|
||
method: "POST",
|
||
body: {
|
||
endpoint: serializedSubscription.endpoint
|
||
}
|
||
});
|
||
|
||
await currentSubscription.unsubscribe().catch(() => undefined);
|
||
};
|
||
|
||
const subscribeToBrowserPush = async () => {
|
||
const registration = await registerServiceWorker();
|
||
if (!registration || !("PushManager" in window)) {
|
||
throw new Error("Push не поддерживается в этом браузере");
|
||
}
|
||
|
||
const vapid = await useApi<{ publicKey: string }>("/vapid-public-key");
|
||
if (!vapid.publicKey || vapid.publicKey.startsWith("replace_")) {
|
||
throw new Error("Push-уведомления еще не настроены на сервере");
|
||
}
|
||
|
||
const subscription = await registration.pushManager.subscribe({
|
||
userVisibleOnly: true,
|
||
applicationServerKey: urlBase64ToUint8Array(vapid.publicKey)
|
||
});
|
||
|
||
await syncWebSubscription(subscription);
|
||
return subscription;
|
||
};
|
||
|
||
const subscribeToNativePush = async () => {
|
||
await initializeNativePush();
|
||
|
||
let permission = await getNativePermissionState();
|
||
if (permission === "default") {
|
||
permission = await requestNativePermission();
|
||
}
|
||
|
||
if (permission !== "granted") {
|
||
throw new Error("Разрешение на уведомления не выдано");
|
||
}
|
||
|
||
const device = await Device.getId();
|
||
nativePushDeviceId.value = device.identifier;
|
||
|
||
return new Promise<string>((resolve, reject) => {
|
||
let finished = false;
|
||
|
||
const finalize = (callback: () => void) => {
|
||
if (finished) {
|
||
return;
|
||
}
|
||
|
||
finished = true;
|
||
callback();
|
||
};
|
||
|
||
PushNotifications.addListener("registration", async (registrationToken: Token) => {
|
||
finalize(() => {
|
||
nativePushToken.value = registrationToken.value;
|
||
void syncNativeSubscription(registrationToken.value);
|
||
resolve(registrationToken.value);
|
||
});
|
||
}).catch(reject);
|
||
|
||
PushNotifications.addListener("registrationError", (error) => {
|
||
finalize(() => {
|
||
reject(new Error(error.error || "Не удалось зарегистрировать устройство для push"));
|
||
});
|
||
}).catch(reject);
|
||
|
||
PushNotifications.register().catch((error) => {
|
||
finalize(() => {
|
||
reject(error instanceof Error ? error : new Error("Не удалось зарегистрировать устройство для push"));
|
||
});
|
||
});
|
||
});
|
||
};
|
||
|
||
const subscribeToPush = () => {
|
||
if (isNativeApp()) {
|
||
return subscribeToNativePush();
|
||
}
|
||
|
||
return subscribeToBrowserPush();
|
||
};
|
||
|
||
const ensurePushSubscription = async () => {
|
||
const permission = await getPermissionState();
|
||
if (permission === "unsupported") {
|
||
throw new Error("Push не поддерживается на этом устройстве");
|
||
}
|
||
|
||
let finalPermission = permission;
|
||
if (finalPermission === "default") {
|
||
finalPermission = await requestPermission();
|
||
}
|
||
|
||
if (finalPermission !== "granted") {
|
||
throw new Error("Разрешение на уведомления не выдано");
|
||
}
|
||
|
||
if (isNativeApp()) {
|
||
if (nativePushToken.value) {
|
||
await syncNativeSubscription(nativePushToken.value);
|
||
return nativePushToken.value;
|
||
}
|
||
|
||
return subscribeToNativePush();
|
||
}
|
||
|
||
const currentSubscription = await getCurrentBrowserSubscription();
|
||
if (currentSubscription) {
|
||
await syncWebSubscription(currentSubscription);
|
||
return currentSubscription;
|
||
}
|
||
|
||
return subscribeToBrowserPush();
|
||
};
|
||
|
||
const getPushStatus = async () => {
|
||
const permission = await getPermissionState();
|
||
const isMobile = isMobileDevice();
|
||
const isIos = isIosDevice();
|
||
const isStandalone = isStandaloneMode();
|
||
const installRequired = !isNativeApp() && isMobile && !isStandalone;
|
||
|
||
if (permission === "unsupported") {
|
||
return {
|
||
supported: false,
|
||
permission,
|
||
isMobile,
|
||
isIos,
|
||
isStandalone,
|
||
installRequired,
|
||
canInstall: canTriggerInstallPrompt(),
|
||
hasBrowserSubscription: false,
|
||
hasServerSubscription: false
|
||
};
|
||
}
|
||
|
||
let hasServerSubscription = false;
|
||
|
||
try {
|
||
if (isNativeApp()) {
|
||
if (user.value) {
|
||
const serverSubscriptions = await useApi<PushSubscriptionsResponse>("/me/native-push-subscriptions");
|
||
hasServerSubscription = serverSubscriptions.hasActiveSubscription;
|
||
} else {
|
||
const clientId = await ensureAnonymousClientId();
|
||
const serverSubscriptions = await useApi<PushSubscriptionsResponse>(`/public/native-push-subscriptions/${clientId}`);
|
||
hasServerSubscription = serverSubscriptions.hasActiveSubscription;
|
||
}
|
||
} else if (user.value) {
|
||
const serverSubscriptions = await useApi<PushSubscriptionsResponse>("/me/push-subscriptions");
|
||
hasServerSubscription = serverSubscriptions.hasActiveSubscription;
|
||
} else {
|
||
const clientId = await ensureAnonymousClientId();
|
||
const serverSubscriptions = await useApi<PushSubscriptionsResponse>(`/public/push-subscriptions/${clientId}`);
|
||
hasServerSubscription = serverSubscriptions.hasActiveSubscription;
|
||
}
|
||
} catch {
|
||
hasServerSubscription = false;
|
||
}
|
||
|
||
return {
|
||
supported: true,
|
||
permission,
|
||
isMobile,
|
||
isIos,
|
||
isStandalone,
|
||
installRequired,
|
||
canInstall: canTriggerInstallPrompt(),
|
||
hasBrowserSubscription: isNativeApp()
|
||
? Boolean(nativePushToken.value)
|
||
: Boolean(await getCurrentBrowserSubscription()),
|
||
hasServerSubscription
|
||
};
|
||
};
|
||
|
||
return {
|
||
registerServiceWorker,
|
||
syncServiceWorkerContext,
|
||
syncNativeSubscription,
|
||
initializeNativePush,
|
||
initializeWebPushRouting,
|
||
isNativeApp,
|
||
isMobileDevice,
|
||
isIosDevice,
|
||
isStandaloneMode,
|
||
getPermissionState,
|
||
requestPermission,
|
||
getCurrentSubscription,
|
||
deactivateCurrentPushSubscription,
|
||
getPushStatus,
|
||
subscribeToPush,
|
||
ensurePushSubscription,
|
||
claimAnonymousPushSubscriptions,
|
||
consumePendingPushRoute,
|
||
setInstallPromptEvent,
|
||
canTriggerInstallPrompt,
|
||
triggerInstallPrompt,
|
||
ensureAnonymousClientId
|
||
};
|
||
}
|