/* eslint-disable @typescript-eslint/no-explicit-any */ import isEmpty from 'lodash/isEmpty'; import isEqual from 'lodash/isEqual'; import { SyncStatePolicy, UserStateManager } from './State'; import { EventLogger, NotiflyInternalEvent } from '../Event'; import { NotiflyStorage, NotiflyStorageKeys } from '../Storage'; import { SetUserIdOptions } from '../Interfaces/Options'; import { SdkStateManager, SdkType } from '../SdkState'; /** * Sets or removes user ID for the current user. * * @async * @param {string | null | undefined} params - A nullable, optional string containing the user ID to set. * @returns {Promise} * @summary If the user ID is null or undefined, the user ID will be removed. Otherwise, the user ID will be set to the provided value. * * @example * await setUserId('myUserID') // Sets the user ID to 'myUserID' * await setUserId(null) // Removes the user ID * await setUserId() // Removes the user ID */ export class UserIdentityManager { private static readonly DEFAULT_SET_USER_ID_OPTIONS = { onlyIfChanged: false, }; static async setUserId(userId?: string | null | undefined, options?: SetUserIdOptions): Promise { UserStateManager.userData.external_user_id = userId?.trim(); const onlyIfChanged = options?.onlyIfChanged ?? this.DEFAULT_SET_USER_ID_OPTIONS.onlyIfChanged; if (onlyIfChanged) { const previousUserId = await this.getUserId(); if (this._areUserIdsIdentical(userId, previousUserId)) { return; } } if (!userId?.trim()) { await this.removeUserId(); } else { await this.setUserProperties({ external_user_id: userId, }); } } static async getUserId(): Promise { return NotiflyStorage.getItem(NotiflyStorageKeys.EXTERNAL_USER_ID); } static async getUserProperties(): Promise | null> { return UserStateManager.state.userData.user_properties || null; } static async setUserProperties(params: Record) { const externalUserId = params.external_user_id?.toString(); if (externalUserId) { const projectId = await NotiflyStorage.getItem(NotiflyStorageKeys.PROJECT_ID); if (!projectId) { throw new Error('[Notifly] Project ID should be set before setting user properties.'); } const [previousExternalUserId, previousNotiflyUserId] = await Promise.all([ NotiflyStorage.getItem(NotiflyStorageKeys.EXTERNAL_USER_ID), NotiflyStorage.getNotiflyUserId(), ]); params.previous_notifly_user_id = previousNotiflyUserId; params.previous_external_user_id = previousExternalUserId; if (!this._areUserIdsIdentical(externalUserId, previousExternalUserId)) { // Caution: order matters here! await NotiflyStorage.setItem(NotiflyStorageKeys.EXTERNAL_USER_ID, externalUserId); await EventLogger.logEvent(NotiflyInternalEvent.SET_USER_PROPERTIES, params, null, true); const policy = previousExternalUserId ? SyncStatePolicy.OVERWRITE // A -> B : SyncStatePolicy.MERGE; // null -> A await UserStateManager.refresh(policy); } } else { if (SdkStateManager.type === SdkType.JS_CAFE24) { // If SDK State is JS_CAFE24, Only send diffs // Update local state const diff: Record = {}; const previousUserProperties = (await this.getUserProperties()) || {}; Object.keys(params).forEach((key) => { if (!isEqual(previousUserProperties[key], params[key])) { diff[key] = params[key]; } }); if (!isEmpty(diff)) { UserStateManager.updateUserProperties(diff); await EventLogger.logEvent(NotiflyInternalEvent.SET_USER_PROPERTIES, diff, null, true); } } else { UserStateManager.updateUserProperties(params); await EventLogger.logEvent(NotiflyInternalEvent.SET_USER_PROPERTIES, params, null, true); } } } static async removeUserId(): Promise { const previousExternalUserId = await NotiflyStorage.getItem(NotiflyStorageKeys.EXTERNAL_USER_ID); if (previousExternalUserId) { // A -> null await this._cleanUserIdInLocalStorage(); await UserStateManager.refresh(); // Should refresh data due to the random bucket number } await EventLogger.logEvent(NotiflyInternalEvent.REMOVE_EXTERNAL_USER_ID, {}, null, true); UserStateManager.clearAll(); } private static async _cleanUserIdInLocalStorage() { await NotiflyStorage.removeItem(NotiflyStorageKeys.EXTERNAL_USER_ID); } private static _areUserIdsIdentical( userId: string | null | undefined, anotherUserId: string | null | undefined ): boolean { return (!userId && !anotherUserId) || userId === anotherUserId; } }