diff --git a/packages/client/src/api/api.service.ts b/packages/client/src/api/api.service.ts index 1ace0890580..51e02145a85 100644 --- a/packages/client/src/api/api.service.ts +++ b/packages/client/src/api/api.service.ts @@ -6,6 +6,7 @@ import { ISessionDto, INotificationDto, MarkMessagesAsEnum, + PreferenceLevelEnum, } from '@novu/shared'; import { HttpClient } from '../http-client'; import { @@ -206,14 +207,28 @@ export class ApiService { return this.httpClient.get('/widgets/organization'); } + /** + * @deprecated use getPreferences instead + */ async getUserPreference(): Promise { return this.httpClient.get('/widgets/preferences'); } + /** + * @deprecated use getPreferences instead + */ async getUserGlobalPreference(): Promise { return this.httpClient.get('/widgets/preferences/global'); } + async getPreferences({ + level = PreferenceLevelEnum.TEMPLATE, + }: { + level?: `${PreferenceLevelEnum}`; + }): Promise> { + return this.httpClient.get(`/widgets/preferences/${level}`); + } + async updateSubscriberPreference( templateId: string, channelType: string, diff --git a/packages/js/src/event-emitter/types.ts b/packages/js/src/event-emitter/types.ts index 4ab5c85c07c..4c1930c3391 100644 --- a/packages/js/src/event-emitter/types.ts +++ b/packages/js/src/event-emitter/types.ts @@ -10,6 +10,8 @@ import type { RemoveAllNotificationsArgs, RemoveNotificationsArgs, } from '../feeds'; +import { Preference } from '../preferences/preference'; +import { FetchPreferencesArgs, UpdatePreferencesArgs } from '../preferences/types'; import type { InitializeSessionArgs } from '../session'; import type { PaginatedResponse, Session } from '../types'; @@ -90,6 +92,8 @@ type NotificationRemoveEvents = BaseEvents< Notification, Notification >; +type PreferencesFetchEvents = BaseEvents<'preferences.fetch', FetchPreferencesArgs, Preference[]>; +type PreferencesUpdateEvents = BaseEvents<'preferences.update', UpdatePreferencesArgs, Preference>; /** * Events that are emitted by Novu Event Emitter. @@ -113,7 +117,9 @@ export type Events = SessionInitializeEvents & FeedRemoveAllNotificationsEvents & NotificationMarkAsEvents & NotificationMarkActionAsEvents & - NotificationRemoveEvents; + NotificationRemoveEvents & + PreferencesFetchEvents & + PreferencesUpdateEvents; export type EventNames = keyof Events; diff --git a/packages/js/src/feeds/notification.ts b/packages/js/src/feeds/notification.ts index efab12c45d3..caade290db8 100644 --- a/packages/js/src/feeds/notification.ts +++ b/packages/js/src/feeds/notification.ts @@ -35,20 +35,20 @@ export class Notification implements Pick { #emitter: NovuEventEmitter; #apiService: ApiService; - _id: string; - _feedId?: string | null; - createdAt: string; - updatedAt: string; - actor?: Actor; - subscriber?: Subscriber; - transactionId: string; - content: string; - read: boolean; - seen: boolean; - deleted: boolean; - cta: Cta; - payload: Record; - overrides: Record; + readonly _id: string; + readonly _feedId?: string | null; + readonly createdAt: string; + readonly updatedAt: string; + readonly actor?: Actor; + readonly subscriber?: Subscriber; + readonly transactionId: string; + readonly content: string; + readonly read: boolean; + readonly seen: boolean; + readonly deleted: boolean; + readonly cta: Cta; + readonly payload: Record; + readonly overrides: Record; constructor(notification: NotificationLike) { this.#emitter = NovuEventEmitter.getInstance(); diff --git a/packages/js/src/preferences/helpers.ts b/packages/js/src/preferences/helpers.ts new file mode 100644 index 00000000000..5cc862f54f4 --- /dev/null +++ b/packages/js/src/preferences/helpers.ts @@ -0,0 +1,64 @@ +import { ApiService } from '@novu/client'; + +import type { NovuEventEmitter } from '../event-emitter'; +import type { ChannelPreferenceOverride, TODO } from '../types'; +import { PreferenceLevel } from '../types'; +import { Preference } from './preference'; +import type { UpdatePreferencesArgs } from './types'; + +export const mapPreference = (apiPreference: { + template?: TODO; + preference: { + enabled: boolean; + channels: { + email?: boolean; + sms?: boolean; + in_app?: boolean; + chat?: boolean; + push?: boolean; + }; + overrides?: ChannelPreferenceOverride[]; + }; +}): Preference => { + const { template: workflow, preference } = apiPreference; + const hasWorkflow = workflow !== undefined; + const level = hasWorkflow ? PreferenceLevel.TEMPLATE : PreferenceLevel.GLOBAL; + + return new Preference({ + level, + enabled: preference.enabled, + channels: preference.channels, + workflow, + overrides: preference.overrides, + }); +}; + +export const updatePreference = async ({ + emitter, + apiService, + args, +}: { + emitter: NovuEventEmitter; + apiService: ApiService; + args: UpdatePreferencesArgs; +}): Promise => { + const { workflowId, enabled, channel } = args; + try { + emitter.emit('preferences.update.pending', { args }); + + let response; + if (workflowId) { + response = await apiService.updateSubscriberPreference(workflowId, channel, enabled); + } else { + response = await apiService.updateSubscriberGlobalPreference([{ channelType: channel, enabled }]); + } + + const preference = new Preference(mapPreference(response)); + emitter.emit('preferences.update.success', { args, result: preference }); + + return preference; + } catch (error) { + emitter.emit('preferences.update.error', { args, error }); + throw error; + } +}; diff --git a/packages/js/src/preferences/preference.ts b/packages/js/src/preferences/preference.ts new file mode 100644 index 00000000000..38ab062750b --- /dev/null +++ b/packages/js/src/preferences/preference.ts @@ -0,0 +1,38 @@ +import { ApiService } from '@novu/client'; + +import { NovuEventEmitter } from '../event-emitter'; +import { ChannelPreference, ChannelPreferenceOverride, ChannelType, PreferenceLevel, WorkflowInfo } from '../types'; +import { ApiServiceSingleton } from '../utils/api-service-signleton'; +import { updatePreference } from './helpers'; + +type PreferenceLike = Pick; + +export class Preference { + #emitter: NovuEventEmitter; + #apiService: ApiService; + + readonly level: PreferenceLevel; + readonly enabled: boolean; + readonly channels: ChannelPreference; + readonly workflow?: WorkflowInfo; + readonly overrides?: ChannelPreferenceOverride[]; + + constructor(preference: PreferenceLike) { + this.#emitter = NovuEventEmitter.getInstance(); + this.#apiService = ApiServiceSingleton.getInstance(); + + this.level = preference.level; + this.enabled = preference.enabled; + this.channels = preference.channels; + this.workflow = preference.workflow; + this.overrides = preference.overrides; + } + + updatePreference({ enabled, channel }: { enabled: boolean; channel: ChannelType }): Promise { + return updatePreference({ + emitter: this.#emitter, + apiService: this.#apiService, + args: { workflowId: this.workflow?._id, enabled, channel }, + }); + } +} diff --git a/packages/js/src/preferences/preferences.ts b/packages/js/src/preferences/preferences.ts index 8ca3d6274b1..8626d74ed28 100644 --- a/packages/js/src/preferences/preferences.ts +++ b/packages/js/src/preferences/preferences.ts @@ -1,3 +1,32 @@ import { BaseModule } from '../base-module'; +import { PreferenceLevel } from '../types'; +import { mapPreference, updatePreference } from './helpers'; +import { Preference } from './preference'; +import type { FetchPreferencesArgs, UpdatePreferencesArgs } from './types'; -export class Preferences extends BaseModule {} +export class Preferences extends BaseModule { + async fetch({ level = PreferenceLevel.TEMPLATE }: FetchPreferencesArgs = {}): Promise { + return this.callWithSession(async () => { + const args = { level }; + try { + this._emitter.emit('preferences.fetch.pending', { args }); + + const response = await this._apiService.getPreferences({ level }); + const modifiedResponse: Preference[] = response.map((el) => new Preference(mapPreference(el))); + + this._emitter.emit('preferences.fetch.success', { args, result: modifiedResponse }); + + return modifiedResponse; + } catch (error) { + this._emitter.emit('preferences.fetch.error', { args, error }); + throw error; + } + }); + } + + async update(args: UpdatePreferencesArgs): Promise { + return this.callWithSession(async () => + updatePreference({ emitter: this._emitter, apiService: this._apiService, args }) + ); + } +} diff --git a/packages/js/src/preferences/types.ts b/packages/js/src/preferences/types.ts new file mode 100644 index 00000000000..4d24307ae8e --- /dev/null +++ b/packages/js/src/preferences/types.ts @@ -0,0 +1,11 @@ +import { ChannelType, PreferenceLevel } from '../types'; + +export type FetchPreferencesArgs = { + level?: PreferenceLevel; +}; + +export type UpdatePreferencesArgs = { + workflowId?: string; + enabled: boolean; + channel: ChannelType; +}; diff --git a/packages/js/src/types.ts b/packages/js/src/types.ts index 49d1d708f3f..60a6d2f3439 100644 --- a/packages/js/src/types.ts +++ b/packages/js/src/types.ts @@ -26,6 +26,25 @@ export enum CtaType { REDIRECT = 'redirect', } +export enum PreferenceLevel { + GLOBAL = 'global', + TEMPLATE = 'template', +} + +export enum ChannelType { + IN_APP = 'in_app', + EMAIL = 'email', + SMS = 'sms', + CHAT = 'chat', + PUSH = 'push', +} + +export enum PreferenceOverrideSource { + SUBSCRIBER = 'subscriber', + TEMPLATE = 'template', + WORKFLOW_OVERRIDE = 'workflowOverride', +} + export type Session = { token: string; profile: { @@ -99,6 +118,29 @@ export type Cta = { action?: MessageAction; }; +export type WorkflowInfo = { + _id: string; + name: string; + critical: boolean; + tags?: string[]; + identifier: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record; +}; + +export type ChannelPreference = { + email?: boolean; + sms?: boolean; + in_app?: boolean; + chat?: boolean; + push?: boolean; +}; + +export type ChannelPreferenceOverride = { + channel: ChannelType; + source: PreferenceOverrideSource; +}; + export type PaginatedResponse = { data: T[]; hasMore: boolean;