diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index 503f0a1a0a..a03cf59c99 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -143,9 +143,6 @@ export default defineComponent({ console.log(HIRING_BANNER); } }, - async initBanners() { - return this.uiStore.initBanners(); - }, async checkForCloudPlanData() { return this.cloudPlanStore.checkForCloudPlanData(); }, @@ -239,7 +236,6 @@ export default defineComponent({ await this.redirectIfNecessary(); void this.checkForNewVersions(); await this.checkForCloudPlanData(); - void this.initBanners(); void this.postAuthenticate(); this.loading = false; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 7988eaa838..eeebd22d1d 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -45,6 +45,7 @@ import type { } from 'n8n-workflow'; import type { BulkCommand, Undoable } from '@/models/history'; import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers'; +import type { Component } from 'vue'; export * from 'n8n-design-system/types'; @@ -1084,7 +1085,7 @@ export interface UIState { addFirstStepOnLoad: boolean; executionSidebarAutoRefresh: boolean; bannersHeight: number; - banners: { [key in BannerName]: { dismissed: boolean; type?: 'temporary' | 'permanent' } }; + bannerStack: BannerName[]; } export type IFakeDoor = { @@ -1192,6 +1193,7 @@ export interface IVersionsState { export interface IUsersState { currentUserId: null | string; users: { [userId: string]: IUser }; + currentUserCloudInfo: Cloud.UserAccount | null; } export interface IWorkflowsState { @@ -1529,6 +1531,13 @@ export declare namespace Cloud { length: number; gracePeriod: number; } + + export type UserAccount = { + confirmed: boolean; + username: string; + email: string; + hasEarlyAccess?: boolean; + }; } export interface CloudPlanState { @@ -1594,3 +1603,10 @@ export type UTMCampaign = | 'open' | 'upgrade-users' | 'upgrade-variables'; + +export type N8nBanners = { + [key in BannerName]: { + priority: number; + component: Component; + }; +}; diff --git a/packages/editor-ui/src/api/cloudPlans.ts b/packages/editor-ui/src/api/cloudPlans.ts index e0d15299dc..c0a8dfe797 100644 --- a/packages/editor-ui/src/api/cloudPlans.ts +++ b/packages/editor-ui/src/api/cloudPlans.ts @@ -1,5 +1,5 @@ import type { Cloud, IRestApiContext, InstanceUsage } from '@/Interface'; -import { get } from '@/utils'; +import { get, post } from '@/utils'; export async function getCurrentPlan(context: IRestApiContext): Promise { return get(context.baseUrl, '/admin/cloud-plan'); @@ -8,3 +8,11 @@ export async function getCurrentPlan(context: IRestApiContext): Promise { return get(context.baseUrl, '/cloud/limits'); } + +export async function getCloudUserInfo(context: IRestApiContext): Promise { + return get(context.baseUrl, '/cloud/proxy/admin/user/me'); +} + +export async function confirmEmail(context: IRestApiContext): Promise { + return post(context.baseUrl, '/cloud/proxy/admin/user/resend-confirmation-email'); +} diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index c22934e85a..8c046dbd1c 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -163,7 +163,6 @@ import { import type { IPermissions } from '@/permissions'; import { getWorkflowPermissions } from '@/permissions'; import { createEventBus } from 'n8n-design-system/utils'; -import { useCloudPlanStore } from '@/stores'; import { nodeViewEventBus } from '@/event-bus'; import { genericHelpers } from '@/mixins/genericHelpers'; @@ -223,7 +222,6 @@ export default defineComponent({ useUsageStore, useWorkflowsStore, useUsersStore, - useCloudPlanStore, useSourceControlStore, ), currentUser(): IUser | null { diff --git a/packages/editor-ui/src/components/__tests__/BannersStack.test.ts b/packages/editor-ui/src/components/__tests__/BannersStack.test.ts index 7458b9804f..25da40a044 100644 --- a/packages/editor-ui/src/components/__tests__/BannersStack.test.ts +++ b/packages/editor-ui/src/components/__tests__/BannersStack.test.ts @@ -1,4 +1,3 @@ -import { within } from '@testing-library/vue'; import { merge } from 'lodash-es'; import userEvent from '@testing-library/user-event'; @@ -11,6 +10,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import type { RenderOptions } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render'; +import { waitFor } from '@testing-library/vue'; let uiStore: ReturnType; let usersStore: ReturnType; @@ -20,11 +20,7 @@ const initialState = { settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), }, [STORES.UI]: { - banners: { - V1: { dismissed: false }, - TRIAL: { dismissed: false }, - TRIAL_OVER: { dismissed: false }, - }, + bannerStack: ['TRIAL_OVER', 'V1', 'NON_PRODUCTION_LICENSE', 'EMAIL_CONFIRMATION'], }, [STORES.USERS]: { currentUserId: 'aaa-bbb', @@ -66,37 +62,14 @@ describe('BannerStack', () => { vi.clearAllMocks(); }); - it('should render default configuration', async () => { - const { getByTestId } = renderComponent(); + it('should render banner with the highest priority', async () => { + const { getByTestId, queryByTestId } = renderComponent(); const bannerStack = getByTestId('banner-stack'); expect(bannerStack).toBeInTheDocument(); - - expect(within(bannerStack).getByTestId('banners-TRIAL')).toBeInTheDocument(); - expect(within(bannerStack).getByTestId('banners-TRIAL_OVER')).toBeInTheDocument(); - expect(within(bannerStack).getByTestId('banners-V1')).toBeInTheDocument(); - }); - - it('should not render dismissed banners', async () => { - const { getByTestId } = renderComponent({ - pinia: createTestingPinia({ - initialState: merge(initialState, { - [STORES.UI]: { - banners: { - V1: { dismissed: true }, - TRIAL: { dismissed: true }, - }, - }, - }), - }), - }); - - const bannerStack = getByTestId('banner-stack'); - expect(bannerStack).toBeInTheDocument(); - - expect(within(bannerStack).queryByTestId('banners-V1')).not.toBeInTheDocument(); - expect(within(bannerStack).queryByTestId('banners-TRIAL')).not.toBeInTheDocument(); - expect(within(bannerStack).getByTestId('banners-TRIAL_OVER')).toBeInTheDocument(); + // Only V1 banner should be visible + expect(getByTestId('banners-V1')).toBeInTheDocument(); + expect(queryByTestId('banners-TRIAL_OVER')).not.toBeInTheDocument(); }); it('should dismiss banner on click', async () => { @@ -104,24 +77,15 @@ describe('BannerStack', () => { const dismissBannerSpy = vi .spyOn(useUIStore(), 'dismissBanner') .mockImplementation(async (banner, mode) => {}); - const closeTrialBannerButton = getByTestId('banner-TRIAL_OVER-close'); + expect(getByTestId('banners-V1')).toBeInTheDocument(); + const closeTrialBannerButton = getByTestId('banner-V1-close'); expect(closeTrialBannerButton).toBeInTheDocument(); await userEvent.click(closeTrialBannerButton); - expect(dismissBannerSpy).toHaveBeenCalledWith('TRIAL_OVER'); + expect(dismissBannerSpy).toHaveBeenCalledWith('V1'); }); it('should permanently dismiss banner on click', async () => { - const { getByTestId } = renderComponent({ - pinia: createTestingPinia({ - initialState: merge(initialState, { - [STORES.UI]: { - banners: { - V1: { dismissed: false }, - }, - }, - }), - }), - }); + const { getByTestId } = renderComponent(); const dismissBannerSpy = vi .spyOn(useUIStore(), 'dismissBanner') .mockImplementation(async (banner, mode) => {}); @@ -144,4 +108,59 @@ describe('BannerStack', () => { }); expect(queryByTestId('banner-confirm-v1')).not.toBeInTheDocument(); }); + + it('should send email confirmation request from the banner', async () => { + const { getByTestId, getByText } = renderComponent({ + pinia: createTestingPinia({ + initialState: { + ...initialState, + [STORES.UI]: { + bannerStack: ['EMAIL_CONFIRMATION'], + }, + }, + }), + }); + const confirmEmailSpy = vi.spyOn(useUsersStore(), 'confirmEmail'); + getByTestId('confirm-email-button').click(); + await waitFor(() => expect(confirmEmailSpy).toHaveBeenCalled()); + await waitFor(() => { + expect(getByText('Confirmation email sent')).toBeInTheDocument(); + }); + }); + + it('should show error message if email confirmation fails', async () => { + const ERROR_MESSAGE = 'Something went wrong'; + const { getByTestId, getByText } = renderComponent({ + pinia: createTestingPinia({ + initialState: { + ...initialState, + [STORES.UI]: { + bannerStack: ['EMAIL_CONFIRMATION'], + }, + }, + }), + }); + const confirmEmailSpy = vi.spyOn(useUsersStore(), 'confirmEmail').mockImplementation(() => { + throw new Error(ERROR_MESSAGE); + }); + getByTestId('confirm-email-button').click(); + await waitFor(() => expect(confirmEmailSpy).toHaveBeenCalled()); + await waitFor(() => { + expect(getByText(ERROR_MESSAGE)).toBeInTheDocument(); + }); + }); + + it('should render empty banner stack when there are no banners to display', async () => { + const { queryByTestId } = renderComponent({ + pinia: createTestingPinia({ + initialState: { + ...initialState, + [STORES.UI]: { + bannerStack: [], + }, + }, + }), + }); + expect(queryByTestId('banner-stack')).toBeEmptyDOMElement(); + }); }); diff --git a/packages/editor-ui/src/components/banners/BannerStack.vue b/packages/editor-ui/src/components/banners/BannerStack.vue index 248ac67f70..a52b7e696f 100644 --- a/packages/editor-ui/src/components/banners/BannerStack.vue +++ b/packages/editor-ui/src/components/banners/BannerStack.vue @@ -1,38 +1,59 @@ - + + diff --git a/packages/editor-ui/src/components/banners/BaseBanner.vue b/packages/editor-ui/src/components/banners/BaseBanner.vue index 3ede4c71b7..5b968c5860 100644 --- a/packages/editor-ui/src/components/banners/BaseBanner.vue +++ b/packages/editor-ui/src/components/banners/BaseBanner.vue @@ -1,5 +1,6 @@ + + diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 443bb43476..a6a0694b65 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -515,6 +515,7 @@ export const enum STORES { NODE_CREATOR = 'nodeCreator', WEBHOOKS = 'webhooks', HISTORY = 'history', + CLOUD_PLAN = 'cloudPlan', } export const enum SignInType { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index ff43e76679..328f829414 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -116,6 +116,13 @@ "auth.signup.setupYourAccount": "Set up your account", "auth.signup.setupYourAccountError": "Problem setting up your account", "auth.signup.tokenValidationError": "Issue validating invite token", + "banners.confirmEmail.message.1": "You need access to your admin email inbox to make sure you can reach your admin dashboard. Please make sure that your admin email", + "banners.confirmEmail.message.2": "is accessible and confirmed.", + "banners.confirmEmail.button": "Confirm email", + "banners.confirmEmail.toast.success.heading": "Confirmation email sent", + "banners.confirmEmail.toast.success.message": "Please check your inbox and click the confirmation link.", + "banners.confirmEmail.toast.error.heading": "Problem sending confirmation email", + "banners.confirmEmail.toast.error.message": "Please try again later.", "banners.nonProductionLicense.message": "This n8n instance is not licensed for production purposes!", "banners.trial.message": "1 day left in your n8n trial | {count} days left in your n8n trial", "banners.trialOver.message": "Your trial is over. Upgrade now to keep automating.", diff --git a/packages/editor-ui/src/stores/__tests__/ui.test.ts b/packages/editor-ui/src/stores/__tests__/ui.test.ts index 59be0c810c..898f02000b 100644 --- a/packages/editor-ui/src/stores/__tests__/ui.test.ts +++ b/packages/editor-ui/src/stores/__tests__/ui.test.ts @@ -1,17 +1,57 @@ import { createPinia, setActivePinia } from 'pinia'; import { useUIStore } from '@/stores/ui.store'; -import { useSettingsStore } from '@/stores/settings.store'; +import { useSettingsStore, useUsersStore } from '@/stores/settings.store'; import { merge } from 'lodash-es'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { useRootStore } from '@/stores/n8nRoot.store'; +import { useCloudPlanStore } from '@/stores/cloudPlan.store'; +import * as cloudPlanApi from '@/api/cloudPlans'; +import { + getTrialExpiredUserResponse, + getTrialingUserResponse, + getUserCloudInfo, +} from './utils/cloudStoreUtils'; let uiStore: ReturnType; let settingsStore: ReturnType; +let rootStore: ReturnType; +let cloudPlanStore: ReturnType; + +function setOwnerUser() { + useUsersStore().addUsers([ + { + id: '1', + isPending: false, + globalRole: { + id: '1', + name: 'owner', + createdAt: new Date(), + }, + }, + ]); + + useUsersStore().currentUserId = '1'; +} + +function setupOwnerAndCloudDeployment() { + setOwnerUser(); + settingsStore.setSettings( + merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { + n8nMetadata: { + userId: '1', + }, + deployment: { type: 'cloud' }, + }), + ); +} describe('UI store', () => { beforeEach(() => { setActivePinia(createPinia()); uiStore = useUIStore(); settingsStore = useSettingsStore(); + rootStore = useRootStore(); + cloudPlanStore = useCloudPlanStore(); }); test.each([ @@ -42,4 +82,79 @@ describe('UI store', () => { expect(uiStore.upgradeLinkUrl('test_source', 'utm-test-campaign')).toBe(expectation); }, ); + + it('should add non-production license banner to stack based on enterprise settings', () => { + settingsStore.setSettings( + merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { + enterprise: { + showNonProdBanner: true, + }, + }), + ); + expect(uiStore.bannerStack).toContain('NON_PRODUCTION_LICENSE'); + }); + + it("should add V1 banner to stack if it's not dismissed", () => { + settingsStore.setSettings( + merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { + versionCli: '1.0.0', + }), + ); + expect(uiStore.bannerStack).toContain('V1'); + }); + + it("should not add V1 banner to stack if it's dismissed", () => { + settingsStore.setSettings( + merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { + versionCli: '1.0.0', + banners: { + dismissed: ['V1'], + }, + }), + ); + expect(uiStore.bannerStack).not.toContain('V1'); + }); + + it('should add trial banner to the the stack', async () => { + const fetchCloudSpy = vi + .spyOn(cloudPlanApi, 'getCurrentPlan') + .mockResolvedValue(getTrialingUserResponse()); + const fetchUserCloudAccountSpy = vi + .spyOn(cloudPlanApi, 'getCloudUserInfo') + .mockResolvedValue(getUserCloudInfo(true)); + setupOwnerAndCloudDeployment(); + await cloudPlanStore.getOwnerCurrentPlan(); + expect(fetchCloudSpy).toHaveBeenCalled(); + expect(fetchUserCloudAccountSpy).toHaveBeenCalled(); + expect(uiStore.bannerStack).toContain('TRIAL'); + }); + + it('should add trial over banner to the the stack', async () => { + const fetchCloudSpy = vi + .spyOn(cloudPlanApi, 'getCurrentPlan') + .mockResolvedValue(getTrialExpiredUserResponse()); + const fetchUserCloudAccountSpy = vi + .spyOn(cloudPlanApi, 'getCloudUserInfo') + .mockResolvedValue(getUserCloudInfo(true)); + setupOwnerAndCloudDeployment(); + await cloudPlanStore.getOwnerCurrentPlan(); + expect(fetchCloudSpy).toHaveBeenCalled(); + expect(fetchUserCloudAccountSpy).toHaveBeenCalled(); + expect(uiStore.bannerStack).toContain('TRIAL_OVER'); + }); + + it('should add email confirmation banner to the the stack', async () => { + const fetchCloudSpy = vi + .spyOn(cloudPlanApi, 'getCurrentPlan') + .mockResolvedValue(getTrialExpiredUserResponse()); + const fetchUserCloudAccountSpy = vi + .spyOn(cloudPlanApi, 'getCloudUserInfo') + .mockResolvedValue(getUserCloudInfo(false)); + setupOwnerAndCloudDeployment(); + await cloudPlanStore.getOwnerCurrentPlan(); + expect(fetchCloudSpy).toHaveBeenCalled(); + expect(fetchUserCloudAccountSpy).toHaveBeenCalled(); + expect(uiStore.bannerStack).toContain('TRIAL_OVER'); + expect(uiStore.bannerStack).toContain('EMAIL_CONFIRMATION'); + }); }); diff --git a/packages/editor-ui/src/stores/__tests__/utils/cloudStoreUtils.ts b/packages/editor-ui/src/stores/__tests__/utils/cloudStoreUtils.ts new file mode 100644 index 0000000000..448f4ba421 --- /dev/null +++ b/packages/editor-ui/src/stores/__tests__/utils/cloudStoreUtils.ts @@ -0,0 +1,44 @@ +import type { Cloud } from '@/Interface'; + +// Mocks cloud plan API responses with different trial expiration dates +function getUserPlanData(trialExpirationDate: Date): Cloud.PlanData { + return { + planId: 0, + monthlyExecutionsLimit: 1000, + activeWorkflowsLimit: 10, + credentialsLimit: 100, + isActive: true, + displayName: 'Trial', + metadata: { + group: 'trial', + slug: 'trial-1', + trial: { + gracePeriod: 3, + length: 7, + }, + version: 'v1', + }, + expirationDate: trialExpirationDate.toISOString(), + }; +} + +// Mocks cloud user API responses with different confirmed states +export function getUserCloudInfo(confirmed: boolean): Cloud.UserAccount { + return { + confirmed, + email: 'test@test.com', + username: 'test', + }; +} + +export function getTrialingUserResponse(): Cloud.PlanData { + const dateInThePast = new Date(); + dateInThePast.setDate(dateInThePast.getDate() + 3); + return getUserPlanData(dateInThePast); +} + +export function getTrialExpiredUserResponse(): Cloud.PlanData { + const dateInThePast = new Date(); + dateInThePast.setDate(dateInThePast.getDate() - 3); + return getUserPlanData(dateInThePast); +} diff --git a/packages/editor-ui/src/stores/cloudPlan.store.ts b/packages/editor-ui/src/stores/cloudPlan.store.ts index cc6e85bf7e..f736aa8bba 100644 --- a/packages/editor-ui/src/stores/cloudPlan.store.ts +++ b/packages/editor-ui/src/stores/cloudPlan.store.ts @@ -3,10 +3,11 @@ import { defineStore } from 'pinia'; import type { CloudPlanState } from '@/Interface'; import { useRootStore } from '@/stores/n8nRoot.store'; import { useSettingsStore } from '@/stores/settings.store'; +import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import { getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans'; import { DateTime } from 'luxon'; -import { CLOUD_TRIAL_CHECK_INTERVAL } from '@/constants'; +import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants'; const DEFAULT_STATE: CloudPlanState = { data: null, @@ -14,7 +15,7 @@ const DEFAULT_STATE: CloudPlanState = { loadingPlan: false, }; -export const useCloudPlanStore = defineStore('cloudPlan', () => { +export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => { const rootStore = useRootStore(); const settingsStore = useSettingsStore(); const usersStore = useUsersStore(); @@ -62,6 +63,21 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => { plan = await getCurrentPlan(rootStore.getRestApiContext); state.data = plan; state.loadingPlan = false; + + if (userIsTrialing.value) { + if (trialExpired.value) { + useUIStore().pushBannerToStack('TRIAL_OVER'); + } else { + useUIStore().pushBannerToStack('TRIAL'); + } + } + + if (useUsersStore().isInstanceOwner) { + await usersStore.fetchUserCloudAccount(); + if (!usersStore.currentUserCloudInfo?.confirmed) { + useUIStore().pushBannerToStack('EMAIL_CONFIRMATION'); + } + } } catch (error) { state.loadingPlan = false; throw new Error(error); diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts index 5f25ca38fe..ae2d0cae0d 100644 --- a/packages/editor-ui/src/stores/settings.store.ts +++ b/packages/editor-ui/src/stores/settings.store.ts @@ -31,7 +31,6 @@ import { useUIStore } from './ui.store'; import { useUsersStore } from './users.store'; import { useVersionsStore } from './versions.store'; import { makeRestApiRequest } from '@/utils'; -import { useCloudPlanStore } from './cloudPlan.store'; export const useSettingsStore = defineStore(STORES.SETTINGS, { state: (): ISettingsState => ({ @@ -205,7 +204,15 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { this.saml.loginLabel = settings.sso.saml.loginLabel; } if (settings.enterprise?.showNonProdBanner) { - useUIStore().banners.NON_PRODUCTION_LICENSE.dismissed = false; + useUIStore().pushBannerToStack('NON_PRODUCTION_LICENSE'); + } + if (settings.versionCli) { + useRootStore().setVersionCli(settings.versionCli); + } + + const isV1BannerDismissedPermanently = (settings.banners?.dismissed || []).includes('V1'); + if (!isV1BannerDismissedPermanently && useRootStore().versionCli.startsWith('1.')) { + useUIStore().pushBannerToStack('V1'); } }, async getSettings(): Promise { @@ -233,15 +240,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { rootStore.setDefaultLocale(settings.defaultLocale); rootStore.setIsNpmAvailable(settings.isNpmAvailable); - const isV1BannerDismissedPermanently = settings.banners.dismissed.includes('V1'); - if ( - !isV1BannerDismissedPermanently && - useRootStore().versionCli.startsWith('1.') && - !useCloudPlanStore().userIsTrialing - ) { - useUIStore().showBanner('V1'); - } - useVersionsStore().setVersionNotificationSettings(settings.versionNotifications); }, stopShowingSetupPage(): void { diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 8fb69aa968..ec9835f53c 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -49,9 +49,9 @@ import type { NewCredentialsModal, } from '@/Interface'; import { defineStore } from 'pinia'; -import { useRootStore } from './n8nRoot.store'; +import { useRootStore } from '@/stores/n8nRoot.store'; import { getCurlToJson } from '@/api/curlHelper'; -import { useWorkflowsStore } from './workflows.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import type { BaseTextKey } from '@/plugins/i18n'; @@ -184,13 +184,8 @@ export const useUIStore = defineStore(STORES.UI, { nodeViewInitialized: false, addFirstStepOnLoad: false, executionSidebarAutoRefresh: true, - banners: { - V1: { dismissed: true }, - TRIAL: { dismissed: true }, - TRIAL_OVER: { dismissed: true }, - NON_PRODUCTION_LICENSE: { dismissed: true }, - }, bannersHeight: 0, + bannerStack: [], }), getters: { contextBasedTranslationKeys() { @@ -563,36 +558,23 @@ export const useUIStore = defineStore(STORES.UI, { bannerName: name, dismissedBanners: useSettingsStore().permanentlyDismissedBanners, }); - this.banners[name].dismissed = true; - this.banners[name].type = 'permanent'; + this.removeBannerFromStack(name); return; } - this.banners[name].dismissed = true; - this.banners[name].type = 'temporary'; - }, - showBanner(name: BannerName): void { - this.banners[name].dismissed = false; + this.removeBannerFromStack(name); }, 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'); - } - } + pushBannerToStack(name: BannerName) { + if (this.bannerStack.includes(name)) return; + this.bannerStack.push(name); }, - async dismissAllBanners() { - return Promise.all([ - this.dismissBanner('TRIAL', 'temporary'), - this.dismissBanner('TRIAL_OVER', 'temporary'), - this.dismissBanner('V1', 'temporary'), - ]); + removeBannerFromStack(name: BannerName) { + this.bannerStack = this.bannerStack.filter((bannerName) => bannerName !== name); + }, + clearBannerStack() { + this.bannerStack = []; }, }, }); diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index 7844e71288..dad8670fad 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -22,6 +22,7 @@ import { } from '@/api/users'; import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants'; import type { + Cloud, ICredentialsResponse, IInviteResponse, IPersonalizationLatestVersion, @@ -39,6 +40,7 @@ import { useSettingsStore } from './settings.store'; import { useUIStore } from './ui.store'; import { useCloudPlanStore } from './cloudPlan.store'; import { disableMfa, enableMfa, getMfaQR, verifyMfaToken } from '@/api/mfa'; +import { confirmEmail, getCloudUserInfo } from '@/api/cloudPlans'; const isDefaultUser = (user: IUserResponse | null) => Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner); @@ -52,6 +54,7 @@ export const useUsersStore = defineStore(STORES.USERS, { state: (): IUsersState => ({ currentUserId: null, users: {}, + currentUserCloudInfo: null, }), getters: { allUsers(): IUser[] { @@ -194,7 +197,8 @@ export const useUsersStore = defineStore(STORES.USERS, { this.currentUserId = null; useCloudPlanStore().reset(); usePostHog().reset(); - await useUIStore().dismissAllBanners(); + this.currentUserCloudInfo = null; + useUIStore().clearBannerStack(); }, async createOwner(params: { firstName: string; @@ -365,5 +369,17 @@ export const useUsersStore = defineStore(STORES.USERS, { currentUser.mfaEnabled = false; } }, + async fetchUserCloudAccount() { + let cloudUser: Cloud.UserAccount | null = null; + try { + cloudUser = await getCloudUserInfo(useRootStore().getRestApiContext); + this.currentUserCloudInfo = cloudUser; + } catch (error) { + throw new Error(error); + } + }, + async confirmEmail() { + await confirmEmail(useRootStore().getRestApiContext); + }, }, }); diff --git a/packages/editor-ui/src/views/SigninView.vue b/packages/editor-ui/src/views/SigninView.vue index e1df9a6025..b8f10e5b13 100644 --- a/packages/editor-ui/src/views/SigninView.vue +++ b/packages/editor-ui/src/views/SigninView.vue @@ -127,7 +127,7 @@ export default defineComponent({ }); this.loading = false; await this.cloudPlanStore.checkForCloudPlanData(); - await this.uiStore.initBanners(); + await this.settingsStore.getSettings(); this.clearAllStickyNotifications(); this.checkRecoveryCodesLeft(); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index a1b505833e..bda4f19f38 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2225,4 +2225,9 @@ export interface SecretsHelpersBase { listSecrets(provider: string): string[]; } -export type BannerName = 'V1' | 'TRIAL_OVER' | 'TRIAL' | 'NON_PRODUCTION_LICENSE'; +export type BannerName = + | 'V1' + | 'TRIAL_OVER' + | 'TRIAL' + | 'NON_PRODUCTION_LICENSE' + | 'EMAIL_CONFIRMATION';