feat: refresh shared chat and server workflows
This commit is contained in:
208
src/app/main/webPushRegistration.ts
Normal file
208
src/app/main/webPushRegistration.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user