Skip to content

Commit

Permalink
feat(framework): cta support with target (novuhq#6394)
Browse files Browse the repository at this point in the history
  • Loading branch information
BiswaViraj authored Aug 28, 2024
1 parent 5abdaaa commit 6062699
Show file tree
Hide file tree
Showing 12 changed files with 90 additions and 19 deletions.
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,9 @@
"Primitivestring",
"Listofobjects",
"Nestedobject",
"Listoflistofobjects"
"Listoflistofobjects",
"noopener",
"noreferrer"
],
"flagWords": [],
"patterns": [
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/app/inbox/utils/notification-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,29 @@ const mapSingleItem = ({
primaryAction: primaryCta && {
label: primaryCta.content,
isCompleted: actionType === ButtonTypeEnum.PRIMARY && actionStatus === MessageActionStatusEnum.DONE,
redirect: primaryCta.url
? {
url: primaryCta.url,
target: primaryCta.target,
}
: undefined,
},
secondaryAction: secondaryCta && {
label: secondaryCta.content,
isCompleted: actionType === ButtonTypeEnum.SECONDARY && actionStatus === MessageActionStatusEnum.DONE,
redirect: secondaryCta.url
? {
url: secondaryCta.url,
target: secondaryCta.target,
}
: undefined,
},
channelType: channel,
tags,
redirect: cta.data?.url
? {
url: cta.data.url,
target: cta.data.target,
}
: undefined,
data,
Expand Down
7 changes: 3 additions & 4 deletions apps/api/src/app/inbox/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChannelTypeEnum } from '@novu/shared';
import type { ChannelTypeEnum, Redirect } from '@novu/shared';

export type Subscriber = {
id: string;
Expand All @@ -11,6 +11,7 @@ export type Subscriber = {
type Action = {
label: string;
isCompleted: boolean;
redirect?: Redirect;
};

export type InboxNotification = {
Expand All @@ -28,10 +29,8 @@ export type InboxNotification = {
secondaryAction?: Action;
channelType: ChannelTypeEnum;
tags?: string[];
redirect?: {
url: string;
};
data?: Record<string, unknown>;
redirect?: Redirect;
};

export type NotificationFilter = {
Expand Down
5 changes: 3 additions & 2 deletions libs/shared/src/entities/messages/messages.interface.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ActorTypeEnum, ChannelCTATypeEnum, ChannelTypeEnum, IEmailBlock, UrlTarget } from '../../types';
import { INotificationTemplate } from '../notification-template';
import { ButtonTypeEnum } from './action.enum';

import { ChannelCTATypeEnum, ChannelTypeEnum, IEmailBlock, ActorTypeEnum } from '../../types';

export interface IMessage {
_id: string;
_templateId: string;
Expand Down Expand Up @@ -33,6 +32,7 @@ export interface IMessageCTA {
type: ChannelCTATypeEnum;
data: {
url?: string;
target?: UrlTarget;
};
action?: IMessageAction;
}
Expand All @@ -51,6 +51,7 @@ export interface IMessageButton {
content: string;
resultContent?: string;
url?: string;
target?: UrlTarget;
}

export enum MessageActionStatusEnum {
Expand Down
7 changes: 7 additions & 0 deletions libs/shared/src/types/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ export enum MessagesStatusEnum {
UNREAD = 'unread',
UNSEEN = 'unseen',
}

export type UrlTarget = '_self' | '_blank' | '_parent' | '_top' | '_unfencedTop';

export type Redirect = {
url: string;
target?: UrlTarget;
};
18 changes: 12 additions & 6 deletions libs/shared/src/utils/bridge.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ButtonTypeEnum, IMessage, IMessageCTA } from '../entities/messages';
import { ChannelCTATypeEnum, WorkflowTypeEnum } from '../types';
import { ChannelCTATypeEnum, Redirect, WorkflowTypeEnum } from '../types';

export const isBridgeWorkflow = (workflowType?: WorkflowTypeEnum): boolean => {
return workflowType === WorkflowTypeEnum.BRIDGE || workflowType === WorkflowTypeEnum.ECHO;
Expand All @@ -19,13 +19,14 @@ type InAppOutput = {
avatar?: string;
primaryAction?: {
label: string;
url?: string;
redirect?: Redirect;
};
secondaryAction?: {
label: string;
url?: string;
redirect?: Redirect;
};
data?: Record<string, unknown>;
redirect?: Redirect;
};

type InAppMessage = Pick<IMessage, 'subject' | 'content' | 'cta' | 'avatar' | 'data'>;
Expand All @@ -36,7 +37,10 @@ type InAppMessage = Pick<IMessage, 'subject' | 'content' | 'cta' | 'avatar' | 'd
export const inAppMessageFromBridgeOutputs = (outputs?: InAppOutput) => {
const cta = {
type: ChannelCTATypeEnum.REDIRECT,
data: {},
data: {
url: outputs?.redirect?.url,
target: outputs?.redirect?.target,
},
action: {
result: {},
buttons: [
Expand All @@ -45,7 +49,8 @@ export const inAppMessageFromBridgeOutputs = (outputs?: InAppOutput) => {
{
type: ButtonTypeEnum.PRIMARY,
content: outputs.primaryAction.label,
url: outputs.primaryAction.url,
url: outputs.primaryAction.redirect?.url,
target: outputs?.primaryAction.redirect?.target,
},
]
: []),
Expand All @@ -54,7 +59,8 @@ export const inAppMessageFromBridgeOutputs = (outputs?: InAppOutput) => {
{
type: ButtonTypeEnum.SECONDARY,
content: outputs.secondaryAction.label,
url: outputs.secondaryAction.url,
url: outputs.secondaryAction.redirect?.url,
target: outputs?.secondaryAction.redirect?.target,
},
]
: []),
Expand Down
13 changes: 12 additions & 1 deletion packages/framework/src/schemas/steps/channels/in-app.schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Schema } from '../../../types/schema.types';

const redirectSchema = {
type: 'object',
properties: {
url: { type: 'string', format: 'uri' },
target: { type: 'string', enum: ['_self', '_blank', '_parent', '_top', '_unfencedTop'], default: '_blank' },
},
required: ['url'],
additionalProperties: false,
} as const satisfies Schema;

const actionSchema = {
type: 'object',
properties: {
label: { type: 'string' },
url: { type: 'string' },
redirect: redirectSchema,
},
required: ['label'],
additionalProperties: false,
Expand All @@ -19,6 +29,7 @@ const inAppOutputSchema = {
primaryAction: actionSchema,
secondaryAction: actionSchema,
data: { type: 'object', additionalProperties: true },
redirect: redirectSchema,
},
required: ['body'],
additionalProperties: false,
Expand Down
10 changes: 7 additions & 3 deletions packages/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,15 @@ export type Subscriber = {
subscriberId: string;
};

export type Redirect = {
url: string;
target?: '_self' | '_blank' | '_parent' | '_top' | '_unfencedTop';
};

export type Action = {
label: string;
isCompleted: boolean;
redirect?: Redirect;
};

export type InboxNotification = {
Expand All @@ -99,10 +105,8 @@ export type InboxNotification = {
secondaryAction?: Action;
channelType: ChannelType;
tags?: string[];
redirect?: {
url: string;
};
data?: Record<string, unknown>;
redirect?: Redirect;
};

export type NotificationFilter = {
Expand Down
15 changes: 13 additions & 2 deletions packages/js/src/ui/components/Notification/DefaultNotification.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createEffect, createMemo, createSignal, JSX, Show } from 'solid-js';

import type { Notification } from '../../../notifications';
import { ActionTypeEnum } from '../../../types';
import { useInboxContext, useLocalization } from '../../context';
import { cn, formatToRelativeTime, useStyle } from '../../helpers';
import { cn, formatToRelativeTime, useStyle, DEFAULT_TARGET, DEFAULT_REFERRER } from '../../helpers';
import { Archive, ReadAll, Unarchive, Unread } from '../../icons';
import type { NotificationActionClickHandler, NotificationClickHandler } from '../../types';
import { NotificationStatus } from '../../types';
Expand Down Expand Up @@ -45,7 +46,8 @@ export const DefaultNotification = (props: DefaultNotificationProps) => {

props.onNotificationClick?.(props.notification);
if (props.notification.redirect?.url) {
window.open(props.notification.redirect?.url, '_blank', 'noreferrer noopener');
const target = props.notification.redirect?.target || DEFAULT_TARGET;
window.open(props.notification.redirect?.url, target, DEFAULT_REFERRER);
}
};

Expand All @@ -55,9 +57,18 @@ export const DefaultNotification = (props: DefaultNotificationProps) => {
if (action === ActionTypeEnum.PRIMARY) {
props.notification.completePrimary();
props.onPrimaryActionClick?.(props.notification);
if (props.notification.primaryAction?.redirect?.url) {
const target = props.notification.primaryAction?.redirect?.target || DEFAULT_TARGET;
window.open(props.notification.primaryAction?.redirect?.url, target, DEFAULT_REFERRER);
}
} else {
props.notification.completeSecondary();
props.onSecondaryActionClick?.(props.notification);

if (props.notification.secondaryAction?.redirect?.url) {
const target = props.notification.secondaryAction?.redirect?.target || DEFAULT_TARGET;
window.open(props.notification.secondaryAction?.redirect?.url, target, DEFAULT_REFERRER);
}
}
};

Expand Down
3 changes: 3 additions & 0 deletions packages/js/src/ui/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export const NV_INBOX_TABS_CHANNEL = 'nv-inbox-tabs-channel';
export const NV_INBOX_WEBSOCKET_LOCK = 'nv-inbox-websocket-lock';

export const DEFAULT_TARGET = '_blank';
export const DEFAULT_REFERRER = 'noopener noreferrer';
1 change: 1 addition & 0 deletions packages/js/src/ui/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './formatToRelativeTime';
export * from './useStyle';
export * from './useUncontrolledState';
export * from './utils';
export * from './constants';
13 changes: 13 additions & 0 deletions packages/js/src/ws/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,29 @@ const mapToNotification = ({
primaryAction: primaryCta && {
label: primaryCta.content,
isCompleted: actionType === ActionTypeEnum.PRIMARY && actionStatus === NotificationActionStatus.DONE,
redirect: primaryCta.url
? {
target: primaryCta.target,
url: primaryCta.url,
}
: undefined,
},
secondaryAction: secondaryCta && {
label: secondaryCta.content,
isCompleted: actionType === ActionTypeEnum.SECONDARY && actionStatus === NotificationActionStatus.DONE,
redirect: secondaryCta.url
? {
target: secondaryCta.target,
url: secondaryCta.url,
}
: undefined,
},
channelType: channel,
tags,
redirect: cta.data?.url
? {
url: cta.data.url,
target: cta.data.target,
}
: undefined,
data,
Expand Down

0 comments on commit 6062699

Please sign in to comment.