web/satellite: project limit increase request
create modal to allow pro users to request project limit increase when trying to create a project if they have reached the project limit. github issue: https://github.com/storj/storj/issues/6298 Change-Id: I1799028e742c55197fa5d944c242053cf4dc3a2c
This commit is contained in:
parent
c8e4f0099c
commit
1aadc0974d
@ -488,7 +488,7 @@ export class AuthHttpApi implements UsersApi {
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public async regenerateUserMFARecoveryCodes(passcode?: string, recoveryCode?:string): Promise<string[]> {
|
||||
public async regenerateUserMFARecoveryCodes(passcode?: string, recoveryCode?: string): Promise<string[]> {
|
||||
if (!passcode && !recoveryCode) {
|
||||
throw new Error('Either passcode or recovery code should be provided');
|
||||
}
|
||||
@ -585,4 +585,23 @@ export class AuthHttpApi implements UsersApi {
|
||||
requestID: response.headers.get('x-request-id'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to request increase for user's project limit.
|
||||
*
|
||||
* @param limit
|
||||
*/
|
||||
public async requestProjectLimitIncrease(limit: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/limit-increase`;
|
||||
const response = await this.http.patch(path, limit);
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json();
|
||||
throw new APIError({
|
||||
status: response.status,
|
||||
message: result.error,
|
||||
requestID: response.headers.get('x-request-id'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,8 +96,12 @@ export const useUsersStore = defineStore('users', () => {
|
||||
state.user = user;
|
||||
}
|
||||
|
||||
async function requestProjectLimitIncrease(limit: string): Promise<void> {
|
||||
await api.requestProjectLimitIncrease(limit);
|
||||
}
|
||||
|
||||
// Does nothing. It is called on login screen, and we just subscribe to this action in dashboard wrappers.
|
||||
function login(): void {}
|
||||
function login(): void { }
|
||||
|
||||
function clear() {
|
||||
state.user = new User();
|
||||
@ -122,5 +126,6 @@ export const useUsersStore = defineStore('users', () => {
|
||||
setUser,
|
||||
updateSettings,
|
||||
getSettings,
|
||||
requestProjectLimitIncrease,
|
||||
};
|
||||
});
|
||||
|
@ -76,6 +76,12 @@ export interface UsersApi {
|
||||
* @throws Error
|
||||
*/
|
||||
regenerateUserMFARecoveryCodes(passcode?: string, recoveryCode?: string): Promise<string[]>;
|
||||
/**
|
||||
* Request increase for user's project limit.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
requestProjectLimitIncrease(limit: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +110,7 @@ export class User {
|
||||
public _createdAt: string | null = null,
|
||||
public signupPromoCode: string = '',
|
||||
public freezeStatus: FreezeStatus = new FreezeStatus(),
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public get createdAt(): Date | null {
|
||||
if (!this._createdAt) {
|
||||
@ -129,7 +135,7 @@ export class UpdatedUser {
|
||||
public constructor(
|
||||
public fullName: string = '',
|
||||
public shortName: string = '',
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public setFullName(value: string): void {
|
||||
this.fullName = value.trim();
|
||||
@ -151,7 +157,7 @@ export class DisableMFARequest {
|
||||
public constructor(
|
||||
public passcode: string = '',
|
||||
public recoveryCode: string = '',
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,7 +167,7 @@ export class TokenInfo {
|
||||
public constructor(
|
||||
public token: string,
|
||||
public expiresAt: Date,
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -174,7 +180,7 @@ export class UserSettings {
|
||||
public onboardingEnd = false,
|
||||
public passphrasePrompt = true,
|
||||
public onboardingStep: string | null = null,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public get sessionDuration(): Duration | null {
|
||||
if (this._sessionDuration) {
|
||||
@ -199,5 +205,5 @@ export class FreezeStatus {
|
||||
public constructor(
|
||||
public frozen = false,
|
||||
public warned = false,
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
@ -12,14 +12,15 @@
|
||||
:scrim="false"
|
||||
@update:model-value="v => model = v"
|
||||
>
|
||||
<v-card rounded="xlg">
|
||||
<v-card ref="innerContent" rounded="xlg">
|
||||
<v-card-item class="pl-7 py-4">
|
||||
<template #prepend>
|
||||
<img class="d-block" src="@/../static/images/common/blueBox.svg" alt="Box">
|
||||
<img v-if="isProjectLimitReached && usersStore.state.user.paidTier && showLimitIncreaseDialog" class="d-block" src="@/../static/images/modals/limit.svg" alt="Speedometer">
|
||||
<img v-else class="d-block" src="@/../static/images/common/blueBox.svg" alt="Box">
|
||||
</template>
|
||||
|
||||
<v-card-title class="font-weight-bold">
|
||||
{{ isProjectLimitReached && billingEnabled ? 'Get More Projects' : 'Create New Project' }}
|
||||
{{ cardTitle }}
|
||||
</v-card-title>
|
||||
|
||||
<template #append>
|
||||
@ -38,10 +39,7 @@
|
||||
|
||||
<v-form v-model="formValid" class="pa-7" @submit.prevent>
|
||||
<v-row>
|
||||
<v-col v-if="isProjectLimitReached && billingEnabled">
|
||||
Upgrade to Pro Account to create more projects and gain access to higher limits.
|
||||
</v-col>
|
||||
<template v-else>
|
||||
<template v-if="!billingEnabled || !isProjectLimitReached">
|
||||
<v-col cols="12">
|
||||
Projects are where you and your team can upload and manage data, and view usage statistics and billing.
|
||||
</v-col>
|
||||
@ -81,6 +79,42 @@
|
||||
/>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else-if="isProjectLimitReached && usersStore.state.user.paidTier && !showLimitIncreaseDialog">
|
||||
<v-col cols="12">
|
||||
Request project limit increase.
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else-if="isProjectLimitReached && usersStore.state.user.paidTier && showLimitIncreaseDialog">
|
||||
<v-col cols="12">
|
||||
Request a projects limit increase for your account.
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<p>Projects Limit</p>
|
||||
<v-text-field
|
||||
class="edit-project-limit__text-field"
|
||||
variant="solo-filled"
|
||||
density="compact"
|
||||
flat
|
||||
readonly
|
||||
:model-value="usersStore.state.user.projectLimit"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<p>Requested Limit</p>
|
||||
<v-text-field
|
||||
class="edit-project-limit__text-field"
|
||||
density="compact"
|
||||
flat
|
||||
type="number"
|
||||
:rules="projectLimitRules"
|
||||
:model-value="inputText"
|
||||
@update:model-value="updateInputText"
|
||||
/>
|
||||
</v-col>
|
||||
</template>
|
||||
<v-col v-else>
|
||||
Upgrade to Pro Account to create more projects and gain access to higher limits.
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
@ -102,7 +136,7 @@
|
||||
:append-icon="isProjectLimitReached && billingEnabled ? 'mdi-arrow-right' : undefined"
|
||||
@click="onPrimaryClick"
|
||||
>
|
||||
{{ isProjectLimitReached && billingEnabled ? 'Upgrade' : 'Create Project' }}
|
||||
{{ buttonTitle }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -118,7 +152,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Component, ref, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
VDialog,
|
||||
@ -169,12 +203,15 @@ const { isLoading, withLoading } = useLoading();
|
||||
const notify = useNotify();
|
||||
const router = useRouter();
|
||||
|
||||
const innerContent = ref<Component | null>(null);
|
||||
const formValid = ref<boolean>(false);
|
||||
const inputText = ref<string>('');
|
||||
const name = ref<string>('');
|
||||
const description = ref<string>('');
|
||||
const isDescriptionShown = ref<boolean>(false);
|
||||
const isProjectLimitReached = ref<boolean>(false);
|
||||
const isUpgradeDialogShown = ref<boolean>(false);
|
||||
const showLimitIncreaseDialog = ref<boolean>(false);
|
||||
|
||||
const nameRules: ValidationRule<string>[] = [
|
||||
RequiredRule,
|
||||
@ -190,20 +227,11 @@ const descriptionRules: ValidationRule<string>[] = [
|
||||
*/
|
||||
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
|
||||
|
||||
function startUpgradeFlow(): void {
|
||||
model.value = false;
|
||||
appStore.toggleUpgradeFlow(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles primary button click.
|
||||
*/
|
||||
async function onPrimaryClick(): Promise<void> {
|
||||
if (isProjectLimitReached.value && billingEnabled.value) {
|
||||
isUpgradeDialogShown.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isProjectLimitReached.value || !billingEnabled.value) {
|
||||
if (!formValid.value) return;
|
||||
await withLoading(async () => {
|
||||
let project: Project;
|
||||
@ -219,13 +247,79 @@ async function onPrimaryClick(): Promise<void> {
|
||||
router.push(`/projects/${project.urlId}/dashboard`);
|
||||
notify.success('Project created.');
|
||||
});
|
||||
} else if (usersStore.state.user.paidTier) {
|
||||
if (showLimitIncreaseDialog.value) {
|
||||
if (!formValid.value) return;
|
||||
await withLoading(async () => {
|
||||
try {
|
||||
await usersStore.requestProjectLimitIncrease(inputText.value);
|
||||
} catch (error) {
|
||||
error.message = `Failed to request project limit increase. ${error.message}`;
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.CREATE_PROJECT_MODAL);
|
||||
return;
|
||||
}
|
||||
model.value = false;
|
||||
notify.success('Project limit increase requested');
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
showLimitIncreaseDialog.value = true;
|
||||
}
|
||||
} else {
|
||||
isUpgradeDialogShown.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => model.value, shown => {
|
||||
if (!shown) return;
|
||||
/*
|
||||
* Returns an array of validation rules applied to the text input.
|
||||
*/
|
||||
const projectLimitRules = computed<ValidationRule<string>[]>(() => {
|
||||
return [
|
||||
RequiredRule,
|
||||
v => !(isNaN(+v) || !Number.isInteger((parseFloat(v)))) || 'Invalid number',
|
||||
v => (parseFloat(v) > 0) || 'Number must be positive',
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates input refs with value from text field.
|
||||
*/
|
||||
function updateInputText(value: string): void {
|
||||
inputText.value = value;
|
||||
}
|
||||
|
||||
const buttonTitle = computed((): string => {
|
||||
if (!isProjectLimitReached.value || !billingEnabled.value) {
|
||||
return 'Create Project';
|
||||
}
|
||||
if (usersStore.state.user.paidTier) {
|
||||
if (showLimitIncreaseDialog.value) {
|
||||
return 'Submit';
|
||||
}
|
||||
return 'Request';
|
||||
}
|
||||
return 'Upgrade';
|
||||
});
|
||||
|
||||
const cardTitle = computed((): string => {
|
||||
if (!isProjectLimitReached.value || !billingEnabled.value) {
|
||||
return 'Create New Project';
|
||||
}
|
||||
if (usersStore.state.user.paidTier && showLimitIncreaseDialog.value) {
|
||||
return 'Projects Limit Request';
|
||||
}
|
||||
return 'Get More Projects';
|
||||
});
|
||||
|
||||
watch(innerContent, comp => {
|
||||
if (comp) {
|
||||
isProjectLimitReached.value = projectsStore.state.projects.length >= usersStore.state.user.projectLimit;
|
||||
isDescriptionShown.value = false;
|
||||
name.value = '';
|
||||
description.value = '';
|
||||
inputText.value = String(usersStore.state.user.projectLimit + 1);
|
||||
} else {
|
||||
showLimitIncreaseDialog.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user