Skip to content

Commit

Permalink
feat(js): js sdk preferences
Browse files Browse the repository at this point in the history
  • Loading branch information
LetItRock committed Jun 10, 2024
1 parent 9448d67 commit 713d403
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 16 deletions.
15 changes: 15 additions & 0 deletions packages/client/src/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ISessionDto,
INotificationDto,
MarkMessagesAsEnum,
PreferenceLevelEnum,
} from '@novu/shared';
import { HttpClient } from '../http-client';
import {
Expand Down Expand Up @@ -206,14 +207,28 @@ export class ApiService {
return this.httpClient.get('/widgets/organization');
}

/**
* @deprecated use getPreferences instead
*/
async getUserPreference(): Promise<IUserPreferenceSettings[]> {
return this.httpClient.get('/widgets/preferences');
}

/**
* @deprecated use getPreferences instead
*/
async getUserGlobalPreference(): Promise<IUserGlobalPreferenceSettings[]> {
return this.httpClient.get('/widgets/preferences/global');
}

async getPreferences({
level = PreferenceLevelEnum.TEMPLATE,
}: {
level?: `${PreferenceLevelEnum}`;
}): Promise<Array<IUserPreferenceSettings | IUserGlobalPreferenceSettings>> {
return this.httpClient.get(`/widgets/preferences/${level}`);
}

async updateSubscriberPreference(
templateId: string,
channelType: string,
Expand Down
8 changes: 7 additions & 1 deletion packages/js/src/event-emitter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand All @@ -113,7 +117,9 @@ export type Events = SessionInitializeEvents &
FeedRemoveAllNotificationsEvents &
NotificationMarkAsEvents &
NotificationMarkActionAsEvents &
NotificationRemoveEvents;
NotificationRemoveEvents &
PreferencesFetchEvents &
PreferencesUpdateEvents;

export type EventNames = keyof Events;

Expand Down
28 changes: 14 additions & 14 deletions packages/js/src/feeds/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@ export class Notification implements Pick<NovuEventEmitter, 'on' | 'off'> {
#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<string, unknown>;
overrides: Record<string, unknown>;
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<string, unknown>;
readonly overrides: Record<string, unknown>;

constructor(notification: NotificationLike) {
this.#emitter = NovuEventEmitter.getInstance();
Expand Down
64 changes: 64 additions & 0 deletions packages/js/src/preferences/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<Preference> => {
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;
}
};
38 changes: 38 additions & 0 deletions packages/js/src/preferences/preference.ts
Original file line number Diff line number Diff line change
@@ -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';

Check warning on line 5 in packages/js/src/preferences/preference.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (signleton)
import { updatePreference } from './helpers';

type PreferenceLike = Pick<Preference, 'level' | 'enabled' | 'channels' | 'workflow' | 'overrides'>;

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<Preference> {
return updatePreference({
emitter: this.#emitter,
apiService: this.#apiService,
args: { workflowId: this.workflow?._id, enabled, channel },
});
}
}
31 changes: 30 additions & 1 deletion packages/js/src/preferences/preferences.ts
Original file line number Diff line number Diff line change
@@ -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<Preference[]> {
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<Preference> {
return this.callWithSession(async () =>
updatePreference({ emitter: this._emitter, apiService: this._apiService, args })
);
}
}
11 changes: 11 additions & 0 deletions packages/js/src/preferences/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ChannelType, PreferenceLevel } from '../types';

export type FetchPreferencesArgs = {
level?: PreferenceLevel;
};

export type UpdatePreferencesArgs = {
workflowId?: string;
enabled: boolean;
channel: ChannelType;
};
42 changes: 42 additions & 0 deletions packages/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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<string, any>;
};

export type ChannelPreference = {
email?: boolean;
sms?: boolean;
in_app?: boolean;
chat?: boolean;
push?: boolean;
};

export type ChannelPreferenceOverride = {
channel: ChannelType;
source: PreferenceOverrideSource;
};

export type PaginatedResponse<T = unknown> = {
data: T[];
hasMore: boolean;
Expand Down

0 comments on commit 713d403

Please sign in to comment.