Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js): js sdk preferences #5701

Merged
merged 14 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`);
}

Comment on lines +224 to +231
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We provide newer APIs but it wasn't exposed with this ApiService class.

async updateSubscriberPreference(
templateId: string,
channelType: string,
Expand Down
8 changes: 4 additions & 4 deletions packages/js/scripts/size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const modules = [
{
name: 'UMD minified',
filePath: umdPath,
limit: '10 kb',
limitInBytes: 20_000,
limit: '90 kb',
limitInBytes: 90_000,
},
{
name: 'UMD gzip',
filePath: umdGzipPath,
limit: '10 kb',
limitInBytes: 10_000,
limit: '25 kb',
limitInBytes: 25_000,
},
];

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
11 changes: 9 additions & 2 deletions packages/js/src/feeds/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ import type {
MarkNotificationActionAsByInstanceArgs,
} from './types';
import { READ_OR_UNREAD, SEEN_OR_UNSEEN } from '../utils/notification-utils';
import { markActionAs, markNotificationAs, markNotificationsAs, remove, removeNotifications } from './helpers';
import {
mapFromApiNotification,
markActionAs,
markNotificationAs,
markNotificationsAs,
remove,
removeNotifications,
} from './helpers';

export class Feeds extends BaseModule {
async fetch({ page = 0, status, ...restOptions }: FetchFeedArgs = {}): Promise<PaginatedResponse<Notification>> {
Expand All @@ -39,7 +46,7 @@ export class Feeds extends BaseModule {
});
const modifiedResponse: PaginatedResponse<Notification> = {
...response,
data: response.data.map((el) => new Notification(el as TODO)),
data: response.data.map((el) => new Notification(mapFromApiNotification(el as TODO))),
};

this._emitter.emit('feeds.fetch.success', { args, result: modifiedResponse });
Expand Down
29 changes: 21 additions & 8 deletions packages/js/src/feeds/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import {
RemoveNotificationsArgs,
} from './types';

export const mapFromApiNotification = (apiNotification: TODO): Notification =>
new Notification({
id: apiNotification.id,
feedIdentifier: apiNotification._feedId,
createdAt: apiNotification.createdAt,
avatar: apiNotification.actor,
body: apiNotification.content,
read: apiNotification.read,
seen: apiNotification.seen,
deleted: apiNotification.deleted,
cta: apiNotification.cta,
});

const getOptimisticMarkAs = (status: NotificationStatus): Partial<Notification> => {
switch (status) {
case NotificationStatus.READ:
Expand Down Expand Up @@ -49,7 +62,7 @@ export const markNotificationAs = async ({
args: MarkNotificationAsArgs;
}): Promise<Notification> => {
const isNotification = typeof notification !== 'undefined';
const notificationId = isNotification ? notification._id : id ?? '';
const notificationId = isNotification ? notification.id : id ?? '';
const args = { id, notification, status };
try {
emitter.emit('notification.mark_as.pending', {
Expand All @@ -61,7 +74,7 @@ export const markNotificationAs = async ({
messageId: [notificationId],
markAs: status,
});
const updatedNotification = new Notification(response[0] as TODO);
const updatedNotification = new Notification(mapFromApiNotification(response[0] as TODO));

emitter.emit('notification.mark_as.success', { args, result: updatedNotification });

Expand Down Expand Up @@ -121,7 +134,7 @@ export const markActionAs = async ({
args: MarkNotificationActionAsArgs;
}): Promise<Notification> => {
const isNotification = typeof notification !== 'undefined';
const notificationId = isNotification ? notification._id : id ?? '';
const notificationId = isNotification ? notification.id : id ?? '';
const args = { id, notification, button, status };
try {
emitter.emit('notification.mark_action_as.pending', {
Expand All @@ -132,7 +145,7 @@ export const markActionAs = async ({
});

const response = await apiService.updateAction(notificationId, button, status);
const updatedNotification = new Notification(response as TODO);
const updatedNotification = new Notification(mapFromApiNotification(response as TODO));

emitter.emit('notification.mark_action_as.success', {
args,
Expand Down Expand Up @@ -162,7 +175,7 @@ export const remove = async ({
args: RemoveNotificationArgs;
}): Promise<Notification | void> => {
const isNotification = typeof notification !== 'undefined';
const notificationId = isNotification ? notification._id : id ?? '';
const notificationId = isNotification ? notification.id : id ?? '';
const args = { id, notification };
try {
const deletedNotification = isNotification
Expand Down Expand Up @@ -205,7 +218,7 @@ export const removeNotifications = async ({
: undefined;
emitter.emit('feeds.remove_notifications.pending', { args, optimistic: optimisticNotifications });

const notificationIds = isNotificationArray ? notifications.map((el) => el._id) : ids ?? [];
const notificationIds = isNotificationArray ? notifications.map((el) => el.id) : ids ?? [];
await apiService.removeMessages(notificationIds);

emitter.emit('feeds.remove_notifications.success', { args, result: optimisticNotifications });
Expand Down Expand Up @@ -239,14 +252,14 @@ export const markNotificationsAs = async ({
emitter.emit('feeds.mark_notifications_as.pending', { args, optimistic: optimisticNotifications });

const notificationIds = isNotificationArray
? notifications.map((el) => (typeof el === 'string' ? el : el._id))
? notifications.map((el) => (typeof el === 'string' ? el : el.id))
: ids ?? [];
const response = await apiService.markMessagesAs({
messageId: notificationIds,
markAs: status,
});

const updatedNotifications = response.map((el) => new Notification(el as TODO));
const updatedNotifications = response.map((el) => new Notification(mapFromApiNotification(el as TODO)));
emitter.emit('feeds.mark_notifications_as.success', {
args,
result: updatedNotifications,
Expand Down
61 changes: 15 additions & 46 deletions packages/js/src/feeds/notification.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,42 @@
import { ApiService } from '@novu/client';

import { EventHandler, EventNames, Events, NovuEventEmitter } from '../event-emitter';
import {
Actor,
NotificationActionStatus,
NotificationButton,
Cta,
NotificationStatus,
Subscriber,
TODO,
} from '../types';
import { Avatar, NotificationActionStatus, NotificationButton, Cta, NotificationStatus, TODO } from '../types';
import { ApiServiceSingleton } from '../utils/api-service-singleton';
import { markActionAs, markNotificationAs, remove } from './helpers';

type NotificationLike = Pick<
Notification,
| '_id'
| '_feedId'
| 'createdAt'
| 'updatedAt'
| 'actor'
| 'subscriber'
| 'transactionId'
| 'content'
| 'read'
| 'seen'
| 'deleted'
| 'cta'
| 'payload'
| 'overrides'
'id' | 'feedIdentifier' | 'createdAt' | 'avatar' | 'body' | 'read' | 'seen' | 'deleted' | 'cta'
>;

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 feedIdentifier?: string | null;
readonly createdAt: string;
readonly avatar?: Avatar;
readonly body: string;
readonly read: boolean;
readonly seen: boolean;
readonly deleted: boolean;
LetItRock marked this conversation as resolved.
Show resolved Hide resolved
readonly cta: Cta;

constructor(notification: NotificationLike) {
this.#emitter = NovuEventEmitter.getInstance();
this.#apiService = ApiServiceSingleton.getInstance();

this._id = notification._id;
this._feedId = notification._feedId;
this.id = notification.id;
this.feedIdentifier = notification.feedIdentifier;
this.createdAt = notification.createdAt;
this.updatedAt = notification.updatedAt;
this.actor = notification.actor;
this.subscriber = notification.subscriber;
this.transactionId = notification.transactionId;
this.content = notification.content;
this.avatar = notification.avatar;
this.body = notification.body;
this.read = notification.read;
this.seen = notification.seen;
this.deleted = notification.deleted;
this.cta = notification.cta;
this.payload = notification.payload;
this.overrides = notification.overrides;
}

markAsRead(): Promise<Notification> {
Expand Down
4 changes: 3 additions & 1 deletion packages/js/src/novu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Session } from './session';
import { Preferences } from './preferences';
import { ApiServiceSingleton } from './utils/api-service-singleton';

const PRODUCTION_BACKEND_URL = 'https://api.novu.co';

type NovuOptions = {
applicationIdentifier: string;
subscriberId: string;
Expand All @@ -20,7 +22,7 @@ export class Novu implements Pick<NovuEventEmitter, 'on' | 'off'> {
public readonly preferences: Preferences;

constructor(options: NovuOptions) {
ApiServiceSingleton.getInstance({ backendUrl: options.backendUrl });
ApiServiceSingleton.getInstance({ backendUrl: options.backendUrl ?? PRODUCTION_BACKEND_URL });
this.#emitter = NovuEventEmitter.getInstance({ recreate: true });
this.#session = new Session({
applicationIdentifier: options.applicationIdentifier,
Expand Down
70 changes: 70 additions & 0 deletions packages/js/src/preferences/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ApiService } from '@novu/client';

import type { NovuEventEmitter } from '../event-emitter';
import type { 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;
};
};
}): 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: hasWorkflow
? {
id: workflow?._id,
name: workflow?.name,
critical: workflow?.critical,
identifier: workflow?.identifier,
data: workflow?.data,
}
: undefined,
});
};

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 }]);
}
LetItRock marked this conversation as resolved.
Show resolved Hide resolved

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;
}
};
Loading
Loading