diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 000000000..6d0bd55e6 --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,82 @@ +/* eslint-disable */ +// firebase-messaging-sw.js +importScripts( + "https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js", +); +importScripts( + "https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js", +); + +const ENV = { + LOCAL: "http://localhost:3000", + DEV: "https://web-dev.common.io", + STAGE: "https://web-staging.common.io", + PRODUCTION: "https://common.io", +}; + +const FIREBASE_CONFIG_ENV = { + DEV: { + apiKey: "AIzaSyDbTFuksgOkIVWDiFe_HG7-BE8X6Dwsg-0", + authDomain: "common-dev-34b09.firebaseapp.com", + databaseURL: "https://common-dev-34b09.firebaseio.com", + projectId: "common-dev-34b09", + storageBucket: "common-dev-34b09.appspot.com", + messagingSenderId: "870639147922", + appId: "1:870639147922:web:9ee954bb1dd52e25cb7f4b", + }, + STAGE: { + apiKey: "AIzaSyBASCWJMV64mZJObeFEitLmdUC1HqmtjJk", + authDomain: "common-staging-1d426.firebaseapp.com", + databaseURL: "https://common-staging-1d426.firebaseio.com", + projectId: "common-staging-1d426", + storageBucket: "common-staging-1d426.appspot.com", + messagingSenderId: "701579202562", + appId: "1:701579202562:web:5729d8a875f98f6709571b", + }, + PRODUCTION: { + apiKey: "AIzaSyAlYrKLd6KNKVkhmNEMKfb0cWHSWicCBOY", + authDomain: "common-production-67641.firebaseapp.com", + databaseURL: "https://common-production-67641.firebaseio.com", + projectId: "common-production-67641", + storageBucket: "common-production-67641.appspot.com", + messagingSenderId: "461029494046", + appId: "1:461029494046:web:4e2e4afbbeb7b487b48d0f", + }, +}; + +let firebaseConfig = {}; + +switch (location.origin) { + case ENV.LOCAL: + case ENV.DEV: { + firebaseConfig = FIREBASE_CONFIG_ENV.DEV; + break; + } + case ENV.STAGE: { + firebaseConfig = FIREBASE_CONFIG_ENV.STAGE; + break; + } + case ENV.PRODUCTION: { + firebaseConfig = FIREBASE_CONFIG_ENV.PRODUCTION; + break; + } + default: { + firebaseConfig = FIREBASE_CONFIG_ENV.DEV; + break; + } +} + +firebase.initializeApp(firebaseConfig); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage((payload) => { + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + data: payload.data, + icon: "/logo.png", + }; + + self.registration.showNotification(notificationTitle, notificationOptions); +}); diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 000000000..94e4735b4 Binary files /dev/null and b/public/logo.png differ diff --git a/src/config.tsx b/src/config.tsx index be0bab8e2..5975af4f0 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -27,6 +27,8 @@ export const local: Configuration = { deadSeaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", parentsForClimateCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", saadiaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", + vapidKey: + "BHVFyNetSC6oA2uFejnUFuDcSUYcas2R5lwW80z6gZc6zODp7rRdh2t8bht3LygJWjyI1toV165EYgdZqxCS_Y4", }; const dev: Configuration = { @@ -44,6 +46,8 @@ const dev: Configuration = { deadSeaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", parentsForClimateCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", saadiaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", + vapidKey: + "BHVFyNetSC6oA2uFejnUFuDcSUYcas2R5lwW80z6gZc6zODp7rRdh2t8bht3LygJWjyI1toV165EYgdZqxCS_Y4", }; const stage: Configuration = { @@ -62,6 +66,8 @@ const stage: Configuration = { deadSeaCommonId: "a55a1e9b-104a-4866-9f4f-3e017bbae281", parentsForClimateCommonId: "a55a1e9b-104a-4866-9f4f-3e017bbae281", saadiaCommonId: "a55a1e9b-104a-4866-9f4f-3e017bbae281", + vapidKey: + "BBvr8z8QaPSJJfIRxmjBrq5Vs49BY95uZK_6QFyR7gKWgwrs5toDy-hvwWEtk-rbkVHBgOu9l2orK45u1n--9M0", }; const production: Configuration = { @@ -80,6 +86,8 @@ const production: Configuration = { deadSeaCommonId: "6cfbfae6-2e5c-4b3b-ba70-e8fd871f48e2", parentsForClimateCommonId: "04ac2ec2-5cb2-4ab9-ae3f-5f223f482768", saadiaCommonId: "7c8c8996-b678-44df-9a57-e291431eb00f", + vapidKey: + "BKJ324iR-B5SoDG42bMrC_Q_poAv7BO-Z3AuMh5Grrg6TxO1QnN6mgzt2KyFFax0JSuuUhUKP-OrcTUPfboVqns", }; const config: ConfigurationObject = { diff --git a/src/pages/App/App.tsx b/src/pages/App/App.tsx index 4ffe2e826..5e0c24206 100644 --- a/src/pages/App/App.tsx +++ b/src/pages/App/App.tsx @@ -13,6 +13,7 @@ import { ThemeHandler, UserNotificationsAmountHandler, WebViewLoginHandler, + NotificationsHandler, } from "./handlers"; import { Router } from "./router"; @@ -34,6 +35,7 @@ const App = () => { + diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx new file mode 100644 index 000000000..de144355b --- /dev/null +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -0,0 +1,48 @@ +import { FC, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { NotificationService } from "@/services"; + +const NotificationsHandler: FC = () => { + const user = useSelector(selectUser()); + const userId = user?.uid; + const [isRegistered, setIsRegistered] = useState(false); + + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker + .register("/firebase-messaging-sw.js") + .then((registration) => { + setIsRegistered(true); + return registration; + }) + .catch((err) => { + console.log("ServiceWorker registration failed: ", err); + }); + } + }, []); + + useEffect(() => { + if (!userId && !isRegistered) { + return; + } + + let unsubscribeOnMessage; + (async () => { + const hasPermissions = await NotificationService.requestPermissions(); + if (hasPermissions) { + await NotificationService.saveFCMToken(); + + unsubscribeOnMessage = NotificationService.onForegroundMessage(); + } + })(); + + return () => { + unsubscribeOnMessage && unsubscribeOnMessage(); + }; + }, [userId, isRegistered]); + + return null; +}; + +export default NotificationsHandler; diff --git a/src/pages/App/handlers/NotificationsHandler/index.ts b/src/pages/App/handlers/NotificationsHandler/index.ts new file mode 100644 index 000000000..c7c53200f --- /dev/null +++ b/src/pages/App/handlers/NotificationsHandler/index.ts @@ -0,0 +1 @@ +export { default as NotificationsHandler } from "./NotificationsHandler"; \ No newline at end of file diff --git a/src/pages/App/handlers/index.ts b/src/pages/App/handlers/index.ts index 712e7a4d4..37e8eb0fb 100644 --- a/src/pages/App/handlers/index.ts +++ b/src/pages/App/handlers/index.ts @@ -3,3 +3,4 @@ export * from "./TextDirectionHandler"; export * from "./UserNotificationsAmountHandler"; export * from "./WebViewLoginHandler"; export * from "./ThemeHandler"; +export * from "./NotificationsHandler"; diff --git a/src/services/Notification.ts b/src/services/Notification.ts new file mode 100644 index 000000000..25e7597b3 --- /dev/null +++ b/src/services/Notification.ts @@ -0,0 +1,73 @@ +import firebase from "@/shared/utils/firebase"; +import firebaseConfig from "@/config"; +import Api from "./Api"; + +enum NOTIFICATIONS_PERMISSIONS { + DEFAULT = "default", + DENIED = "denied", + GRANTED = "granted" +} + + +class NotificationService { + private endpoints: { + setFCMToken: string; + }; + + constructor() { + this.endpoints = { + setFCMToken: '/users/auth/google/set-fcm-token', + }; + } + + public requestPermissions = async (): Promise => { + try { + if(Notification.permission === NOTIFICATIONS_PERMISSIONS.GRANTED) { + return true; + } + const permission = await Notification.requestPermission(); + if (permission === NOTIFICATIONS_PERMISSIONS.GRANTED) { + return true; + } else { + return false; + } + } catch (err) { + return false; + } + } + + public saveFCMToken = async (): Promise => { + try { + const token = await firebase.messaging().getToken({ vapidKey: firebaseConfig.vapidKey }); + if (token) { + + await Api.post( + this.endpoints.setFCMToken, + { + token, + } + ); + } + } catch (error) { + console.error("An error occurred while retrieving token. ", error); + } + } + + public onForegroundMessage = () => { + const unsubscribe = firebase.messaging().onMessage((payload) => { + + const { title, body } = payload.notification; + if (Notification.permission === 'granted') { + new Notification(title, { + body, + data: payload?.data, + icon: "/logo.png", + }); + } + }); + + return unsubscribe; + } +} + +export default new NotificationService(); diff --git a/src/services/index.ts b/src/services/index.ts index 03d9bf706..bf6d10f1e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -25,3 +25,4 @@ export { } from "./DiscussionMessage"; export { default as NotionService } from "./Notion"; export { default as FeatureFlagService } from "./FeatureFlag"; +export { default as NotificationService } from "./Notification"; diff --git a/src/shared/interfaces/Configuration.tsx b/src/shared/interfaces/Configuration.tsx index 94c71bcfc..73f6e96ef 100644 --- a/src/shared/interfaces/Configuration.tsx +++ b/src/shared/interfaces/Configuration.tsx @@ -15,6 +15,7 @@ export interface Configuration { deadSeaCommonId: string; parentsForClimateCommonId: string; saadiaCommonId: string; + vapidKey: string; } export type ConfigurationObject = Record; diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index 3db4544c9..a7f790cf0 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -1,6 +1,7 @@ import firebase from "firebase/compat/app"; import "firebase/compat/auth"; import "firebase/compat/firestore"; +import "firebase/compat/messaging"; import "firebase/compat/performance"; import "firebase/compat/storage"; import { getPerformance } from "firebase/performance"; diff --git a/src/shared/utils/tests/mockConfig.ts b/src/shared/utils/tests/mockConfig.ts index 502ef3bcf..8f9410f5a 100644 --- a/src/shared/utils/tests/mockConfig.ts +++ b/src/shared/utils/tests/mockConfig.ts @@ -18,5 +18,6 @@ jest.mock( deadSeaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", parentsForClimateCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", saadiaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", + vapidKey: "VAPID_KEY", }) );