mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
fix(editor): Trial banner does not disappear after sign out (no-changelog) (#6930)
to test in staging use version `PR-6930-ado-990-trial-banner-does-not-disappear-after-sign-out` <img width="875" alt="image" src="https://github.com/n8n-io/n8n/assets/16496553/dfffe60f-bec3-4c48-bd9c-5990c68afa52">
This commit is contained in:
parent
ad15d3eae9
commit
d3f01270c7
67
cypress/e2e/27-opt-in-trial-banner.cy.ts
Normal file
67
cypress/e2e/27-opt-in-trial-banner.cy.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { BannerStack, MainSidebar, WorkflowPage } from '../pages';
|
||||
import planData from '../fixtures/Plan_data_opt_in_trial.json';
|
||||
import { INSTANCE_OWNER } from '../constants';
|
||||
|
||||
const mainSidebar = new MainSidebar();
|
||||
const bannerStack = new BannerStack();
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
describe('BannerStack', { disableAutoLogin: true }, () => {
|
||||
before(() => {
|
||||
const now = new Date();
|
||||
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
|
||||
planData.expirationDate = fiveDaysFromNow.toJSON();
|
||||
});
|
||||
|
||||
it('should render trial banner for opt-in cloud user', () => {
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
|
||||
});
|
||||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.intercept('GET', '/rest/admin/cloud-plan', {
|
||||
body: planData,
|
||||
}).as('getPlanData');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
||||
cy.wait('@getPlanData');
|
||||
|
||||
bannerStack.getters.banner().should('be.visible');
|
||||
|
||||
mainSidebar.actions.signout();
|
||||
|
||||
bannerStack.getters.banner().should('not.be.visible');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
||||
bannerStack.getters.banner().should('be.visible');
|
||||
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('should not render opt-in-trial banner for non cloud deployment', () => {
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
data: { ...res.body.data, deployment: { type: 'default' } },
|
||||
});
|
||||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
||||
bannerStack.getters.banner().should('not.be.visible');
|
||||
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
});
|
29
cypress/fixtures/Plan_data_opt_in_trial.json
Normal file
29
cypress/fixtures/Plan_data_opt_in_trial.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"id": 200,
|
||||
"planId": 1,
|
||||
"pruneExecutionsInterval": 168,
|
||||
"monthlyExecutionsLimit": 1000,
|
||||
"activeWorkflowsLimit": 20,
|
||||
"credentialsLimit": 100,
|
||||
"supportTier": "community",
|
||||
"displayName": "Trial",
|
||||
"enabledFeatures": ["userManagement", "advancedExecutionFilters", "sharing"],
|
||||
"licenseFeatures": {
|
||||
"feat:sharing": true,
|
||||
"feat:advancedExecutionFilters": true,
|
||||
"quota:users": -1,
|
||||
"quota:maxVariables": -1,
|
||||
"feat:variables": true,
|
||||
"feat:apiDisabled": true
|
||||
},
|
||||
"metadata": {
|
||||
"version": "v1",
|
||||
"group": "trial",
|
||||
"slug": "trial-2",
|
||||
"trial": {
|
||||
"length": 14,
|
||||
"gracePeriod": 3
|
||||
}
|
||||
},
|
||||
"expirationDate": "2023-08-30T15:47:27.611Z"
|
||||
}
|
8
cypress/pages/bannerStack.ts
Normal file
8
cypress/pages/bannerStack.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { BasePage } from './base';
|
||||
|
||||
export class BannerStack extends BasePage {
|
||||
getters = {
|
||||
banner: () => cy.getByTestId('banner-stack'),
|
||||
};
|
||||
actions = {};
|
||||
}
|
|
@ -7,3 +7,4 @@ export * from './settings-users';
|
|||
export * from './settings-log-streaming';
|
||||
export * from './sidebar';
|
||||
export * from './ndv';
|
||||
export * from './bannerStack';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BasePage } from '../base';
|
||||
import { WorkflowsPage } from '../workflows';
|
||||
|
||||
export class MainSidebar extends BasePage {
|
||||
getters = {
|
||||
|
@ -9,7 +10,7 @@ export class MainSidebar extends BasePage {
|
|||
workflows: () => this.getters.menuItem('Workflows'),
|
||||
credentials: () => this.getters.menuItem('Credentials'),
|
||||
executions: () => this.getters.menuItem('Executions'),
|
||||
userMenu: () => cy.getByTestId('main-sidebar-user-menu'),
|
||||
userMenu: () => cy.get('div[class="action-dropdown-container"]'),
|
||||
};
|
||||
actions = {
|
||||
goToSettings: () => {
|
||||
|
@ -26,5 +27,15 @@ export class MainSidebar extends BasePage {
|
|||
openUserMenu: () => {
|
||||
this.getters.userMenu().find('[role="button"]').last().click();
|
||||
},
|
||||
openUserMenu: () => {
|
||||
this.getters.userMenu().click();
|
||||
},
|
||||
signout: () => {
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
cy.visit(workflowsPage.url);
|
||||
this.actions.openUserMenu();
|
||||
cy.getByTestId('user-menu-item-logout').click();
|
||||
cy.wrap(Cypress.session.clearAllSavedSessions());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ import BannerStack from '@/components/banners/BannerStack.vue';
|
|||
import Modals from '@/components/Modals.vue';
|
||||
import LoadingView from '@/views/LoadingView.vue';
|
||||
import Telemetry from '@/components/Telemetry.vue';
|
||||
import { CLOUD_TRIAL_CHECK_INTERVAL, HIRING_BANNER, LOCAL_STORAGE_THEME, VIEWS } from '@/constants';
|
||||
import { HIRING_BANNER, LOCAL_STORAGE_THEME, VIEWS } from '@/constants';
|
||||
|
||||
import { userHelpers } from '@/mixins/userHelpers';
|
||||
import { loadLanguage } from '@/plugins/i18n';
|
||||
|
@ -143,6 +143,12 @@ export default defineComponent({
|
|||
console.log(HIRING_BANNER);
|
||||
}
|
||||
},
|
||||
async initBanners() {
|
||||
return this.uiStore.initBanners();
|
||||
},
|
||||
async checkForCloudPlanData() {
|
||||
return this.cloudPlanStore.checkForCloudPlanData();
|
||||
},
|
||||
async initialize(): Promise<void> {
|
||||
await this.initSettings();
|
||||
await Promise.all([this.loginWithCookie(), this.initTemplates()]);
|
||||
|
@ -209,35 +215,6 @@ export default defineComponent({
|
|||
window.document.body.classList.add(`theme-${theme}`);
|
||||
}
|
||||
},
|
||||
async checkForCloudPlanData(): Promise<void> {
|
||||
try {
|
||||
await this.cloudPlanStore.getOwnerCurrentPlan();
|
||||
if (!this.cloudPlanStore.userIsTrialing) return;
|
||||
await this.cloudPlanStore.getInstanceCurrentUsage();
|
||||
this.startPollingInstanceUsageData();
|
||||
} catch {}
|
||||
},
|
||||
startPollingInstanceUsageData() {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await this.cloudPlanStore.getInstanceCurrentUsage();
|
||||
if (this.cloudPlanStore.trialExpired || this.cloudPlanStore.allExecutionsUsed) {
|
||||
clearTimeout(interval);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}, CLOUD_TRIAL_CHECK_INTERVAL);
|
||||
},
|
||||
async initBanners(): Promise<void> {
|
||||
if (this.cloudPlanStore.userIsTrialing) {
|
||||
await this.uiStore.dismissBanner('V1', 'temporary');
|
||||
if (this.cloudPlanStore.trialExpired) {
|
||||
this.uiStore.showBanner('TRIAL_OVER');
|
||||
} else {
|
||||
this.uiStore.showBanner('TRIAL');
|
||||
}
|
||||
}
|
||||
},
|
||||
async postAuthenticate() {
|
||||
if (this.postAuthenticateDone) {
|
||||
return;
|
||||
|
@ -262,9 +239,7 @@ export default defineComponent({
|
|||
await this.redirectIfNecessary();
|
||||
void this.checkForNewVersions();
|
||||
await this.checkForCloudPlanData();
|
||||
await this.initBanners();
|
||||
|
||||
void this.checkForCloudPlanData();
|
||||
void this.initBanners();
|
||||
void this.postAuthenticate();
|
||||
|
||||
this.loading = false;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
|||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
||||
import { DateTime } from 'luxon';
|
||||
import { CLOUD_TRIAL_CHECK_INTERVAL } from '@/constants';
|
||||
|
||||
const DEFAULT_STATE: CloudPlanState = {
|
||||
data: null,
|
||||
|
@ -28,6 +29,11 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
|||
state.usage = data;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
state.data = null;
|
||||
state.usage = null;
|
||||
};
|
||||
|
||||
const userIsTrialing = computed(() => state.data?.metadata?.group === 'trial');
|
||||
|
||||
const currentPlanData = computed(() => state.data);
|
||||
|
@ -89,6 +95,27 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
|||
return Math.ceil(differenceInDays);
|
||||
});
|
||||
|
||||
const startPollingInstanceUsageData = () => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await getInstanceCurrentUsage();
|
||||
if (trialExpired.value || allExecutionsUsed.value) {
|
||||
clearTimeout(interval);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}, CLOUD_TRIAL_CHECK_INTERVAL);
|
||||
};
|
||||
|
||||
const checkForCloudPlanData = async (): Promise<void> => {
|
||||
try {
|
||||
await getOwnerCurrentPlan();
|
||||
if (!userIsTrialing.value) return;
|
||||
await getInstanceCurrentUsage();
|
||||
startPollingInstanceUsageData();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
getOwnerCurrentPlan,
|
||||
|
@ -100,5 +127,7 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
|||
currentUsageData,
|
||||
trialExpired,
|
||||
allExecutionsUsed,
|
||||
reset,
|
||||
checkForCloudPlanData,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -49,8 +49,8 @@ import { defineStore } from 'pinia';
|
|||
import { useRootStore } from './n8nRoot.store';
|
||||
import { getCurlToJson } from '@/api/curlHelper';
|
||||
import { useWorkflowsStore } from './workflows.store';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { useCloudPlanStore } from './cloudPlan.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useTelemetryStore } from '@/stores/telemetry.store';
|
||||
|
@ -562,5 +562,23 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
updateBannersHeight(newHeight: number): void {
|
||||
this.bannersHeight = newHeight;
|
||||
},
|
||||
async initBanners(): Promise<void> {
|
||||
const cloudPlanStore = useCloudPlanStore();
|
||||
if (cloudPlanStore.userIsTrialing) {
|
||||
await this.dismissBanner('V1', 'temporary');
|
||||
if (cloudPlanStore.trialExpired) {
|
||||
this.showBanner('TRIAL_OVER');
|
||||
} else {
|
||||
this.showBanner('TRIAL');
|
||||
}
|
||||
}
|
||||
},
|
||||
async dismissAllBanners() {
|
||||
return Promise.all([
|
||||
this.dismissBanner('TRIAL', 'temporary'),
|
||||
this.dismissBanner('TRIAL_OVER', 'temporary'),
|
||||
this.dismissBanner('V1', 'temporary'),
|
||||
]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ import { useRootStore } from './n8nRoot.store';
|
|||
import { usePostHog } from './posthog.store';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { useUIStore } from './ui.store';
|
||||
import { useCloudPlanStore } from './cloudPlan.store';
|
||||
|
||||
const isDefaultUser = (user: IUserResponse | null) =>
|
||||
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
||||
|
@ -182,7 +183,9 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
|||
const rootStore = useRootStore();
|
||||
await logout(rootStore.getRestApiContext);
|
||||
this.currentUserId = null;
|
||||
useCloudPlanStore().reset();
|
||||
usePostHog().reset();
|
||||
await useUIStore().dismissAllBanners();
|
||||
},
|
||||
async createOwner(params: {
|
||||
firstName: string;
|
||||
|
|
|
@ -18,6 +18,7 @@ import { VIEWS } from '@/constants';
|
|||
import { mapStores } from 'pinia';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useCloudPlanStore, useUIStore } from '@/stores';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SigninView',
|
||||
|
@ -36,7 +37,7 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUsersStore, useSettingsStore),
|
||||
...mapStores(useUsersStore, useSettingsStore, useUIStore, useCloudPlanStore),
|
||||
},
|
||||
mounted() {
|
||||
let emailLabel = this.$locale.baseText('auth.email');
|
||||
|
@ -87,6 +88,8 @@ export default defineComponent({
|
|||
try {
|
||||
this.loading = true;
|
||||
await this.usersStore.loginWithCreds(values as { email: string; password: string });
|
||||
await this.cloudPlanStore.checkForCloudPlanData();
|
||||
await this.uiStore.initBanners();
|
||||
this.clearAllStickyNotifications();
|
||||
this.loading = false;
|
||||
|
||||
|
|
Loading…
Reference in a new issue