feat(editor): Support autologin for upgrade path (#7316)

Github issue / Community forum post (link here to close automatically):
This commit is contained in:
Ricardo Espinoza 2023-10-06 13:16:27 +02:00 committed by GitHub
parent ab647b231d
commit 1dfa052301
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 101 additions and 54 deletions

View file

@ -1627,7 +1627,8 @@ export type CloudUpdateLinkSourceType =
| 'sso' | 'sso'
| 'usage_page' | 'usage_page'
| 'settings-users' | 'settings-users'
| 'variables'; | 'variables'
| 'community-nodes';
export type UTMCampaign = export type UTMCampaign =
| 'upgrade-custom-data-filter' | 'upgrade-custom-data-filter'
@ -1642,7 +1643,8 @@ export type UTMCampaign =
| 'upgrade-sso' | 'upgrade-sso'
| 'open' | 'open'
| 'upgrade-users' | 'upgrade-users'
| 'upgrade-variables'; | 'upgrade-variables'
| 'upgrade-community-nodes';
export type N8nBanners = { export type N8nBanners = {
[key in BannerName]: { [key in BannerName]: {

View file

@ -16,3 +16,7 @@ export async function getCloudUserInfo(context: IRestApiContext): Promise<Cloud.
export async function confirmEmail(context: IRestApiContext): Promise<Cloud.UserAccount> { export async function confirmEmail(context: IRestApiContext): Promise<Cloud.UserAccount> {
return post(context.baseUrl, '/cloud/proxy/user/resend-confirmation-email'); return post(context.baseUrl, '/cloud/proxy/user/resend-confirmation-email');
} }
export async function getAdminPanelLoginCode(context: IRestApiContext): Promise<{ code: string }> {
return get(context.baseUrl, '/admin/login/code');
}

View file

@ -186,7 +186,7 @@ export default defineComponent({
this.modalBus.emit('close'); this.modalBus.emit('close');
}, },
goToUpgrade() { goToUpgrade() {
this.uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing'); void this.uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
}, },
}, },
mounted() { mounted() {

View file

@ -141,7 +141,7 @@ const onFilterReset = () => {
}; };
const goToUpgrade = () => { const goToUpgrade = () => {
uiStore.goToUpgrade('custom-data-filter', 'upgrade-custom-data-filter'); void uiStore.goToUpgrade('custom-data-filter', 'upgrade-custom-data-filter');
}; };
onBeforeMount(() => { onBeforeMount(() => {

View file

@ -114,7 +114,7 @@ const maxExecutions = computed(() => {
}); });
const onUpgradeClicked = () => { const onUpgradeClicked = () => {
useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect'); void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
}; };
</script> </script>

View file

@ -624,7 +624,7 @@ export default defineComponent({
} }
}, },
goToUpgrade() { goToUpgrade() {
this.uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing'); void this.uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
}, },
}, },
watch: { watch: {

View file

@ -456,7 +456,7 @@ export default defineComponent({
}); });
}, },
goToUpgrade() { goToUpgrade() {
this.uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing'); void this.uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
}, },
async initialize() { async initialize() {
if (this.isSharingEnabled) { if (this.isSharingEnabled) {

View file

@ -18,7 +18,7 @@ const messageText = computed(() => {
}); });
function onUpdatePlanClick() { function onUpdatePlanClick() {
useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect'); void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
} }
</script> </script>

View file

@ -4,7 +4,7 @@ import { i18n as locale } from '@/plugins/i18n';
import { useUIStore } from '@/stores'; import { useUIStore } from '@/stores';
function onUpdatePlanClick() { function onUpdatePlanClick() {
useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect'); void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
} }
</script> </script>

View file

@ -125,7 +125,7 @@ export const useExecutionDebugging = () => {
title: i18n.baseText(uiStore.contextBasedTranslationKeys.feature.unavailable.title), title: i18n.baseText(uiStore.contextBasedTranslationKeys.feature.unavailable.title),
footerButtonAction: () => { footerButtonAction: () => {
uiStore.closeModal(DEBUG_PAYWALL_MODAL_KEY); uiStore.closeModal(DEBUG_PAYWALL_MODAL_KEY);
uiStore.goToUpgrade('debug', 'upgrade-debug'); void uiStore.goToUpgrade('debug', 'upgrade-debug');
}, },
}, },
}); });

View file

@ -2008,9 +2008,6 @@
"contextual.users.settings.unavailable.button.desktop": "View plans", "contextual.users.settings.unavailable.button.desktop": "View plans",
"contextual.communityNodes.unavailable.description.desktop": "Community nodes feature is unavailable on desktop. Please choose one of our available self-hosting plans.", "contextual.communityNodes.unavailable.description.desktop": "Community nodes feature is unavailable on desktop. Please choose one of our available self-hosting plans.",
"contextual.communityNodes.unavailable.button.desktop": "View plans", "contextual.communityNodes.unavailable.button.desktop": "View plans",
"contextual.upgradeLinkUrl": "https://n8n.io/pricing/",
"contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/account/change-plan",
"contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop",
"contextual.feature.unavailable.title": "Available on the Enterprise Plan", "contextual.feature.unavailable.title": "Available on the Enterprise Plan",
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan", "contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
"contextual.feature.unavailable.title.desktop": "Available on cloud hosting", "contextual.feature.unavailable.title.desktop": "Available on cloud hosting",

View file

@ -47,26 +47,53 @@ function setupOwnerAndCloudDeployment() {
} }
describe('UI store', () => { describe('UI store', () => {
let mockedCloudStore;
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()); setActivePinia(createPinia());
uiStore = useUIStore(); uiStore = useUIStore();
settingsStore = useSettingsStore(); settingsStore = useSettingsStore();
rootStore = useRootStore(); rootStore = useRootStore();
cloudPlanStore = useCloudPlanStore(); cloudPlanStore = useCloudPlanStore();
mockedCloudStore = vi.spyOn(cloudPlanStore, 'getAutoLoginCode');
mockedCloudStore.mockImplementationOnce(async () => ({
code: '123',
}));
global.window = Object.create(window);
const url = 'https://test.app.n8n.cloud';
Object.defineProperty(window, 'location', {
value: {
href: url,
},
writable: true,
});
}); });
test.each([ test.each([
['default', 'production', 'https://n8n.io/pricing/?ref=test_source'],
['default', 'development', 'https://n8n.io/pricing/?ref=test_source'],
[ [
'desktop_win', 'default',
'production', 'production',
'https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop&utm_campaign=utm-test-campaign', 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
],
[
'default',
'development',
'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
],
[
'cloud',
'production',
`https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent(
'/account/change-plan',
)}&utm_campaign=utm-test-campaign&source=test_source`,
], ],
['cloud', 'production', 'https://app.n8n.cloud/account/change-plan'],
])( ])(
'"upgradeLinkUrl" should generate the correct URL for "%s" deployment and "%s" license environment', '"upgradeLinkUrl" should generate the correct URL for "%s" deployment and "%s" license environment',
(type, environment, expectation) => { async (type, environment, expectation) => {
settingsStore.setSettings( settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, {
deployment: { deployment: {
@ -80,7 +107,9 @@ describe('UI store', () => {
}), }),
); );
expect(uiStore.upgradeLinkUrl('test_source', 'utm-test-campaign')).toBe(expectation); const updateLinkUrl = await uiStore.upgradeLinkUrl('test_source', 'utm-test-campaign', type);
expect(updateLinkUrl).toBe(expectation);
}, },
); );

View file

@ -5,7 +5,7 @@ import { useRootStore } from '@/stores/n8nRoot.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans'; import { getAdminPanelLoginCode, getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants'; import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants';
@ -71,6 +71,10 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
} }
}; };
const getAutoLoginCode = async (): Promise<{ code: string }> => {
return getAdminPanelLoginCode(rootStore.getRestApiContext);
};
const getOwnerCurrentPlan = async () => { const getOwnerCurrentPlan = async () => {
if (!hasCloudPlan.value) throw new Error('User does not have a cloud plan'); if (!hasCloudPlan.value) throw new Error('User does not have a cloud plan');
state.loadingPlan = true; state.loadingPlan = true;
@ -161,5 +165,6 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
reset, reset,
checkForCloudPlanData, checkForCloudPlanData,
fetchUserCloudAccount, fetchUserCloudAccount,
getAutoLoginCode,
}; };
}); });

View file

@ -35,6 +35,7 @@ import {
SOURCE_CONTROL_PUSH_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY,
SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PULL_MODAL_KEY,
DEBUG_PAYWALL_MODAL_KEY, DEBUG_PAYWALL_MODAL_KEY,
N8N_PRICING_PAGE_URL,
} from '@/constants'; } from '@/constants';
import type { import type {
CloudUpdateLinkSourceType, CloudUpdateLinkSourceType,
@ -56,8 +57,6 @@ import { getCurlToJson } from '@/api/curlHelper';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.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'; import { useTelemetryStore } from '@/stores/telemetry.store';
import { getStyleTokenValue } from '@/utils/htmlUtils'; import { getStyleTokenValue } from '@/utils/htmlUtils';
import { dismissBannerPermanently } from '@/api/ui'; import { dismissBannerPermanently } from '@/api/ui';
@ -214,7 +213,6 @@ export const useUIStore = defineStore(STORES.UI, {
} }
return { return {
upgradeLinkUrl: `contextual.upgradeLinkUrl${contextKey}`,
feature: { feature: {
unavailable: { unavailable: {
title: `contextual.feature.unavailable.title${contextKey}`, title: `contextual.feature.unavailable.title${contextKey}`,
@ -343,18 +341,29 @@ export const useUIStore = defineStore(STORES.UI, {
}; };
}, },
upgradeLinkUrl() { upgradeLinkUrl() {
return (source: string, utm_campaign: string): string => { return async (source: string, utm_campaign: string, deploymentType: string) => {
const linkUrlTranslationKey = this.contextBasedTranslationKeys let linkUrl = '';
.upgradeLinkUrl as BaseTextKey;
let linkUrl = locale.baseText(linkUrlTranslationKey);
if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) { const searchParams = new URLSearchParams();
linkUrl = `${linkUrl}?ref=${source}`;
} else if (linkUrlTranslationKey.endsWith('.desktop')) { if (deploymentType === 'cloud') {
linkUrl = `${linkUrl}&utm_campaign=${utm_campaign || source}`; const { code } = await useCloudPlanStore().getAutoLoginCode();
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
linkUrl = `https://${adminPanelHost}/login`;
searchParams.set('code', code);
searchParams.set('returnPath', '/account/change-plan');
} else {
linkUrl = N8N_PRICING_PAGE_URL;
} }
return linkUrl; if (utm_campaign) {
searchParams.set('utm_campaign', utm_campaign);
}
if (source) {
searchParams.set('source', source);
}
return `${linkUrl}?${searchParams.toString()}`;
}; };
}, },
headerHeight() { headerHeight() {
@ -542,25 +551,30 @@ export const useUIStore = defineStore(STORES.UI, {
const rootStore = useRootStore(); const rootStore = useRootStore();
return getCurlToJson(rootStore.getRestApiContext, curlCommand); return getCurlToJson(rootStore.getRestApiContext, curlCommand);
}, },
goToUpgrade( async goToUpgrade(
source: CloudUpdateLinkSourceType, source: CloudUpdateLinkSourceType,
utm_campaign: UTMCampaign, utm_campaign: UTMCampaign,
mode: 'open' | 'redirect' = 'open', mode: 'open' | 'redirect' = 'open',
): void { ): Promise<void> {
const { usageLeft, trialDaysLeft, userIsTrialing } = useCloudPlanStore(); const { usageLeft, trialDaysLeft, userIsTrialing } = useCloudPlanStore();
const { executionsLeft, workflowsLeft } = usageLeft; const { executionsLeft, workflowsLeft } = usageLeft;
const deploymentType = useSettingsStore().deploymentType;
useTelemetryStore().track('User clicked upgrade CTA', { useTelemetryStore().track('User clicked upgrade CTA', {
source, source,
isTrial: userIsTrialing, isTrial: userIsTrialing,
deploymentType: useSettingsStore().deploymentType, deploymentType,
trialDaysLeft, trialDaysLeft,
executionsLeft, executionsLeft,
workflowsLeft, workflowsLeft,
}); });
const upgradeLink = await this.upgradeLinkUrl(source, utm_campaign, deploymentType);
if (mode === 'open') { if (mode === 'open') {
window.open(this.upgradeLinkUrl(source, utm_campaign), '_blank'); window.open(upgradeLink, '_blank');
} else { } else {
location.href = this.upgradeLinkUrl(source, utm_campaign); location.href = upgradeLink;
} }
}, },
async dismissBanner( async dismissBanner(

View file

@ -142,7 +142,7 @@ export default defineComponent({
}, },
methods: { methods: {
onUpgrade() { onUpgrade() {
this.uiStore.goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect'); void this.uiStore.goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect');
}, },
async showDeleteModal() { async showDeleteModal() {
const confirmed = await this.confirm( const confirmed = await this.confirm(

View file

@ -7,7 +7,7 @@ const uiStore = useUIStore();
const auditLogsStore = useAuditLogsStore(); const auditLogsStore = useAuditLogsStore();
const goToUpgrade = () => { const goToUpgrade = () => {
uiStore.goToUpgrade('audit-logs', 'upgrade-audit-logs'); void uiStore.goToUpgrade('audit-logs', 'upgrade-audit-logs');
}; };
</script> </script>

View file

@ -214,11 +214,7 @@ export default defineComponent({
this.openInstallModal(); this.openInstallModal();
}, },
goToUpgrade(): void { goToUpgrade(): void {
const linkUrl = `${this.$locale.baseText( void this.uiStore.goToUpgrade('community-nodes', 'upgrade-community-nodes');
'contextual.upgradeLinkUrl.desktop',
)}&utm_campaign=upgrade-community-nodes&selfHosted=true`;
window.open(linkUrl, '_blank');
}, },
openInstallModal(): void { openInstallModal(): void {
const telemetryPayload = { const telemetryPayload = {

View file

@ -28,7 +28,7 @@ onMounted(() => {
}); });
function goToUpgrade() { function goToUpgrade() {
uiStore.goToUpgrade('external-secrets', 'upgrade-external-secrets'); void uiStore.goToUpgrade('external-secrets', 'upgrade-external-secrets');
} }
</script> </script>

View file

@ -222,7 +222,7 @@ export default defineComponent({
}, },
methods: { methods: {
goToUpgrade() { goToUpgrade() {
this.uiStore.goToUpgrade('ldap', 'upgrade-ldap'); void this.uiStore.goToUpgrade('ldap', 'upgrade-ldap');
}, },
cellClassStyle({ row, column }: CellStyle<TableRow>) { cellClassStyle({ row, column }: CellStyle<TableRow>) {
if (column.property === 'status') { if (column.property === 'status') {

View file

@ -188,7 +188,7 @@ export default defineComponent({
this.$forceUpdate(); this.$forceUpdate();
}, },
goToUpgrade() { goToUpgrade() {
this.uiStore.goToUpgrade('log-streaming', 'upgrade-log-streaming'); void this.uiStore.goToUpgrade('log-streaming', 'upgrade-log-streaming');
}, },
storeHasItems(): boolean { storeHasItems(): boolean {
return this.logStreamingStore.items && Object.keys(this.logStreamingStore.items).length > 0; return this.logStreamingStore.items && Object.keys(this.logStreamingStore.items).length > 0;

View file

@ -96,7 +96,7 @@ const onSelect = async (b: string) => {
}; };
const goToUpgrade = () => { const goToUpgrade = () => {
uiStore.goToUpgrade('source-control', 'upgrade-source-control'); void uiStore.goToUpgrade('source-control', 'upgrade-source-control');
}; };
const initialize = async () => { const initialize = async () => {

View file

@ -119,7 +119,7 @@ const onTest = async () => {
}; };
const goToUpgrade = () => { const goToUpgrade = () => {
uiStore.goToUpgrade('sso', 'upgrade-sso'); void uiStore.goToUpgrade('sso', 'upgrade-sso');
}; };
onMounted(async () => { onMounted(async () => {

View file

@ -103,7 +103,7 @@ const onAddActivationKey = () => {
}; };
const onViewPlans = () => { const onViewPlans = () => {
uiStore.goToUpgrade('usage_page', 'open'); void uiStore.goToUpgrade('usage_page', 'open');
sendUsageTelemetry('view_plans'); sendUsageTelemetry('view_plans');
}; };

View file

@ -214,7 +214,7 @@ export default defineComponent({
} }
}, },
goToUpgrade() { goToUpgrade() {
this.uiStore.goToUpgrade('settings-users', 'upgrade-users'); void this.uiStore.goToUpgrade('settings-users', 'upgrade-users');
}, },
}, },
}); });

View file

@ -204,7 +204,7 @@ async function deleteVariable(data: EnvironmentVariable) {
} }
function goToUpgrade() { function goToUpgrade() {
uiStore.goToUpgrade('variables', 'upgrade-variables'); void uiStore.goToUpgrade('variables', 'upgrade-variables');
} }
function displayName(resource: EnvironmentVariable) { function displayName(resource: EnvironmentVariable) {

View file

@ -156,7 +156,7 @@ const onPreview = async ({ event, id }: { event: MouseEvent; id: WorkflowVersion
}; };
const onUpgrade = () => { const onUpgrade = () => {
uiStore.goToUpgrade('workflow-history', 'upgrade-workflow-history'); void uiStore.goToUpgrade('workflow-history', 'upgrade-workflow-history');
}; };
watchEffect(async () => { watchEffect(async () => {