diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index 68c1972a4680e..41a55f050a358 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -2,3 +2,4 @@ export { PasswordUpdateRequestDto } from './user/password-update-request.dto'; export { RoleChangeRequestDto } from './user/role-change-request.dto'; export { SettingsUpdateRequestDto } from './user/settings-update-request.dto'; export { UserUpdateRequestDto } from './user/user-update-request.dto'; +export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto'; diff --git a/packages/@n8n/api-types/src/dto/license/__tests__/community-registered-request.dto.test.ts b/packages/@n8n/api-types/src/dto/license/__tests__/community-registered-request.dto.test.ts new file mode 100644 index 0000000000000..84e583e63b03a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/license/__tests__/community-registered-request.dto.test.ts @@ -0,0 +1,27 @@ +import { CommunityRegisteredRequestDto } from '../community-registered-request.dto'; + +describe('CommunityRegisteredRequestDto', () => { + it('should fail validation for missing email', () => { + const invalidRequest = {}; + + const result = CommunityRegisteredRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0]).toEqual( + expect.objectContaining({ message: 'Required', path: ['email'] }), + ); + }); + + it('should fail validation for an invalid email', () => { + const invalidRequest = { + email: 'invalid-email', + }; + + const result = CommunityRegisteredRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0]).toEqual( + expect.objectContaining({ message: 'Invalid email', path: ['email'] }), + ); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/license/community-registered-request.dto.ts b/packages/@n8n/api-types/src/dto/license/community-registered-request.dto.ts new file mode 100644 index 0000000000000..9763787767f40 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/license/community-registered-request.dto.ts @@ -0,0 +1,4 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class CommunityRegisteredRequestDto extends Z.class({ email: z.string().email() }) {} diff --git a/packages/cli/src/events/maps/relay.event-map.ts b/packages/cli/src/events/maps/relay.event-map.ts index a495820283067..21b673a2b552c 100644 --- a/packages/cli/src/events/maps/relay.event-map.ts +++ b/packages/cli/src/events/maps/relay.event-map.ts @@ -420,6 +420,11 @@ export type RelayEventMap = { success: boolean; }; + 'license-community-plus-registered': { + email: string; + licenseKey: string; + }; + // #endregion // #region Variable diff --git a/packages/cli/src/events/relays/telemetry.event-relay.ts b/packages/cli/src/events/relays/telemetry.event-relay.ts index c813926bf1635..11d84751d0d2b 100644 --- a/packages/cli/src/events/relays/telemetry.event-relay.ts +++ b/packages/cli/src/events/relays/telemetry.event-relay.ts @@ -54,6 +54,7 @@ export class TelemetryEventRelay extends EventRelay { 'source-control-user-finished-push-ui': (event) => this.sourceControlUserFinishedPushUi(event), 'license-renewal-attempted': (event) => this.licenseRenewalAttempted(event), + 'license-community-plus-registered': (event) => this.licenseCommunityPlusRegistered(event), 'variable-created': () => this.variableCreated(), 'external-secrets-provider-settings-saved': (event) => this.externalSecretsProviderSettingsSaved(event), @@ -234,6 +235,16 @@ export class TelemetryEventRelay extends EventRelay { }); } + private licenseCommunityPlusRegistered({ + email, + licenseKey, + }: RelayEventMap['license-community-plus-registered']) { + this.telemetry.track('User registered for license community plus', { + email, + licenseKey, + }); + } + // #endregion // #region Variable diff --git a/packages/cli/src/license/__tests__/license.service.test.ts b/packages/cli/src/license/__tests__/license.service.test.ts index 5fe4d6c69209e..77afe04a2c1bc 100644 --- a/packages/cli/src/license/__tests__/license.service.test.ts +++ b/packages/cli/src/license/__tests__/license.service.test.ts @@ -1,4 +1,5 @@ import type { TEntitlement } from '@n8n_io/license-sdk'; +import axios, { AxiosError } from 'axios'; import { mock } from 'jest-mock-extended'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; @@ -7,6 +8,8 @@ import type { EventService } from '@/events/event.service'; import type { License } from '@/license'; import { LicenseErrors, LicenseService } from '@/license/license.service'; +jest.mock('axios'); + describe('LicenseService', () => { const license = mock(); const workflowRepository = mock(); @@ -84,4 +87,37 @@ describe('LicenseService', () => { }); }); }); + + describe('registerCommunityEdition', () => { + test('on success', async () => { + jest + .spyOn(axios, 'post') + .mockResolvedValueOnce({ data: { title: 'Title', text: 'Text', licenseKey: 'abc-123' } }); + const data = await licenseService.registerCommunityEdition({ + email: 'test@ema.il', + instanceId: '123', + instanceUrl: 'http://localhost', + licenseType: 'community-registered', + }); + + expect(data).toEqual({ title: 'Title', text: 'Text' }); + expect(eventService.emit).toHaveBeenCalledWith('license-community-plus-registered', { + email: 'test@ema.il', + licenseKey: 'abc-123', + }); + }); + + test('on failure', async () => { + jest.spyOn(axios, 'post').mockRejectedValueOnce(new AxiosError('Failed')); + await expect( + licenseService.registerCommunityEdition({ + email: 'test@ema.il', + instanceId: '123', + instanceUrl: 'http://localhost', + licenseType: 'community-registered', + }), + ).rejects.toThrowError('Failed'); + expect(eventService.emit).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/cli/src/license/license.controller.ts b/packages/cli/src/license/license.controller.ts index e1644046bb395..db895ef4a0d5c 100644 --- a/packages/cli/src/license/license.controller.ts +++ b/packages/cli/src/license/license.controller.ts @@ -1,14 +1,21 @@ +import { CommunityRegisteredRequestDto } from '@n8n/api-types'; import type { AxiosError } from 'axios'; +import { InstanceSettings } from 'n8n-core'; -import { Get, Post, RestController, GlobalScope } from '@/decorators'; +import { Get, Post, RestController, GlobalScope, Body } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { AuthenticatedRequest, LicenseRequest } from '@/requests'; +import { AuthenticatedRequest, AuthlessRequest, LicenseRequest } from '@/requests'; +import { UrlService } from '@/services/url.service'; import { LicenseService } from './license.service'; @RestController('/license') export class LicenseController { - constructor(private readonly licenseService: LicenseService) {} + constructor( + private readonly licenseService: LicenseService, + private readonly instanceSettings: InstanceSettings, + private readonly urlService: UrlService, + ) {} @Get('/') async getLicenseData() { @@ -32,6 +39,20 @@ export class LicenseController { } } + @Post('/enterprise/community-registered') + async registerCommunityEdition( + _req: AuthlessRequest, + _res: Response, + @Body payload: CommunityRegisteredRequestDto, + ) { + return await this.licenseService.registerCommunityEdition({ + email: payload.email, + instanceId: this.instanceSettings.instanceId, + instanceUrl: this.urlService.getInstanceBaseUrl(), + licenseType: 'community-registered', + }); + } + @Post('/activate') @GlobalScope('license:manage') async activateLicense(req: LicenseRequest.Activate) { diff --git a/packages/cli/src/license/license.service.ts b/packages/cli/src/license/license.service.ts index ee0e27bccb82d..9e4ab2382c69e 100644 --- a/packages/cli/src/license/license.service.ts +++ b/packages/cli/src/license/license.service.ts @@ -1,4 +1,5 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; +import { ensureError } from 'n8n-workflow'; import { Service } from 'typedi'; import type { User } from '@/databases/entities/user'; @@ -60,6 +61,43 @@ export class LicenseService { }); } + async registerCommunityEdition({ + email, + instanceId, + instanceUrl, + licenseType, + }: { + email: string; + instanceId: string; + instanceUrl: string; + licenseType: string; + }): Promise<{ title: string; text: string }> { + try { + const { + data: { licenseKey, ...rest }, + } = await axios.post<{ title: string; text: string; licenseKey: string }>( + 'https://enterprise.n8n.io/community-registered', + { + email, + instanceId, + instanceUrl, + licenseType, + }, + ); + this.eventService.emit('license-community-plus-registered', { email, licenseKey }); + return rest; + } catch (e: unknown) { + if (e instanceof AxiosError) { + const error = e as AxiosError<{ message: string }>; + const errorMsg = error.response?.data?.message ?? e.message; + throw new BadRequestError('Failed to register community edition: ' + errorMsg); + } else { + this.logger.error('Failed to register community edition', { error: ensureError(e) }); + throw new BadRequestError('Failed to register community edition'); + } + } + } + getManagementJwt(): string { return this.license.getManagementJwt(); } diff --git a/packages/editor-ui/src/api/usage.ts b/packages/editor-ui/src/api/usage.ts index 321d05131afa8..274175b99ea34 100644 --- a/packages/editor-ui/src/api/usage.ts +++ b/packages/editor-ui/src/api/usage.ts @@ -1,3 +1,4 @@ +import type { CommunityRegisteredRequestDto } from '@n8n/api-types'; import { makeRestApiRequest } from '@/utils/apiUtils'; import type { IRestApiContext, UsageState } from '@/Interface'; @@ -21,3 +22,15 @@ export const requestLicenseTrial = async ( ): Promise => { return await makeRestApiRequest(context, 'POST', '/license/enterprise/request_trial'); }; + +export const registerCommunityEdition = async ( + context: IRestApiContext, + params: CommunityRegisteredRequestDto, +): Promise<{ title: string; text: string }> => { + return await makeRestApiRequest( + context, + 'POST', + '/license/enterprise/community-registered', + params, + ); +}; diff --git a/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts new file mode 100644 index 0000000000000..84a955d80321a --- /dev/null +++ b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.test.ts @@ -0,0 +1,171 @@ +import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; +import { mockedStore } from '@/__tests__/utils'; +import { createComponentRenderer } from '@/__tests__/render'; +import CommunityPlusEnrollmentModal from '@/components/CommunityPlusEnrollmentModal.vue'; +import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants'; +import { useUsageStore } from '@/stores/usage.store'; +import { useToast } from '@/composables/useToast'; +import { useTelemetry } from '@/composables/useTelemetry'; +import { useUsersStore } from '@/stores/users.store'; + +vi.mock('@/composables/useToast', () => { + const showMessage = vi.fn(); + const showError = vi.fn(); + return { + useToast: () => { + return { + showMessage, + showError, + }; + }, + }; +}); + +vi.mock('@/composables/useTelemetry', () => { + const track = vi.fn(); + return { + useTelemetry: () => { + return { + track, + }; + }, + }; +}); + +const renderComponent = createComponentRenderer(CommunityPlusEnrollmentModal, { + global: { + stubs: { + Modal: { + template: + '
', + }, + }, + }, +}); + +describe('CommunityPlusEnrollmentModal', () => { + const buttonLabel = 'Send me a free license key'; + + beforeEach(() => { + createTestingPinia(); + }); + + it('should test enrolling', async () => { + const closeCallbackSpy = vi.fn(); + const usageStore = mockedStore(useUsageStore); + const toast = useToast(); + + usageStore.registerCommunityEdition.mockResolvedValue({ + title: 'Title', + text: 'Text', + }); + + const props = { + modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL, + data: { + closeCallback: closeCallbackSpy, + }, + }; + + const { getByRole } = renderComponent({ props }); + const emailInput = getByRole('textbox'); + expect(emailInput).toBeVisible(); + + await userEvent.type(emailInput, 'not-an-email'); + expect(emailInput).toHaveValue('not-an-email'); + expect(getByRole('button', { name: buttonLabel })).toBeDisabled(); + + await userEvent.clear(emailInput); + await userEvent.type(emailInput, 'test@ema.il'); + expect(emailInput).toHaveValue('test@ema.il'); + expect(getByRole('button', { name: buttonLabel })).toBeEnabled(); + + await userEvent.click(getByRole('button', { name: buttonLabel })); + expect(usageStore.registerCommunityEdition).toHaveBeenCalledWith('test@ema.il'); + expect(toast.showMessage).toHaveBeenCalledWith({ + title: 'Title', + message: 'Text', + type: 'success', + duration: 0, + }); + expect(closeCallbackSpy).toHaveBeenCalled(); + }); + + it('should test enrolling error', async () => { + const closeCallbackSpy = vi.fn(); + const usageStore = mockedStore(useUsageStore); + const toast = useToast(); + + usageStore.registerCommunityEdition.mockRejectedValue( + new Error('Failed to register community edition'), + ); + + const props = { + modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL, + data: { + closeCallback: closeCallbackSpy, + }, + }; + + const { getByRole } = renderComponent({ props }); + const emailInput = getByRole('textbox'); + expect(emailInput).toBeVisible(); + + await userEvent.type(emailInput, 'test@ema.il'); + expect(emailInput).toHaveValue('test@ema.il'); + expect(getByRole('button', { name: buttonLabel })).toBeEnabled(); + + await userEvent.click(getByRole('button', { name: buttonLabel })); + expect(usageStore.registerCommunityEdition).toHaveBeenCalledWith('test@ema.il'); + expect(toast.showError).toHaveBeenCalledWith( + new Error('Failed to register community edition'), + 'License request failed', + ); + expect(closeCallbackSpy).not.toHaveBeenCalled(); + }); + + it('should track skipping', async () => { + const closeCallbackSpy = vi.fn(); + const telemetry = useTelemetry(); + + const props = { + modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL, + data: { + closeCallback: closeCallbackSpy, + }, + }; + + const { getByRole } = renderComponent({ props }); + const skipButton = getByRole('button', { name: 'Skip' }); + expect(skipButton).toBeVisible(); + + await userEvent.click(skipButton); + expect(telemetry.track).toHaveBeenCalledWith('User skipped community plus'); + }); + + it('should use user email if possible', async () => { + const closeCallbackSpy = vi.fn(); + const usersStore = mockedStore(useUsersStore); + + usersStore.currentUser = { + id: '1', + email: 'test@n8n.io', + isDefaultUser: false, + isPending: false, + mfaEnabled: false, + isPendingUser: false, + }; + + const props = { + modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL, + data: { + closeCallback: closeCallbackSpy, + }, + }; + + const { getByRole } = renderComponent({ props }); + const emailInput = getByRole('textbox'); + expect(emailInput).toHaveValue('test@n8n.io'); + }); +}); diff --git a/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue new file mode 100644 index 0000000000000..6798decc2e146 --- /dev/null +++ b/packages/editor-ui/src/components/CommunityPlusEnrollmentModal.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index a243ce13a54ef..4f431fee84ed7 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -32,6 +32,7 @@ import { SETUP_CREDENTIALS_MODAL_KEY, PROJECT_MOVE_RESOURCE_MODAL, PROMPT_MFA_CODE_MODAL_KEY, + COMMUNITY_PLUS_ENROLLMENT_MODAL, } from '@/constants'; import AboutModal from '@/components/AboutModal.vue'; @@ -67,6 +68,7 @@ import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentials import ProjectMoveResourceModal from '@/components/Projects/ProjectMoveResourceModal.vue'; import NewAssistantSessionModal from '@/components/AskAssistant/NewAssistantSessionModal.vue'; import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue'; +import CommunityPlusEnrollmentModal from '@/components/CommunityPlusEnrollmentModal.vue'; + + + + diff --git a/packages/editor-ui/src/components/PersonalizationModal.test.ts b/packages/editor-ui/src/components/PersonalizationModal.test.ts index a4de7db0fc9ee..a009bf34dcca4 100644 --- a/packages/editor-ui/src/components/PersonalizationModal.test.ts +++ b/packages/editor-ui/src/components/PersonalizationModal.test.ts @@ -15,6 +15,16 @@ import { DEVOPS_AUTOMATION_GOAL_KEY, } from '@/constants'; +vi.mock('vue-router', () => ({ + useRouter: () => ({ + replace: vi.fn(), + }), + useRoute: () => ({ + location: {}, + }), + RouterLink: vi.fn(), +})); + const renderModal = createComponentRenderer(PersonalizationModal, { global: { stubs: { diff --git a/packages/editor-ui/src/components/PersonalizationModal.vue b/packages/editor-ui/src/components/PersonalizationModal.vue index 6be5286618ae1..ee7e1ff4d34b9 100644 --- a/packages/editor-ui/src/components/PersonalizationModal.vue +++ b/packages/editor-ui/src/components/PersonalizationModal.vue @@ -80,6 +80,7 @@ import { REPORTED_SOURCE_OTHER_KEY, VIEWS, MORE_ONBOARDING_OPTIONS_EXPERIMENT, + COMMUNITY_PLUS_ENROLLMENT_MODAL, } from '@/constants'; import { useToast } from '@/composables/useToast'; import Modal from '@/components/Modal.vue'; @@ -91,6 +92,7 @@ import { usePostHog } from '@/stores/posthog.store'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useI18n } from '@/composables/useI18n'; import { useRoute, useRouter } from 'vue-router'; +import { useUIStore } from '@/stores/ui.store'; const SURVEY_VERSION = 'v4'; @@ -104,6 +106,7 @@ const usersStore = useUsersStore(); const posthogStore = usePostHog(); const route = useRoute(); const router = useRouter(); +const uiStore = useUIStore(); const formValues = ref>({}); const isSaving = ref(false); @@ -547,14 +550,21 @@ const onSave = () => { const closeDialog = () => { modalBus.emit('close'); - const isPartOfOnboardingExperiment = - posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) === - MORE_ONBOARDING_OPTIONS_EXPERIMENT.control; - // In case the redirect to homepage for new users didn't happen - // we try again after closing the modal - if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) { - void router.replace({ name: VIEWS.HOMEPAGE }); - } + uiStore.openModalWithData({ + name: COMMUNITY_PLUS_ENROLLMENT_MODAL, + data: { + closeCallback: () => { + const isPartOfOnboardingExperiment = + posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) === + MORE_ONBOARDING_OPTIONS_EXPERIMENT.control; + // In case the redirect to homepage for new users didn't happen + // we try again after closing the modal + if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) { + void router.replace({ name: VIEWS.HOMEPAGE }); + } + }, + }, + }); }; const onSubmit = async (values: IPersonalizationLatestVersion) => { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index ed8254911995a..c5f19f0049f40 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -71,6 +71,7 @@ export const SETUP_CREDENTIALS_MODAL_KEY = 'setupCredentials'; export const PROJECT_MOVE_RESOURCE_MODAL = 'projectMoveResourceModal'; export const NEW_ASSISTANT_SESSION_MODAL = 'newAssistantSession'; export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider'; +export const COMMUNITY_PLUS_ENROLLMENT_MODAL = 'communityPlusEnrollment'; export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = { UNINSTALL: 'uninstall', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 5a89b853a1288..4c46a77050ec9 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -2658,5 +2658,20 @@ "becomeCreator.closeButtonTitle": "Close", "feedback.title": "Was this helpful?", "feedback.positive": "I found this helpful", - "feedback.negative": "I didn't find 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.error.title": "License request failed", + "communityPlusModal.success.title": "Request sent", + "communityPlusModal.success.message": "License key will be sent to {email}", + "communityPlusModal.description": "Receive a free activation key for the advanced features below - lifetime access.", + "communityPlusModal.features.first.title": "Workflow history", + "communityPlusModal.features.first.description": "Review and restore any workflow version from the last 24 hours", + "communityPlusModal.features.second.title": "Advanced debugging", + "communityPlusModal.features.second.description": "Easily fix any workflow execution thatโ€™s errored, then re-run it", + "communityPlusModal.features.third.title": "Execution search and tagging", + "communityPlusModal.features.third.description": "Search and organize past workflow executions for easier review", + "communityPlusModal.input.email.label": "Enter email to receive your license key", + "communityPlusModal.button.skip": "Skip", + "communityPlusModal.button.confirm": "Send me a free license key" } diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index a1c51632836ac..4121099291044 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -37,6 +37,7 @@ import { PROJECT_MOVE_RESOURCE_MODAL, NEW_ASSISTANT_SESSION_MODAL, PROMPT_MFA_CODE_MODAL_KEY, + COMMUNITY_PLUS_ENROLLMENT_MODAL, } from '@/constants'; import type { CloudUpdateLinkSourceType, @@ -126,6 +127,7 @@ export const useUIStore = defineStore(STORES.UI, () => { SETUP_CREDENTIALS_MODAL_KEY, PROJECT_MOVE_RESOURCE_MODAL, NEW_ASSISTANT_SESSION_MODAL, + COMMUNITY_PLUS_ENROLLMENT_MODAL, ].map((modalKey) => [modalKey, { open: false }]), ), [DELETE_USER_MODAL_KEY]: { diff --git a/packages/editor-ui/src/stores/usage.store.ts b/packages/editor-ui/src/stores/usage.store.ts index ae2965529da77..6fc91c4221d07 100644 --- a/packages/editor-ui/src/stores/usage.store.ts +++ b/packages/editor-ui/src/stores/usage.store.ts @@ -1,7 +1,7 @@ import { computed, reactive } from 'vue'; import { defineStore } from 'pinia'; import type { UsageState } from '@/Interface'; -import { activateLicenseKey, getLicense, renewLicense, requestLicenseTrial } from '@/api/usage'; +import * as usageApi from '@/api/usage'; import { useRootStore } from '@/stores/root.store'; import { useSettingsStore } from '@/stores/settings.store'; @@ -63,19 +63,19 @@ export const useUsageStore = defineStore('usage', () => { }; const getLicenseInfo = async () => { - const data = await getLicense(rootStore.restApiContext); + const data = await usageApi.getLicense(rootStore.restApiContext); setData(data); }; const activateLicense = async (activationKey: string) => { - const data = await activateLicenseKey(rootStore.restApiContext, { activationKey }); + const data = await usageApi.activateLicenseKey(rootStore.restApiContext, { activationKey }); setData(data); await settingsStore.getSettings(); }; const refreshLicenseManagementToken = async () => { try { - const data = await renewLicense(rootStore.restApiContext); + const data = await usageApi.renewLicense(rootStore.restApiContext); setData(data); } catch (error) { await getLicenseInfo(); @@ -83,9 +83,12 @@ export const useUsageStore = defineStore('usage', () => { }; const requestEnterpriseLicenseTrial = async () => { - await requestLicenseTrial(rootStore.restApiContext); + await usageApi.requestLicenseTrial(rootStore.restApiContext); }; + const registerCommunityEdition = async (email: string) => + await usageApi.registerCommunityEdition(rootStore.restApiContext, { email }); + return { setLoading, getLicenseInfo, @@ -93,6 +96,7 @@ export const useUsageStore = defineStore('usage', () => { activateLicense, refreshLicenseManagementToken, requestEnterpriseLicenseTrial, + registerCommunityEdition, planName, planId, executionLimit, diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts b/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts index 2002d2f7318fb..18e3d261d3a26 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts @@ -42,10 +42,10 @@ describe('SettingsUsageAndPlan', () => { usageStore.isLoading = false; usageStore.viewPlansUrl = 'https://subscription.n8n.io'; usageStore.managePlanUrl = 'https://subscription.n8n.io'; - usageStore.planName = 'Community registered'; + usageStore.planName = 'Registered Community'; const { getByRole, container } = renderComponent(); expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community Edition'); expect(getByRole('heading', { level: 3 })).toContain(container.querySelector('.n8n-badge')); - expect(container.querySelector('.n8n-badge')).toHaveTextContent('registered'); + expect(container.querySelector('.n8n-badge')).toHaveTextContent('Registered'); }); }); diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue index 81506dfadcd33..b57bb98f7fe97 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue @@ -33,7 +33,7 @@ const canUserActivateLicense = computed(() => ); const badgedPlanName = computed(() => { - const [name, badge] = usageStore.planName.split(' '); + const [badge, name] = usageStore.planName.split(' '); return { name, badge, @@ -41,7 +41,7 @@ const badgedPlanName = computed(() => { }); const isCommunityEditionRegistered = computed( - () => usageStore.planName.toLowerCase() === 'community registered', + () => usageStore.planName.toLowerCase() === 'registered community', ); const showActivationSuccess = () => {