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:
Ricardo Espinoza 2023-08-17 07:57:40 -04:00 committed by GitHub
parent ad15d3eae9
commit d3f01270c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 181 additions and 37 deletions

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

View 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"
}

View file

@ -0,0 +1,8 @@
import { BasePage } from './base';
export class BannerStack extends BasePage {
getters = {
banner: () => cy.getByTestId('banner-stack'),
};
actions = {};
}

View file

@ -7,3 +7,4 @@ export * from './settings-users';
export * from './settings-log-streaming';
export * from './sidebar';
export * from './ndv';
export * from './bannerStack';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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