From c2adfc85451c5103eaad068f882066fd36c4aebe Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Oct 2024 13:21:34 +0200 Subject: [PATCH] feat: Opt in to additional features on community for existing users (#11166) --- packages/cli/src/license/license.service.ts | 3 +- .../CommunityPlusEnrollmentModal.test.ts | 22 +++++++++++++ .../CommunityPlusEnrollmentModal.vue | 6 ++-- .../src/plugins/i18n/locales/en.json | 4 ++- .../src/views/SettingsUsageAndPlan.test.ts | 23 +++++++++++-- .../src/views/SettingsUsageAndPlan.vue | 33 ++++++++++++++----- 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/license/license.service.ts b/packages/cli/src/license/license.service.ts index 9e4ab2382c..43f9961334 100644 --- a/packages/cli/src/license/license.service.ts +++ b/packages/cli/src/license/license.service.ts @@ -14,8 +14,7 @@ type LicenseError = Error & { errorId?: keyof typeof LicenseErrors }; export const LicenseErrors = { SCHEMA_VALIDATION: 'Activation key is in the wrong format', - RESERVATION_EXHAUSTED: - 'Activation key has been used too many times. Please contact sales@n8n.io if you would like to extend it', + RESERVATION_EXHAUSTED: 'Activation key has been used too many times', RESERVATION_EXPIRED: 'Activation key has expired', NOT_FOUND: 'Activation key not found', RESERVATION_CONFLICT: 'Activation key not found', diff --git a/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts index 84a955d803..fffed9a4c1 100644 --- a/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts +++ b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts @@ -51,6 +51,14 @@ describe('CommunityPlusEnrollmentModal', () => { createTestingPinia(); }); + it('should not throw error opened only with the name', () => { + const props = { + modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL, + }; + + expect(() => renderComponent({ props })).not.toThrow(); + }); + it('should test enrolling', async () => { const closeCallbackSpy = vi.fn(); const usageStore = mockedStore(useUsageStore); @@ -168,4 +176,18 @@ describe('CommunityPlusEnrollmentModal', () => { const emailInput = getByRole('textbox'); expect(emailInput).toHaveValue('test@n8n.io'); }); + + it('should not throw error if no close callback provided', async () => { + const consoleErrorSpy = vi.spyOn(console, 'error'); + const props = { + modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL, + }; + + const { getByRole } = renderComponent({ props }); + const skipButton = getByRole('button', { name: 'Skip' }); + expect(skipButton).toBeVisible(); + + await userEvent.click(skipButton); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); }); diff --git a/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue index 6798decc2e..420b193b30 100644 --- a/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue +++ b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue @@ -13,8 +13,8 @@ import { useUsersStore } from '@/stores/users.store'; const props = defineProps<{ modalName: string; - data: { - closeCallback: () => void; + data?: { + closeCallback?: () => void; }; }>(); @@ -51,7 +51,7 @@ const modalBus = createEventBus(); const closeModal = () => { telemetry.track('User skipped community plus'); modalBus.emit('close'); - props.data.closeCallback(); + props.data?.closeCallback?.(); }; const confirm = async () => { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 521900e620..26c21a3dcc 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1788,6 +1788,8 @@ "settings.usageAndPlan.title": "Usage and plan", "settings.usageAndPlan.description": "You’re on the {name} {type}", "settings.usageAndPlan.plan": "Plan", + "settings.usageAndPlan.callOut": "{link} selected paid features for free (forever)", + "settings.usageAndPlan.callOut.link": "Unlock", "settings.usageAndPlan.edition": "Edition", "settings.usageAndPlan.error": "@:_reusableBaseText.error", "settings.usageAndPlan.activeWorkflows": "Active workflows", @@ -2662,7 +2664,7 @@ "feedback.positive": "I found this helpful", "feedback.negative": "I didn't find this helpful", "communityPlusModal.badge": "Time limited offer", - "communityPlusModal.title": "Unlock select paid features for free (forever)", + "communityPlusModal.title": "Get paid features for free (forever)", "communityPlusModal.error.title": "License request failed", "communityPlusModal.success.title": "Request sent", "communityPlusModal.success.message": "License key will be sent to {email}", diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts b/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts index 18e3d261d3..bd88c02228 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts @@ -1,8 +1,11 @@ import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; import { createComponentRenderer } from '@/__tests__/render'; import { mockedStore } from '@/__tests__/utils'; import { useUsageStore } from '@/stores/usage.store'; import SettingsUsageAndPlan from '@/views/SettingsUsageAndPlan.vue'; +import { useUIStore } from '@/stores/ui.store'; +import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants'; vi.mock('vue-router', () => { return { @@ -19,6 +22,7 @@ vi.mock('vue-router', () => { }); let usageStore: ReturnType>; +let uiStore: ReturnType>; const renderComponent = createComponentRenderer(SettingsUsageAndPlan); @@ -26,6 +30,10 @@ describe('SettingsUsageAndPlan', () => { beforeEach(() => { createTestingPinia(); usageStore = mockedStore(useUsageStore); + uiStore = mockedStore(useUIStore); + + usageStore.viewPlansUrl = 'https://subscription.n8n.io'; + usageStore.managePlanUrl = 'https://subscription.n8n.io'; }); it('should not throw errors when rendering', async () => { @@ -38,10 +46,21 @@ describe('SettingsUsageAndPlan', () => { expect(getByRole('heading').nextElementSibling).toBeNull(); }); + it('should not show badge but unlock notice', async () => { + usageStore.isLoading = false; + usageStore.planName = 'Community'; + const { getByRole, container } = renderComponent(); + expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community'); + expect(container.querySelector('.n8n-badge')).toBeNull(); + + expect(getByRole('button', { name: 'Unlock' })).toBeVisible(); + + await userEvent.click(getByRole('button', { name: 'Unlock' })); + expect(uiStore.openModal).toHaveBeenCalledWith(COMMUNITY_PLUS_ENROLLMENT_MODAL); + }); + it('should show community registered badge', async () => { usageStore.isLoading = false; - usageStore.viewPlansUrl = 'https://subscription.n8n.io'; - usageStore.managePlanUrl = 'https://subscription.n8n.io'; usageStore.planName = 'Registered Community'; const { getByRole, container } = renderComponent(); expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community Edition'); diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue index b57bb98f7f..b1df9d2fa9 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue @@ -9,6 +9,8 @@ import { useUIStore } from '@/stores/ui.store'; import { useToast } from '@/composables/useToast'; import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { hasPermission } from '@/utils/rbac/permissions'; +import N8nInfoTip from 'n8n-design-system/components/N8nInfoTip'; +import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants'; const usageStore = useUsageStore(); const route = useRoute(); @@ -40,6 +42,8 @@ const badgedPlanName = computed(() => { }; }); +const isCommunity = computed(() => usageStore.planName.toLowerCase() === 'community'); + const isCommunityEditionRegistered = computed( () => usageStore.planName.toLowerCase() === 'registered community', ); @@ -133,6 +137,10 @@ const onDialogClosed = () => { const onDialogOpened = () => { activationKeyInput.value?.focus(); }; + +const openCommunityRegisterModal = () => { + uiStore.openModal(COMMUNITY_PLUS_ENROLLMENT_MODAL); +};