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

Firebase for Push notifications #685

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
935 changes: 935 additions & 0 deletions simplq/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions simplq/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"aos": "^2.3.4",
"axios": "^0.21.1",
"components": "^0.1.0",
"firebase": "^9.1.3",
"google-libphonenumber": "^3.2.13",
"moment": "^2.29.1",
"node-sass": "^4.14.1",
Expand Down
23 changes: 23 additions & 0 deletions simplq/public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* eslint-disable */
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/9.1.3/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.1.3/firebase-messaging-compat.js');

// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
apiKey: 'AIzaSyCCW7gmWZli24N61NShh-8ALxVy3WtjqNU',
authDomain: 'simplq-fe712.firebaseapp.com',
projectId: 'simplq-fe712',
storageBucket: 'simplq-fe712.appspot.com',
messagingSenderId: '348531792421',
appId: '1:348531792421:web:c481f1740405522d0f3dcc',
measurementId: 'G-8N2SDV8VF5',
});

// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();
13 changes: 4 additions & 9 deletions simplq/src/api/auth.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useAuth0 } from '@auth0/auth0-react';
import { v4 as uuidv4 } from 'uuid';
import axios from 'axios';
import * as Sentry from '@sentry/react';

// config.js is generated at runtime, so disabling eslint warning
/* eslint-disable import/no-unresolved, import/extensions */
import { raiseException } from 'services/alerts';
import { baseURL } from '../config';

const ANONYMOUS_DEVICE_ID_KEY = 'anonymous-device-id';
Expand Down Expand Up @@ -53,14 +53,9 @@ const useMakeAuthedRequest = () => {
Authorization: await getAuthHeaderValue(auth),
},
}).catch((error) => {
// log error to sentry for alerting
let eventId;
Sentry.withScope((scope) => {
scope.setTag('Caught-at', 'API request');
eventId = Sentry.captureException(error);
});
// eslint-disable-next-line no-console
console.log(`Sentry exception captured, event id is ${eventId}`);
// log error to alerting
raiseException(error, 'API request');

// In case of request failure, extract error from response body
if (error.response) {
// Response has been received from the server
Expand Down
2 changes: 2 additions & 0 deletions simplq/src/api/requestFactory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ export {
notifyToken,
getTokenByContactNumber,
} from './token';

export { linkDevice, unlinkDevice } from './owner';
21 changes: 21 additions & 0 deletions simplq/src/api/requestFactory/owner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Subscribe to backend for notifications
*
* @param {String} deviceId - device token that identifies this device
* @returns {Object} request - partial axios request without baseURL
*/
export const linkDevice = (deviceId) => ({
method: 'put',
url: `/owner/link?deviceId=${deviceId}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this resource should be named something else. I feel that owner doesn't fit its purpose. Do you think /notifications/subscribe would fit?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach I can think of is simply a /notifications endpoint with body

{
  subscribe: <BOOL>,
  deviceId: <ID>
}

for PUT and PATCH.

});

/**
* Unsubscribe to backend for notifications
*
* @param {String} deviceId - device token that identifies this device
* @returns {Object} request - partial axios request without baseURL
*/
export const unlinkDevice = (deviceId) => ({
method: 'patch',
url: `/owner/unlink?deviceId=${deviceId}`,
});
15 changes: 3 additions & 12 deletions simplq/src/components/ErrorHandler.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import * as Sentry from '@sentry/react';
import { raiseException } from 'services/alerts';
import PageNotFound from './pages/PageNotFound';

// eslint-disable-next-line import/prefer-default-export
Expand All @@ -19,17 +19,8 @@ export class ErrorBoundary extends React.Component {
}

componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
// logErrorToMyService(error, errorInfo);
// log error to sentry for alerting
let eventId;
Sentry.withScope((scope) => {
scope.setTag('Caught-at', 'Error Boundary');
scope.setExtras(errorInfo);
eventId = Sentry.captureException(error);
});
// eslint-disable-next-line no-console
console.log(`Sentry exception captured, event id is ${eventId}`);
// log the error to our error reporting service
raiseException(error, 'Error Boundary', errorInfo);
}

render() {
Expand Down
15 changes: 15 additions & 0 deletions simplq/src/components/pages/Join/JoinForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import StepLabel from '@material-ui/core/StepLabel';
import Typography from '@material-ui/core/Typography';
import { useJoinQueue } from 'store/asyncActions';
import { useGetTokenByContactNumber } from 'store/asyncActions/getTokenByContactNumber';
import { useRegisterForNotifications } from 'services/notification/firebase';
import styles from './JoinForm.module.scss';
import Checkbox from '../../common/Checkbox/Checkbox';
import { selectQueueInfo } from '../../../store/queueInfo';
Expand All @@ -29,7 +30,9 @@ export function JoinQueueForm({ queueId, isAdminPage, buttonText }) {
const [activeStep, setActiveStep] = React.useState(0);
const queueInfo = useSelector(selectQueueInfo);
const [saveToLocalStorage, setSaveToLocalStorage] = useState(true);
const [notifyDevice, setNotifyDevice] = useState(true);
const getTokenByContactNumber = useCallback(useGetTokenByContactNumber(), []);
const registerForNotifications = useCallback(useRegisterForNotifications(), []);

const { notifyByEmail } = useSelector(selectQueueInfo);
const collectEmail = !!notifyByEmail;
Expand Down Expand Up @@ -141,6 +144,10 @@ export function JoinQueueForm({ queueId, isAdminPage, buttonText }) {
localStorage.removeItem('email');
}

if (notifyDevice) {
registerForNotifications();
}

joinQueueHandler();
// reset to first step on queue page (pages/Admin/AddMember.jsx)
if (isAdminPage) setActiveStep(0);
Expand Down Expand Up @@ -226,6 +233,14 @@ export function JoinQueueForm({ queueId, isAdminPage, buttonText }) {
setSaveToLocalStorage(!saveToLocalStorage);
}}
/>
<Checkbox
name="notification"
label="Send me notifications on this device"
checked={notifyDevice}
onChange={() => {
setNotifyDevice(!notifyDevice);
}}
/>
<div className={styles.formBoxVerticalButtons}>
<LoadingStatus dependsOn="joinQueue">
<StandardButton disabled={checkJoinDisabled()} onClick={onSubmit}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import Switch from '@material-ui/core/Switch';
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
import { setNotificationPreference } from 'services/notification';
import { setNotificationPreference } from 'services/notification/system';
import styles from './status.module.scss';

export default () => {
Expand Down
8 changes: 0 additions & 8 deletions simplq/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import ReactDOM from 'react-dom';
import './index.css';
import { createTheme, ThemeProvider } from '@material-ui/core/styles';
import { Provider } from 'react-redux';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import AOS from 'aos';
import { Auth0Provider } from '@auth0/auth0-react';
import { store } from './store';
Expand All @@ -13,12 +11,6 @@ import Layout from './components/Layout/Layout';

AOS.init();

Sentry.init({
dsn: 'https://[email protected]/5420492',
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 1.0,
});

const theme = createTheme({
palette: {
primary: {
Expand Down
35 changes: 35 additions & 0 deletions simplq/src/services/alerts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';

Sentry.init({
dsn: 'https://[email protected]/5420492',
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 1.0,
});

/**
* Send exception to monitoring framework.
*
* @param {Exception} ex - exception that was captured.
* @param {string} caughtAt - Optional. A tag indicating where the exception was caught.
* @param {object} extras - Optional. Set an object that will be merged sent as extra data with the event.
*/
export function raiseException(ex, caughtAt, extras) {
Sentry.withScope((scope) => {
if (caughtAt) {
scope.setTag('Caught-at', caughtAt);
}
if (extras) {
scope.setExtras(extras);
}

const eventId = Sentry.captureException(ex);

// eslint-disable-next-line no-console
console.log(`Sentry exception captured, event id is ${eventId}`);
// eslint-disable-next-line no-console
console.error(ex);
});
}

export default raiseException;
47 changes: 47 additions & 0 deletions simplq/src/services/notification/firebase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getMessaging, getToken } from 'firebase/messaging';
import { initializeApp } from 'firebase/app';
import { setErrorPopupMessage } from 'store/appSlice';
import { store } from 'store';
import { raiseException } from 'services/alerts';
import { useLinkDevice } from 'store/asyncActions';

// Public key generated from firebase console
const vapidKey =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, it's okay to expose this key, right?

'BCAlBO-AnqZIo_3MEiR5zEwJTFNNWBR6MdmZ5RpStXxTN6vfgUV2mL3c_hz8vQkcQ2bb_a7IMlGUhAnaw3eBZm4';
// Firebase configuration
const firebaseConfig = {
apiKey: 'AIzaSyCCW7gmWZli24N61NShh-8ALxVy3WtjqNU',
authDomain: 'simplq-fe712.firebaseapp.com',
projectId: 'simplq-fe712',
storageBucket: 'simplq-fe712.appspot.com',
messagingSenderId: '348531792421',
appId: '1:348531792421:web:c481f1740405522d0f3dcc',
measurementId: 'G-8N2SDV8VF5',
};

// Initialize Firebase
const firebaseApp = initializeApp(firebaseConfig);
const messaging = getMessaging(firebaseApp);

/**
* React hook that lets you register for notifications.
*/
export function useRegisterForNotifications() {
const linkDevice = useLinkDevice();

const registerForNotifications = () => {
getToken(messaging, { vapidKey })
.then((deviceId) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, is deviceId generated internally by firebase? Is it unique for every device?

store.dispatch(linkDevice(deviceId));
})
.catch((ex) => {
store.dispatch(setErrorPopupMessage('An error occurred while setting up notifcations.'));
raiseException(ex, 'firebase/registerNotifications');
});
};

return registerForNotifications;
}

// TODO
export function useDeregisterNotifications() {}
10 changes: 8 additions & 2 deletions simplq/src/store/appSlice.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
import { createQueue, deleteQueue, joinQueue } from 'store/asyncActions';
import { createQueue, deleteQueue, joinQueue, linkDevice } from 'store/asyncActions';

function isRejectedAction(action) {
return action.type.endsWith('rejected');
Expand All @@ -11,7 +11,9 @@ const appSlice = createSlice({
initialState: {
errorText: '',
infoText: '',
notificationPermission: null, // This state value is initialised by the notification service.
// This value is initilised at start by services/notification/system.js
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: typo for initialised

notificationPermission: null,
firebaseNotificationDeviceId: null,
},
reducers: {
setErrorPopupMessage: (state, action) => {
Expand Down Expand Up @@ -44,6 +46,10 @@ const appSlice = createSlice({
.addCase(deleteQueue.fulfilled, (state, action) => {
state.infoText = `Deleted ${action.payload.queueName}`;
})
.addCase(linkDevice.fulfilled, (state, action) => {
state.notificationPermission = true;
state.firebaseNotificationDeviceId = action.payload.deviceId;
})
.addMatcher(isRejectedAction, (state, action) => {
// All failed network calls are handled here
state.errorText = action.error.message;
Expand Down
2 changes: 2 additions & 0 deletions simplq/src/store/asyncActions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* in separate files and can depend on each other.
*/

export { linkDevice, useLinkDevice } from './linkDevice';

export { getUserQueues, useGetUserQueues } from './getUserQueues';
export { getUserTokens, useGetUserTokens } from './getUserTokens';
export { deleteQueue, useDeleteQueue } from './deleteQueue';
Expand Down
24 changes: 24 additions & 0 deletions simplq/src/store/asyncActions/linkDevice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { useMakeAuthedRequest } from 'api/auth';
import * as RequestFactory from 'api/requestFactory';

const typePrefix = 'linkDevice/action';

/**
* A hook to access the linkDevice async action creator.
*
* @returns — linkDevice async action creator
*/
const useLinkDevice = () => {
const makeAuthedRequest = useMakeAuthedRequest();

const linkDevice = createAsyncThunk(typePrefix, async (deviceId) => {
return makeAuthedRequest(RequestFactory.linkDevice(deviceId));
});

return linkDevice;
};

const linkDevice = createAsyncThunk(typePrefix);

export { linkDevice, useLinkDevice };