web/satellite/vuetify-poc: add account setup dialog

This change copies the account setup dialog from the static v2 repo and adds functionality to it

Issue: #6481

Change-Id: I0cacec3edf054c1945df7593b73e3c0f0f96677c
This commit is contained in:
Wilfred Asomani 2023-11-07 11:23:33 +00:00 committed by Storj Robot
parent ea022ede46
commit 50c9f4c85a
12 changed files with 593 additions and 0 deletions

View File

@ -5,6 +5,7 @@ import { ErrorBadRequest } from '@/api/errors/ErrorBadRequest';
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
import { ErrorTooManyRequests } from '@/api/errors/ErrorTooManyRequests';
import {
AccountSetupData,
FreezeStatus,
SetUserSettingsData,
TokenInfo,
@ -210,6 +211,26 @@ export class AuthHttpApi implements UsersApi {
});
}
/**
* Used to update user details after signup.
*
* @param userInfo - the information to be added to account.
* @throws Error
*/
public async setupAccount(userInfo: AccountSetupData): Promise<void> {
const path = `${this.ROOT_PATH}/account/setup`;
const response = await this.http.patch(path, JSON.stringify(userInfo));
if (response.ok) {
return;
}
throw new APIError({
status: response.status,
message: 'Can not setup account',
requestID: response.headers.get('x-request-id'),
});
}
/**
* Used to get user data.
*

View File

@ -150,6 +150,18 @@ export class UpdatedUser {
}
}
/**
* Describes data used to set up user account.
*/
export interface AccountSetupData {
fullName: string
isProfessional: boolean
position?: string
companyName?: string
employeeCount?: string
storageNeeds?: string
}
/**
* DisableMFARequest represents a request to disable multi-factor authentication.
*/
@ -207,3 +219,13 @@ export class FreezeStatus {
public warned = false,
) { }
}
/**
* AccountSetupStep are the steps in the account setup dialog.
*/
export enum AccountSetupStep {
Choice,
Personal,
Business,
Success,
}

View File

@ -64,6 +64,7 @@ export enum AnalyticsErrorEventSource {
ACCESS_GRANTS_PAGE = 'Access grants page',
ACCOUNT_PAGE = 'Account page',
ACCOUNT_SETTINGS_AREA = 'Account settings area',
ACCOUNT_SETUP_DIALOG = 'Account setup dialog',
BILLING_HISTORY_TAB = 'Billing history tab',
BILLING_COUPONS_TAB = 'Billing coupons tab',
BILLING_OVERVIEW_TAB = 'Billing overview tab',

View File

@ -0,0 +1,62 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog :model-value="shouldSetup" height="87%" width="87%" persistent transition="fade-transition" scrollable>
<v-card ref="innerContent">
<!-- Choice step -->
<choice-step v-if="step === AccountSetupStep.Choice" @next="(s) => step = s" />
<!-- Business step -->
<business-step v-if="step === AccountSetupStep.Business" @next="(s) => step = s" />
<!-- Personal step -->
<personal-step v-if="step === AccountSetupStep.Personal" @next="(s) => step = s" />
<!-- Final step -->
<success-step v-if="step === AccountSetupStep.Success" />
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { Component, computed, ref, watch } from 'vue';
import { VCard, VDialog } from 'vuetify/components';
import { useNotify } from '@/utils/hooks';
import { useUsersStore } from '@/store/modules/usersStore';
import { AccountSetupStep } from '@/types/users';
import ChoiceStep from '@poc/components/dialogs/accountSetupSteps/ChoiceStep.vue';
import BusinessStep from '@poc/components/dialogs/accountSetupSteps/BusinessStep.vue';
import PersonalStep from '@poc/components/dialogs/accountSetupSteps/PersonalStep.vue';
import SuccessStep from '@poc/components/dialogs/accountSetupSteps/SuccessStep.vue';
const userStore = useUsersStore();
const notify = useNotify();
const innerContent = ref<Component | null>(null);
const step = ref<AccountSetupStep>(AccountSetupStep.Choice);
const shouldSetup = computed(() => {
// settings are fetched on the projects page.
const onboardingEnd = userStore.state.settings.onboardingEnd;
if (onboardingEnd) {
return false;
}
if (!userStore.state.user.email) {
// user has not been fetched yet.
return false;
}
// If the user has a name, they've already completed the account setup.
return !userStore.userName;
});
watch(innerContent, comp => {
if (comp) return;
step.value = AccountSetupStep.Choice;
});
</script>

View File

@ -0,0 +1,183 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-container>
<v-row justify="center">
<v-col class="text-center pt-10 pb-7">
<icon-business />
<div class="text-overline mt-2 mb-1">
Business Account
</div>
<h2 class="pb-3">Experience Storj for your business</h2>
<p>Tell us a bit about yourself and the company.</p>
</v-col>
</v-row>
<v-form v-model="formValid">
<v-row justify="center">
<v-col cols="12" sm="5" md="4" lg="3">
<v-text-field
v-model="firstName"
:rules="[RequiredRule]"
label="First Name"
hide-details="auto"
required
/>
</v-col>
<v-col cols="12" sm="5" md="4" lg="3">
<v-text-field
v-model="lastName"
label="Last Name"
hide-details="auto"
/>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12" sm="5" md="4" lg="3">
<v-text-field
v-model="companyName"
:rules="[RequiredRule]"
label="Company Name"
hide-details="auto"
required
/>
</v-col>
<v-col cols="12" sm="5" md="4" lg="3">
<v-text-field
v-model="position"
:rules="[RequiredRule]"
label="Job Role"
hide-details="auto"
required
/>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12" sm="5" md="4" lg="3">
<v-select
v-model="functionalArea"
:items="['Cloud Storage', 'IT Security', 'IT Operations', 'Business', 'Other']"
label="Functional Area"
variant="outlined"
hide-details="auto"
/>
</v-col>
<v-col cols="12" sm="5" md="4" lg="3">
<v-select
v-model="employeeCount"
:items="['1-50', '50-1000', '1000+']"
label="Number of Employees"
variant="outlined"
hide-details="auto"
/>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12" sm="5" md="4" lg="3">
<v-select
v-model="storageNeeds"
:items="['Less than 150TB', '150-499TB', '500-999TB', '1PB+']"
label="Storage Needs"
variant="outlined"
hide-details="auto"
/>
</v-col>
<v-col cols="12" sm="5" md="4" lg="3">
<v-select
v-model="useCase"
:items="['Video Streaming', 'Media Sharing & Collaboration', 'Large File Distribution', 'Backup/Archive', 'Web3 Storage', 'Other']"
label="Use Case"
variant="outlined"
hide-details="auto"
/>
</v-col>
</v-row>
</v-form>
<v-row justify="center" class="mt-4">
<v-col cols="12" sm="5" md="4" lg="3">
<v-btn
size="large"
variant="tonal"
prepend-icon="mdi-chevron-left"
color="default"
:disabled="isLoading"
block
@click="emit('next', AccountSetupStep.Choice)"
>
Back
</v-btn>
</v-col>
<v-col cols="12" sm="5" md="4" lg="3">
<v-btn
size="large"
append-icon="mdi-chevron-right"
:loading="isLoading"
:disabled="!formValid"
block
@click="setupAccount()"
>
Continue
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { VBtn, VCol, VContainer, VForm, VRow, VSelect, VTextField } from 'vuetify/components';
import { ref } from 'vue';
import { AccountSetupStep } from '@/types/users';
import { AuthHttpApi } from '@/api/auth';
import { useNotify } from '@/utils/hooks';
import { useLoading } from '@/composables/useLoading';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { RequiredRule } from '@poc/types/common';
import IconBusiness from '@poc/components/icons/IconBusiness.vue';
const auth = new AuthHttpApi();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const formValid = ref(false);
const firstName = ref('');
const lastName = ref('');
const companyName = ref('');
const position = ref('');
const employeeCount = ref<string>();
const storageNeeds = ref<string>();
const useCase = ref<string>();
const functionalArea = ref<string>();
const emit = defineEmits<{
next: [AccountSetupStep];
}>();
function setupAccount() {
withLoading(async () => {
if (!formValid.value) {
return;
}
try {
await auth.setupAccount({
fullName: `${firstName.value} ${lastName.value}`,
position: position.value,
employeeCount: employeeCount.value,
storageNeeds: storageNeeds.value,
isProfessional: true,
});
emit('next', AccountSetupStep.Success);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.ACCOUNT_SETUP_DIALOG);
}
});
}
</script>

View File

@ -0,0 +1,76 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-container>
<v-row justify="center">
<v-col class="text-center pt-5 pt-md-10 pb-md-7">
<icon-storj-logo />
<p class="text-overline mt-2 mb-1">
Welcome to Storj
</p>
<h2 class="pb-3">Start by setting up your account</h2>
<p>Select your account type to customize your Storj experience.</p>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12" sm="6" lg="4">
<v-card border class="px-3 py-5" @click="emit('next', AccountSetupStep.Personal)">
<v-card-item>
<div>
<icon-personal />
<p class="text-overline mt-2 mb-1">
Personal
</p>
<p class="text-h6 mb-1">
I'm using Storj for myself.
</p>
<p class="text-caption">Securely store, backup, share, stream, and collaborate on files and media from any device.</p>
</div>
</v-card-item>
<v-card-item>
<v-btn append-icon="mdi-chevron-right">Continue</v-btn>
</v-card-item>
</v-card>
</v-col>
<v-col cols="12" sm="6" lg="4">
<v-card border class="px-3 py-5" @click="emit('next', AccountSetupStep.Business)">
<v-card-item>
<div>
<icon-business />
<p class="text-overline mt-2 mb-1">
Business
</p>
<p class="text-h6 mb-1">
I'm using Storj for business.
</p>
<p class="text-caption">Save your company 80% or more on cloud storage costs with better security and performance.</p>
</div>
</v-card-item>
<v-card-item>
<v-btn append-icon="mdi-chevron-right">Continue</v-btn>
</v-card-item>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { VBtn, VCard, VCardItem, VCol, VContainer, VRow } from 'vuetify/components';
import { AccountSetupStep } from '@/types/users';
import IconPersonal from '@poc/components/icons/IconPersonal.vue';
import IconBusiness from '@poc/components/icons/IconBusiness.vue';
import IconStorjLogo from '@poc/components/icons/IconStorjLogo.vue';
const emit = defineEmits<{
next: [AccountSetupStep];
}>();
</script>

View File

@ -0,0 +1,115 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-container>
<v-row justify="center">
<v-col class="text-center pt-10 pb-4">
<icon-personal />
<p class="text-overline mt-2 mb-1">
Personal Account
</p>
<h2 class="pb-3">Great, almost there.</h2>
<p>Please complete your account information.</p>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12" sm="8" md="6" lg="4">
<v-form v-model="formValid">
<v-text-field
v-model="name"
:rules="[RequiredRule]"
label="Name"
placeholder="Enter your name"
required
/>
<v-select
v-model="useCase"
:items="['Backup & Recovery', 'Media & Entertainment', 'Generative AI', 'Other']"
label="Use Case (optional)"
placeholder="Select your use case"
variant="outlined"
/>
</v-form>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="6" sm="4" md="3" lg="2">
<v-btn
size="large"
variant="tonal"
prepend-icon="mdi-chevron-left"
color="default"
:disabled="isLoading"
block
@click="emit('next', AccountSetupStep.Choice)"
>
Back
</v-btn>
</v-col>
<v-col cols="6" sm="4" md="3" lg="2">
<v-btn
size="large"
append-icon="mdi-chevron-right"
:loading="isLoading"
:disabled="!formValid"
block
@click="setupAccount()"
>
Continue
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { VBtn, VCol, VContainer, VForm, VRow, VSelect, VTextField } from 'vuetify/components';
import { ref } from 'vue';
import { AccountSetupStep } from '@/types/users';
import { RequiredRule } from '@poc/types/common';
import { useLoading } from '@/composables/useLoading';
import { AuthHttpApi } from '@/api/auth';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import IconPersonal from '@poc/components/icons/IconPersonal.vue';
const auth = new AuthHttpApi();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const formValid = ref(false);
const name = ref('');
const useCase = ref<string>();
const emit = defineEmits<{
next: [AccountSetupStep];
}>();
function setupAccount() {
withLoading(async () => {
if (!formValid.value) {
return;
}
try {
await auth.setupAccount({
fullName: name.value,
workingOn: useCase.value,
isProfessional: false,
});
emit('next', AccountSetupStep.Success);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.ACCOUNT_SETUP_DIALOG);
}
});
}
</script>

View File

@ -0,0 +1,54 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-container class="fill-height" fluid>
<v-row justify="center" align="center">
<v-col class="text-center py-10">
<icon-blue-checkmark />
<p class="text-overline mt-4 mb-2">
Account Complete
</p>
<h2 class="mb-3">You are now ready to use Storj</h2>
<p>Create your first project, and upload files to share with the world.</p>
<p>Let us know if you need any help getting started!</p>
<v-btn
class="mt-7"
size="large"
append-icon="mdi-chevron-right"
:loading="isLoading"
@click="finishSetup()"
>
Continue
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { VBtn, VCol, VContainer, VRow } from 'vuetify/components';
import { useUsersStore } from '@/store/modules/usersStore';
import { useNotify } from '@/utils/hooks';
import { useLoading } from '@/composables/useLoading';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import IconBlueCheckmark from '@poc/components/icons/IconBlueCheckmark.vue';
const userStore = useUsersStore();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
function finishSetup() {
withLoading(async () => {
try {
await userStore.getUser();
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.ACCOUNT_SETUP_DIALOG);
}
});
}
</script>

View File

@ -0,0 +1,22 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg :width="size" :height="size" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.1058 0H58.3658C72.2088 0 77.7012 1.53431 83.0884 4.41541C88.4756 7.29652 92.7035 11.5244 95.5846 16.9116L95.8036 17.3264C98.5073 22.5168 99.9614 27.9999 100 41.1058V58.3658C100 72.2087 98.4657 77.701 95.5846 83.0882C92.7035 88.4754 88.4756 92.7033 83.0884 95.5844L82.6736 95.8034C77.4832 98.5072 72.0001 99.9613 58.8942 99.9999H41.6342C27.7912 99.9999 22.2988 98.4656 16.9116 95.5844C11.5244 92.7033 7.29653 88.4754 4.41542 83.0882L4.19645 82.6735C1.49268 77.4831 0.0385546 72 0 58.8941V41.6341C0 27.7912 1.53431 22.2988 4.41542 16.9116C7.29653 11.5244 11.5244 7.29652 16.9116 4.41541L17.3264 4.19645C22.5168 1.49268 27.9999 0.0385546 41.1058 0Z" fill="#0218A7" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M78.0316 76.6013C84.6558 76.6013 90.8697 78.3506 96.2329 81.4106C95.9652 81.9772 95.6827 82.5332 95.3859 83.0882C92.5048 88.4755 88.2769 92.7034 82.8897 95.5846L82.4749 95.8035C77.3083 98.495 71.8516 99.9481 58.8748 99.9994L43.8017 99.9999C49.0929 86.3133 62.4234 76.6013 78.0316 76.6013Z" fill="#003DC1" />
<path d="M78.0991 70.1861C86.1203 70.1861 92.6227 63.7108 92.6227 55.7232C92.6227 47.7355 86.1203 41.2603 78.0991 41.2603C70.078 41.2603 63.5756 47.7355 63.5756 55.7232C63.5756 63.7108 70.078 70.1861 78.0991 70.1861Z" fill="#0149FF" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.6346 76.6013C37.2429 76.6013 50.5734 86.3132 55.8646 99.9997L41.4355 99.9999C27.5925 99.9999 22.1002 98.4656 16.7129 95.5844C11.3257 92.7033 7.09783 88.4754 4.21672 83.0882L3.99776 82.6734C3.78515 82.2653 3.58026 81.8553 3.38318 81.44C8.75757 78.3616 14.9897 76.6013 21.6346 76.6013Z" fill="#003DC1" />
<path d="M21.7021 70.1861C29.7232 70.1861 36.2256 63.7108 36.2256 55.7232C36.2256 47.7355 29.7232 41.2603 21.7021 41.2603C13.681 41.2603 7.17865 47.7355 7.17865 55.7232C7.17865 63.7108 13.681 70.1861 21.7021 70.1861Z" fill="#0149FF" />
<path d="M50.0434 70.1859C59.8806 70.1859 67.8553 62.2445 67.8553 52.4482C67.8553 42.6519 59.8806 34.7104 50.0434 34.7104C40.2061 34.7104 32.2314 42.6519 32.2314 52.4482C32.2314 62.2445 40.2061 70.1859 50.0434 70.1859Z" fill="#FFC600" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.9324 76.6013C63.9338 76.6013 76.1022 84.4164 82.2788 95.9052L82.4749 95.8034C77.3082 98.4948 71.8515 99.948 58.8748 99.9993L41.4354 99.9999C28.2911 99.9999 22.6758 98.6165 17.5304 96.0103C23.6884 84.4642 35.8883 76.6013 49.9324 76.6013Z" fill="#7B61FF" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
size: number | string;
}>(), {
size: 50,
});
</script>

View File

@ -0,0 +1,18 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg :width="size" :height="size" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.1058 0H58.3658C72.2088 0 77.7012 1.53431 83.0884 4.41541C88.4756 7.29652 92.7035 11.5244 95.5846 16.9116L95.8036 17.3264C98.5073 22.5168 99.9614 27.9999 100 41.1058V58.3658C100 72.2087 98.4657 77.701 95.5846 83.0882C92.7035 88.4754 88.4756 92.7033 83.0884 95.5844L82.6736 95.8034C77.4832 98.5072 72.0001 99.9613 58.8942 99.9999H41.6342C27.7912 99.9999 22.2988 98.4656 16.9116 95.5844C11.5244 92.7033 7.29653 88.4754 4.41542 83.0882L4.19645 82.6735C1.49268 77.4831 0.0385546 72 0 58.8941V41.6341C0 27.7912 1.53431 22.2988 4.41542 16.9116C7.29653 11.5244 11.5244 7.29652 16.9116 4.41541L17.3264 4.19645C22.5168 1.49268 27.9999 0.0385546 41.1058 0Z" fill="#0218A7" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.9324 76.6013C63.9338 76.6013 76.1022 84.4164 82.2788 95.9052L82.4749 95.8034C77.3082 98.4948 71.8515 99.948 58.8748 99.9993L41.4354 99.9999C28.2911 99.9999 22.6758 98.6165 17.5304 96.0103C23.6884 84.4642 35.8883 76.6013 49.9324 76.6013Z" fill="#7B61FF" />
<path d="M50.0434 70.1859C59.8806 70.1859 67.8553 62.2445 67.8553 52.4482C67.8553 42.6519 59.8806 34.7104 50.0434 34.7104C40.2061 34.7104 32.2314 42.6519 32.2314 52.4482C32.2314 62.2445 40.2061 70.1859 50.0434 70.1859Z" fill="#FFC600" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
size: number | string;
}>(), {
size: 50,
});
</script>

File diff suppressed because one or more lines are too long

View File

@ -97,6 +97,7 @@
/>
<create-project-dialog v-model="isCreateProjectDialogShown" />
<add-team-member-dialog v-model="isAddMemberDialogShown" :project-id="addMemberProjectId" />
<account-setup-dialog />
</template>
<script setup lang="ts">
@ -131,6 +132,7 @@ import AddTeamMemberDialog from '@poc/components/dialogs/AddTeamMemberDialog.vue
import IconCardView from '@poc/components/icons/IconCardView.vue';
import IconTableView from '@poc/components/icons/IconTableView.vue';
import LowTokenBalanceBanner from '@poc/components/LowTokenBalanceBanner.vue';
import AccountSetupDialog from '@poc/components/dialogs/AccountSetupDialog.vue';
const appStore = useAppStore();
const projectsStore = useProjectsStore();