mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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 './settings-log-streaming';
|
||||||
export * from './sidebar';
|
export * from './sidebar';
|
||||||
export * from './ndv';
|
export * from './ndv';
|
||||||
|
export * from './bannerStack';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { BasePage } from '../base';
|
import { BasePage } from '../base';
|
||||||
|
import { WorkflowsPage } from '../workflows';
|
||||||
|
|
||||||
export class MainSidebar extends BasePage {
|
export class MainSidebar extends BasePage {
|
||||||
getters = {
|
getters = {
|
||||||
|
@ -9,7 +10,7 @@ export class MainSidebar extends BasePage {
|
||||||
workflows: () => this.getters.menuItem('Workflows'),
|
workflows: () => this.getters.menuItem('Workflows'),
|
||||||
credentials: () => this.getters.menuItem('Credentials'),
|
credentials: () => this.getters.menuItem('Credentials'),
|
||||||
executions: () => this.getters.menuItem('Executions'),
|
executions: () => this.getters.menuItem('Executions'),
|
||||||
userMenu: () => cy.getByTestId('main-sidebar-user-menu'),
|
userMenu: () => cy.get('div[class="action-dropdown-container"]'),
|
||||||
};
|
};
|
||||||
actions = {
|
actions = {
|
||||||
goToSettings: () => {
|
goToSettings: () => {
|
||||||
|
@ -26,5 +27,15 @@ export class MainSidebar extends BasePage {
|
||||||
openUserMenu: () => {
|
openUserMenu: () => {
|
||||||
this.getters.userMenu().find('[role="button"]').last().click();
|
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 Modals from '@/components/Modals.vue';
|
||||||
import LoadingView from '@/views/LoadingView.vue';
|
import LoadingView from '@/views/LoadingView.vue';
|
||||||
import Telemetry from '@/components/Telemetry.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 { userHelpers } from '@/mixins/userHelpers';
|
||||||
import { loadLanguage } from '@/plugins/i18n';
|
import { loadLanguage } from '@/plugins/i18n';
|
||||||
|
@ -143,6 +143,12 @@ export default defineComponent({
|
||||||
console.log(HIRING_BANNER);
|
console.log(HIRING_BANNER);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async initBanners() {
|
||||||
|
return this.uiStore.initBanners();
|
||||||
|
},
|
||||||
|
async checkForCloudPlanData() {
|
||||||
|
return this.cloudPlanStore.checkForCloudPlanData();
|
||||||
|
},
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
await this.initSettings();
|
await this.initSettings();
|
||||||
await Promise.all([this.loginWithCookie(), this.initTemplates()]);
|
await Promise.all([this.loginWithCookie(), this.initTemplates()]);
|
||||||
|
@ -209,35 +215,6 @@ export default defineComponent({
|
||||||
window.document.body.classList.add(`theme-${theme}`);
|
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() {
|
async postAuthenticate() {
|
||||||
if (this.postAuthenticateDone) {
|
if (this.postAuthenticateDone) {
|
||||||
return;
|
return;
|
||||||
|
@ -262,9 +239,7 @@ export default defineComponent({
|
||||||
await this.redirectIfNecessary();
|
await this.redirectIfNecessary();
|
||||||
void this.checkForNewVersions();
|
void this.checkForNewVersions();
|
||||||
await this.checkForCloudPlanData();
|
await this.checkForCloudPlanData();
|
||||||
await this.initBanners();
|
void this.initBanners();
|
||||||
|
|
||||||
void this.checkForCloudPlanData();
|
|
||||||
void this.postAuthenticate();
|
void this.postAuthenticate();
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
import { getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { CLOUD_TRIAL_CHECK_INTERVAL } from '@/constants';
|
||||||
|
|
||||||
const DEFAULT_STATE: CloudPlanState = {
|
const DEFAULT_STATE: CloudPlanState = {
|
||||||
data: null,
|
data: null,
|
||||||
|
@ -28,6 +29,11 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
||||||
state.usage = data;
|
state.usage = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
state.data = null;
|
||||||
|
state.usage = null;
|
||||||
|
};
|
||||||
|
|
||||||
const userIsTrialing = computed(() => state.data?.metadata?.group === 'trial');
|
const userIsTrialing = computed(() => state.data?.metadata?.group === 'trial');
|
||||||
|
|
||||||
const currentPlanData = computed(() => state.data);
|
const currentPlanData = computed(() => state.data);
|
||||||
|
@ -89,6 +95,27 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
||||||
return Math.ceil(differenceInDays);
|
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 {
|
return {
|
||||||
state,
|
state,
|
||||||
getOwnerCurrentPlan,
|
getOwnerCurrentPlan,
|
||||||
|
@ -100,5 +127,7 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => {
|
||||||
currentUsageData,
|
currentUsageData,
|
||||||
trialExpired,
|
trialExpired,
|
||||||
allExecutionsUsed,
|
allExecutionsUsed,
|
||||||
|
reset,
|
||||||
|
checkForCloudPlanData,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,8 +49,8 @@ import { defineStore } from 'pinia';
|
||||||
import { useRootStore } from './n8nRoot.store';
|
import { useRootStore } from './n8nRoot.store';
|
||||||
import { getCurlToJson } from '@/api/curlHelper';
|
import { getCurlToJson } from '@/api/curlHelper';
|
||||||
import { useWorkflowsStore } from './workflows.store';
|
import { useWorkflowsStore } from './workflows.store';
|
||||||
import { useSettingsStore } from './settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useCloudPlanStore } from './cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useTelemetryStore } from '@/stores/telemetry.store';
|
import { useTelemetryStore } from '@/stores/telemetry.store';
|
||||||
|
@ -562,5 +562,23 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
updateBannersHeight(newHeight: number): void {
|
updateBannersHeight(newHeight: number): void {
|
||||||
this.bannersHeight = newHeight;
|
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 { usePostHog } from './posthog.store';
|
||||||
import { useSettingsStore } from './settings.store';
|
import { useSettingsStore } from './settings.store';
|
||||||
import { useUIStore } from './ui.store';
|
import { useUIStore } from './ui.store';
|
||||||
|
import { useCloudPlanStore } from './cloudPlan.store';
|
||||||
|
|
||||||
const isDefaultUser = (user: IUserResponse | null) =>
|
const isDefaultUser = (user: IUserResponse | null) =>
|
||||||
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
||||||
|
@ -182,7 +183,9 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
await logout(rootStore.getRestApiContext);
|
await logout(rootStore.getRestApiContext);
|
||||||
this.currentUserId = null;
|
this.currentUserId = null;
|
||||||
|
useCloudPlanStore().reset();
|
||||||
usePostHog().reset();
|
usePostHog().reset();
|
||||||
|
await useUIStore().dismissAllBanners();
|
||||||
},
|
},
|
||||||
async createOwner(params: {
|
async createOwner(params: {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { VIEWS } from '@/constants';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useCloudPlanStore, useUIStore } from '@/stores';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SigninView',
|
name: 'SigninView',
|
||||||
|
@ -36,7 +37,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useUsersStore, useSettingsStore),
|
...mapStores(useUsersStore, useSettingsStore, useUIStore, useCloudPlanStore),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
let emailLabel = this.$locale.baseText('auth.email');
|
let emailLabel = this.$locale.baseText('auth.email');
|
||||||
|
@ -87,6 +88,8 @@ export default defineComponent({
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
await this.usersStore.loginWithCreds(values as { email: string; password: string });
|
await this.usersStore.loginWithCreds(values as { email: string; password: string });
|
||||||
|
await this.cloudPlanStore.checkForCloudPlanData();
|
||||||
|
await this.uiStore.initBanners();
|
||||||
this.clearAllStickyNotifications();
|
this.clearAllStickyNotifications();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue