From 808c9473d250da32c716b560bd408c7eae85702e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=B8=EC=A7=84?= <50974359+hozzijeong@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:37:32 +0900 Subject: [PATCH] =?UTF-8?q?FCM=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=20(#469)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: friebase FCM 관리 모듈로 분리 * refactor: Notification 제거 및 구독 함수 수정 --- .../mypage/PushAlert/PushToggle.tsx | 22 ++++++ .../src/components/mypage/PushAlert/index.tsx | 17 ++--- frontend/src/hooks/@common/usePushAlert.ts | 9 +-- frontend/src/hooks/queries/auth/useWebPush.ts | 10 +-- frontend/src/models/FCMMessaging.ts | 70 +++++++++++++++++++ frontend/src/models/PushStatus.ts | 8 +-- frontend/src/registerServiceWork.ts | 22 +++--- frontend/src/utils/firebase.ts | 63 ----------------- 8 files changed, 122 insertions(+), 99 deletions(-) create mode 100644 frontend/src/components/mypage/PushAlert/PushToggle.tsx create mode 100644 frontend/src/models/FCMMessaging.ts delete mode 100644 frontend/src/utils/firebase.ts diff --git a/frontend/src/components/mypage/PushAlert/PushToggle.tsx b/frontend/src/components/mypage/PushAlert/PushToggle.tsx new file mode 100644 index 000000000..6b6337267 --- /dev/null +++ b/frontend/src/components/mypage/PushAlert/PushToggle.tsx @@ -0,0 +1,22 @@ +import Toggle from 'components/@common/Toggle'; +import usePushAlert from 'hooks/@common/usePushAlert'; +import PushStatus from 'models/PushStatus'; + +const PushToggle = () => { + const pushSupport = PushStatus.getIsSupport(); + const notificationDenied = PushStatus.getPermission(); + const { currentSubscribe, subscribeAlert, unSubscribeAlert } = usePushAlert(); + + return ( + + ); +}; + +export default PushToggle; diff --git a/frontend/src/components/mypage/PushAlert/index.tsx b/frontend/src/components/mypage/PushAlert/index.tsx index c8ac82be1..29876d233 100644 --- a/frontend/src/components/mypage/PushAlert/index.tsx +++ b/frontend/src/components/mypage/PushAlert/index.tsx @@ -1,11 +1,9 @@ -import Toggle from 'components/@common/Toggle'; +import { Suspense } from 'react'; import { PushAlertContent, PushAlertWrapper, WarnParagraph } from './PushAlert.style'; -import usePushAlert from 'hooks/@common/usePushAlert'; import PushStatus from 'models/PushStatus'; +import PushToggle from './PushToggle'; const PushAlert = () => { - const { currentSubscribe, subscribeAlert, unSubscribeAlert } = usePushAlert(); - const pushSupport = PushStatus.getIsSupport(); const notificationDenied = PushStatus.getPermission(); @@ -13,14 +11,9 @@ const PushAlert = () => {

리마인더 알림 받기

- + 로딩중잉ㅂ니다?}> + +
{!pushSupport && 지원하지 않는 브라우저 또는 os입니다.} {notificationDenied === 'denied' && ( diff --git a/frontend/src/hooks/@common/usePushAlert.ts b/frontend/src/hooks/@common/usePushAlert.ts index 757a63abf..c82abd35a 100644 --- a/frontend/src/hooks/@common/usePushAlert.ts +++ b/frontend/src/hooks/@common/usePushAlert.ts @@ -1,6 +1,6 @@ import useWebPush from 'hooks/queries/auth/useWebPush'; +import FCMMessaging from 'models/FCMMessaging'; import PushStatus from 'models/PushStatus'; -import { getCurrentToken } from 'utils/firebase'; import useAddToast from './useAddToast'; const usePushAlert = () => { @@ -14,21 +14,22 @@ const usePushAlert = () => { } // subscribe를 하지 않으려면 해당 토큰을 제거해야 한다. + const permission = await Notification.requestPermission(); + PushStatus.setPermission(permission); if (permission !== 'granted') { addToast({ type: 'info', message: '알림이 허용되지 않았습니다', time: 3000 }); - PushStatus.setPermission(permission); return; } - // TODO: 사용자가 처음 알림 설정을 한 것인지 아니면 기존에 try { let token = PushStatus.getCurrentToken(); // 이중 throw... 이게 괜찮은걸까? if (token === null) { - token = await getCurrentToken(); + // TODO: 여기서 시간이 좀 걸림;; + token = await FCMMessaging.getCurrentToken(); if (token === null) throw new Error(); } diff --git a/frontend/src/hooks/queries/auth/useWebPush.ts b/frontend/src/hooks/queries/auth/useWebPush.ts index 2b56d00d3..e4d85305e 100644 --- a/frontend/src/hooks/queries/auth/useWebPush.ts +++ b/frontend/src/hooks/queries/auth/useWebPush.ts @@ -1,8 +1,8 @@ import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import useAddToast from 'hooks/@common/useAddToast'; +import FCMMessaging from 'models/FCMMessaging'; import PushStatus from 'models/PushStatus'; import WebPushSubscribeAPI, { SUBSCRIBE_URL } from 'apis/webPush'; -import { deleteCurrentToken, getCurrentToken } from 'utils/firebase'; import noRetryIfUnauthorized from 'utils/noRetryIfUnauthorized'; import throwOnInvalidStatus from 'utils/throwOnInvalidStatus'; @@ -34,8 +34,8 @@ const useWebPush = () => { queryClient.setQueryData([SUBSCRIBE_URL], context?.prevData); addToast({ type: 'error', message: error.message }); - await deleteCurrentToken(); - const currentToken = await getCurrentToken(); + await FCMMessaging.deleteCurrentToken(); + const currentToken = await FCMMessaging.getCurrentToken(); PushStatus.setCurrentToken(currentToken); }, onSettled: () => { @@ -59,8 +59,8 @@ const useWebPush = () => { return { prevData }; }, onSuccess: async () => { - await deleteCurrentToken(); - const currentToken = await getCurrentToken(); + await FCMMessaging.deleteCurrentToken(); + const currentToken = await FCMMessaging.getCurrentToken(); PushStatus.setCurrentToken(currentToken); }, onError: (error, __, context) => { diff --git a/frontend/src/models/FCMMessaging.ts b/frontend/src/models/FCMMessaging.ts new file mode 100644 index 000000000..a4d3fb3cf --- /dev/null +++ b/frontend/src/models/FCMMessaging.ts @@ -0,0 +1,70 @@ +import { Analytics, getAnalytics } from 'firebase/analytics'; +import { FirebaseApp, FirebaseOptions, initializeApp } from 'firebase/app'; +import { Messaging, deleteToken, getMessaging, getToken, onMessage } from 'firebase/messaging'; + +const firebaseConfig = { + apiKey: 'AIzaSyAOEUhyDZ1FQ2Ly77t-TNEqzb-686teUKU', + authDomain: 'pium-7ddfe.firebaseapp.com', + projectId: 'pium-7ddfe', + storageBucket: 'pium-7ddfe.appspot.com', + messagingSenderId: '66938335591', + appId: '1:66938335591:web:88ebf4f7f9dba08031ffc2', + measurementId: 'G-8SL2D547VW', +}; + +class FCMMessaging { + private app: FirebaseApp | null = null; + private messaging: Messaging | null = null; + private analytics: Analytics | null = null; + + constructor(config: FirebaseOptions) { + this.app = initializeApp(config); + this.analytics = getAnalytics(this.app); + } + + checkMessaging() { + return this.messaging ? true : false; + } + + registerMessaging() { + if (!this.app) throw new Error('메세지 등록을 위해서는 FCM 초기화가 필요합니다.'); + this.messaging = getMessaging(this.app); + } + + setOnMessaging() { + if (!this.messaging) return; + onMessage(this.messaging, (payload) => { + const { notification } = payload; + + const notificationTitle = notification?.title ?? '피움 알림'; + const notificationOptions = { + body: notification?.body ?? '내용이 없습니다', + icon: notification?.icon ?? './assets/favicon-32x32.png', + }; + const foregroundNotification = new Notification(notificationTitle, notificationOptions); + + foregroundNotification.onclick = function (event) { + event.preventDefault(); + foregroundNotification.close(); + }; + }); + } + + async getCurrentToken() { + if (!this.messaging) throw new Error('등록된 메세지가 없어서 토큰을 반환할 수 없습니다'); + const permission = Notification.permission; + + if (permission !== 'granted') return null; + + return await getToken(this.messaging, { + vapidKey: process.env.VAPID_PUBLIC_KEY ?? '', + }); + } + + async deleteCurrentToken() { + if (!this.messaging) throw new Error('등록된 메세지가 없어서 토큰을 삭제할 수 없습니다'); + return await deleteToken(this.messaging); + } +} + +export default new FCMMessaging(firebaseConfig); diff --git a/frontend/src/models/PushStatus.ts b/frontend/src/models/PushStatus.ts index da659114f..edbf5c058 100644 --- a/frontend/src/models/PushStatus.ts +++ b/frontend/src/models/PushStatus.ts @@ -1,5 +1,5 @@ // TODO: 클래스로 변환해서 사용해보기 -import { deleteCurrentToken } from 'utils/firebase'; +import FCMMessaging from './FCMMessaging'; type NotificationPermission = 'granted' | 'default' | 'denied'; @@ -17,7 +17,7 @@ export const isSupported = const initialPushStatus: PushStatusState = { pushSupport: isSupported, currentToken: null, - notificationPermission: Notification.permission, + notificationPermission: 'default', }; class PushStatus { @@ -28,7 +28,7 @@ class PushStatus { if (pushStatus.notificationPermission !== 'granted') { this.pushStatus.currentToken = null; - await deleteCurrentToken(); + await FCMMessaging.deleteCurrentToken(); } } @@ -47,7 +47,7 @@ class PushStatus { async setPermission(permission: NotificationPermission) { if (permission !== 'granted') { this.pushStatus.currentToken = null; - await deleteCurrentToken(); + await FCMMessaging.deleteCurrentToken(); } this.pushStatus.notificationPermission = permission; diff --git a/frontend/src/registerServiceWork.ts b/frontend/src/registerServiceWork.ts index afd686020..53bbb20ff 100644 --- a/frontend/src/registerServiceWork.ts +++ b/frontend/src/registerServiceWork.ts @@ -1,9 +1,12 @@ -import PushStatus, { isSupported } from 'models/PushStatus'; -import { getCurrentToken } from 'utils/firebase'; +import { isSupported } from 'firebase/messaging'; +import FCMMessaging from 'models/FCMMessaging'; +import PushStatus, { isSupported as isBrowserSupport } from 'models/PushStatus'; const registerPwaServiceWorker = async (workerPath: string) => { // 지원하지 않는 브라우저라면 return; - if (!isSupported) { + const FCMSupported = await isSupported(); + + if (!isBrowserSupport || !FCMSupported) { return; } // 기존에 있던 서비스 워커를 가져옴 @@ -22,16 +25,13 @@ const registerPwaServiceWorker = async (workerPath: string) => { } } - // 새로운 서비스워커로 업데이트 - // 초기에 시작할 때 currentToken을 일단 받음 - // permission을 물어보지 않아서 getCurrentToken을 받아올 수 없음. - - const currentToken = await getCurrentToken(); + FCMMessaging.registerMessaging(); + FCMMessaging.setOnMessaging(); PushStatus.updatePushStatus({ - notificationPermission: Notification.permission, - currentToken, - pushSupport: isSupported, + notificationPermission: 'default', + currentToken: null, + pushSupport: FCMSupported && isBrowserSupport, }); }; diff --git a/frontend/src/utils/firebase.ts b/frontend/src/utils/firebase.ts deleted file mode 100644 index 76617b5c5..000000000 --- a/frontend/src/utils/firebase.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Import the functions you need from the SDKs you need -import { getAnalytics } from 'firebase/analytics'; -import { initializeApp } from 'firebase/app'; -import { deleteToken, getMessaging, getToken, onMessage } from 'firebase/messaging'; - -// TODO: Add SDKs for Firebase products that you want to use -// https://firebase.google.com/docs/web/setup#available-libraries - -// Your web app's Firebase configuration -// For Firebase JS SDK v7.20.0 and later, measurementId is optional -const firebaseConfig = { - apiKey: 'AIzaSyAOEUhyDZ1FQ2Ly77t-TNEqzb-686teUKU', - authDomain: 'pium-7ddfe.firebaseapp.com', - projectId: 'pium-7ddfe', - storageBucket: 'pium-7ddfe.appspot.com', - messagingSenderId: '66938335591', - appId: '1:66938335591:web:88ebf4f7f9dba08031ffc2', - measurementId: 'G-8SL2D547VW', -}; - -// Initialize Firebase -const app = initializeApp(firebaseConfig); -const analytics = getAnalytics(app); -const messaging = getMessaging(app); - -/** - * firebase에서 pushManager를 등록하지 않고도 값 사용이 가능한 이유. - * - * getToken 메서드에서 getTokenInternal이라는 메서드를 return 하는데, getPushSubscription 이라는 메서드에서 pushManager를 통해서 serviceWorker에 등록을 하기 때문. - * getTokenInternal를 호출 하기 전 vapidKey를 업데이트 하고, 해당 messaging 객체를 넘겨준다. - * - * https://github.dev/firebase/firebase-js-sdk - */ - -const getCurrentToken = async () => { - const permission = Notification.permission; - - if (permission !== 'granted') return null; - - return await getToken(messaging, { - vapidKey: process.env.VAPID_PUBLIC_KEY ?? '', - }); -}; - -const deleteCurrentToken = async () => await deleteToken(messaging); - -onMessage(messaging, (payload) => { - const { notification } = payload; - - const notificationTitle = notification?.title ?? '피움 알림'; - const notificationOptions = { - body: notification?.body ?? '내용이 없습니다', - icon: notification?.icon ?? './assets/favicon-32x32.png', - }; - const foregroundNotification = new Notification(notificationTitle, notificationOptions); - - foregroundNotification.onclick = function (event) { - event.preventDefault(); - foregroundNotification.close(); - }; -}); - -export { analytics, messaging, getCurrentToken, deleteCurrentToken };