209 lines
5.9 KiB
TypeScript
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);
|
|
}
|