From 0118a76e5e2329dadc024d080426505f4bf88747 Mon Sep 17 00:00:00 2001
From: "Richard T. Carback III" <rick.carback@gmail.com>
Date: Mon, 22 May 2023 23:36:41 +0000
Subject: [PATCH] Updats for Notifications

---
 go.mod                                        |   2 +-
 go.sum                                        |   2 +
 indexedDb/impl/channels/callbacks.go          |   7 +
 indexedDb/impl/channels/implementation.go     |   1 -
 .../impl/channels/implementation_test.go      |   4 +
 indexedDb/worker/channels/tags.go             |   9 +-
 main.go                                       |   2 +
 wasm/channels.go                              | 243 ++++++++++++------
 wasm/cmix.go                                  |   3 +-
 wasm/collective.go                            |  35 +--
 wasm/e2eHandler.go                            |  19 +-
 wasm/follow.go                                |  61 ++++-
 wasm/notifications.go                         |  12 +-
 13 files changed, 278 insertions(+), 122 deletions(-)

diff --git a/go.mod b/go.mod
index 0015cceb..52e77d9c 100644
--- a/go.mod
+++ b/go.mod
@@ -68,7 +68,7 @@ require (
 	gitlab.com/elixxir/bloomfilter v0.0.0-20230322223210-fa84f6842de8 // indirect
 	gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938 // indirect
 	gitlab.com/elixxir/ekv v0.3.1-0.20230504190918-f5e96603c2e0 // indirect
-	gitlab.com/elixxir/wasm-utils v0.0.0-20230519212008-29635852d8c7 // indirect
+	gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2 // indirect
 	gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d // indirect
 	gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 // indirect
 	gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
diff --git a/go.sum b/go.sum
index 82582a0c..5fd785d4 100644
--- a/go.sum
+++ b/go.sum
@@ -551,6 +551,8 @@ gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c h1:muG8ff95wo
 gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c/go.mod h1:phun4PLkHJA6wcL4JIhhxZztrmCyJHWPNppBP3DUD2Y=
 gitlab.com/elixxir/wasm-utils v0.0.0-20230519212008-29635852d8c7 h1:3pimSfFr0uy3OhOMM9IHXZBeG2xJX0nor2yph9xx3oM=
 gitlab.com/elixxir/wasm-utils v0.0.0-20230519212008-29635852d8c7/go.mod h1:wB7Vh/7LWUm8wYRBSd+6lxfpk4CnDaHTkLCIVKfL2TA=
+gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2 h1:GQb350yPBkWRkPRgNSVFF0ZZDOAlXWIKQBI/1Ff6biU=
+gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2/go.mod h1:wB7Vh/7LWUm8wYRBSd+6lxfpk4CnDaHTkLCIVKfL2TA=
 gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d h1:AZf2h0fxyO1KxhZPP9//jG3Swb2BcuKbxtNXJgooLss=
 gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d/go.mod h1:8cwPyH6G8C4qf/U5KDghn1ksOh79MrNqthjKDrfvbXY=
 gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd h1:IleH6U5D/c2zF6YL/z3cBKqBPnI5ApNMCtU7ia4t228=
diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go
index 6fc2235b..2550dbb9 100644
--- a/indexedDb/impl/channels/callbacks.go
+++ b/indexedDb/impl/channels/callbacks.go
@@ -79,6 +79,13 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
 	return []byte{}, nil
 }
 
+// NotificationUpdate implements [bindings.ChannelUICallbacks.NotificationUpdate
+func (m *manager) NotificationUpdate(notificationFilterListJSON,
+	changedNotificationStatesJSON, deletedNotificationStatesJSON []byte,
+	maxState int) {
+	jww.FATAL.Panicf("unimplemented")
+}
+
 // MessageReceived implements [bindings.ChannelUICallbacks.MessageReceived].
 func (m *manager) MessageReceived(uuid int64, channelID []byte, update bool) {
 	// Package parameters for sending
diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go
index b7aa1f71..04d7fb7e 100644
--- a/indexedDb/impl/channels/implementation.go
+++ b/indexedDb/impl/channels/implementation.go
@@ -29,7 +29,6 @@ import (
 	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 	"gitlab.com/xx_network/primitives/id"
 )
 
diff --git a/indexedDb/impl/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go
index d13f725d..7de828ef 100644
--- a/indexedDb/impl/channels/implementation_test.go
+++ b/indexedDb/impl/channels/implementation_test.go
@@ -50,6 +50,10 @@ func (c *dummyCbs) MessageDeleted(messageId []byte)                           {}
 func (c *dummyCbs) NicknameUpdate(channelIdBytes []byte, nickname string,
 	exists bool) {
 }
+func (c *dummyCbs) NotificationUpdate(notificationFilterListJSON,
+	changedNotificationStatesJSON, deletedNotificationStatesJSON []byte,
+	maxState int) {
+}
 
 // Happy path test for receiving, updating, getting, and deleting a File.
 func TestWasmModel_ReceiveFile(t *testing.T) {
diff --git a/indexedDb/worker/channels/tags.go b/indexedDb/worker/channels/tags.go
index d3555e54..01991128 100644
--- a/indexedDb/worker/channels/tags.go
+++ b/indexedDb/worker/channels/tags.go
@@ -14,10 +14,11 @@ import "gitlab.com/elixxir/xxdk-wasm/worker"
 // List of tags that can be used when sending a message or registering a handler
 // to receive a message.
 const (
-	NewWASMEventModelTag       worker.Tag = "NewWASMEventModel"
-	MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback"
-	DeletedMessageCallbackTag  worker.Tag = "DeletedMessageCallback"
-	MutedUserCallbackTag       worker.Tag = "MutedUserCallback"
+	NewWASMEventModelTag          worker.Tag = "NewWASMEventModel"
+	NotificationUpdateCallbackTag worker.Tag = "NotificationUpdateCallback"
+	MessageReceivedCallbackTag    worker.Tag = "MessageReceivedCallback"
+	DeletedMessageCallbackTag     worker.Tag = "DeletedMessageCallback"
+	MutedUserCallbackTag          worker.Tag = "MutedUserCallback"
 
 	JoinChannelTag         worker.Tag = "JoinChannel"
 	LeaveChannelTag        worker.Tag = "LeaveChannel"
diff --git a/main.go b/main.go
index 07b25c75..4171227e 100644
--- a/main.go
+++ b/main.go
@@ -136,6 +136,8 @@ func setGlobals() {
 	js.Global().Set("CheckNoMessageErr", js.FuncOf(wasm.CheckNoMessageErr))
 	js.Global().Set("NewChannelsDatabaseCipher",
 		js.FuncOf(wasm.NewChannelsDatabaseCipher))
+	js.Global().Set("GetNotificationReportsForMe",
+		js.FuncOf(wasm.GetNotificationReportsForMe))
 
 	// wasm/dm.go
 	js.Global().Set("InitChannelsFileTransfer",
diff --git a/wasm/channels.go b/wasm/channels.go
index 71e27547..e7f3449f 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -17,16 +17,11 @@ import (
 	"syscall/js"
 
 	"gitlab.com/elixxir/client/v4/channels"
-	"gitlab.com/elixxir/primitives/notifications"
 	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/client/v4/channels"
-	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/wasm-utils/utils"
-	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
-	"gitlab.com/xx_network/primitives/id"
 )
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -83,6 +78,11 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 
 		// Channel Receiving Logic and Callback Registration
 		"RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler),
+
+		// Notifications
+		"SetMobileNotificationsLevel": js.FuncOf(
+			cm.SetMobileNotificationsLevel),
+		"GetNotificationLevel": js.FuncOf(cm.GetNotificationLevel),
 	}
 
 	return channelsManagerMap
@@ -258,27 +258,34 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - ID of [bindings.Notifications] object in tracker (int). Can be
+//     retrieved using [Notifications.GetID].
+//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - JSON of an array of integers of [channels.ExtensionBuilder]
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
 //     IDs. The ID can be retrieved from an object with an extension builder
 //     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
 //     using extension builders. Example: `[2,11,5]` (Uint8Array).
-//   - args[3] - A function that initialises and returns a Javascript object
+//   - args[4] - A function that initialises and returns a Javascript object
 //     that matches the [bindings.EventModel] interface. The function must match
 //     the Build function in [bindings.EventModelBuilder].
+//   - args[4] - A callback object which implements the
+//     [bindings.ChannelUICallbacks] javascript functions.
 //
 // Returns:
 //   - Javascript representation of the [ChannelsManager] object.
 //   - Throws a TypeError if creating the manager fails.
 func NewChannelsManager(_ js.Value, args []js.Value) any {
 	cmixId := args[0].Int()
-	privateIdentity := utils.CopyBytesToGo(args[1])
-	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[2])
-	em := newEventModelBuilder(args[3])
+	notificationsId := args[1].Int()
+	privateIdentity := utils.CopyBytesToGo(args[2])
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	em := newEventModelBuilder(args[4])
+	channelCbs := newChannelUI(args[5])
 
 	cm, err := bindings.NewChannelsManager(
-		cmixId, privateIdentity, extensionBuilderIDsJSON, em, nil)
+		cmixId, privateIdentity, extensionBuilderIDsJSON, em,
+		notificationsId, channelCbs)
 	if err != nil {
 		exception.ThrowTrace(err)
 		return nil
@@ -298,22 +305,27 @@ func NewChannelsManager(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - The storage tag associated with the previously created channel
+//   - args[1] - ID of [bindings.Notifications] object in tracker (int). Can be
+//     retrieved using [Notifications.GetID].
+//   - args[2] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[2] - A function that initialises and returns a Javascript object
+//   - args[3] - A function that initialises and returns a Javascript object
 //     that matches the [bindings.EventModel] interface. The function must match
 //     the Build function in [bindings.EventModelBuilder].
-//   - args[3] - A callback object which implements the
+//   - args[4] - A callback object which implements the
 //     [bindings.ChannelUICallbacks] javascript functions.
 //
 // Returns:
 //   - Javascript representation of the [ChannelsManager] object.
 //   - Throws a TypeError if loading the manager fails.
 func LoadChannelsManager(_ js.Value, args []js.Value) any {
-	em := newEventModelBuilder(args[2])
-	cUI := newChannelUI(args[3])
-	cm, err := bindings.LoadChannelsManager(args[0].Int(), args[1].String(),
-		em, cUI)
+	cMixID := args[0].Int()
+	notificationsID := args[1].Int()
+	storageTag := args[2].String()
+	em := newEventModelBuilder(args[3])
+	cUI := newChannelUI(args[4])
+	cm, err := bindings.LoadChannelsManager(cMixID, storageTag, em,
+		notificationsID, cUI)
 	if err != nil {
 		exception.ThrowTrace(err)
 		return nil
@@ -336,16 +348,18 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Path to Javascript file that starts the worker (string).
-//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - ID of [bindings.Notifications] object in tracker (int). Can be
+//     retrieved using [Notifications.GetID].
+//   - args[2] - Path to Javascript file that starts the worker (string).
+//   - args[3] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//   - args[4] - JSON of an array of integers of [channels.ExtensionBuilder]
 //     IDs. The ID can be retrieved from an object with an extension builder
 //     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
 //     using extension builders. Example: `[2,11,5]` (Uint8Array).
-//   - args[4] - A callback object which implements the
+//   - args[5] - A callback object which implements the
 //     [bindings.ChannelUICallbacks] javascript functions.
-//   - args[5] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//   - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -355,18 +369,20 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //   - Throws a TypeError if the cipher ID does not correspond to a cipher.
 func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	wasmJsPath := args[1].String()
-	privateIdentity := utils.CopyBytesToGo(args[2])
-	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
-	channelCbs := newChannelUI(args[4])
-	cipherID := args[5].Int()
+	notificationsID := args[1].Int()
+	wasmJsPath := args[2].String()
+	privateIdentity := utils.CopyBytesToGo(args[3])
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[4])
+	channelCbs := newChannelUI(args[5])
+	cipherID := args[6].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
 		exception.ThrowTrace(err)
 	}
 
-	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
+	return newChannelsManagerWithIndexedDb(cmixID, notificationsID,
+		wasmJsPath, privateIdentity,
 		extensionBuilderIDsJSON, channelCbs, cipher)
 }
 
@@ -385,14 +401,16 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Path to Javascript file that starts the worker (string).
-//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - ID of [bindings.Notifications] object in tracker (int). Can be
+//     retrieved using [Notifications.GetID].
+//   - args[2] - Path to Javascript file that starts the worker (string).
+//   - args[3] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//   - args[4] - JSON of an array of integers of [channels.ExtensionBuilder]
 //     IDs. The ID can be retrieved from an object with an extension builder
 //     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
 //     using extension builders. Example: `[2,11,5]` (Uint8Array).
-//   - args[4] - A callback object which implements the
+//   - args[5] - A callback object which implements the
 //     [bindings.ChannelUICallbacks] javascript functions.
 //
 // Returns a promise:
@@ -402,17 +420,19 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 // FIXME: package names in comments for indexedDb
 func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	wasmJsPath := args[1].String()
-	privateIdentity := utils.CopyBytesToGo(args[2])
-	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
-	channelsCbs := newChannelUI(args[4])
-
-	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
+	notificationsID := args[1].Int()
+	wasmJsPath := args[2].String()
+	privateIdentity := utils.CopyBytesToGo(args[3])
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[4])
+	channelsCbs := newChannelUI(args[5])
+
+	return newChannelsManagerWithIndexedDb(cmixID, notificationsID,
+		wasmJsPath, privateIdentity,
 		extensionBuilderIDsJSON, channelsCbs, nil)
 }
 
-func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
-	privateIdentity, extensionBuilderIDsJSON []byte,
+func newChannelsManagerWithIndexedDb(cmixID, notificationsID int,
+	wasmJsPath string, privateIdentity, extensionBuilderIDsJSON []byte,
 	channelsCbs bindings.ChannelUICallbacks,
 	cipher *bindings.ChannelDbCipher) any {
 
@@ -421,7 +441,8 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.NewChannelsManagerGoEventModel(
-			cmixID, privateIdentity, extensionBuilderIDsJSON, model, channelsCbs)
+			cmixID, privateIdentity, extensionBuilderIDsJSON, model,
+			notificationsID, channelsCbs)
 		if err != nil {
 			reject(exception.NewTrace(err))
 		} else {
@@ -443,12 +464,14 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Path to Javascript file that starts the worker (string).
-//   - args[2] - The storage tag associated with the previously created channel
+//   - args[1] - ID of [bindings.Notifications] object in tracker (int). Can be
+//     retrieved using [Notifications.GetID].
+//   - args[2] - Path to Javascript file that starts the worker (string).
+//   - args[3] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[3] - A callback object which implements the
+//   - args[4] - A callback object which implements the
 //     [bindings.ChannelUICallbacks] javascript functions.
-//   - args[4] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//   - args[5] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -458,17 +481,19 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
 //   - Throws a TypeError if the cipher ID does not correspond to a cipher.
 func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	wasmJsPath := args[1].String()
-	storageTag := args[2].String()
-	channelsCbs := newChannelUI(args[3])
-	cipherID := args[4].Int()
+	notificationsID := args[1].Int()
+	wasmJsPath := args[2].String()
+	storageTag := args[3].String()
+	channelsCbs := newChannelUI(args[4])
+	cipherID := args[5].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
 		exception.ThrowTrace(err)
 	}
 
-	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
+	return loadChannelsManagerWithIndexedDb(cmixID, notificationsID,
+		wasmJsPath, storageTag,
 		channelsCbs, cipher)
 }
 
@@ -485,10 +510,12 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Path to Javascript file that starts the worker (string).
-//   - args[2] - The storage tag associated with the previously created channel
+//   - args[1] - ID of [bindings.Notifications] object in tracker (int). Can be
+//     retrieved using [Notifications.GetID].
+//   - args[2] - Path to Javascript file that starts the worker (string).
+//   - args[3] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[3] - A callback object which implements the
+//   - args[4] - A callback object which implements the
 //     [bindings.ChannelUICallbacks] javascript functions.
 //
 // Returns a promise:
@@ -496,15 +523,18 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //   - Rejected with an error if loading indexedDb or the manager fails.
 func LoadChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	wasmJsPath := args[1].String()
-	storageTag := args[2].String()
-	cUI := newChannelUI(args[3])
+	notificationsID := args[1].Int()
+	wasmJsPath := args[2].String()
+	storageTag := args[3].String()
+	cUI := newChannelUI(args[4])
 
-	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
+	return loadChannelsManagerWithIndexedDb(cmixID, notificationsID,
+		wasmJsPath, storageTag,
 		cUI, nil)
 }
 
-func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
+func loadChannelsManagerWithIndexedDb(cmixID, notificationsID int,
+	wasmJsPath, storageTag string,
 	channelsCbs bindings.ChannelUICallbacks,
 	cipher *bindings.ChannelDbCipher) any {
 
@@ -513,7 +543,8 @@ func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(
-			cmixID, storageTag, model, nil, channelsCbs)
+			cmixID, storageTag, model, nil, notificationsID,
+			channelsCbs)
 		if err != nil {
 			reject(exception.NewTrace(err))
 		} else {
@@ -943,6 +974,9 @@ func ValidForever(js.Value, []js.Value) any {
 //     to the user should be tracked while all actions should not be (boolean).
 //   - args[5] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[6] - pingBytes - An array of public keys of users that
+//     should receive mobile notifications for the message (JSON of
+//     []Uint8Array list).
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -954,10 +988,16 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 	leaseTimeMS := int64(args[3].Int())
 	tracked := args[4].Bool()
 	cmixParamsJSON := utils.CopyBytesToGo(args[5])
+	var pingBytes [][]byte
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType,
-			msg, leaseTimeMS, tracked, cmixParamsJSON)
+		err := json.Unmarshal(utils.CopyBytesToGo(args[6]), pingBytes)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		}
+		sendReport, err := cm.api.SendGeneric(marshalledChanId,
+			messageType, msg, leaseTimeMS, tracked,
+			cmixParamsJSON, pingBytes)
 		if err != nil {
 			reject(exception.NewTrace(err))
 		} else {
@@ -987,6 +1027,9 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 //     be enumerated here. Use [ValidForever] to last the max message life.
 //   - args[3] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[4] - pingBytes - An array of public keys of users that
+//     should receive mobile notifications for the message (JSON of
+//     []Uint8Array list).
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -996,10 +1039,16 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 	msg := args[1].String()
 	leaseTimeMS := int64(args[2].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[3])
+	var pingBytes [][]byte
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := json.Unmarshal(utils.CopyBytesToGo(args[6]), pingBytes)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		}
 		sendReport, err := cm.api.SendMessage(
-			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON)
+			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON,
+			pingBytes)
 		if err != nil {
 			reject(exception.NewTrace(err))
 		} else {
@@ -1036,6 +1085,9 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 //     be enumerated here. Use [ValidForever] to last the max message life.
 //   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[6] - pingBytes - An array of public keys of users that
+//     should receive mobile notifications for the message (JSON of
+//     []Uint8Array list).
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -1046,10 +1098,16 @@ func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 	messageToReactTo := utils.CopyBytesToGo(args[2])
 	leaseTimeMS := int64(args[3].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+	var pingBytes [][]byte
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := json.Unmarshal(utils.CopyBytesToGo(args[6]), pingBytes)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		}
 		sendReport, err := cm.api.SendReply(marshalledChanId, msg,
-			messageToReactTo, leaseTimeMS, cmixParamsJSON)
+			messageToReactTo, leaseTimeMS, cmixParamsJSON,
+			pingBytes)
 		if err != nil {
 			reject(exception.NewTrace(err))
 		} else {
@@ -1082,20 +1140,26 @@ func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 //     be enumerated here. Use [ValidForever] to last the max message life.
 //   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[5] - pingBytes - An array of public keys of users that
+//     should receive mobile notifications for the message (JSON of
+//     []Uint8Array list).
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
 //   - Rejected with an error if sending fails.
 func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
-	var (
-		marshalledChanId = utils.CopyBytesToGo(args[0])
-		reaction         = args[1].String()
-		messageToReactTo = utils.CopyBytesToGo(args[2])
-		leaseTimeMS      = int64(args[3].Int())
-		cmixParamsJSON   = utils.CopyBytesToGo(args[4])
-	)
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+	reaction := args[1].String()
+	messageToReactTo := utils.CopyBytesToGo(args[2])
+	leaseTimeMS := int64(args[3].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+	var pingBytes [][]byte
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := json.Unmarshal(utils.CopyBytesToGo(args[6]), pingBytes)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		}
 		sendReport, err := cm.api.SendReaction(marshalledChanId, reaction,
 			messageToReactTo, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
@@ -1480,7 +1544,7 @@ func (cm *ChannelsManager) GetNotificationLevel(_ js.Value,
 
 	level, err := cm.api.GetNotificationLevel(channelIDBytes)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1511,7 +1575,7 @@ func (cm *ChannelsManager) SetMobileNotificationsLevel(_ js.Value,
 	err := cm.api.SetMobileNotificationsLevel(channelIDBytes, level,
 		status, push)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 	return nil
 }
@@ -1535,7 +1599,7 @@ func GetNotificationReportsForMe(_ js.Value, args []js.Value) any {
 	nrs, err := bindings.GetNotificationReportsForMe(notificationFilterJSON,
 		notificationDataJSON)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -2278,20 +2342,33 @@ func (c *ChannelDbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any {
 // channelUI callbacks implementation struct.
 func newChannelUI(cbImpl js.Value) *channelUI {
 	return &channelUI{
-		messageReceived: utils.WrapCB(cbImpl, "MessageReceived"),
-		userMuted:       utils.WrapCB(cbImpl, "UserMuted"),
-		messageDeleted:  utils.WrapCB(cbImpl, "MessageDeleted"),
-		nicknameUpdate:  utils.WrapCB(cbImpl, "NicknameUpdate"),
+		notificationUpdate: utils.WrapCB(cbImpl, "NotificationUpdate"),
+		messageReceived:    utils.WrapCB(cbImpl, "MessageReceived"),
+		userMuted:          utils.WrapCB(cbImpl, "UserMuted"),
+		messageDeleted:     utils.WrapCB(cbImpl, "MessageDeleted"),
+		nicknameUpdate:     utils.WrapCB(cbImpl, "NicknameUpdate"),
 	}
 }
 
 // eventModel wraps Javascript callbacks to adhere to the
 // [bindings.ChannelUICallbacks] interface.
 type channelUI struct {
-	messageReceived func(args ...any) js.Value
-	userMuted       func(args ...any) js.Value
-	messageDeleted  func(args ...any) js.Value
-	nicknameUpdate  func(args ...any) js.Value
+	notificationUpdate func(args ...any) js.Value
+	messageReceived    func(args ...any) js.Value
+	userMuted          func(args ...any) js.Value
+	messageDeleted     func(args ...any) js.Value
+	nicknameUpdate     func(args ...any) js.Value
+}
+
+// NotificationUpdate implements
+// [bindings.ChannelUICallbacks.NotificationUpdate].
+func (c *channelUI) NotificationUpdate(notificationFilterListJSON,
+	changedNotificationStatesJSON, deletedNotificationStatesJSON []byte,
+	maxState int) {
+	c.notificationUpdate(notificationFilterListJSON,
+		changedNotificationStatesJSON,
+		deletedNotificationStatesJSON,
+		maxState)
 }
 
 // MessageReceived implements [bindings.ChannelUICallbacks.MessageReceived].
diff --git a/wasm/cmix.go b/wasm/cmix.go
index 3b735f4e..be7ad6d3 100644
--- a/wasm/cmix.go
+++ b/wasm/cmix.go
@@ -15,7 +15,6 @@ import (
 	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/wasm-utils/utils"
-	"syscall/js"
 )
 
 // Cmix wraps the [bindings.Cmix] object so its methods can be wrapped to be
@@ -168,7 +167,7 @@ func LoadSynchronizedCmix(_ js.Value, args []js.Value) any {
 		net, err := bindings.LoadSynchronizedCmix(storageDir, password,
 			rs, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newCmixJS(net))
 		}
diff --git a/wasm/collective.go b/wasm/collective.go
index 5b35b3d4..3ce0c125 100644
--- a/wasm/collective.go
+++ b/wasm/collective.go
@@ -13,6 +13,7 @@ import (
 	"syscall/js"
 
 	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
@@ -72,7 +73,7 @@ func (r *RemoteKV) Get(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		value, err := r.api.Get(key, version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(value))
 		}
@@ -97,7 +98,7 @@ func (r *RemoteKV) Delete(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := r.api.Delete(key, version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -128,7 +129,7 @@ func (r *RemoteKV) Set(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := r.api.Set(key, value)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -178,7 +179,7 @@ func (r *RemoteKV) Prefix(_ js.Value, args []js.Value) any {
 		newAPI, err := r.api.Prefix(prefix)
 
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newRemoteKvJS(newAPI))
 		}
@@ -193,7 +194,7 @@ func (r *RemoteKV) Root(_ js.Value, args []js.Value) any {
 		newAPI, err := r.api.Root()
 
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newRemoteKvJS(newAPI))
 		}
@@ -260,7 +261,7 @@ func (r *RemoteKV) StoreMapElement(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := r.api.StoreMapElement(mapName, elementKey, val, version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -291,7 +292,7 @@ func (r *RemoteKV) StoreMap(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := r.api.StoreMap(mapName, val, version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -321,7 +322,7 @@ func (r *RemoteKV) DeleteMapElement(_ js.Value, args []js.Value) any {
 		deleted, err := r.api.DeleteMapElement(mapName, elementKey,
 			version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(deleted))
 		}
@@ -350,7 +351,7 @@ func (r *RemoteKV) GetMap(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		mapJSON, err := r.api.GetMap(mapName, version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(mapJSON))
 		}
@@ -381,7 +382,7 @@ func (r *RemoteKV) GetMapElement(_ js.Value, args []js.Value) any {
 		deleted, err := r.api.GetMapElement(mapName, elementKey,
 			version)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(deleted))
 		}
@@ -411,7 +412,7 @@ func (r *RemoteKV) ListenOnRemoteKey(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		deleted, err := r.api.ListenOnRemoteKey(key, version, cb)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(deleted))
 		}
@@ -441,7 +442,7 @@ func (r *RemoteKV) ListenOnRemoteMap(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		deleted, err := r.api.ListenOnRemoteMap(mapName, version, cb)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(deleted))
 		}
@@ -487,7 +488,7 @@ func newRemoteStore(arg js.Value) *RemoteStore {
 func (rsCB *RemoteStore) Read(path string) ([]byte, error) {
 
 	fn := func() js.Value { return rsCB.read(path) }
-	v, err := utils.RunAndCatch(fn)
+	v, err := exception.RunAndCatch(fn)
 	if err != nil {
 		return nil, err
 	}
@@ -504,7 +505,7 @@ func (rsCB *RemoteStore) Read(path string) ([]byte, error) {
 //   - Catches any thrown errors (of type Error) and returns it as an error.
 func (rsCB *RemoteStore) Write(path string, data []byte) error {
 	fn := func() js.Value { return rsCB.write(path, utils.CopyBytesToJS(data)) }
-	_, err := utils.RunAndCatch(fn)
+	_, err := exception.RunAndCatch(fn)
 	return err
 }
 
@@ -518,7 +519,7 @@ func (rsCB *RemoteStore) Write(path string, data []byte) error {
 //   - Catches any thrown errors (of type Error) and returns it as an error.
 func (rsCB *RemoteStore) GetLastModified(path string) ([]byte, error) {
 	fn := func() js.Value { return rsCB.getLastModified(path) }
-	v, err := utils.RunAndCatch(fn)
+	v, err := exception.RunAndCatch(fn)
 	if err != nil {
 		return nil, err
 	}
@@ -532,7 +533,7 @@ func (rsCB *RemoteStore) GetLastModified(path string) ([]byte, error) {
 //   - Catches any thrown errors (of type Error) and returns it as an error.
 func (rsCB *RemoteStore) GetLastWrite() ([]byte, error) {
 	fn := func() js.Value { return rsCB.getLastWrite() }
-	v, err := utils.RunAndCatch(fn)
+	v, err := exception.RunAndCatch(fn)
 	if err != nil {
 		return nil, err
 	}
@@ -549,7 +550,7 @@ func (rsCB *RemoteStore) GetLastWrite() ([]byte, error) {
 //   - Catches any thrown errors (of type Error) and returns it as an error.
 func (rsCB *RemoteStore) ReadDir(path string) ([]byte, error) {
 	fn := func() js.Value { return rsCB.readDir(path) }
-	v, err := utils.RunAndCatch(fn)
+	v, err := exception.RunAndCatch(fn)
 	if err != nil {
 		return nil, err
 	}
diff --git a/wasm/e2eHandler.go b/wasm/e2eHandler.go
index 7f3b5966..fd107b0b 100644
--- a/wasm/e2eHandler.go
+++ b/wasm/e2eHandler.go
@@ -10,9 +10,10 @@
 package wasm
 
 import (
+	"syscall/js"
+
 	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/wasm-utils/utils"
-	"syscall/js"
 )
 
 // GetReceptionID returns the marshalled default IDs.
@@ -200,13 +201,19 @@ type processor struct {
 //
 // Parameters:
 //   - message - Returns the message contents (Uint8Array).
+//   - tags - a byte array representing the tags on the message (Uint8Array)
+//   - metadata - other arbitrary metadata (Uint8Array)
 //   - receptionId - Returns the marshalled bytes of the sender's [id.ID]
 //     (Uint8Array).
 //   - ephemeralId - Returns the ephemeral ID of the sender (int).
 //   - roundId - Returns the ID of the round sent on (int).
-func (p *processor) Process(
-	message, receptionId []byte, ephemeralId, roundId int64) {
-	p.process(utils.CopyBytesToJS(message), utils.CopyBytesToJS(receptionId),
+func (p *processor) Process(message, tags, metadata, receptionId []byte,
+	ephemeralId int64, roundId int64) {
+
+	p.process(utils.CopyBytesToJS(message),
+		utils.CopyBytesToJS(tags),
+		utils.CopyBytesToJS(metadata),
+		utils.CopyBytesToJS(receptionId),
 		ephemeralId, roundId)
 }
 
@@ -233,7 +240,9 @@ func (p *processor) String() string {
 //   - Throws TypeError if registering the service fails.
 func (e *E2e) AddService(_ js.Value, args []js.Value) any {
 	p := &processor{
-		utils.WrapCB(args[1], "Process"), utils.WrapCB(args[1], "String")}
+		utils.WrapCB(args[1], "Process"),
+		utils.WrapCB(args[1], "String"),
+	}
 
 	err := e.api.AddService(args[0].String(), p)
 	if err != nil {
diff --git a/wasm/follow.go b/wasm/follow.go
index b215bf54..a0218835 100644
--- a/wasm/follow.go
+++ b/wasm/follow.go
@@ -10,10 +10,11 @@
 package wasm
 
 import (
+	"syscall/js"
+
 	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
-	"syscall/js"
 )
 
 // StartNetworkFollower kicks off the tracking of the network. It starts long-
@@ -381,6 +382,53 @@ func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) {
 	tsc.callback(utils.CopyBytesToJS(marshalData), exception.NewTrace(err))
 }
 
+// trackServicesCallback adheres to the [bindings.TrackServicesCallback]
+// interface.
+type trackCompressedServicesCallback struct {
+	callback func(args ...any) js.Value
+}
+
+// Callback is the callback for [Cmix.TrackCompressedServices]. This
+// will pass to the user a JSON-marshalled list of backend
+// services. If there was an error retrieving or marshalling the
+// service list, there is an error for the second parameter, which
+// will be non-null.
+//
+// Parameters:
+//   - marshalData - Returns the JSON of
+//     [message.CompressedServiceList] (Uint8Array).
+//   - err - Returns an error on failure (Error).
+//
+// Example JSON:
+//
+//		{
+//	   "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [
+//	     {
+//	       "Identifier": null,
+//	       "Tags": ["test"],
+//	       "Metadata": null
+//	     }
+//	   ],
+//	   "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [
+//	     {
+//	       "Identifier": null,
+//	       "Tags": ["test"],
+//	       "Metadata": null
+//	     }
+//	   ],
+//	   "AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [
+//	     {
+//	       "Identifier": null,
+//	       "Tags": ["test"],
+//	       "Metadata": null
+//	     }
+//	   ]
+//	 }
+func (tcsc *trackCompressedServicesCallback) Callback(marshalData []byte,
+	err error) {
+	tcsc.callback(utils.CopyBytesToJS(marshalData), exception.NewTrace(err))
+}
+
 // TrackServicesWithIdentity will return via a callback the list of services the
 // backend keeps track of for the provided identity. This may be passed into
 // other bindings call which may need context on the available services for this
@@ -389,13 +437,20 @@ func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) {
 // Parameters:
 //   - args[0] - ID of [E2e] object in tracker (int).
 //   - args[1] - Javascript object that has functions that implement the
-//     [bindings.ClientError] interface.
+//     [bindings.TrackServicesCallback] interface.
+//   - args[1] - Javascript object that has functions that implement the
+//     [bindings.TrackCompressedServicesCallback] interface.
 //
 // Returns:
 //   - Throws TypeError if the [E2e] ID is invalid.
 func (c *Cmix) TrackServicesWithIdentity(_ js.Value, args []js.Value) any {
 	err := c.api.TrackServicesWithIdentity(args[0].Int(),
-		&trackServicesCallback{utils.WrapCB(args[0], "Callback")})
+		&trackServicesCallback{
+			utils.WrapCB(args[0], "Callback"),
+		},
+		&trackCompressedServicesCallback{
+			utils.WrapCB(args[0], "Callback"),
+		})
 	if err != nil {
 		exception.ThrowTrace(err)
 		return nil
diff --git a/wasm/notifications.go b/wasm/notifications.go
index 39ee8d08..2a488729 100644
--- a/wasm/notifications.go
+++ b/wasm/notifications.go
@@ -14,7 +14,7 @@ import (
 
 	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/notifications"
-	"gitlab.com/elixxir/wasm-utils/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
 )
 
 type Notifications struct {
@@ -46,7 +46,7 @@ func LoadNotifications(_ js.Value, args []js.Value) any {
 	cMixID := args[0].Int()
 	api, err := bindings.LoadNotifications(cMixID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -64,7 +64,7 @@ func LoadNotificationsDummy(_ js.Value, args []js.Value) any {
 	cMixID := args[0].Int()
 	api, err := bindings.LoadNotificationsDummy(cMixID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -89,7 +89,7 @@ func (n *Notifications) AddToken(_ js.Value, args []js.Value) any {
 
 	err := n.api.AddToken(newToken, app)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return nil
@@ -101,7 +101,7 @@ func (n *Notifications) AddToken(_ js.Value, args []js.Value) any {
 func (n *Notifications) RemoveToken(_ js.Value, args []js.Value) any {
 	err := n.api.RemoveToken()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 	return nil
 }
@@ -117,7 +117,7 @@ func (n *Notifications) SetMaxState(_ js.Value, args []js.Value) any {
 
 	err := n.api.SetMaxState(notifications.NotificationState(maxState))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return nil
-- 
GitLab