/** * FCM (Firebase Cloud Messaging) Push Notification Handler * Capacitor 앱에서만 동작, 웹 브라우저에서는 무시됨 * * Payload data 스키마: * - type: 알림 타입 (invoice_failed, order_completed 등) * - url: 클릭 시 이동 URL * - sound_key: 사운드 파일 키 (sounds/{sound_key}.wav) */ (function () { 'use strict'; const DEBUG = window.SAM_CONFIG?.debug || false; const log = (...args) => { if (DEBUG) console.log('[FCM]', ...args); }; const CONFIG = { apiBaseUrl: window.SAM_CONFIG?.apiBaseUrl || 'https://api.codebridge-x.com', fcmTokenKey: 'fcm_token', apiTokenKey: 'api_access_token', apiKeyHeader: window.SAM_CONFIG?.apiKey || '', soundBasePath: '/sounds/', defaultSound: 'default', }; // 앱 상태 (포그라운드 여부) let isAppForeground = true; // DOM 준비되면 실행 (이미 로드됐으면 즉시) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap); } else { bootstrap(); } async function bootstrap() { // Capacitor 네이티브 환경(ios, android)에서만 실행 const platform = window.Capacitor?.getPlatform?.(); if (platform !== 'ios' && platform !== 'android') { log(' Not running in native app (platform:', platform || 'web', ')'); return; } if (!window.Capacitor?.Plugins?.PushNotifications) { log(' PushNotifications plugin not available'); return; } // 앱 상태 리스너 (포그라운드/백그라운드) if (window.Capacitor?.Plugins?.App) { const { App } = Capacitor.Plugins; App.addListener('appStateChange', ({ isActive }) => { isAppForeground = isActive; log(' App state:', isActive ? 'foreground' : 'background'); }); } await initializeFCM(); } async function initializeFCM() { const { PushNotifications } = Capacitor.Plugins; try { // ✅ 1. 기존 리스너 제거 PushNotifications.removeAllListeners(); // ✅ 2. 리스너를 먼저 등록 (가장 중요) PushNotifications.addListener('registration', async (token) => { log(' 🔥 registration event fired'); log(' Token received:', token.value?.substring(0, 20) + '...'); await handleTokenRegistration(token.value); }); PushNotifications.addListener('registrationError', (err) => { console.error('[FCM] Registration error:', err); }); PushNotifications.addListener('pushNotificationReceived', (notification) => { log(' Push received (foreground):', notification); handleForegroundNotification(notification); }); PushNotifications.addListener('pushNotificationActionPerformed', (action) => { log(' Push action performed:', action); const data = action.notification?.data; if (data?.url) { window.location.href = data.url; } }); // ✅ 3. 그 다음에 권한 요청 const perm = await PushNotifications.requestPermissions(); log(' Push permission:', perm.receive); if (perm.receive !== 'granted') { log(' Push permission not granted'); return; } // ✅ 4. 마지막에 register 호출 await PushNotifications.register(); } catch (error) { console.error('[FCM] Initialization error:', error); } } async function handleTokenRegistration(newToken) { const oldToken = sessionStorage.getItem(CONFIG.fcmTokenKey); if (oldToken === newToken) { log(' Token unchanged, skip'); return; } const success = await registerTokenToServer(newToken); if (success) { sessionStorage.setItem(CONFIG.fcmTokenKey, newToken); log(' Token saved to sessionStorage'); } } async function registerTokenToServer(token) { const accessToken = sessionStorage.getItem(CONFIG.apiTokenKey); if (!accessToken) { console.warn('[FCM] No API access token'); return false; } try { const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${accessToken}`, }; if (CONFIG.apiKeyHeader) { headers['X-API-KEY'] = CONFIG.apiKeyHeader; } const response = await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/register-token`, { method: 'POST', headers, body: JSON.stringify({ token, platform: getDevicePlatform(), device_name: getDeviceName(), app_version: getAppVersion(), }), }); if (response.ok) { log(' Token registered successfully'); return true; } console.error('[FCM] Token registration failed:', response.status); return false; } catch (error) { console.error('[FCM] Failed to send token:', error); return false; } } async function unregisterToken() { const token = sessionStorage.getItem(CONFIG.fcmTokenKey); const accessToken = sessionStorage.getItem(CONFIG.apiTokenKey); if (!token) return true; try { if (accessToken) { await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/unregister-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, }, body: JSON.stringify({ token }), }); } } catch (e) { console.warn('[FCM] Unregister failed, clearing local token'); } sessionStorage.removeItem(CONFIG.fcmTokenKey); return true; } function getDevicePlatform() { if (window.Capacitor) { const p = Capacitor.getPlatform(); if (p === 'ios' || p === 'android') return p; } return 'web'; } function getDeviceName() { return navigator.userAgent?.substring(0, 100) || null; } function getAppVersion() { return window.SAM_CONFIG?.appVersion || null; } /** * 포그라운드 알림 처리 */ function handleForegroundNotification(notification) { const data = notification.data || {}; const type = data.type || 'default'; const url = data.url; const soundKey = data.sound_key; log(' Notification data:', { type, url, soundKey }); // 1. 포그라운드에서만 사운드 재생 (백그라운드는 OS 채널 사운드) if (isAppForeground && soundKey) { playNotificationSound(soundKey); } // 2. 타입별 토스트 메시지 표시 const title = notification.title || '알림'; const body = notification.body || ''; const toastType = getToastTypeByNotificationType(type); if (typeof showToast === 'function') { showToast(`${title}: ${body}`, toastType); } // 3. 클릭 가능한 토스트 (URL이 있는 경우) if (url && typeof showClickableToast === 'function') { showClickableToast(title, body, () => { window.location.href = url; }); } } /** * 알림 사운드 재생 (포그라운드 전용) */ function playNotificationSound(soundKey) { try { const soundPath = `${CONFIG.soundBasePath}${soundKey}.wav`; const audio = new Audio(soundPath); audio.volume = 0.5; audio.play().catch(err => { console.warn('[FCM] Sound play failed, trying default:', err.message); // 기본 사운드 시도 if (soundKey !== CONFIG.defaultSound) { const defaultAudio = new Audio(`${CONFIG.soundBasePath}${CONFIG.defaultSound}.wav`); defaultAudio.volume = 0.5; defaultAudio.play().catch(() => {}); } }); } catch (err) { console.warn('[FCM] Sound error:', err); } } /** * 알림 타입별 토스트 스타일 결정 */ function getToastTypeByNotificationType(type) { const typeMap = { // 긴급/에러 'invoice_failed': 'error', 'payment_failed': 'error', 'order_cancelled': 'error', // 경고 'approval_required': 'warning', 'stock_low': 'warning', // 성공 'order_completed': 'success', 'payment_completed': 'success', 'approval_approved': 'success', // 기본 'default': 'info', }; return typeMap[type] || 'info'; } /** * 클릭 가능한 토스트 (URL 이동용) * showClickableToast가 없으면 기본 토스트 사용 */ if (typeof window.showClickableToast === 'undefined') { window.showClickableToast = function(title, body, onClick) { // 기본 구현: 일반 토스트 + 클릭 핸들러는 무시 if (typeof showToast === 'function') { showToast(`${title}: ${body}`, 'info'); } log(' showClickableToast not implemented, URL click ignored'); }; } window.FCM = { unregisterToken, reinitialize: initializeFCM, // 디버그/테스트용 playSound: playNotificationSound, testForeground: (data) => handleForegroundNotification({ title: 'Test', body: 'Test notification', data }), }; })();