From 3bc42cb32b5d6fb936c54d18689e4e17d078b0c9 Mon Sep 17 00:00:00 2001 From: Biswajeet Das Date: Tue, 13 Aug 2024 14:32:51 +0530 Subject: [PATCH] feat(web): reference to new Inbox (#6299) --- .../create-novu-integrations.usecase.ts | 31 +- .../src/app/integrations/usecases/index.ts | 2 + .../e2e/create-organization.e2e.ts | 7 +- .../integrations/IntegrationsListModal.tsx | 31 +- .../pages/integrations/UpdateProviderPage.tsx | 13 +- .../CreateProviderInstanceSidebar.tsx | 2 +- .../v2/UpdateProviderSidebarV2.tsx | 424 ++++++++++++++++++ .../components/multi-provider/v2/index.ts | 1 + .../components/v2/NovuInAppFrameworksV2.tsx | 79 ++++ .../pages/integrations/components/v2/index.ts | 1 + .../quick-start/components/SetupTimeline.tsx | 18 +- apps/web/src/pages/quick-start/consts.tsx | 24 + .../web/tests/integrations-list-modal.spec.ts | 11 +- apps/web/tests/integrations-list-page.spec.ts | 10 +- .../src/consts/providers/channels/in-app.ts | 4 +- 15 files changed, 621 insertions(+), 37 deletions(-) create mode 100644 apps/web/src/pages/integrations/components/multi-provider/v2/UpdateProviderSidebarV2.tsx create mode 100644 apps/web/src/pages/integrations/components/multi-provider/v2/index.ts create mode 100644 apps/web/src/pages/integrations/components/v2/NovuInAppFrameworksV2.tsx create mode 100644 apps/web/src/pages/integrations/components/v2/index.ts diff --git a/apps/api/src/app/integrations/usecases/create-novu-integrations/create-novu-integrations.usecase.ts b/apps/api/src/app/integrations/usecases/create-novu-integrations/create-novu-integrations.usecase.ts index da544cabc2e..9fcbbf6c93f 100644 --- a/apps/api/src/app/integrations/usecases/create-novu-integrations/create-novu-integrations.usecase.ts +++ b/apps/api/src/app/integrations/usecases/create-novu-integrations/create-novu-integrations.usecase.ts @@ -1,11 +1,22 @@ import { Injectable } from '@nestjs/common'; import { IntegrationRepository } from '@novu/dal'; -import { areNovuEmailCredentialsSet, areNovuSmsCredentialsSet } from '@novu/application-generic'; +import { + areNovuEmailCredentialsSet, + areNovuSmsCredentialsSet, + GetFeatureFlag, + GetFeatureFlagCommand, +} from '@novu/application-generic'; import { CreateNovuIntegrationsCommand } from './create-novu-integrations.command'; import { CreateIntegration } from '../create-integration/create-integration.usecase'; import { CreateIntegrationCommand } from '../create-integration/create-integration.command'; -import { ChannelTypeEnum, EmailProviderIdEnum, InAppProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; +import { + ChannelTypeEnum, + EmailProviderIdEnum, + FeatureFlagsKeysEnum, + InAppProviderIdEnum, + SmsProviderIdEnum, +} from '@novu/shared'; import { SetIntegrationAsPrimary } from '../set-integration-as-primary/set-integration-as-primary.usecase'; import { SetIntegrationAsPrimaryCommand } from '../set-integration-as-primary/set-integration-as-primary.command'; @@ -14,7 +25,8 @@ export class CreateNovuIntegrations { constructor( private createIntegration: CreateIntegration, private integrationRepository: IntegrationRepository, - private setIntegrationAsPrimary: SetIntegrationAsPrimary + private setIntegrationAsPrimary: SetIntegrationAsPrimary, + private getFeatureFlag: GetFeatureFlag ) {} private async createEmailIntegration(command: CreateNovuIntegrationsCommand) { @@ -96,12 +108,23 @@ export class CreateNovuIntegrations { _organizationId: command.organizationId, _environmentId: command.environmentId, }); + if (inAppIntegrationCount === 0) { + const isV2Enabled = await this.getFeatureFlag.execute( + GetFeatureFlagCommand.create({ + userId: command.userId, + environmentId: command.environmentId, + organizationId: command.organizationId, + key: FeatureFlagsKeysEnum.IS_V2_ENABLED, + }) + ); + + const name = isV2Enabled ? 'Novu Inbox' : 'Novu In-App'; await this.createIntegration.execute( CreateIntegrationCommand.create({ + name, providerId: InAppProviderIdEnum.Novu, channel: ChannelTypeEnum.IN_APP, - name: 'Novu In-App', active: true, check: false, userId: command.userId, diff --git a/apps/api/src/app/integrations/usecases/index.ts b/apps/api/src/app/integrations/usecases/index.ts index b54a509e97b..eb99a32be1b 100644 --- a/apps/api/src/app/integrations/usecases/index.ts +++ b/apps/api/src/app/integrations/usecases/index.ts @@ -4,6 +4,7 @@ import { CalculateLimitNovuIntegration, ConditionsFilter, NormalizeVariables, + getFeatureFlag, } from '@novu/application-generic'; import { GetWebhookSupportStatus } from './get-webhook-support-status/get-webhook-support-status.usecase'; @@ -35,4 +36,5 @@ export const USE_CASES = [ SetIntegrationAsPrimary, CreateNovuIntegrations, NormalizeVariables, + getFeatureFlag, ]; diff --git a/apps/api/src/app/organization/e2e/create-organization.e2e.ts b/apps/api/src/app/organization/e2e/create-organization.e2e.ts index 82fba343a45..ae6c2e1c362 100644 --- a/apps/api/src/app/organization/e2e/create-organization.e2e.ts +++ b/apps/api/src/app/organization/e2e/create-organization.e2e.ts @@ -10,6 +10,7 @@ import { import { UserSession } from '@novu/testing'; import { ApiServiceLevelEnum, + ChannelTypeEnum, EmailProviderIdEnum, ICreateOrganizationDto, InAppProviderIdEnum, @@ -110,13 +111,13 @@ describe('Create Organization - /organizations (POST) @skip-in-ee', async () => const productionEnv = environments.find((e) => e.name === 'Production'); const developmentEnv = environments.find((e) => e.name === 'Development'); const novuEmailIntegration = integrations.filter( - (i) => i.active && i.name === 'Novu Email' && i.providerId === EmailProviderIdEnum.Novu + (i) => i.active && i.channel === ChannelTypeEnum.EMAIL && i.providerId === EmailProviderIdEnum.Novu ); const novuSmsIntegration = integrations.filter( - (i) => i.active && i.name === 'Novu SMS' && i.providerId === SmsProviderIdEnum.Novu + (i) => i.active && i.channel === ChannelTypeEnum.SMS && i.providerId === SmsProviderIdEnum.Novu ); const novuInAppIntegration = integrations.filter( - (i) => i.active && i.name === 'Novu In-App' && i.providerId === InAppProviderIdEnum.Novu + (i) => i.active && i.channel === ChannelTypeEnum.IN_APP && i.providerId === InAppProviderIdEnum.Novu ); const novuEmailIntegrationProduction = novuEmailIntegration.filter( (el) => el._environmentId === productionEnv?._id diff --git a/apps/web/src/pages/integrations/IntegrationsListModal.tsx b/apps/web/src/pages/integrations/IntegrationsListModal.tsx index 258fd206b20..72834578e59 100644 --- a/apps/web/src/pages/integrations/IntegrationsListModal.tsx +++ b/apps/web/src/pages/integrations/IntegrationsListModal.tsx @@ -1,8 +1,8 @@ import { useCallback, useEffect, useReducer, useState } from 'react'; import { Group, Modal, ActionIcon, createStyles, MantineTheme } from '@mantine/core'; -import { ChannelTypeEnum } from '@novu/shared'; +import { ChannelTypeEnum, FeatureFlagsKeysEnum } from '@novu/shared'; -import { useKeyDown } from '../../hooks'; +import { useFeatureFlag, useKeyDown } from '../../hooks'; import { colors, Close } from '@novu/design-system'; import { useSegment } from '../../components/providers/SegmentProvider'; import { IntegrationsStoreModalAnalytics } from './constants'; @@ -11,7 +11,8 @@ import { IntegrationsList } from './IntegrationsList'; import { Row } from 'react-table'; import { SelectProviderSidebar } from './components/multi-provider/SelectProviderSidebar'; import { CreateProviderInstanceSidebar } from './components/multi-provider/CreateProviderInstanceSidebar'; -import { UpdateProviderSidebar } from './components/multi-provider/UpdateProviderSidebar'; +import { UpdateProviderSidebar as UpdateProviderSidebarOld } from './components/multi-provider/UpdateProviderSidebar'; +import { UpdateProviderSidebar } from './components/multi-provider/v2'; enum SidebarType { SELECT = 'select', @@ -85,6 +86,8 @@ export function IntegrationsListModal({ provider: selectedProvider, }); + const isV2Enabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_V2_ENABLED); + const segment = useSegment(); const { classes } = useModalStyles(); @@ -185,12 +188,22 @@ export function IntegrationsListModal({ providerId={provider?.providerId} channel={provider?.channel} /> - + + {isV2Enabled ? ( + + ) : ( + + )} ); } diff --git a/apps/web/src/pages/integrations/UpdateProviderPage.tsx b/apps/web/src/pages/integrations/UpdateProviderPage.tsx index d7f44be497f..2d4f0cff795 100644 --- a/apps/web/src/pages/integrations/UpdateProviderPage.tsx +++ b/apps/web/src/pages/integrations/UpdateProviderPage.tsx @@ -1,15 +1,22 @@ import { useNavigate, useParams } from 'react-router-dom'; - +import { FeatureFlagsKeysEnum } from '@novu/shared'; +import { useFeatureFlag } from '../../hooks'; import { ROUTES } from '../../constants/routes'; -import { UpdateProviderSidebar } from './components/multi-provider/UpdateProviderSidebar'; +import { UpdateProviderSidebar } from './components/multi-provider/v2'; +import { UpdateProviderSidebar as UpdateProviderSidebarOld } from './components/multi-provider/UpdateProviderSidebar'; export function UpdateProviderPage() { const { integrationId } = useParams(); const navigate = useNavigate(); + const isV2Enabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_V2_ENABLED); const onClose = () => { navigate(ROUTES.INTEGRATIONS); }; - return ; + return isV2Enabled ? ( + + ) : ( + + ); } 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 0c4752f2bbc..94c7bde5dd4 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -130,7 +130,7 @@ export function CreateProviderInstanceSidebar({ name: data.name, environmentId, }); - successMessage('Instance configuration is created'); + successMessage('Integration was created'); onIntegrationCreated(integrationId ?? ''); queryClient.refetchQueries({ diff --git a/apps/web/src/pages/integrations/components/multi-provider/v2/UpdateProviderSidebarV2.tsx b/apps/web/src/pages/integrations/components/multi-provider/v2/UpdateProviderSidebarV2.tsx new file mode 100644 index 00000000000..f224fc1588e --- /dev/null +++ b/apps/web/src/pages/integrations/components/multi-provider/v2/UpdateProviderSidebarV2.tsx @@ -0,0 +1,424 @@ +import styled from '@emotion/styled'; +import { Box, Center, Group } from '@mantine/core'; +import { useClipboard, useDisclosure } from '@mantine/hooks'; +import { Button, Check, colors, Copy, Input, Sidebar, Text } from '@novu/design-system'; +import { + CHANNELS_WITH_PRIMARY, + CredentialsKeyEnum, + EmailProviderIdEnum, + IConfigCredentials, + IConstructIntegrationDto, + ICredentialsDto, + InAppProviderIdEnum, + SmsProviderIdEnum, +} from '@novu/shared'; +import { useEffect, useMemo, useState } from 'react'; +import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; +import slugify from 'slugify'; +import { useWebhookSupportStatus } from '../../../../../api/hooks'; +import { useUpdateIntegration } from '../../../../../api/hooks/useUpdateIntegration'; +import { Conditions, IConditions } from '../../../../../components/conditions'; +import { When } from '../../../../../components/utils/When'; +import { useEnvironment } from '../../../../../hooks'; +import { successMessage } from '../../../../../utils/notifications'; +import { Faq } from '../../../../quick-start/components/QuickStartWrapper'; +import { SetupTimeline } from '../../../../quick-start/components/SetupTimeline'; +import { FrameworkEnum } from '../../../../quick-start/consts'; +import { defaultIntegrationConditionsProps } from '../../../constants'; +import type { IIntegratedProvider } from '../../../types'; +import { useProviders } from '../../../useProviders'; +import { IntegrationInput } from '../../IntegrationInput'; +import { ShareableUrl } from '../../Modal/ConnectIntegrationForm'; +import { NovuInAppFrameworkHeader } from '../../NovuInAppFrameworkHeader'; +import { SetupWarning } from '../../SetupWarning'; +import { UpdateIntegrationCommonFields } from '../../UpdateIntegrationCommonFields'; +import { UpdateIntegrationSidebarHeader } from '../../UpdateIntegrationSidebarHeader'; +import { NovuInAppFrameworks } from '../../v2'; +import { NovuProviderSidebarContent } from '../NovuProviderSidebarContent'; +import { useSelectPrimaryIntegrationModal } from '../useSelectPrimaryIntegrationModal'; + +interface IProviderForm { + name: string; + credentials: ICredentialsDto; + active: boolean; + identifier: string; + conditions: IConditions[]; +} + +enum SidebarStateEnum { + NORMAL = 'normal', + EXPANDED = 'expanded', +} + +export function UpdateProviderSidebar({ + isOpened, + integrationId, + onClose, +}: { + isOpened: boolean; + integrationId?: string; + onClose: () => void; +}) { + const { isLoaded: isEnvironmentLoaded } = useEnvironment(); + const [sidebarState, setSidebarState] = useState(SidebarStateEnum.NORMAL); + const [framework, setFramework] = useState(null); + const { providers, isLoading: areProvidersLoading } = useProviders(); + const [selectedProvider, setSelectedProvider] = useState(() => { + const provider = providers.find((el) => el.integrationId === integrationId); + + return provider ?? null; + }); + const isNovuInAppProvider = selectedProvider?.providerId === InAppProviderIdEnum.Novu; + const { openModal: openSelectPrimaryIntegrationModal, SelectPrimaryIntegrationModal } = + useSelectPrimaryIntegrationModal(); + const [conditionsFormOpened, { close: closeConditionsForm, open: openConditionsForm }] = useDisclosure(false); + const webhookUrlClipboard = useClipboard({ timeout: 1000 }); + + const { updateIntegration, isLoadingUpdate } = useUpdateIntegration(selectedProvider?.integrationId || ''); + + const { isWebhookEnabled, webhookUrl } = useWebhookSupportStatus({ + hasCredentials: selectedProvider?.hasCredentials, + integrationId: selectedProvider?.integrationId, + channel: selectedProvider?.channel, + }); + + const methods = useForm({ + shouldUseNativeValidation: false, + shouldFocusError: false, + defaultValues: { + name: '', + credentials: {}, + active: false, + identifier: '', + conditions: [], + }, + }); + const { + control, + handleSubmit, + reset, + watch, + setValue, + getValues, + formState: { errors, isDirty, dirtyFields }, + } = methods; + + const credentials = watch('credentials'); + const isActive = watch('active'); + const isSidebarOpened = !!selectedProvider && isOpened; + + const haveAllCredentials = useMemo(() => { + if (selectedProvider === null) { + return false; + } + const missingCredentials = selectedProvider.credentials + .filter((credential) => credential.required) + .filter((credential) => { + const value = credentials[credential.key]; + + return !value; + }); + + return missingCredentials.length === 0; + }, [selectedProvider, credentials]); + + useEffect(() => { + if (selectedProvider && !selectedProvider?.identifier) { + const newIdentifier = slugify(selectedProvider?.displayName, { + lower: true, + strict: true, + }); + + setValue('identifier', newIdentifier); + } + }, [setValue, selectedProvider]); + + useEffect(() => { + if (integrationId === undefined || providers.length === 0) { + return; + } + const foundProvider = providers.find((provider) => provider.integrationId === integrationId); + if (!foundProvider) { + return; + } + + setSelectedProvider(foundProvider); + reset({ + name: foundProvider.name ?? foundProvider.displayName, + identifier: foundProvider.identifier, + credentials: foundProvider.credentials.reduce((prev, credential) => { + prev[credential.key] = credential.value; + + return prev; + }, {} as any), + conditions: foundProvider.conditions, + active: foundProvider.active, + }); + }, [reset, integrationId, providers]); + + const onFrameworkClickCallback = (newFramework: FrameworkEnum) => { + setSidebarState(SidebarStateEnum.EXPANDED); + setFramework(newFramework); + }; + + const onBack = () => { + if (sidebarState === SidebarStateEnum.EXPANDED) { + setSidebarState(SidebarStateEnum.NORMAL); + setFramework(null); + } + }; + + const onSidebarClose = () => { + if (sidebarState === SidebarStateEnum.EXPANDED) { + setSidebarState(SidebarStateEnum.NORMAL); + } + onClose(); + }; + + const updateAndSelectPrimaryIntegration = async (data: IConstructIntegrationDto) => { + if (!selectedProvider) { + return; + } + + const { channel: selectedChannel, environmentId, primary, conditions } = selectedProvider; + const isActiveFieldChanged = dirtyFields.active; + const hasSameChannelActiveIntegration = !!providers + .filter((el) => el.integrationId !== selectedProvider.integrationId) + .find((el) => el.active && el.channel === selectedChannel && el.environmentId === environmentId); + const isChannelSupportPrimary = CHANNELS_WITH_PRIMARY.includes(selectedChannel); + + const isChangedToActive = + isActiveFieldChanged && isChannelSupportPrimary && isActive && hasSameChannelActiveIntegration; + + const isChangedToInactiveAndIsPrimary = + isActiveFieldChanged && isChannelSupportPrimary && !isActive && primary && hasSameChannelActiveIntegration; + + const isPrimaryAndHasConditionsApplied = + primary && conditions && conditions.length > 0 && hasSameChannelActiveIntegration; + + const hasNoConditions = !conditions || conditions.length === 0; + + const hasUpdatedConditions = data.conditions && data.conditions.length > 0; + + const hasConditionsAndIsPrimary = hasUpdatedConditions && primary && dirtyFields.conditions; + + if ( + (hasNoConditions && isChangedToActive) || + isChangedToInactiveAndIsPrimary || + isPrimaryAndHasConditionsApplied || + hasConditionsAndIsPrimary + ) { + openSelectPrimaryIntegrationModal({ + environmentId: selectedProvider?.environmentId, + channelType: selectedProvider?.channel, + exclude: !isActive || hasConditionsAndIsPrimary ? (el) => el._id === selectedProvider.integrationId : undefined, + onClose: () => { + updateIntegration(data); + }, + }); + + return; + } + + updateIntegration(data); + }; + + const onSubmit: React.FormEventHandler = (e) => { + e.stopPropagation(); + e.preventDefault(); + + handleSubmit(updateAndSelectPrimaryIntegration)(e); + }; + + const hmacEnabled = useWatch({ + control, + name: `credentials.${CredentialsKeyEnum.Hmac}`, + }); + + const updateConditions = (conditions: IConditions[]) => { + setValue('conditions', conditions, { shouldDirty: true }); + }; + + if (conditionsFormOpened) { + const [conditions, name] = getValues(['conditions', 'name']); + + return ( + + ); + } + + if ( + SmsProviderIdEnum.Novu === selectedProvider?.providerId || + EmailProviderIdEnum.Novu === selectedProvider?.providerId + ) { + return ( + + + Test Provider + + } + data-test-id="update-provider-sidebar-novu" + customFooter={ + + + + } + > + + + + + + ); + } + + return ( + + + ) : ( + <> + + + + + ) + } + customFooter={ + +
+ Explore our + + + set-up guide + + +
+ +
+ } + data-test-id="update-provider-sidebar" + > + + + + {selectedProvider?.credentials.map((credential: IConfigCredentials) => ( + + ( + + )} + /> + + ))} + {isWebhookEnabled && ( + + webhookUrlClipboard.copy(webhookUrl)}> + {webhookUrlClipboard.copied ? : } + + } + data-test-id="provider-webhook-url" + /> + + )} + + {isNovuInAppProvider && } + + + { + setSidebarState(SidebarStateEnum.NORMAL); + successMessage('Successfully configured Inbox'); + }} + onConfigureLater={() => { + setSidebarState(SidebarStateEnum.NORMAL); + }} + /> + + + + +
+ +
+ ); +} + +const InputWrapper = styled.div` + > div { + width: 100%; + } +`; + +const Free = styled.span` + color: ${colors.success}; + font-size: 14px; + min-width: fit-content; + margin-left: -4px; +`; + +const CopyWrapper = styled.div` + cursor: pointer; + &:hover { + opacity: 0.8; + } +`; diff --git a/apps/web/src/pages/integrations/components/multi-provider/v2/index.ts b/apps/web/src/pages/integrations/components/multi-provider/v2/index.ts new file mode 100644 index 00000000000..704d534ae61 --- /dev/null +++ b/apps/web/src/pages/integrations/components/multi-provider/v2/index.ts @@ -0,0 +1 @@ +export * from './UpdateProviderSidebarV2'; diff --git a/apps/web/src/pages/integrations/components/v2/NovuInAppFrameworksV2.tsx b/apps/web/src/pages/integrations/components/v2/NovuInAppFrameworksV2.tsx new file mode 100644 index 00000000000..cf878303f1f --- /dev/null +++ b/apps/web/src/pages/integrations/components/v2/NovuInAppFrameworksV2.tsx @@ -0,0 +1,79 @@ +import styled from '@emotion/styled'; +import { colors, JavaScriptLogo, ReactLogo, Text } from '@novu/design-system'; +import { UTM_CAMPAIGN_QUERY_PARAM } from '@novu/shared'; + +import { FrameworkEnum } from '../../../quick-start/consts'; + +const NovuInAppFrameworksHolder = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const FrameworksGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-gap: 16px; +`; + +const frameworkHolderStyles = (theme) => ` + display: flex; + align-items: center; + gap: 12px; + flex: 1 0 0; + padding: 16px; + border-radius: 8px; + background: ${theme.colorScheme === 'dark' ? colors.B20 : colors.BGLight}; + cursor: pointer; + user-select: none; + transition: all 250ms ease-in-out; + + &:hover { + filter: ${theme.colorScheme === 'dark' ? 'brightness(1.1)' : 'brightness(0.95)'}; + } +`; + +const FrameworkHolder = styled.div` + ${({ theme }) => frameworkHolderStyles(theme)}; +`; + +const FrameworkHolderLink = styled.a` + ${({ theme }) => frameworkHolderStyles(theme)}; +`; + +const frameworks = [ + { icon: ReactLogo, name: 'React', frameworkEnum: FrameworkEnum.REACT }, + { + icon: JavaScriptLogo, + name: 'Headless', + href: `https://docs.novu.co/inbox/introduction${UTM_CAMPAIGN_QUERY_PARAM}`, + }, +]; + +export const NovuInAppFrameworks = ({ onFrameworkClick }: { onFrameworkClick: (framework: FrameworkEnum) => void }) => { + return ( + + Integrate Inbox using a framework below + + {frameworks.map(({ name, icon: Icon, frameworkEnum, href }) => + frameworkEnum ? ( + { + onFrameworkClick(frameworkEnum); + }} + > + + {name} + + ) : ( + + + {name} + + ) + )} + + + ); +}; diff --git a/apps/web/src/pages/integrations/components/v2/index.ts b/apps/web/src/pages/integrations/components/v2/index.ts new file mode 100644 index 00000000000..c0457021dc8 --- /dev/null +++ b/apps/web/src/pages/integrations/components/v2/index.ts @@ -0,0 +1 @@ +export * from './NovuInAppFrameworksV2'; diff --git a/apps/web/src/pages/quick-start/components/SetupTimeline.tsx b/apps/web/src/pages/quick-start/components/SetupTimeline.tsx index 6740631e6d7..2ccf7982ec4 100644 --- a/apps/web/src/pages/quick-start/components/SetupTimeline.tsx +++ b/apps/web/src/pages/quick-start/components/SetupTimeline.tsx @@ -1,17 +1,24 @@ import styled from '@emotion/styled'; import { Stack, Timeline, useMantineColorScheme } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; - import { getApiKeys } from '../../../api/environment'; import { When } from '../../../components/utils/When'; import { API_ROOT, ENV, IS_DOCKER_HOSTED, WS_URL } from '../../../config'; import { colors, shadows, Text } from '@novu/design-system'; -import { useEnvironment } from '../../../hooks'; +import { useEnvironment, useFeatureFlag } from '../../../hooks'; import { PrismOnCopy } from '../../settings/tabs/components/Prism'; import { SetupStatus } from './SetupStatus'; -import { API_KEY, APPLICATION_IDENTIFIER, BACKEND_API_URL, BACKEND_SOCKET_URL, frameworkInstructions } from '../consts'; +import { + API_KEY, + APPLICATION_IDENTIFIER, + BACKEND_API_URL, + BACKEND_SOCKET_URL, + frameworkInstructions, + frameworkInstructionsV2, +} from '../consts'; import { QueryKeys } from '../../../api/query.keys'; import { useInAppActivated } from '../../../api/hooks'; +import { FeatureFlagsKeysEnum } from '@novu/shared'; export const SetupTimeline = ({ framework, @@ -29,10 +36,11 @@ export const SetupTimeline = ({ const apiKey = apiKeys?.length ? apiKeys[0].key : ''; const { colorScheme } = useMantineColorScheme(); const isDark = colorScheme === 'dark'; - + const isV2Enabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_V2_ENABLED); const { isInAppActive } = useInAppActivated(); - const instructions = frameworkInstructions.find((instruction) => instruction.key === framework)?.value ?? []; + const finalFrameworkInstructions = isV2Enabled ? frameworkInstructionsV2 : frameworkInstructions; + const instructions = finalFrameworkInstructions.find((instruction) => instruction.key === framework)?.value ?? []; const environmentIdentifier = environment?.identifier ?? ''; return ( diff --git a/apps/web/src/pages/quick-start/consts.tsx b/apps/web/src/pages/quick-start/consts.tsx index 9bc1ad09607..e95ea92c51f 100644 --- a/apps/web/src/pages/quick-start/consts.tsx +++ b/apps/web/src/pages/quick-start/consts.tsx @@ -33,6 +33,7 @@ interface ISnippetInstructions { } const installReactNotificationCenter = 'npm install @novu/notification-center'; +const installReactInbox = 'npm install @novu/react'; const installAngularNotificationCenter = 'npm install @novu/notification-center-angular'; const installVueNotificationCenter = 'npm install @novu/notification-center-vue'; @@ -53,6 +54,15 @@ export const Header = () => { ); };`; +export const reactStarterSnippetV2 = `import React from 'react'; +import { Inbox } from '@novu/react'; + +export const Header = () => { + return ( + + ); +};`; + const angularInteractions = (
@@ -252,6 +262,20 @@ export const frameworkInstructions: { key: string; value: ISnippetInstructions[] }, ]; +export const frameworkInstructionsV2: { key: string; value: ISnippetInstructions[] }[] = [ + { + key: FrameworkEnum.REACT, + value: [ + { + instruction: 'First you have to install the package:', + snippet: installReactInbox, + language: 'bash', + }, + { instruction: 'Then import and render the components:', snippet: reactStarterSnippetV2 }, + ], + }, +]; + export enum OnBoardingAnalyticsEnum { FRAMEWORK_SETUP_VISIT = 'In app frameworks select', FRAMEWORKS_SETUP_VISIT = 'Framework Setup Page Visit', diff --git a/apps/web/tests/integrations-list-modal.spec.ts b/apps/web/tests/integrations-list-modal.spec.ts index bd31eafc46c..0e9b3add299 100644 --- a/apps/web/tests/integrations-list-modal.spec.ts +++ b/apps/web/tests/integrations-list-modal.spec.ts @@ -25,6 +25,7 @@ import { deleteProvider, SessionData } from './utils/plugins'; let session: SessionData; test.beforeEach(async ({ page }) => { + await setFeatureFlag(page, FeatureFlagsKeysEnum.IS_V2_ENABLED, false); ({ session } = await initializeSession(page)); }); @@ -127,9 +128,9 @@ test('should show the table loading skeleton and then table', async ({ page }) = }); await checkTableRow(page, { - name: 'Novu In-App', + name: 'Novu Inbox', isFree: false, - provider: 'Novu In-App', + provider: 'Novu Inbox', channel: 'In-App', environment: 'Development', status: 'Active', @@ -617,7 +618,7 @@ test('should allow to delete the mailjet integration', async ({ page }) => { test('should show the Novu in-app integration', async ({ page }) => { await navigateToGetStarted(page); - await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + await clickOnListRow(page, new RegExp(`Novu Inbox.*Development`)); const updateProviderSidebar = page.getByTestId('update-provider-sidebar'); await expect(updateProviderSidebar).toBeVisible(); @@ -649,7 +650,7 @@ test('should show the Novu in-app integration', async ({ page }) => { const selectedProviderName = page.getByTestId('provider-instance-name').first(); await expect(selectedProviderName).toBeVisible(); - await expect(selectedProviderName).toHaveValue('Novu In-App'); + await expect(selectedProviderName).toHaveValue('Novu Inbox'); const identifier = page.getByTestId('provider-instance-identifier'); await expect(identifier).toHaveValue(/novu-in-app/); @@ -674,7 +675,7 @@ test('should show the Novu in-app integration', async ({ page }) => { test('should show the Novu in-app integration - React guide', async ({ page }) => { await navigateToGetStarted(page); - await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + await clickOnListRow(page, new RegExp(`Novu Inbox.*Development`)); let updateProviderSidebar = page.getByTestId('update-provider-sidebar'); await expect(updateProviderSidebar).toBeVisible(); diff --git a/apps/web/tests/integrations-list-page.spec.ts b/apps/web/tests/integrations-list-page.spec.ts index 3b24521d3f0..36740ef30f2 100644 --- a/apps/web/tests/integrations-list-page.spec.ts +++ b/apps/web/tests/integrations-list-page.spec.ts @@ -110,9 +110,9 @@ test('should show the table loading skeleton and then table', async ({ page }) = }); await checkTableRow(page, { - name: 'Novu In-App', + name: 'Novu Inbox', isFree: false, - provider: 'Novu In-App', + provider: 'Novu Inbox', channel: 'In-App', environment: 'Development', status: 'Active', @@ -888,7 +888,7 @@ test('should show the Novu in-app integration', async ({ page }) => { await page.goto('/integrations'); await expect(page).toHaveURL(/\/integrations/); - await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + await clickOnListRow(page, new RegExp(`Novu Inbox.*Development`)); const updateProviderSidebar = page.getByTestId('update-provider-sidebar'); await expect(updateProviderSidebar).toBeVisible(); @@ -920,7 +920,7 @@ test('should show the Novu in-app integration', async ({ page }) => { const selectedProviderName = page.getByTestId('provider-instance-name').first(); await expect(selectedProviderName).toBeVisible(); - await expect(selectedProviderName).toHaveValue('Novu In-App'); + await expect(selectedProviderName).toHaveValue('Novu Inbox'); const identifier = page.getByTestId('provider-instance-identifier'); await expect(identifier).toHaveValue(/novu-in-app/); @@ -946,7 +946,7 @@ test('should show the Novu in-app integration - React guide', async ({ page }) = await page.goto('/integrations'); await expect(page).toHaveURL(/\/integrations/); - await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + await clickOnListRow(page, new RegExp(`Novu Inbox.*Development`)); let updateProviderSidebar = page.getByTestId('update-provider-sidebar'); await expect(updateProviderSidebar).toBeVisible(); diff --git a/libs/shared/src/consts/providers/channels/in-app.ts b/libs/shared/src/consts/providers/channels/in-app.ts index b36cecf3998..d5d892980bb 100644 --- a/libs/shared/src/consts/providers/channels/in-app.ts +++ b/libs/shared/src/consts/providers/channels/in-app.ts @@ -9,10 +9,10 @@ import { UTM_CAMPAIGN_QUERY_PARAM } from '../../../ui'; export const inAppProviders: IProviderConfig[] = [ { id: InAppProviderIdEnum.Novu, - displayName: 'Novu In-App', + displayName: 'Novu Inbox', channel: ChannelTypeEnum.IN_APP, credentials: novuInAppConfig, - docReference: `https://docs.novu.co/notification-center/introduction${UTM_CAMPAIGN_QUERY_PARAM}`, + docReference: `https://docs.novu.co/inbox/introduction${UTM_CAMPAIGN_QUERY_PARAM}`, logoFileName: { light: 'novu.png', dark: 'novu.png' }, }, ];