Files
ai-code-app/src/app/main/webPushRegistration.ts

209 lines
5.9 KiB
TypeScript

import {
fetchWebPushConfig,
registerWebPushSubscription,
unregisterWebPushSubscription,
type WebPushSubscriptionPayload,
} from './notificationApi';
import { getOrCreateClientId } from './clientIdentity';
import { getSavedNotificationDeviceId } from './notificationIdentity';
const WEB_PUSH_METADATA_STORAGE_KEY = 'work-server.web-push.registration-meta.v1';
type WebPushRegistrationMetadata = {
deviceId: string;
clientId: string;
userAgent: string;
appOrigin: string;
appDomain: string;
enabled: boolean;
updatedAt: string;
};
function getCurrentAppOrigin() {
if (typeof window === 'undefined') {
return '';
}
return window.location.origin;
}
function getCurrentAppDomain() {
if (typeof window === 'undefined') {
return '';
}
return window.location.hostname;
}
function buildWebPushRegistrationMetadata(enabled: boolean): WebPushRegistrationMetadata {
return {
deviceId: getSavedNotificationDeviceId().trim(),
clientId: getOrCreateClientId().trim(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
appOrigin: getCurrentAppOrigin().trim(),
appDomain: getCurrentAppDomain().trim(),
enabled,
updatedAt: new Date().toISOString(),
};
}
function storeWebPushRegistrationMetadata(metadata: WebPushRegistrationMetadata | null) {
if (typeof window === 'undefined') {
return;
}
try {
if (metadata) {
window.localStorage.setItem(WEB_PUSH_METADATA_STORAGE_KEY, JSON.stringify(metadata));
return;
}
window.localStorage.removeItem(WEB_PUSH_METADATA_STORAGE_KEY);
} catch {
// Ignore storage failures in restricted runtimes.
}
}
async function syncWebPushRegistrationMetadataWithServiceWorker(
registration: ServiceWorkerRegistration | null,
metadata: WebPushRegistrationMetadata | null,
) {
if (!registration) {
return;
}
const target =
registration.active ??
registration.waiting ??
registration.installing ??
(typeof navigator !== 'undefined' && 'serviceWorker' in navigator ? navigator.serviceWorker.controller : null);
if (!target) {
return;
}
target.postMessage({
type: metadata ? 'WEB_PUSH_SYNC_METADATA' : 'WEB_PUSH_CLEAR_METADATA',
payload: metadata,
});
}
export function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let index = 0; index < rawData.length; index += 1) {
outputArray[index] = rawData.charCodeAt(index);
}
return outputArray;
}
function isSamePushApplicationServerKey(leftKey: ArrayBuffer | null, rightKey: Uint8Array) {
if (!leftKey) {
return false;
}
const leftBytes = new Uint8Array(leftKey);
if (leftBytes.byteLength !== rightKey.byteLength) {
return false;
}
for (let index = 0; index < leftBytes.byteLength; index += 1) {
if (leftBytes[index] !== rightKey[index]) {
return false;
}
}
return true;
}
export function serializePushSubscription(subscription: PushSubscription): WebPushSubscriptionPayload {
const json = subscription.toJSON();
return {
endpoint: subscription.endpoint,
expirationTime: subscription.expirationTime,
keys: {
p256dh: json.keys?.p256dh ?? '',
auth: json.keys?.auth ?? '',
},
};
}
export async function ensureWebPushSubscriptionRegistered(
registration: ServiceWorkerRegistration,
options?: { deviceId?: string },
) {
const config = await fetchWebPushConfig();
if (!config.enabled || !config.publicKey) {
throw new Error('서버 Web Push 설정이 비어 있습니다.');
}
const expectedApplicationServerKey = urlBase64ToUint8Array(config.publicKey);
let subscription = await registration.pushManager.getSubscription();
if (
subscription &&
!isSamePushApplicationServerKey(subscription.options.applicationServerKey, expectedApplicationServerKey)
) {
await unregisterWebPushSubscription(subscription.endpoint).catch(() => undefined);
await subscription.unsubscribe().catch(() => undefined);
subscription = null;
}
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: expectedApplicationServerKey,
});
}
await registerWebPushSubscription(serializePushSubscription(subscription), options?.deviceId ?? getSavedNotificationDeviceId());
const metadata = buildWebPushRegistrationMetadata(true);
storeWebPushRegistrationMetadata(metadata);
await syncWebPushRegistrationMetadataWithServiceWorker(registration, metadata);
return subscription;
}
export async function syncExistingWebPushSubscriptionRegistration(
registration: ServiceWorkerRegistration,
options?: { deviceId?: string },
) {
const config = await fetchWebPushConfig();
if (!config.enabled || !config.publicKey || typeof Notification === 'undefined' || Notification.permission !== 'granted') {
return null;
}
const subscription = await registration.pushManager.getSubscription();
if (!subscription) {
return null;
}
await registerWebPushSubscription(serializePushSubscription(subscription), options?.deviceId ?? getSavedNotificationDeviceId());
const metadata = buildWebPushRegistrationMetadata(true);
storeWebPushRegistrationMetadata(metadata);
await syncWebPushRegistrationMetadataWithServiceWorker(registration, metadata);
return subscription;
}
export async function clearWebPushSubscriptionRegistration(registration: ServiceWorkerRegistration) {
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await unregisterWebPushSubscription(subscription.endpoint);
await subscription.unsubscribe();
}
storeWebPushRegistrationMetadata(null);
await syncWebPushRegistrationMetadataWithServiceWorker(registration, null);
}