feat(editor): Community+ enrollment (#10776)

This commit is contained in:
Csaba Tuncsik 2024-10-07 13:09:58 +02:00 committed by GitHub
parent 42c0733990
commit 92cf860f9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 585 additions and 22 deletions

View file

@ -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';

View file

@ -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'] }),
);
});
});

View file

@ -0,0 +1,4 @@
import { z } from 'zod';
import { Z } from 'zod-class';
export class CommunityRegisteredRequestDto extends Z.class({ email: z.string().email() }) {}

View file

@ -420,6 +420,11 @@ export type RelayEventMap = {
success: boolean;
};
'license-community-plus-registered': {
email: string;
licenseKey: string;
};
// #endregion
// #region Variable

View file

@ -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

View file

@ -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<License>();
const workflowRepository = mock<WorkflowRepository>();
@ -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();
});
});
});

View file

@ -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) {

View file

@ -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();
}

View file

@ -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<UsageState['data']> => {
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,
);
};

View file

@ -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:
'<div role="dialog"><slot name="header" /><slot name="content" /><slot name="footer" /></div>',
},
},
},
});
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');
});
});

View file

@ -0,0 +1,186 @@
<script lang="ts" setup="">
import { ref } from 'vue';
import { createEventBus } from 'n8n-design-system/utils';
import type { Validatable, IValidator } from 'n8n-design-system';
import { N8nFormInput } from 'n8n-design-system';
import { VALID_EMAIL_REGEX } from '@/constants';
import Modal from '@/components/Modal.vue';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useUsageStore } from '@/stores/usage.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useUsersStore } from '@/stores/users.store';
const props = defineProps<{
modalName: string;
data: {
closeCallback: () => void;
};
}>();
const i18n = useI18n();
const toast = useToast();
const usageStore = useUsageStore();
const telemetry = useTelemetry();
const usersStore = useUsersStore();
const valid = ref(false);
const email = ref(usersStore.currentUser?.email ?? '');
const validationRules = ref([{ name: 'email' }]);
const validators = ref<{ [key: string]: IValidator }>({
email: {
validate: (value: Validatable) => {
if (typeof value !== 'string') {
return false;
}
if (!VALID_EMAIL_REGEX.test(value)) {
return {
messageKey: 'settings.users.invalidEmailError',
options: { interpolate: { email: value } },
};
}
return false;
},
},
});
const modalBus = createEventBus();
const closeModal = () => {
telemetry.track('User skipped community plus');
modalBus.emit('close');
props.data.closeCallback();
};
const confirm = async () => {
try {
const { title, text } = await usageStore.registerCommunityEdition(email.value);
closeModal();
toast.showMessage({
title: title ?? i18n.baseText('communityPlusModal.success.title'),
message:
text ??
i18n.baseText('communityPlusModal.success.message', {
interpolate: { email: email.value },
}),
type: 'success',
duration: 0,
});
} catch (error) {
toast.showError(error, i18n.baseText('communityPlusModal.error.title'));
}
};
</script>
<template>
<Modal
width="500px"
:name="props.modalName"
:event-bus="modalBus"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #content>
<div>
<p :class="$style.top">
<N8nBadge>{{ i18n.baseText('communityPlusModal.badge') }}</N8nBadge>
</p>
<N8nText tag="h1" align="center" size="xlarge" class="mb-m">{{
i18n.baseText('communityPlusModal.title')
}}</N8nText>
<N8nText tag="p">{{ i18n.baseText('communityPlusModal.description') }}</N8nText>
<ul :class="$style.features">
<li>
<i>🕰</i>
<N8nText>
<strong>{{ i18n.baseText('communityPlusModal.features.first.title') }}</strong>
{{ i18n.baseText('communityPlusModal.features.first.description') }}
</N8nText>
</li>
<li>
<i>🐞</i>
<N8nText>
<strong>{{ i18n.baseText('communityPlusModal.features.second.title') }}</strong>
{{ i18n.baseText('communityPlusModal.features.second.description') }}
</N8nText>
</li>
<li>
<i>🔎</i>
<N8nText>
<strong>{{ i18n.baseText('communityPlusModal.features.third.title') }}</strong>
{{ i18n.baseText('communityPlusModal.features.third.description') }}
</N8nText>
</li>
</ul>
<N8nFormInput
id="email"
v-model="email"
:label="i18n.baseText('communityPlusModal.input.email.label')"
type="email"
name="email"
label-size="small"
tag-size="small"
required
:show-required-asterisk="true"
:validate-on-blur="false"
:validation-rules="validationRules"
:validators="validators"
@validate="valid = $event"
/>
</div>
</template>
<template #footer>
<div :class="$style.buttons">
<N8nButton :class="$style.skip" type="secondary" text @click="closeModal">{{
i18n.baseText('communityPlusModal.button.skip')
}}</N8nButton>
<N8nButton :disabled="!valid" type="primary" @click="confirm">
{{ i18n.baseText('communityPlusModal.button.confirm') }}
</N8nButton>
</div>
</template>
</Modal>
</template>
<style lang="scss" module>
.top {
display: flex;
justify-content: center;
margin: 0 0 var(--spacing-s);
}
.features {
padding: var(--spacing-s) var(--spacing-l) 0;
list-style: none;
li {
display: flex;
padding: 0 var(--spacing-s) var(--spacing-m) 0;
i {
display: inline-block;
margin: var(--spacing-5xs) var(--spacing-xs) 0 0;
font-style: normal;
font-size: var(--font-size-s);
}
strong {
display: block;
margin-bottom: var(--spacing-4xs);
}
}
}
.buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
.skip {
padding: 0;
}
</style>

View file

@ -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';
</script>
<template>
@ -252,5 +254,11 @@ import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue';
<NewAssistantSessionModal :name="modalName" :data="data" />
</template>
</ModalRoot>
<ModalRoot :name="COMMUNITY_PLUS_ENROLLMENT_MODAL">
<template #default="{ modalName, data }">
<CommunityPlusEnrollmentModal :modal-name="modalName" :data="data" />
</template>
</ModalRoot>
</div>
</template>

View file

@ -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: {

View file

@ -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<Record<string, string>>({});
const isSaving = ref(false);
@ -547,6 +550,10 @@ const onSave = () => {
const closeDialog = () => {
modalBus.emit('close');
uiStore.openModalWithData({
name: COMMUNITY_PLUS_ENROLLMENT_MODAL,
data: {
closeCallback: () => {
const isPartOfOnboardingExperiment =
posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
MORE_ONBOARDING_OPTIONS_EXPERIMENT.control;
@ -555,6 +562,9 @@ const closeDialog = () => {
if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) {
void router.replace({ name: VIEWS.HOMEPAGE });
}
},
},
});
};
const onSubmit = async (values: IPersonalizationLatestVersion) => {

View file

@ -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',

View file

@ -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 thats 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"
}

View file

@ -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]: {

View file

@ -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,

View file

@ -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');
});
});

View file

@ -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 = () => {