Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Notification Handling and Rename Firebase Service Worker Scope #497

Merged
merged 9 commits into from
Jan 23, 2025
114 changes: 50 additions & 64 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, Suspense } from 'react';
import React, { Suspense } from 'react';
import { Routes, Route, Outlet, useLocation } from 'react-router-dom';
// Import i18next and set up translations
import { I18nextProvider } from 'react-i18next';
Expand All @@ -7,7 +7,7 @@ import i18n from './i18n';
import { withSessionContext } from './context/SessionContext';

import FadeInContentTransition from './components/Transitions/FadeInContentTransition';
import HandlerNotification from './components/Notifications/HandlerNotification';
import NewCredentialNotification from './components/Notifications/NewCredentialNotification';
import Snowfalling from './components/ChristmasAnimation/Snowfalling';
import Spinner from './components/Shared/Spinner';

Expand All @@ -19,6 +19,8 @@ import { withUriHandler } from './UriHandler';
import { withCredentialParserContext } from './context/CredentialParserContext';
import { withOpenID4VPContext } from './context/OpenID4VPContext';
import { withOpenID4VCIContext } from './context/OpenID4VCIContext';
import useNewCredentialListener from './hooks/useNewCredentialListener';
import BackgroundNotificationClickHandler from './components/Notifications/BackgroundNotificationClickHandler';

const reactLazyWithNonDefaultExports = (load, ...names) => {
const nonDefaults = (names ?? []).map(name => {
Expand Down Expand Up @@ -88,70 +90,54 @@ const NotFound = lazyWithDelay(() => import('./pages/NotFound/NotFound'), 400);

function App() {
const location = useLocation();
useEffect(() => {
if (navigator?.serviceWorker) {
navigator.serviceWorker.addEventListener('message', handleMessage);
// Clean up the event listener when the component unmounts
return () => {
navigator.serviceWorker.removeEventListener('message', handleMessage);
};
}

}, []);

// Handle messages received from the service worker
const handleMessage = (event) => {
if (event.data.type === 'navigate') {
// Remove any parameters from the URL
const homeURL = window.location.origin + window.location.pathname;
// Redirect the current tab to the home URL
window.location.href = homeURL;
}
};
const { notification, clearNotification } = useNewCredentialListener();

return (
<I18nextProvider i18n={i18n}>
<Snowfalling />
<Suspense fallback={<Spinner />}>
<HandlerNotification />
<UpdateNotification />
<Routes>
<Route element={
<PrivateRoute>
<Layout>
<Suspense fallback={<Spinner size='small' />}>
<PrivateRoute.NotificationPermissionWarning />
<FadeInContentTransition appear reanimateKey={location.pathname}>
<Outlet />
</FadeInContentTransition>
</Suspense>
</Layout>
</PrivateRoute>
}>
<Route path="/settings" element={<Settings />} />
<Route path="/" element={<Home />} />
<Route path="/credential/:credentialId" element={<Credential />} />
<Route path="/credential/:credentialId/history" element={<CredentialHistory />} />
<Route path="/credential/:credentialId/details" element={<CredentialDetails />} />
<Route path="/history" element={<History />} />
<Route path="/history/:historyId" element={<HistoryDetail />} />
<Route path="/add" element={<AddCredentials />} />
<Route path="/send" element={<SendCredentials />} />
<Route path="/verification/result" element={<VerificationResult />} />
<Route path="/cb/*" element={<Home />} />
</Route>
<Route element={
<FadeInContentTransition reanimateKey={location.pathname}>
<Outlet />
</FadeInContentTransition>
}>
<Route path="/login" element={<Login />} />
<Route path="/login-state" element={<LoginState />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Suspense>
</I18nextProvider>
<>
<BackgroundNotificationClickHandler />
<I18nextProvider i18n={i18n}>
<Snowfalling />
<Suspense fallback={<Spinner />}>
<NewCredentialNotification notification={notification} clearNotification={clearNotification} />
<UpdateNotification />
<Routes>
<Route element={
<PrivateRoute>
<Layout>
<Suspense fallback={<Spinner size='small' />}>
<PrivateRoute.NotificationPermissionWarning />
<FadeInContentTransition appear reanimateKey={location.pathname}>
<Outlet />
</FadeInContentTransition>
</Suspense>
</Layout>
</PrivateRoute>
}>
<Route path="/settings" element={<Settings />} />
<Route path="/" element={<Home />} />
<Route path="/credential/:credentialId" element={<Credential />} />
<Route path="/credential/:credentialId/history" element={<CredentialHistory />} />
<Route path="/credential/:credentialId/details" element={<CredentialDetails />} />
<Route path="/history" element={<History />} />
<Route path="/history/:historyId" element={<HistoryDetail />} />
<Route path="/add" element={<AddCredentials />} />
<Route path="/send" element={<SendCredentials />} />
<Route path="/verification/result" element={<VerificationResult />} />
<Route path="/cb/*" element={<Home />} />
</Route>
<Route element={
<FadeInContentTransition reanimateKey={location.pathname}>
<Outlet />
</FadeInContentTransition>
}>
<Route path="/login" element={<Login />} />
<Route path="/login-state" element={<LoginState />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Suspense>
</I18nextProvider>
</>
);
}

Expand Down
28 changes: 28 additions & 0 deletions src/components/Notifications/BackgroundNotificationClickHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from 'react';

const BackgroundNotificationClickHandler = () => {
useEffect(() => {
const handleNotificationClickMessage = (event) => {
if (event.data?.type === 'navigate') {
const targetUrl = event.data.url || '/';
window.location.href = targetUrl; // Navigate to the target URL
}
};

// Add the service worker message listener
if (navigator?.serviceWorker) {
navigator.serviceWorker.addEventListener('message', handleNotificationClickMessage);
}

// Cleanup the listener on unmount
return () => {
if (navigator?.serviceWorker) {
navigator.serviceWorker.removeEventListener('message', handleNotificationClickMessage);
}
};
}, []);

return null; // This component does not render any UI
};

export default BackgroundNotificationClickHandler;
73 changes: 0 additions & 73 deletions src/components/Notifications/HandlerNotification.js

This file was deleted.

50 changes: 50 additions & 0 deletions src/components/Notifications/NewCredentialNotification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useEffect, useCallback } from 'react';
import toast, { Toaster } from 'react-hot-toast';
import { AiOutlineClose } from 'react-icons/ai';
import Logo from '../Logo/Logo';
import { useLocation } from "react-router-dom";

const ToastDisplay = ({ id, notification }) => {
return (
<div
className="flex justify-between items-center p-3 bg-white rounded-lg text-gray-600 max-w-3xl mx-auto cursor-pointer"
onClick={() => window.location.href = '/'}
>
<div className="w-1/3 flex items-center justify-start mr-6">
<Logo />
</div>
<div className="flex-grow text-center">
<p className="font-bold text-lg">{notification?.title}</p>
<p>{notification?.body}</p>
</div>
<button onClick={(e) => {
toast.dismiss(id);
e.stopPropagation();
}}
className="focus:outline-none ml-6"
>
<AiOutlineClose size={24} />
</button>
</div>
);
};

const NewCredentialNotification = ({ notification, clearNotification }) => {
const location = useLocation();
const showToast = useCallback(() => {
toast((t) => <ToastDisplay id={t.id} notification={notification} />);
clearNotification();
}, [notification, clearNotification]);

useEffect(() => {
if (notification && location.pathname === '/') {
showToast();
}
}, [notification, location, showToast]);

return (
<Toaster />
);
};

export default NewCredentialNotification;
23 changes: 12 additions & 11 deletions src/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ export async function isEnabledAndIsSupported() {
export async function register() {
if (await isEnabledAndIsSupported() && 'serviceWorker' in navigator) {
try {
const existingRegistration = await navigator.serviceWorker.getRegistration('/notifications/');
const existingRegistration = await navigator.serviceWorker.getRegistration('/firebase-cloud-messaging-push-scope');
if (existingRegistration) {
console.log('Service Worker is already registered. Scope:', existingRegistration.scope);
} else {
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/notifications/' });
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/firebase-cloud-messaging-push-scope' });
console.log('App: Firebase Messaging Service Worker registered! Scope is:', registration.scope);
}
} catch (err) {
Expand Down Expand Up @@ -70,7 +70,7 @@ const reRegisterServiceWorkerAndGetToken = async () => {
if ('serviceWorker' in navigator) {
try {
// Re-register the service worker
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/notifications/' });
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/firebase-cloud-messaging-push-scope' });
if (registration) {
console.log('Service Worker re-registered', registration);
const token = await requestForToken();
Expand Down Expand Up @@ -112,14 +112,15 @@ export const fetchToken = async () => {
return null; // Return null in case of failure
};

export const onMessageListener = () =>
new Promise(async (resolve) => {
if (await isEnabledAndIsSupported()) {
onMessage(messaging, (payload) => {
resolve(payload);
});
}
});
export const onMessageListener = async (callback) => {
if (await isEnabledAndIsSupported()) {
onMessage(messaging, (payload) => {
callback(payload);
});
} else {
console.error('Messaging is not supported or enabled');
}
};

const initializeFirebaseAndMessaging = async () => {
if (notificationApiIsSupported) {
Expand Down
56 changes: 56 additions & 0 deletions src/hooks/useNewCredentialListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useState, useContext, useRef } from 'react';
import { onMessageListener } from '../firebase';
import CredentialsContext from '../context/CredentialsContext';

const useNewCredentialListener = () => {
const [notification, setNotification] = useState(() => {
// Retrieve notification from sessionStorage on initial load
const savedNotification = sessionStorage.getItem('newCredentialNotification');
return savedNotification ? JSON.parse(savedNotification) : null;
});

const { getData } = useContext(CredentialsContext);

// Use a ref to store getData to prevent triggering useEffect when it changes
const getDataRef = useRef(getData);

useEffect(() => {
getDataRef.current = getData; // Keep the ref updated with the latest getData function
}, [getData]);

useEffect(() => {
const listenForNotification = (payload) => {
console.log('Notification received:', payload);

const newNotification = {
title: payload?.notification?.title,
body: payload?.notification?.body,
};
// Save notification to sessionStorage
sessionStorage.setItem('newCredentialNotification', JSON.stringify(newNotification));

// Update the state
setNotification(newNotification);

getDataRef.current();
};

onMessageListener(listenForNotification);

// Optional cleanup function for consistency and future-proofing
return () => {
// Firebase's `onMessage` does not require unsubscription
// Add cleanup logic here if needed in the future
};

}, []);

const clearNotification = () => {
setNotification(null);
sessionStorage.removeItem('newCredentialNotification'); // Clear from sessionStorage
};

return { notification, clearNotification };
};

export default useNewCredentialListener;
Loading