Skip to content

Commit

Permalink
feat: enable background notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
czabaj committed Dec 18, 2024
1 parent 562ad47 commit 7f08b3a
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 62 deletions.
18 changes: 15 additions & 3 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import {
getPrivateCollection,
} from "./helpers";

const CORS = [`https://check.beer`, /localhost:\d+$/];
const CORS = [
`https://check.beer`,
/localhost:\d+$/,
/^https:\/\/beerbook2-da255--pr2-vg-web-push-\w+\.web\.app$/,
];
const REGION = `europe-west3`;

initializeApp();
Expand Down Expand Up @@ -163,7 +167,7 @@ export const truncateUserInDb = auth.user().onDelete(async (user) => {
* This function has access to a private collection and stores there the notification registration token of the user.
*/
export const updateNotificationToken = onCall<UpdateDeviceTokenMessage>(
{ cors: CORS, region: REGION },
{ cors: CORS, enforceAppCheck: true, region: REGION },
async (request) => {
const uid = request.auth?.uid;
if (!uid) {
Expand Down Expand Up @@ -249,7 +253,7 @@ const getUserFamiliarName = async (
*
*/
export const dispatchNotification = onCall<NotificationEventMessages>(
{ cors: CORS, region: REGION },
{ cors: CORS, enforceAppCheck: true, region: REGION },
async (request) => {
const uid = request.auth?.uid;
if (!uid) {
Expand Down Expand Up @@ -278,6 +282,10 @@ export const dispatchNotification = onCall<NotificationEventMessages>(
subscribedUsers
);
if (subscribedNotificationTokens.length === 0) {
logger.log(
`No registration tokens stored for notification event`,
request.data
);
return;
}
const currentUserFamiliarName = await getUserFamiliarName(
Expand Down Expand Up @@ -318,6 +326,10 @@ export const dispatchNotification = onCall<NotificationEventMessages>(
subscribedUsers
);
if (subscribedNotificationTokens.length === 0) {
logger.log(
`No registration tokens stored for notification event`,
request.data
);
return;
}
const kegData = keg.data()!;
Expand Down
23 changes: 21 additions & 2 deletions src/backend/Firebase.res
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,30 @@ module Messaging = {
@module("firebase/messaging")
external _getToken: (t, getTokenOptions) => promise<string> = "getToken"

let getToken = (messaging: t, serviceWorkerRegistration) =>
_getToken(
let getToken = async (messaging: t) => {
let serviceWorkerRegistration = await ServiceWorker.serviceWorkerRegistration
await _getToken(
messaging,
{serviceWorkerRegistration, vapidKey: %raw(`import.meta.env.VITE_FIREBASE_VAPID_KEY`)},
)
}

type fcmOptions = {
analyticsLabel?: string,
link?: string,
}

type messagePayload = {
collapseKey: string,
data: Js.Dict.t<string>,
fcmOptions: fcmOptions,
from: string,
messageId: string,
notification: Js.Dict.t<string>,
}

@module("firebase/messaging")
external onMessage: (t, messagePayload => unit) => unit => unit = "onMessage"
}

module Timestamp = {
Expand Down
24 changes: 17 additions & 7 deletions src/backend/NotificationHooks.res
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,45 @@ let useDispatchFreeTableNotification = (
let firestore = Reactfire.useFirestore()
let functions = Reactfire.useFunctions()
let dispatchNotification = Firebase.Functions.httpsCallable(functions, "dispatchNotification")
let subsciredUsers = useGetSubscibedUsers(~currentUserUid, ~event=FreeTable, ~place)
let subscribedUsers = useGetSubscibedUsers(~currentUserUid, ~event=FreeTable, ~place)
let freeTableSituation = React.useMemo2(() => {
subsciredUsers->Array.length > 0 &&
subscribedUsers->Array.length > 0 &&
recentConsumptionsByUser
->Map.values
->Array.fromIterator
->Array.every(consumptions => consumptions->Array.length === 0)
}, (recentConsumptionsByUser, subsciredUsers))
}, (recentConsumptionsByUser, subscribedUsers))
() =>
if freeTableSituation {
let placeRef = Db.placeDocument(firestore, Db.getUid(place))
dispatchNotification(FreeTableMessage({place: placeRef.path, users: subsciredUsers}))->ignore
dispatchNotification(FreeTableMessage({place: placeRef.path, users: subscribedUsers}))->ignore
}
}

let useDispatchFreshKegNotification = (~currentUserUid: string, ~place: FirestoreModels.place) => {
let firestore = Reactfire.useFirestore()
let functions = Reactfire.useFunctions()
let dispatchNotification = Firebase.Functions.httpsCallable(functions, "dispatchNotification")
let subsciredUsers = useGetSubscibedUsers(~currentUserUid, ~event=FreshKeg, ~place)
let subscribedUsers = useGetSubscibedUsers(~currentUserUid, ~event=FreshKeg, ~place)
(keg: Db.kegConverted) => {
let freshKegSituation = subsciredUsers->Array.length > 0 && keg.consumptionsSum === 0
let freshKegSituation = subscribedUsers->Array.length > 0 && keg.consumptionsSum === 0
if freshKegSituation {
let kegRef = Db.kegDoc(firestore, Db.getUid(place), Db.getUid(keg))
dispatchNotification(FreshKegMessage({keg: kegRef.path, users: subsciredUsers}))->ignore
dispatchNotification(FreshKegMessage({keg: kegRef.path, users: subscribedUsers}))->ignore
}
}
}

// let useDispatchTestNotification = (~currentUserUid: string, ~place: FirestoreModels.place) => {
// let firestore = Reactfire.useFirestore()
// let functions = Reactfire.useFunctions()
// let dispatchNotification = Firebase.Functions.httpsCallable(functions, "dispatchNotification")
// () => {
// let placeRef = Db.placeDocument(firestore, Db.getUid(place))
// dispatchNotification(FreeTableMessage({place: placeRef.path, users: [currentUserUid]}))->ignore
// }
// }

let useUpdateNotificationToken = () => {
let functions = Reactfire.useFunctions()
let updateDeviceToken = Firebase.Functions.httpsCallable(functions, "updateNotificationToken")
Expand Down
41 changes: 19 additions & 22 deletions src/components/FcmTokenSync/FcmTokenSync.res
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
let isMobileIOs: bool = %raw(`navigator.userAgent.match(/(iPhone|iPad)/)`)
// on iOS the notifications are only allowed in standalone mode
let canSubscribe = !isMobileIOs || %raw(`window.navigator.standalone === true`)
let canSubscribe =
// avoid excessive call of the cloud function in development
%raw(`import.meta.env.PROD`) &&
(// on iOS the notifications are only allowed in standalone mode
!isMobileIOs || %raw(`window.navigator.standalone === true`))

let isSubscribedToNotificationsRx = (auth, firestore, placeId) => {
open Rxjs
Expand All @@ -22,7 +25,7 @@ let isSubscribedToNotificationsRx = (auth, firestore, placeId) => {
}

@react.component
let make = (~placeId) => {
let make = React.memo((~placeId) => {
if canSubscribe {
let auth = Reactfire.useAuth()
let firestore = Reactfire.useFirestore()
Expand All @@ -32,32 +35,26 @@ let make = (~placeId) => {
~observableId="isSubscribedToNotifications",
~source=isSubscribedToNotificationsRx(auth, firestore, placeId),
)
let serviceWorkerRegistration = Reactfire.useObservable(
~observableId="serviceWorkerRegistration",
~source=Rxjs.toObservable(ServiceWorker.serviceWorkerRegistrationSubject),
~config={
idField: #uid,
suspense: false,
},
)
React.useEffect2(() => {
switch (isSubscribedToNotifications.data, serviceWorkerRegistration.data) {
| (Some(true), Some(swRegistration)) =>
React.useEffect(() => {
switch isSubscribedToNotifications.data {
| Some(true) =>
messaging
->Firebase.Messaging.getToken(swRegistration)
->Firebase.Messaging.getToken
->Promise.then(updateNotificationToken)
->Promise.then(_ => Promise.resolve())
->Promise.catch(error => {
let exn = Js.Exn.asJsExn(error)->Option.getExn
LogUtils.captureException(exn)
Promise.resolve()
})
->Promise.catch(
error => {
let exn = Js.Exn.asJsExn(error)->Option.getExn
LogUtils.captureException(exn)
Promise.resolve()
},
)
->ignore
| _ => ()
}
None
}, (isSubscribedToNotifications.data, serviceWorkerRegistration.data))
}, [isSubscribedToNotifications.data])
}

React.null
}
})
8 changes: 0 additions & 8 deletions src/pages/Place/Place.res
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,6 @@ let make = (~placeId) => {
let isUserAuthorized = UserRoles.isAuthorized(currentUserRole, ...)
let formatConsumption = BackendUtils.getFormatConsumption(place.consumptionSymbols)

let dispatchTestNotification = NotificationHooks.useDispatchTestNotification(
~currentUserUid=currentUser.uid,
~place,
)

<FormattedCurrency.Provider value={place.currency}>
<div className={`${Styles.page.narrow} ${classes.root}`}>
<PlaceHeader
Expand All @@ -152,9 +147,6 @@ let make = (~placeId) => {
createdTimestamp={place.createdAt}
placeName={place.name}
/>
<button onClick={_ => dispatchTestNotification()}>
{React.string("Test notification")}
</button>
<main>
<BeerList
currentUserUid={currentUser.uid}
Expand Down
23 changes: 3 additions & 20 deletions src/serviceWorker/ServiceWorker.res
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,8 @@ type registerSWOptions = {
}

@module("virtual:pwa-register")
external _registerSW: registerSWOptions => unit = "registerSW"
external registerSW: registerSWOptions => unit = "registerSW"

let serviceWorkerRegistrationSubject: Rxjs.ReplaySubject.t<
let serviceWorkerRegistration: promise<
serviceWorkerRegistration,
> = Rxjs.ReplaySubject.make(~bufferSize=1)

let registerSW = (options: registerSWOptions) => {
_registerSW({
...options,
onRegisteredSW: (swUrl, maybeServiceWorkerRegistration) => {
switch maybeServiceWorkerRegistration {
| Some(registration) =>
serviceWorkerRegistrationSubject->Rxjs.ReplaySubject.next(registration)
| None => ()
}
switch options.onRegisteredSW {
| Some(fn) => fn(swUrl, maybeServiceWorkerRegistration)
| _ => ()
}
},
})
}
> = %raw(`navigator.serviceWorker.ready`)
15 changes: 15 additions & 0 deletions src/serviceWorker/sw.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { getMessaging, onBackgroundMessage } from "firebase/messaging/sw";
import { initializeApp } from "firebase/app";
import { clientsClaim } from "workbox-core";
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";

import { firebaseConfig } from "../backend/firebaseConfig";

declare const self: ServiceWorkerGlobalScope;

self.skipWaiting();
Expand All @@ -13,3 +17,14 @@ precacheAndRoute(self.__WB_MANIFEST);
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting();
});

const firebaseApp = initializeApp(firebaseConfig);
const messaging = getMessaging(firebaseApp);
onBackgroundMessage(messaging, (payload) => {
const notificationTitle = payload.notification?.title;
if (!notificationTitle) return;
self.registration.showNotification(notificationTitle, {
body: payload.notification!.body,
icon: "/pwa-192.png",
});
});

0 comments on commit 7f08b3a

Please sign in to comment.