From d3f01270c7adafbc740fd3e4cb94533bc197f925 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 17 Aug 2023 07:57:40 -0400 Subject: [PATCH] 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` image --- cypress/e2e/27-opt-in-trial-banner.cy.ts | 67 +++++++++++++++++++ cypress/fixtures/Plan_data_opt_in_trial.json | 29 ++++++++ cypress/pages/bannerStack.ts | 8 +++ cypress/pages/index.ts | 1 + cypress/pages/sidebar/main-sidebar.ts | 13 +++- packages/editor-ui/src/App.vue | 41 +++--------- .../editor-ui/src/stores/cloudPlan.store.ts | 29 ++++++++ packages/editor-ui/src/stores/ui.store.ts | 22 +++++- packages/editor-ui/src/stores/users.store.ts | 3 + packages/editor-ui/src/views/SigninView.vue | 5 +- 10 files changed, 181 insertions(+), 37 deletions(-) create mode 100644 cypress/e2e/27-opt-in-trial-banner.cy.ts create mode 100644 cypress/fixtures/Plan_data_opt_in_trial.json create mode 100644 cypress/pages/bannerStack.ts diff --git a/cypress/e2e/27-opt-in-trial-banner.cy.ts b/cypress/e2e/27-opt-in-trial-banner.cy.ts new file mode 100644 index 0000000000..0f66236bb0 --- /dev/null +++ b/cypress/e2e/27-opt-in-trial-banner.cy.ts @@ -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(); + }); +}); diff --git a/cypress/fixtures/Plan_data_opt_in_trial.json b/cypress/fixtures/Plan_data_opt_in_trial.json new file mode 100644 index 0000000000..504805de32 --- /dev/null +++ b/cypress/fixtures/Plan_data_opt_in_trial.json @@ -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" +} diff --git a/cypress/pages/bannerStack.ts b/cypress/pages/bannerStack.ts new file mode 100644 index 0000000000..dce3222126 --- /dev/null +++ b/cypress/pages/bannerStack.ts @@ -0,0 +1,8 @@ +import { BasePage } from './base'; + +export class BannerStack extends BasePage { + getters = { + banner: () => cy.getByTestId('banner-stack'), + }; + actions = {}; +} diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 33ddcda6e5..4d95611a12 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -7,3 +7,4 @@ export * from './settings-users'; export * from './settings-log-streaming'; export * from './sidebar'; export * from './ndv'; +export * from './bannerStack'; diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index fc9d8557a2..789d63545f 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -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()); + }, }; } diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index e8b4ea949f..503f0a1a0a 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -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 { 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 { - 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 { - 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; diff --git a/packages/editor-ui/src/stores/cloudPlan.store.ts b/packages/editor-ui/src/stores/cloudPlan.store.ts index 38194ee15a..cc6e85bf7e 100644 --- a/packages/editor-ui/src/stores/cloudPlan.store.ts +++ b/packages/editor-ui/src/stores/cloudPlan.store.ts @@ -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 => { + 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, }; }); diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index bf1724d637..c3c51cae26 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -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 { + 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'), + ]); + }, }, }); diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index 74e3672969..3d49c649dd 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -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; diff --git a/packages/editor-ui/src/views/SigninView.vue b/packages/editor-ui/src/views/SigninView.vue index ca03634f1d..4071a6c486 100644 --- a/packages/editor-ui/src/views/SigninView.vue +++ b/packages/editor-ui/src/views/SigninView.vue @@ -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;