web/satellite{/vuetify-poc}: show upgrade dialog when trying to invite

This change displays a dialog prompting free tier users to upgrade when
the button to invite project members is clicked.

Also, the Create New Project dialog in the Vuetify UI now opens the
upgrade dialog when its Upgrade button is clicked.

Change-Id: I6e233bd15fd14a486a3e9008bbc6fba3e669d67e
This commit is contained in:
Jeremy Wharton 2023-10-17 03:34:07 -05:00
parent 4721d2bd4e
commit 24ae79345b
13 changed files with 191 additions and 77 deletions

View File

@ -7,14 +7,22 @@
<div class="modal">
<div class="modal__header">
<TeamMembersIcon />
<h1 class="modal__header__title">Invite team member</h1>
<h1 class="modal__header__title">
{{ isPaidTier ? 'Invite team member' : 'Upgrade to Pro' }}
</h1>
</div>
<p class="modal__info">
Add a team member to contribute to this project.
<template v-if="isPaidTier">
Add a team member to contribute to this project.
</template>
<template v-else>
Upgrade now to unlock collaboration and bring your team together in this project.
</template>
</p>
<VInput
v-if="isPaidTier"
class="modal__input"
label="Email"
height="38px"
@ -35,13 +43,17 @@
:on-press="closeModal"
/>
<VButton
label="Invite"
:label="isPaidTier ? 'Invite' : 'Upgrade'"
height="48px"
font-size="14px"
border-radius="10px"
:on-press="onInviteClick"
:on-press="onPrimaryClick"
:is-disabled="!!formError || isLoading"
/>
>
<template v-if="!isPaidTier" #icon-right>
<ArrowIcon />
</template>
</VButton>
</div>
</div>
</template>
@ -60,12 +72,14 @@ import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useLoading } from '@/composables/useLoading';
import { MODALS } from '@/utils/constants/appStatePopUps';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
import VInput from '@/components/common/VInput.vue';
import TeamMembersIcon from '@/../static/images/team/teamMembers.svg';
import ArrowIcon from '@/../static/images/onboardingTour/arrowRight.svg';
const analyticsStore = useAnalyticsStore();
const appStore = useAppStore();
@ -84,6 +98,7 @@ const email = ref<string>('');
* or a message describing the validation error.
*/
const formError = computed<string | boolean>(() => {
if (!isPaidTier.value) return false;
if (!email.value) return true;
if (email.value.toLocaleLowerCase() === usersStore.state.user.email.toLowerCase()) {
return `You can't add yourself to the project.`;
@ -95,9 +110,21 @@ const formError = computed<string | boolean>(() => {
});
/**
* Tries to add the user with the input email to the current project.
* Returns user's paid tier status from store.
*/
async function onInviteClick(): Promise<void> {
const isPaidTier = computed<boolean>(() => {
return usersStore.state.user.paidTier;
});
/**
* Handles primary button click.
*/
async function onPrimaryClick(): Promise<void> {
if (!isPaidTier.value) {
appStore.updateActiveModal(MODALS.upgradeAccount);
return;
}
await withLoading(async () => {
try {
await pmStore.inviteMember(email.value, projectsStore.state.selectedProject.id);

View File

@ -37,7 +37,7 @@
@update:itemsPerPage="onLimitChange"
>
<template #item="{ props: rowProps }">
<v-data-table-row class="pos-relative" v-bind="rowProps">
<v-data-table-row v-bind="rowProps">
<template #item.name="{ item }: ItemSlotProps">
<v-btn
class="rounded-lg w-100 px-1 justify-start font-weight-bold"

View File

@ -3,61 +3,68 @@
<template>
<v-dialog
v-model="model"
:model-value="model && !isUpgradeDialogShown"
width="auto"
max-width="420px"
transition="fade-transition"
:persistent="isLoading"
:scrim="false"
@update:model-value="v => model = v"
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-card-title class="font-weight-bold">
Add Member
</v-card-title>
</template>
<v-card-item class="pl-7 py-4">
<template #prepend>
<img class="d-block" src="@/../static/images/team/teamMembers.svg" alt="Team members">
</template>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
</v-sheet>
<v-card-title class="font-weight-bold">
{{ isPaidTier ? 'Add Member' : 'Upgrade to Pro' }}
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
<v-divider />
<v-form v-model="valid" class="pa-7 pb-4" @submit.prevent="onInviteClick">
<v-form v-model="valid" class="pa-7 pb-4" @submit.prevent="onPrimaryClick">
<v-row>
<v-col cols="12">
<p class="mb-5">Invite a team member to join you in this project.</p>
<v-alert
variant="tonal"
color="info"
title="Important Information"
text="All team members should use the same passphrase to access the same data."
rounded="lg"
density="comfortable"
border
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="email"
variant="outlined"
:rules="emailRules"
label="Enter e-mail"
hint="Members will have read & write permissions."
required
autofocus
class="my-2"
/>
<template v-if="isPaidTier">
<v-col cols="12">
<p class="mb-5">Invite a team member to join you in this project.</p>
<v-alert
variant="tonal"
color="info"
title="Important Information"
text="All team members should use the same passphrase to access the same data."
rounded="lg"
density="comfortable"
border
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="email"
variant="outlined"
:rules="emailRules"
label="Enter e-mail"
hint="Members will have read & write permissions."
required
autofocus
class="my-2"
/>
</v-col>
</template>
<v-col v-else>
Upgrade now to unlock collaboration and bring your team together in this project.
</v-col>
</v-row>
</v-form>
@ -70,12 +77,33 @@
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">Cancel</v-btn>
</v-col>
<v-col>
<v-btn color="primary" variant="flat" block :loading="isLoading" @click="onInviteClick">Send Invite</v-btn>
<v-btn
color="primary"
variant="flat"
block
:loading="isLoading"
:append-icon="!isPaidTier ? 'mdi-arrow-right' : undefined"
@click="onPrimaryClick"
>
{{ isPaidTier ? 'Send Invite' : 'Upgrade' }}
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
<upgrade-account-dialog
:scrim="false"
:model-value="model && isUpgradeDialogShown"
@update:model-value="v => model = isUpgradeDialogShown = v"
/>
<teleport to="body">
<v-fade-transition>
<div v-show="model" class="v-overlay__scrim custom-scrim" />
</v-fade-transition>
</teleport>
</template>
<script setup lang="ts">
@ -83,7 +111,6 @@ import { computed, ref } from 'vue';
import {
VDialog,
VCard,
VSheet,
VCardItem,
VCardTitle,
VBtn,
@ -94,6 +121,7 @@ import {
VAlert,
VTextField,
VCardActions,
VFadeTransition,
} from 'vuetify/components';
import { RequiredRule, ValidationRule } from '@poc/types/common';
@ -102,14 +130,17 @@ import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useNotify } from '@/utils/hooks';
import { useLoading } from '@/composables/useLoading';
import { useUsersStore } from '@/store/modules/usersStore';
import UpgradeAccountDialog from '@poc/components/dialogs/upgradeAccountFlow/UpgradeAccountDialog.vue';
const props = defineProps<{
modelValue: boolean,
projectId: string,
modelValue: boolean;
projectId: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean],
'update:modelValue': [value: boolean];
}>();
const model = computed<boolean>({
@ -117,6 +148,7 @@ const model = computed<boolean>({
set: value => emit('update:modelValue', value),
});
const usersStore = useUsersStore();
const analyticsStore = useAnalyticsStore();
const pmStore = useProjectMembersStore();
const notify = useNotify();
@ -124,6 +156,7 @@ const { isLoading, withLoading } = useLoading();
const valid = ref<boolean>(false);
const email = ref<string>('');
const isUpgradeDialogShown = ref<boolean>(false);
const emailRules: ValidationRule<string>[] = [
RequiredRule,
@ -131,9 +164,21 @@ const emailRules: ValidationRule<string>[] = [
];
/**
* Sends a project invitation to the input email.
* Returns user's paid tier status from store.
*/
async function onInviteClick(): Promise<void> {
const isPaidTier = computed<boolean>(() => {
return usersStore.state.user.paidTier;
});
/**
* Handles primary button click.
*/
async function onPrimaryClick(): Promise<void> {
if (!isPaidTier.value) {
isUpgradeDialogShown.value = true;
return;
}
if (!valid.value) return;
await withLoading(async () => {

View File

@ -3,10 +3,12 @@
<template>
<v-dialog
v-model="model"
:model-value="model && !isUpgradeDialogShown"
width="410px"
transition="fade-transition"
:persistent="isLoading"
:scrim="false"
@update:model-value="v => model = v"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4">
@ -95,7 +97,8 @@
variant="flat"
:loading="isLoading"
block
@click="() => !isProjectLimitReached && onCreateClicked()"
:append-icon="isProjectLimitReached ? 'mdi-arrow-right' : undefined"
@click="onPrimaryClick"
>
{{ !isProjectLimitReached ? 'Create Project' : 'Upgrade' }}
</v-btn>
@ -104,10 +107,22 @@
</v-card-actions>
</v-card>
</v-dialog>
<upgrade-account-dialog
:scrim="false"
:model-value="model && isUpgradeDialogShown"
@update:model-value="v => model = isUpgradeDialogShown = v"
/>
<teleport to="body">
<v-fade-transition>
<div v-show="model" class="v-overlay__scrim custom-scrim" />
</v-fade-transition>
</teleport>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, Teleport } from 'vue';
import { useRouter } from 'vue-router';
import {
VDialog,
@ -121,6 +136,7 @@ import {
VRow,
VCol,
VTextField,
VFadeTransition,
} from 'vuetify/components';
import { RequiredRule, ValidationRule } from '@poc/types/common';
@ -131,6 +147,8 @@ import { useUsersStore } from '@/store/modules/usersStore';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import UpgradeAccountDialog from '@poc/components/dialogs/upgradeAccountFlow/UpgradeAccountDialog.vue';
const props = defineProps<{
modelValue: boolean,
}>();
@ -155,6 +173,7 @@ const name = ref<string>('');
const description = ref<string>('');
const isDescriptionShown = ref<boolean>(false);
const isProjectLimitReached = ref<boolean>(false);
const isUpgradeDialogShown = ref<boolean>(false);
const nameRules: ValidationRule<string>[] = [
RequiredRule,
@ -166,9 +185,14 @@ const descriptionRules: ValidationRule<string>[] = [
];
/**
* Creates new project.
* Handles primary button click.
*/
async function onCreateClicked(): Promise<void> {
async function onPrimaryClick(): Promise<void> {
if (isProjectLimitReached.value) {
isUpgradeDialogShown.value = true;
return;
}
if (!formValid.value) return;
await withLoading(async () => {
let project: Project;

View File

@ -9,7 +9,7 @@
:persistent="isLoading"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4 pos-relative">
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"

View File

@ -8,7 +8,7 @@
transition="fade-transition"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4 pos-relative">
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"

View File

@ -4,7 +4,7 @@
<template>
<v-dialog v-model="model" max-width="420" transition="fade-transition">
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="pl-7 py-4 pos-relative">
<v-card-item class="pl-7 py-4">
<template #prepend>
<img class="d-block" :src="stepInfo[step].ref.value?.iconSrc || LockIcon">
</template>

View File

@ -9,7 +9,7 @@
:persistent="isLoading"
>
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="pl-7 py-4 pos-relative">
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"

View File

@ -10,6 +10,7 @@
:max-width="step === UpgradeAccountStep.Info || step === UpgradeAccountStep.PricingPlanSelection ? '700px' : '460px'"
transition="fade-transition"
:persistent="loading"
:scrim="scrim"
>
<v-card ref="content" rounded="xlg">
<v-card-item class="pl-7 py-4">
@ -123,9 +124,20 @@ const loading = ref<boolean>(false);
const plan = ref<PricingPlanInfo | null>(null);
const content = ref<HTMLElement | null>(null);
const props = withDefaults(defineProps<{
modelValue: boolean,
scrim: boolean,
}>(), {
scrim: true,
});
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const model = computed<boolean>({
get: () => appStore.state.isUpgradeFlowDialogShown,
set: value => appStore.toggleUpgradeFlow(value),
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const stepTitles = computed(() => {
@ -195,7 +207,7 @@ async function setSecondStep() {
try {
pkgAvailable = await payments.pricingPackageAvailable();
} catch (error) {
notify.notifyError(error, null);
notify.notifyError(error);
setStep(UpgradeAccountStep.Options);
loading.value = false;
return;

View File

@ -8,13 +8,13 @@
<account-nav />
<default-view />
<UpgradeAccountDialog />
<UpgradeAccountDialog v-model="appStore.state.isUpgradeFlowDialogShown" />
</session-wrapper>
</v-app>
</template>
<script setup lang="ts">
import { onBeforeMount } from 'vue';
import { computed, onBeforeMount } from 'vue';
import { VApp } from 'vuetify/components';
import DefaultBar from './AppBar.vue';

View File

@ -7,18 +7,19 @@
<default-bar />
<default-view />
<UpgradeAccountDialog />
<UpgradeAccountDialog v-model="appStore.state.isUpgradeFlowDialogShown" />
</session-wrapper>
</v-app>
</template>
<script setup lang="ts">
import { VApp } from 'vuetify/components';
import { onBeforeMount } from 'vue';
import { computed, onBeforeMount } from 'vue';
import DefaultBar from './AppBar.vue';
import DefaultView from './View.vue';
import { useAppStore } from '@poc/store/appStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
@ -26,6 +27,7 @@ import { useNotify } from '@/utils/hooks';
import SessionWrapper from '@poc/components/utils/SessionWrapper.vue';
import UpgradeAccountDialog from '@poc/components/dialogs/upgradeAccountFlow/UpgradeAccountDialog.vue';
const appStore = useAppStore();
const usersStore = useUsersStore();
const notify = useNotify();

View File

@ -11,7 +11,7 @@
<ProjectNav />
<default-view />
<UpgradeAccountDialog />
<UpgradeAccountDialog v-model="appStore.state.isUpgradeFlowDialogShown" />
</session-wrapper>
</v-app>
</template>

View File

@ -122,6 +122,10 @@ html {
opacity: 0.75;
}
.custom-scrim {
z-index: 2000;
}
// Align the checkboxes in the tables
.v-selection-control {
contain: inherit;
@ -281,4 +285,4 @@ table {
// text styles
.text-cursor-pointer {
cursor: pointer;
}
}