diff --git a/apps/api/src/app/integrations/e2e/create-integration.e2e.ts b/apps/api/src/app/integrations/e2e/create-integration.e2e.ts
index 72c87d6401c..646d2419949 100644
--- a/apps/api/src/app/integrations/e2e/create-integration.e2e.ts
+++ b/apps/api/src/app/integrations/e2e/create-integration.e2e.ts
@@ -505,6 +505,50 @@ describe('Create Integration - /integration (POST)', function () {
expect(second.active).to.equal(false);
expect(second.priority).to.equal(0);
});
+
+ it('should not allow creating the same novu provider on same environment twice', async function () {
+ const inAppPayload = {
+ name: InAppProviderIdEnum.Novu,
+ providerId: InAppProviderIdEnum.Novu,
+ channel: ChannelTypeEnum.IN_APP,
+ credentials: {},
+ active: true,
+ check: false,
+ };
+
+ const inAppResult = await session.testAgent.post('/v1/integrations').send(inAppPayload);
+
+ expect(inAppResult.body.statusCode).to.equal(400);
+ expect(inAppResult.body.message).to.equal('One environment can only have one In app provider');
+
+ const emailPayload = {
+ name: EmailProviderIdEnum.Novu,
+ providerId: EmailProviderIdEnum.Novu,
+ channel: ChannelTypeEnum.EMAIL,
+ credentials: {},
+ active: true,
+ check: false,
+ };
+
+ const emailResult = await session.testAgent.post('/v1/integrations').send(emailPayload);
+
+ expect(emailResult.body.statusCode).to.equal(409);
+ expect(emailResult.body.message).to.equal('Integration with novu provider for email channel already exists');
+
+ const smsPayload = {
+ name: SmsProviderIdEnum.Novu,
+ providerId: SmsProviderIdEnum.Novu,
+ channel: ChannelTypeEnum.SMS,
+ credentials: {},
+ active: true,
+ check: false,
+ };
+
+ const smsResult = await session.testAgent.post('/v1/integrations').send(smsPayload);
+
+ expect(smsResult.body.statusCode).to.equal(409);
+ expect(smsResult.body.message).to.equal('Integration with novu provider for sms channel already exists');
+ });
});
async function insertIntegrationTwice(
diff --git a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts
index 2220db57af2..6d8163ce6dc 100644
--- a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts
+++ b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts
@@ -114,7 +114,7 @@ export class CreateIntegration {
if (command.providerId === SmsProviderIdEnum.Novu || command.providerId === EmailProviderIdEnum.Novu) {
const count = await this.integrationRepository.count({
_environmentId: command.environmentId,
- providerId: EmailProviderIdEnum.Novu,
+ providerId: command.providerId,
channel: command.channel,
});
diff --git a/apps/web/cypress/tests/integrations-list-page.spec.ts b/apps/web/cypress/tests/integrations-list-page.spec.ts
index e3684f0bc12..f90fbd0a27e 100644
--- a/apps/web/cypress/tests/integrations-list-page.spec.ts
+++ b/apps/web/cypress/tests/integrations-list-page.spec.ts
@@ -1000,4 +1000,43 @@ describe('Integrations List Page', function () {
expect(el.get(0).innerText).to.eq('20 messages per month');
});
});
+
+ it('should not allow creating a novu provider for the same environment if it already exists', () => {
+ cy.intercept('*/integrations', async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }).as('getIntegrations');
+ cy.intercept('*/environments').as('getEnvironments');
+
+ cy.visit('/integrations');
+
+ cy.wait('@getIntegrations');
+ cy.wait('@getEnvironments');
+
+ cy.getByTestId('add-provider').should('be.enabled').click();
+ cy.getByTestId('select-provider-sidebar').should('be.visible');
+
+ cy.getByTestId(`provider-${EmailProviderIdEnum.Novu}`).contains('Novu').click();
+
+ cy.window().then((win) => {
+ if (win.isDarkTheme) {
+ cy.getByTestId(`selected-provider-image-${EmailProviderIdEnum.Novu}`).should(
+ 'have.attr',
+ 'src',
+ `/static/images/providers/dark/square/${EmailProviderIdEnum.Novu}.svg`
+ );
+ return;
+ }
+
+ cy.getByTestId(`selected-provider-image-${EmailProviderIdEnum.Novu}`).should(
+ 'have.attr',
+ 'src',
+ `/static/images/providers/light/square/${EmailProviderIdEnum.Novu}.svg`
+ );
+ });
+ cy.getByTestId('selected-provider-name').should('be.visible').contains('Novu');
+
+ cy.getByTestId('select-provider-sidebar-next').should('not.be.disabled').contains('Next').click();
+ cy.getByTestId('novu-provider-error').contains('You can only create one Novu Email per environment.');
+ cy.getByTestId('create-provider-instance-sidebar-create').should('be.disabled');
+ });
});
diff --git a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx
index 39f08a02096..52c3d37a996 100644
--- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx
+++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx
@@ -1,24 +1,24 @@
+import styled from '@emotion/styled';
import { ActionIcon, Group, Radio, Text } from '@mantine/core';
-import { useEffect, useMemo } from 'react';
+import { ChannelTypeEnum, ICreateIntegrationBodyDto, NOVU_PROVIDERS, providers } from '@novu/shared';
import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
-import styled from '@emotion/styled';
-import { ChannelTypeEnum, ICreateIntegrationBodyDto, InAppProviderIdEnum, providers } from '@novu/shared';
+import { createIntegration } from '../../../../api/integration';
+import { QueryKeys } from '../../../../api/query.keys';
+import { useSegment } from '../../../../components/providers/SegmentProvider';
+import { When } from '../../../../components/utils/When';
import { Button, colors, NameInput, Sidebar } from '../../../../design-system';
-import { ArrowLeft } from '../../../../design-system/icons';
import { inputStyles } from '../../../../design-system/config/inputs.styles';
+import { ArrowLeft } from '../../../../design-system/icons';
import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments';
-import { useSegment } from '../../../../components/providers/SegmentProvider';
-import { createIntegration } from '../../../../api/integration';
-import { IntegrationsStoreModalAnalytics } from '../../constants';
-import { errorMessage, successMessage } from '../../../../utils/notifications';
-import { QueryKeys } from '../../../../api/query.keys';
-import { ProviderImage } from './SelectProviderSidebar';
import { CHANNEL_TYPE_TO_STRING } from '../../../../utils/channels';
+import { errorMessage, successMessage } from '../../../../utils/notifications';
+import { IntegrationsStoreModalAnalytics } from '../../constants';
import type { IntegrationEntity } from '../../types';
import { useProviders } from '../../useProviders';
-import { When } from '../../../../components/utils/When';
+import { ProviderImage } from './SelectProviderSidebar';
interface ICreateProviderInstanceForm {
name: string;
@@ -67,8 +67,8 @@ export function CreateProviderInstanceSidebar({
const selectedEnvironmentId = watch('environmentId');
- const showInAppErrorMessage = useMemo(() => {
- if (!provider || integrations.length === 0 || provider.id !== InAppProviderIdEnum.Novu) {
+ const showNovuProvidersErrorMessage = useMemo(() => {
+ if (!provider || integrations.length === 0 || !NOVU_PROVIDERS.includes(provider.id)) {
return false;
}
@@ -170,7 +170,7 @@ export function CreateProviderInstanceSidebar({
Cancel
-
+
- You can only create one {provider.displayName} per environment.
+
+ You can only create one {provider.displayName} per environment.
+
diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx
index 4daba2af82f..a6a0561af0a 100644
--- a/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx
+++ b/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx
@@ -9,7 +9,6 @@ import {
inAppProviders,
chatProviders,
InAppProviderIdEnum,
- NOVU_SMS_EMAIL_PROVIDERS,
} from '@novu/shared';
import { colors, Sidebar } from '../../../../design-system';
@@ -32,14 +31,12 @@ const filterSearch = (list, search: string) =>
list.filter((prov) => prov.displayName.toLowerCase().includes(search.toLowerCase()));
const mapStructure = (listProv): IIntegratedProvider[] =>
- listProv
- .filter((providerItem) => !NOVU_SMS_EMAIL_PROVIDERS.includes(providerItem.id))
- .map((providerItem) => ({
- providerId: providerItem.id,
- displayName: providerItem.displayName,
- channel: providerItem.channel,
- docReference: providerItem.docReference,
- }));
+ listProv.map((providerItem) => ({
+ providerId: providerItem.id,
+ displayName: providerItem.displayName,
+ channel: providerItem.channel,
+ docReference: providerItem.docReference,
+ }));
const initialProvidersList = {
[ChannelTypeEnum.EMAIL]: mapStructure(emailProviders),