Skip to content

Commit

Permalink
Merge branch 'next' of github.com:novuhq/novu into feat/secretManager
Browse files Browse the repository at this point in the history
  • Loading branch information
merrcury committed Dec 30, 2024
2 parents dcd3ad9 + d7f5cf2 commit c41676f
Show file tree
Hide file tree
Showing 36 changed files with 764 additions and 1,183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,6 @@ export class ParseEventRequest {
// eslint-disable-next-line no-param-reassign
command.payload = merge({}, defaultPayload, command.payload);

await this.sendInAppNudgeForTeamMemberInvite(command);

return await this.dispatchEvent(command, transactionId);
}

Expand Down Expand Up @@ -308,113 +306,4 @@ export class ParseEventRequest {

return reservedVariables?.map((reservedVariable) => reservedVariable.type) || [];
}

@Instrument()
@CachedEntity({
builder: (command: ParseEventRequestCommand) =>
buildHasNotificationKey({
_organizationId: command.organizationId,
}),
})
private async getNotificationCount(command: ParseEventRequestCommand): Promise<number> {
return await this.notificationRepository.count(
{
_organizationId: command.organizationId,
},
1
);
}

@Instrument()
private async sendInAppNudgeForTeamMemberInvite(command: ParseEventRequestCommand): Promise<void> {
try {
const isEnabled = await this.getFeatureFlag.execute(
GetFeatureFlagCommand.create({
key: FeatureFlagsKeysEnum.IS_TEAM_MEMBER_INVITE_NUDGE_ENABLED,
organizationId: command.organizationId,
userId: 'system',
environmentId: 'system',
})
);

if (!isEnabled) return;

// check if this is first trigger
const notificationCount = await this.getNotificationCount(command);

if (notificationCount > 0) return;

/*
* After the first trigger, we invalidate the cache to ensure the next event trigger
* will update the cache with a count of 1.
*/
this.invalidateCacheService.invalidateByKey({
key: buildHasNotificationKey({
_organizationId: command.organizationId,
}),
});

// check if user is using personal email
const user = await this.userRepository.findById(command.userId);

if (!user) throw new ApiException('User not found');

if (this.isBlockedEmail(user.email)) return;

// check if organization has more than 1 member
const membersCount = await this.memberRepository.count(
{
_organizationId: command.organizationId,
},
2
);

if (membersCount > 1) return;

Logger.log('No notification found', LOG_CONTEXT);

if (process.env.NOVU_API_KEY) {
if (!command.payload[INVITE_TEAM_MEMBER_NUDGE_PAYLOAD_KEY]) {
const novu = new Novu({ apiKey: process.env.NOVU_API_KEY });

await novu.trigger({
name: process.env.NOVU_INVITE_TEAM_MEMBER_NUDGE_TRIGGER_IDENTIFIER,
to: [
{
subscriberId: command.userId,
email: user?.email as string,
},
],
payload: {
[INVITE_TEAM_MEMBER_NUDGE_PAYLOAD_KEY]: true,
webhookUrl: `${process.env.API_ROOT_URL}/v1/invites/webhook`,
organizationId: command.organizationId,
},
});

this.analyticsService.track('Invite Nudge Sent', command.userId, {
_organization: command.organizationId,
});
}
}
} catch (error) {
Logger.error(error, 'Invite nudge failed', LOG_CONTEXT);
}
}

private isBlockedEmail(email: string): boolean {
return BLOCKED_DOMAINS.some((domain) => email.includes(domain));
}
}

const BLOCKED_DOMAINS = [
'@gmail',
'@outlook',
'@yahoo',
'@icloud',
'@mail',
'@hotmail',
'@protonmail',
'@gmx',
'@novu',
];
16 changes: 1 addition & 15 deletions apps/api/src/app/invites/invites.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import { ResendInviteCommand } from './usecases/resend-invite/resend-invite.comm
import { ResendInvite } from './usecases/resend-invite/resend-invite.usecase';
import { ThrottlerCost } from '../rate-limiting/guards';
import { ApiCommonResponses } from '../shared/framework/response.decorator';
import { InviteNudgeWebhookCommand } from './usecases/invite-nudge/invite-nudge.command';
import { InviteNudgeWebhook } from './usecases/invite-nudge/invite-nudge.usecase';
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';

@UseInterceptors(ClassSerializerInterceptor)
Expand All @@ -47,8 +45,7 @@ export class InvitesController {
private bulkInviteUsecase: BulkInvite,
private acceptInviteUsecase: AcceptInvite,
private getInvite: GetInvite,
private resendInviteUsecase: ResendInvite,
private inviteNudgeWebhookUsecase: InviteNudgeWebhook
private resendInviteUsecase: ResendInvite
) {}

@Get('/:inviteToken')
Expand Down Expand Up @@ -130,15 +127,4 @@ export class InvitesController {

return response;
}

@Post('/webhook')
async inviteCheckWebhook(@Headers('nv-hmac-256') hmacHeader: string, @Body() body: InviteWebhookDto) {
const command = InviteNudgeWebhookCommand.create({
hmacHeader,
subscriber: body.subscriber,
organizationId: body.payload.organizationId,
});

return await this.inviteNudgeWebhookUsecase.execute(command);
}
}
3 changes: 1 addition & 2 deletions apps/api/src/app/invites/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ import { GetInvite } from './get-invite/get-invite.usecase';
import { BulkInvite } from './bulk-invite/bulk-invite.usecase';
import { InviteMember } from './invite-member/invite-member.usecase';
import { ResendInvite } from './resend-invite/resend-invite.usecase';
import { InviteNudgeWebhook } from './invite-nudge/invite-nudge.usecase';

export const USE_CASES = [AcceptInvite, GetInvite, BulkInvite, InviteMember, ResendInvite, InviteNudgeWebhook];
export const USE_CASES = [AcceptInvite, GetInvite, BulkInvite, InviteMember, ResendInvite];

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,7 @@ export class MarkMessageAs {
$in: command.messageIds,
},
});
const isEnabled = await this.getFeatureFlag.execute(
GetFeatureFlagCommand.create({
key: FeatureFlagsKeysEnum.IS_TEAM_MEMBER_INVITE_NUDGE_ENABLED,
organizationId: command.organizationId,
userId: 'system',
environmentId: 'system',
})
);
if (command.mark.seen != null) {
if (isEnabled && (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'production')) {
await this.sendAnalyticsEventForInviteTeamNudge(messages);
}
await this.updateServices(command, subscriber, messages, MarkEnum.SEEN);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function ActivityJobItem({ job, isFirst, isLast }: ActivityJobItemProps)
{getJobIcon(job.type)}
</div>
</div>
<span className="text-foreground-950 text-xs capitalize">{formatJobType(job.type)}</span>
<span className="text-foreground-950 text-xs capitalize">{job?.step?.name || formatJobType(job.type)}</span>
</div>

<Button
Expand Down
18 changes: 4 additions & 14 deletions apps/dashboard/src/components/side-navigation/free-trial-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { RiArrowRightDoubleLine, RiInformationFill } from 'react-icons/ri';
import { Progress } from '../primitives/progress';
import { Button } from '../primitives/button';
import { Tooltip, TooltipContent, TooltipTrigger, TooltipArrow } from '../primitives/tooltip';
import { LEGACY_ROUTES, ROUTES } from '@/utils/routes';
import { ROUTES } from '@/utils/routes';
import { Link } from 'react-router-dom';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { FeatureFlagsKeysEnum, GetSubscriptionDto } from '@novu/shared';
import { GetSubscriptionDto } from '@novu/shared';

const transition = 'transition-all duration-300 ease-out';

Expand Down Expand Up @@ -70,22 +69,13 @@ const CardContent = ({
export const FreeTrialCard = ({ subscription, daysLeft }: { subscription?: GetSubscriptionDto; daysLeft: number }) => {
const daysTotal = subscription && subscription.trial.daysTotal > 0 ? subscription.trial.daysTotal : 100;
const pluralizedDays = pluralizeDaysLeft(daysLeft);
const isV2BillingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_V2_DASHBOARD_BILLING_ENABLED);

const cardClassName =
'bg-background group relative left-2 mb-2 flex w-[calc(100%-1rem)] cursor-pointer flex-col gap-2 rounded-lg p-3 shadow';

if (isV2BillingEnabled) {
return (
<Link to={ROUTES.SETTINGS_BILLING} className={cardClassName}>
<CardContent pluralizedDays={pluralizedDays} daysTotal={daysTotal} daysLeft={daysLeft} />
</Link>
);
}

return (
<a href={LEGACY_ROUTES.BILLING} className={cardClassName}>
<Link to={ROUTES.SETTINGS_BILLING} className={cardClassName}>
<CardContent pluralizedDays={pluralizedDays} daysTotal={daysTotal} daysLeft={daysLeft} />
</a>
</Link>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ import { useTelemetry } from '@/hooks/use-telemetry';
import { TelemetryEvent } from '@/utils/telemetry';
import { Button } from '../primitives/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { FeatureFlagsKeysEnum } from '@novu/shared';

export function GettingStartedMenuItem() {
const { totalSteps, completedSteps, steps } = useOnboardingSteps();
const isGettingStartedEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_GETTING_STARTED_ENABLED);

const { currentEnvironment } = useEnvironment();
const { user } = useUser();
Expand All @@ -41,7 +38,7 @@ export function GettingStartedMenuItem() {
});
};

if (!isGettingStartedEnabled || user?.unsafeMetadata?.hideGettingStarted) {
if (user?.unsafeMetadata?.hideGettingStarted) {
return null;
}

Expand Down
22 changes: 3 additions & 19 deletions apps/dashboard/src/components/side-navigation/side-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import {
RiUserAddLine,
} from 'react-icons/ri';
import { useEnvironment } from '@/context/environment/hooks';
import { buildRoute, LEGACY_ROUTES, ROUTES } from '@/utils/routes';
import { buildRoute, ROUTES } from '@/utils/routes';
import { TelemetryEvent } from '@/utils/telemetry';
import { useTelemetry } from '@/hooks/use-telemetry';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { FeatureFlagsKeysEnum } from '@novu/shared';
import { EnvironmentDropdown } from './environment-dropdown';
import { OrganizationDropdown } from './organization-dropdown';
import { FreeTrialCard } from './free-trial-card';
Expand All @@ -41,8 +39,6 @@ export const SideNavigation = () => {

const { currentEnvironment, environments, switchEnvironment } = useEnvironment();
const track = useTelemetry();
const isNewActivityFeedEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_ACTIVITY_FEED_ENABLED, false);
const isNewIntegrationStoreEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_ND_INTEGRATION_STORE_ENABLED, false);
const environmentNames = useMemo(() => environments?.map((env) => env.name), [environments]);

const onEnvironmentChange = (value: string) => {
Expand Down Expand Up @@ -83,26 +79,14 @@ export const SideNavigation = () => {
</NavigationGroup>
<NavigationGroup label="Monitor">
<NavigationLink
to={
isNewActivityFeedEnabled
? buildRoute(ROUTES.ACTIVITY_FEED, { environmentSlug: currentEnvironment?.slug ?? '' })
: LEGACY_ROUTES.ACTIVITY_FEED
}
isExternal={!isNewActivityFeedEnabled}
to={buildRoute(ROUTES.ACTIVITY_FEED, { environmentSlug: currentEnvironment?.slug ?? '' })}
>
<RiBarChartBoxLine className="size-4" />
<span>Activity Feed</span>
</NavigationLink>
</NavigationGroup>
<NavigationGroup label="Developer">
<NavigationLink
to={
isNewIntegrationStoreEnabled
? buildRoute(ROUTES.INTEGRATIONS, { environmentSlug: currentEnvironment?.slug ?? '' })
: LEGACY_ROUTES.INTEGRATIONS
}
isExternal={!isNewIntegrationStoreEnabled}
>
<NavigationLink to={buildRoute(ROUTES.INTEGRATIONS, { environmentSlug: currentEnvironment?.slug ?? '' })}>
<RiStore3Line className="size-4" />
<span>Integration Store</span>
</NavigationLink>
Expand Down
Loading

0 comments on commit c41676f

Please sign in to comment.