From 5756ae67774b0295499ae0540b140081dc975ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Gr=C3=B6hling?= Date: Wed, 30 Oct 2024 17:36:22 +0100 Subject: [PATCH] feat: implement token sync logic in client code --- src/App.res | 28 ++++++++--- src/backend/Firebase.res | 17 +++++-- src/backend/NotificationEvents.res | 8 --- src/backend/Reactfire.res | 8 +++ src/components/FcmTokenSync/FcmTokenSync.res | 50 +++++++++++++++++++ .../FirebaseAuthProvider.res | 5 +- 6 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 src/components/FcmTokenSync/FcmTokenSync.res diff --git a/src/App.res b/src/App.res index 21f3cc0..d69e314 100644 --- a/src/App.res +++ b/src/App.res @@ -19,13 +19,27 @@ let make = () => { | list{"misto", ..._} => - {switch List.tail(url.path) { - | Some(list{}) => - | Some(list{placeId}) => - | Some(list{placeId, "nastaveni"}) => - | Some(list{placeId, "nastaveni", "osob"}) => - | _ => - }} + { + let maybePlaceSubPath = List.tail(url.path) + switch maybePlaceSubPath { + | None + | Some(list{}) => + + | Some(placeSubPath) => { + let placeId = List.headExn(placeSubPath) + let placeIdSub = List.tail(placeSubPath) + <> + + {switch placeIdSub { + | Some(list{}) => + | Some(list{"nastaveni"}) => + | Some(list{"nastaveni", "osob"}) => + | _ => + }} + + } + } + } | list{"s", linkId} => diff --git a/src/backend/Firebase.res b/src/backend/Firebase.res index 1360200..7800872 100644 --- a/src/backend/Firebase.res +++ b/src/backend/Firebase.res @@ -437,14 +437,21 @@ module AppCheckProvider = { module Messaging = { type t - type getTokenOptions = { vapidKey: string } + @module("firebase/messaging") + external getMessaging: FirebaseApp.t => t = "getMessaging" + + type getTokenOptions = {vapidKey: string} + // Subscribes the Messaging instance to push notifications. Returns a Firebase Cloud Messaging registration token that + // can be used to send push messages to that Messaging instance. If notification permission isn't already granted, + // this method asks the user for permission. The returned promise rejects if the user does not allow the app to show + // notifications. @module("firebase/messaging") - external getToken: (t, getTokenOptions) => unit = "getToken" -} + external _getToken: (t, getTokenOptions) => promise = "getToken" -@module("firebase/messaging") -external getMessaging: FirebaseApp.t => Messaging.t = "getMessaging" + let getToken = (messaging: t) => + _getToken(messaging, {vapidKey: %raw(`import.meta.env.VITE_FIREBASE_VAPID_KEY`)}) +} module Timestamp = { @genType.import("firebase/firestore") @genType.as("Timestamp") diff --git a/src/backend/NotificationEvents.res b/src/backend/NotificationEvents.res index beb5edf..108ed0b 100644 --- a/src/backend/NotificationEvents.res +++ b/src/backend/NotificationEvents.res @@ -5,14 +5,6 @@ type notificationEvent = @module("./NotificationEvents.ts") external notificationEvent: notificationEvent = "NotificationEvent" -let notificationEventFromInt = (notificationEvent: int) => - switch notificationEvent { - | 0 => Some(Unsubscribed) - | 1 => Some(FreeTable) - | 2 => Some(FreshKeg) - | _ => None - } - let roleI18n = (notificationEvent: notificationEvent) => switch notificationEvent { | Unsubscribed => "Nepřihlášen" diff --git a/src/backend/Reactfire.res b/src/backend/Reactfire.res index 1c3a418..77a7c48 100644 --- a/src/backend/Reactfire.res +++ b/src/backend/Reactfire.res @@ -30,6 +30,14 @@ module FunctionsProvider = { "FunctionsProvider" } +let messagingContext = React.createContext((None: option)) + +module MessagingProvider = { + let make = React.Context.provider(messagingContext) +} + +let useMessaging = () => React.useContext(messagingContext)->Option.getExn + type observableStatus<'a> = { data: option<'a>, error: option, diff --git a/src/components/FcmTokenSync/FcmTokenSync.res b/src/components/FcmTokenSync/FcmTokenSync.res new file mode 100644 index 0000000..d94c1cb --- /dev/null +++ b/src/components/FcmTokenSync/FcmTokenSync.res @@ -0,0 +1,50 @@ +let isSubscribedToNotificationsRx = (auth, firestore, placeId) => { + open Rxjs + let currentUserRx = Rxfire.user(auth)->op(keepMap(Null.toOption)) + let placeRef = Db.placeDocument(firestore, placeId) + let placeRx = Rxfire.docData(placeRef) + combineLatest2(currentUserRx, placeRx)->op( + map(((currentUser: Firebase.User.t, place: option), _) => { + switch (currentUser, place) { + | (user, Some(place)) => + place.accounts + ->Dict.get(user.uid) + ->Option.map(((_, notificationSetting)) => notificationSetting > 0) + ->Option.getOr(false) + | _ => false + } + }), + ) +} + +@react.component +let make = (~placeId) => { + let auth = Reactfire.useAuth() + let firestore = Reactfire.useFirestore() + let messaging = Reactfire.useMessaging() + let updateNotificationToken = NotificationEvents.useUpdateNotificationToken() + let isStandaloneModeStatus = DomUtils.useIsStandaloneMode() + let isSubscribedToNotifications = Reactfire.useObservable( + ~observableId="isSubscribedToNotifications", + ~source=isSubscribedToNotificationsRx(auth, firestore, placeId), + ) + React.useEffect2(() => { + switch (isSubscribedToNotifications.data, isStandaloneModeStatus.data) { + | (Some(true), Some(true)) => + messaging + ->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() + }) + ->ignore + | _ => () + } + None + }, (isSubscribedToNotifications.data, isStandaloneModeStatus.data)) + + React.null +} diff --git a/src/components/FirebaseAuthProvider/FirebaseAuthProvider.res b/src/components/FirebaseAuthProvider/FirebaseAuthProvider.res index e1f58f5..ea4652c 100644 --- a/src/components/FirebaseAuthProvider/FirebaseAuthProvider.res +++ b/src/components/FirebaseAuthProvider/FirebaseAuthProvider.res @@ -15,11 +15,14 @@ let make = (~children) => { let analytics = app->Analytics.getAnalytics let functions = app->Functions.getFunctions + let messaging = app->Messaging.getMessaging - {children} + + {children} +