diff --git a/web/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js
--- a/web/push-notif/notif-crypto-utils.js
+++ b/web/push-notif/notif-crypto-utils.js
@@ -9,21 +9,21 @@
 } from 'lib/types/crypto-types.js';
 import type {
   PlainTextWebNotification,
-  PlainTextWebNotificationPayload,
   EncryptedWebNotification,
 } from 'lib/types/notif-types.js';
 
 import {
+  type EncryptedData,
   decryptData,
   encryptData,
   importJWKKey,
-  type EncryptedData,
 } from '../crypto/aes-gcm-crypto-utils.js';
 import {
   NOTIFICATIONS_OLM_DATA_CONTENT,
   NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY,
 } from '../database/utils/constants.js';
 import { isDesktopSafari } from '../database/utils/db-utils.js';
+import { initOlm } from '../olm/olm-utils.js';
 
 export type WebNotifDecryptionError = {
   +id: string,
@@ -36,10 +36,10 @@
   +staffCanSee: boolean,
 };
 
-type DecryptionResult = {
+type DecryptionResult<T> = {
   +newPendingSessionUpdate: string,
   +newUpdateCreationTimestamp: number,
-  +decryptedNotification: PlainTextWebNotificationPayload,
+  +decryptedNotification: T,
 };
 
 export const WEB_NOTIFS_SERVICE_UTILS_KEY = 'webNotifsServiceUtils';
@@ -51,27 +51,9 @@
 ): Promise<PlainTextWebNotification | WebNotifDecryptionError> {
   const { id, encryptedPayload } = encryptedNotification;
 
-  const retrieveEncryptionKeyPromise: Promise<?CryptoKey> = (async () => {
-    if (!isDesktopSafari) {
-      return await localforage.getItem<CryptoKey>(
-        NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY,
-      );
-    }
-    // Safari doesn't support structured clone algorithm in service
-    // worker context so we have to store CryptoKey as JSON
-    const persistedCryptoKey =
-      await localforage.getItem<SubtleCrypto$JsonWebKey>(
-        NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY,
-      );
-    if (!persistedCryptoKey) {
-      return null;
-    }
-    return await importJWKKey(persistedCryptoKey);
-  })();
-
   const [encryptedOlmData, encryptionKey, utilsData] = await Promise.all([
     localforage.getItem<EncryptedData>(NOTIFICATIONS_OLM_DATA_CONTENT),
-    retrieveEncryptionKeyPromise,
+    retrieveEncryptionKey(),
     localforage.getItem<WebNotifsServiceUtilsData>(
       WEB_NOTIFS_SERVICE_UTILS_KEY,
     ),
@@ -93,91 +75,139 @@
   try {
     await olm.init({ locateFile: () => olmWasmPath });
 
-    const serializedOlmData = await decryptData(
+    const decryptedNotification = await commonDecrypt<PlainTextWebNotification>(
       encryptedOlmData,
       encryptionKey,
-    );
-    const {
-      mainSession,
-      picklingKey,
-      pendingSessionUpdate,
-      updateCreationTimestamp,
-    }: NotificationsOlmDataType = JSON.parse(
-      new TextDecoder().decode(serializedOlmData),
+      encryptedPayload,
     );
 
-    let updatedOlmData: NotificationsOlmDataType;
-    let decryptedNotification: PlainTextWebNotificationPayload;
+    return { id, ...decryptedNotification };
+  } catch (e) {
+    return {
+      id,
+      error: e.message,
+      displayErrorMessage: staffCanSee,
+    };
+  }
+}
 
-    const shouldUpdateMainSession =
-      Date.now() - updateCreationTimestamp > SESSION_UPDATE_MAX_PENDING_TIME;
+async function decryptDesktopNotification(
+  encryptedPayload: string,
+  staffCanSee: boolean,
+): Promise<{ +[string]: mixed }> {
+  let encryptedOlmData, encryptionKey;
+  try {
+    [encryptedOlmData, encryptionKey] = await Promise.all([
+      localforage.getItem<EncryptedData>(NOTIFICATIONS_OLM_DATA_CONTENT),
+      retrieveEncryptionKey(),
+      initOlm(),
+    ]);
+  } catch (e) {
+    return {
+      error: e.message,
+      displayErrorMessage: staffCanSee,
+    };
+  }
 
-    const decryptionWithPendingSessionResult = decryptWithPendingSession(
-      pendingSessionUpdate,
-      picklingKey,
-      encryptedPayload,
-    );
+  if (!encryptionKey || !encryptedOlmData) {
+    return {
+      error: 'Received encrypted notification but olm session was not created',
+      displayErrorMessage: staffCanSee,
+    };
+  }
 
-    if (decryptionWithPendingSessionResult.decryptedNotification) {
-      const {
-        decryptedNotification: notifDecryptedWithPendingSession,
-        newPendingSessionUpdate,
-        newUpdateCreationTimestamp,
-      } = decryptionWithPendingSessionResult;
-
-      decryptedNotification = notifDecryptedWithPendingSession;
-      updatedOlmData = {
-        mainSession: shouldUpdateMainSession
-          ? pendingSessionUpdate
-          : mainSession,
-        pendingSessionUpdate: newPendingSessionUpdate,
-        updateCreationTimestamp: newUpdateCreationTimestamp,
-        picklingKey,
-      };
-    } else {
-      const {
-        newUpdateCreationTimestamp,
-        decryptedNotification: notifDecryptedWithMainSession,
-      } = decryptWithSession(mainSession, picklingKey, encryptedPayload);
-
-      decryptedNotification = notifDecryptedWithMainSession;
-      updatedOlmData = {
-        mainSession: mainSession,
-        pendingSessionUpdate,
-        updateCreationTimestamp: newUpdateCreationTimestamp,
-        picklingKey,
-      };
-    }
-
-    const updatedEncryptedSession = await encryptData(
-      new TextEncoder().encode(JSON.stringify(updatedOlmData)),
+  try {
+    return await commonDecrypt(
+      encryptedOlmData,
       encryptionKey,
+      encryptedPayload,
     );
-
-    await localforage.setItem(
-      NOTIFICATIONS_OLM_DATA_CONTENT,
-      updatedEncryptedSession,
-    );
-
-    return { id, ...decryptedNotification };
   } catch (e) {
     return {
-      id,
       error: e.message,
-      displayErrorMessage: staffCanSee,
+      staffCanSee,
     };
   }
 }
 
-function decryptWithSession(
+async function commonDecrypt<T>(
+  encryptedOlmData: EncryptedData,
+  encryptionKey: CryptoKey,
+  encryptedPayload: string,
+): Promise<T> {
+  const serializedOlmData = await decryptData(encryptedOlmData, encryptionKey);
+  const {
+    mainSession,
+    picklingKey,
+    pendingSessionUpdate,
+    updateCreationTimestamp,
+  }: NotificationsOlmDataType = JSON.parse(
+    new TextDecoder().decode(serializedOlmData),
+  );
+
+  let updatedOlmData: NotificationsOlmDataType;
+  let decryptedNotification: T;
+
+  const shouldUpdateMainSession =
+    Date.now() - updateCreationTimestamp > SESSION_UPDATE_MAX_PENDING_TIME;
+
+  const decryptionWithPendingSessionResult = decryptWithPendingSession<T>(
+    pendingSessionUpdate,
+    picklingKey,
+    encryptedPayload,
+  );
+
+  if (decryptionWithPendingSessionResult.decryptedNotification) {
+    const {
+      decryptedNotification: notifDecryptedWithPendingSession,
+      newPendingSessionUpdate,
+      newUpdateCreationTimestamp,
+    } = decryptionWithPendingSessionResult;
+
+    decryptedNotification = notifDecryptedWithPendingSession;
+    updatedOlmData = {
+      mainSession: shouldUpdateMainSession ? pendingSessionUpdate : mainSession,
+      pendingSessionUpdate: newPendingSessionUpdate,
+      updateCreationTimestamp: newUpdateCreationTimestamp,
+      picklingKey,
+    };
+  } else {
+    const {
+      newUpdateCreationTimestamp,
+      decryptedNotification: notifDecryptedWithMainSession,
+    } = decryptWithSession<T>(mainSession, picklingKey, encryptedPayload);
+
+    decryptedNotification = notifDecryptedWithMainSession;
+    updatedOlmData = {
+      mainSession: mainSession,
+      pendingSessionUpdate,
+      updateCreationTimestamp: newUpdateCreationTimestamp,
+      picklingKey,
+    };
+  }
+
+  const updatedEncryptedSession = await encryptData(
+    new TextEncoder().encode(JSON.stringify(updatedOlmData)),
+    encryptionKey,
+  );
+
+  await localforage.setItem(
+    NOTIFICATIONS_OLM_DATA_CONTENT,
+    updatedEncryptedSession,
+  );
+
+  return decryptedNotification;
+}
+
+function decryptWithSession<T>(
   pickledSession: string,
   picklingKey: string,
   encryptedPayload: string,
-): DecryptionResult {
+): DecryptionResult<T> {
   const session = new olm.Session();
 
   session.unpickle(picklingKey, pickledSession);
-  const decryptedNotification: PlainTextWebNotificationPayload = JSON.parse(
+  const decryptedNotification: T = JSON.parse(
     session.decrypt(olmEncryptedMessageTypes.TEXT, encryptedPayload),
   );
 
@@ -191,17 +221,21 @@
   };
 }
 
-function decryptWithPendingSession(
+function decryptWithPendingSession<T>(
   pendingSessionUpdate: string,
   picklingKey: string,
   encryptedPayload: string,
-): DecryptionResult | { +error: string } {
+): DecryptionResult<T> | { +error: string } {
   try {
     const {
       decryptedNotification,
       newPendingSessionUpdate,
       newUpdateCreationTimestamp,
-    } = decryptWithSession(pendingSessionUpdate, picklingKey, encryptedPayload);
+    } = decryptWithSession<T>(
+      pendingSessionUpdate,
+      picklingKey,
+      encryptedPayload,
+    );
     return {
       newPendingSessionUpdate,
       newUpdateCreationTimestamp,
@@ -211,4 +245,22 @@
     return { error: e.message };
   }
 }
-export { decryptWebNotification };
+
+async function retrieveEncryptionKey(): Promise<?CryptoKey> {
+  if (!isDesktopSafari) {
+    return await localforage.getItem<CryptoKey>(
+      NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY,
+    );
+  }
+  // Safari doesn't support structured clone algorithm in service
+  // worker context so we have to store CryptoKey as JSON
+  const persistedCryptoKey = await localforage.getItem<SubtleCrypto$JsonWebKey>(
+    NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY,
+  );
+  if (!persistedCryptoKey) {
+    return null;
+  }
+  return await importJWKKey(persistedCryptoKey);
+}
+
+export { decryptWebNotification, decryptDesktopNotification };