web/satellite: enable customers to purchase packages after onboarding

This change allows users who are eligible to purchase a package plan to
do so, even if they missed the opportunity during onboarding.

Now, if the user is eligible, an opportunity to select a package for purchase
is presented as part of the upgrade modal.

Issue: https://github.com/storj/storj/issues/5932

Change-Id: I45575274839701bf7b80815330a4ae86a1d32093
This commit is contained in:
Moby von Briesen 2023-06-06 18:32:33 -04:00 committed by Storj Robot
parent bb620e746b
commit 62e3b2cfe6
4 changed files with 168 additions and 4 deletions

View File

@ -6,7 +6,7 @@
<template #content>
<div v-if="!isSuccess" class="content">
<div class="content__top">
<h1 class="content__top__title">Activate your account</h1>
<h1 class="content__top__title">Activate your plan</h1>
<div class="content__top__icon">
<CheckIcon />
</div>
@ -57,7 +57,7 @@
<CircleCheck />
</div>
<h1 class="content-success__title">Success</h1>
<p class="content-success__subtitle">Your account has been successfully activated.</p>
<p class="content-success__subtitle">Your plan has been successfully activated.</p>
<div class="content-success__info">
<ThinCheck class="content-success__info__icon" />
<p class="content-success__info__title">
@ -135,10 +135,14 @@ const isFree = computed((): boolean => {
});
/**
* Closes the modal and advances to the next step in the onboarding tour.
* Closes the modal. If the user has not completed the onboarding tour, advance to the next step.
*/
function onClose(): void {
appStore.removeActiveModal();
// do not reroute if the user has already completed onboarding
if (usersStore.state.settings.onboardingEnd) {
return;
}
if (isSuccess.value) {
if (configStore.state.config.allProjectsDashboard) {
router.push(RouteConfig.AllProjectsDashboard.path);

View File

@ -0,0 +1,108 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<UpgradeAccountWrapper title="Upgrade">
<template #content>
<div class="pricing-area">
<VLoader v-if="isLoading" class="pricing-area__loader" width="90px" height="90px" />
<template v-else>
<div class="pricing-area__plans">
<PricingPlanContainer
v-for="(plan, index) in plans"
:key="index"
:plan="plan"
/>
</div>
</template>
</div>
</template>
</UpgradeAccountWrapper>
</template>
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue';
import { useRouter } from 'vue-router';
import { RouteConfig } from '@/router';
import { PricingPlanInfo, PricingPlanType } from '@/types/common';
import { User } from '@/types/users';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useUsersStore } from '@/store/modules/usersStore';
import UpgradeAccountWrapper from '@/components/modals/upgradeAccountFlow/UpgradeAccountWrapper.vue';
import PricingPlanContainer from '@/components/account/billing/pricingPlans/PricingPlanContainer.vue';
import VLoader from '@/components/common/VLoader.vue';
const usersStore = useUsersStore();
const router = useRouter();
const notify = useNotify();
const isLoading = ref<boolean>(true);
const plans = ref<PricingPlanInfo[]>([
new PricingPlanInfo(
PricingPlanType.PRO,
'Pro Account',
'25 GB Free',
'Only pay for what you need. $4/TB stored per month* $7/TB for egress bandwidth.',
'*Additional per-segment fee of $0.0000088 applies.',
null,
null,
'Add a credit card to activate your Pro Account.<br><br>Get 25GB free storage and egress. Only pay for what you use beyond that.',
'No charge today',
'25GB Free',
),
]);
/*
* Loads pricing plan config. Assumes that user is already eligible for a plan prior to component being mounted.
*/
onBeforeMount(async () => {
const user: User = usersStore.state.user;
let config;
try {
config = require('@/components/account/billing/pricingPlans/pricingPlanConfig.json');
} catch {
notify.error('No pricing plan configuration file.', null);
return;
}
const plan = config[user.partner] as PricingPlanInfo;
if (!plan) {
notify.error(`No pricing plan configuration for partner '${user.partner}'.`, null);
return;
}
plan.type = PricingPlanType.PARTNER;
plans.value.unshift(plan);
isLoading.value = false;
});
</script>
<style scoped lang="scss">
.pricing-area {
&__loader {
position: fixed;
inset: 0;
align-items: center;
}
&__plans {
margin-top: 41px;
display: flex;
gap: 30px;
}
}
@media screen and (width <= 963px) {
.pricing-area__plans {
max-width: 444px;
flex-direction: column;
}
}
</style>

View File

@ -6,7 +6,8 @@
<template #content>
<UpgradeInfoStep
v-if="step === UpgradeAccountStep.Info"
:on-upgrade="() => setStep(UpgradeAccountStep.Options)"
:on-upgrade="setSecondStep"
:loading="loading"
/>
<UpgradeOptionsStep
v-if="step === UpgradeAccountStep.Options"
@ -23,6 +24,9 @@
v-if="step === UpgradeAccountStep.Success"
:on-continue="closeModal"
/>
<PricingPlanStep
v-if="step === UpgradeAccountStep.PricingPlan"
/>
</template>
</VModal>
</template>
@ -30,9 +34,12 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useConfigStore } from '@/store/modules/configStore';
import { useAppStore } from '@/store/modules/appStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useBillingStore } from '@/store/modules/billingStore';
import { useNotify } from '@/utils/hooks';
import { PaymentsHttpApi } from '@/api/payments';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsHttpApi } from '@/api/analytics';
@ -42,6 +49,7 @@ import UpgradeOptionsStep from '@/components/modals/upgradeAccountFlow/UpgradeOp
import AddCreditCardStep from '@/components/modals/upgradeAccountFlow/AddCreditCardStep.vue';
import SuccessStep from '@/components/modals/upgradeAccountFlow/SuccessStep.vue';
import AddTokensStep from '@/components/modals/upgradeAccountFlow/AddTokensStep.vue';
import PricingPlanStep from '@/components/modals/upgradeAccountFlow/PricingPlanStep.vue';
enum UpgradeAccountStep {
Info = 'infoStep',
@ -49,11 +57,15 @@ enum UpgradeAccountStep {
AddCC = 'addCCStep',
AddTokens = 'addTokensStep',
Success = 'successStep',
PricingPlan = 'pricingPlanStep',
}
const configStore = useConfigStore();
const appStore = useAppStore();
const usersStore = useUsersStore();
const billingStore = useBillingStore();
const notify = useNotify();
const payments: PaymentsHttpApi = new PaymentsHttpApi();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
@ -88,6 +100,44 @@ function setStep(s: UpgradeAccountStep) {
step.value = s;
}
/**
* Sets second step in the flow (after user clicks to upgrade).
* Most users will go to the Options step, but if a user is eligible for a
* pricing plan (and pricing plans are enabled), they will be sent to the PricingPlan step.
*/
async function setSecondStep() {
if (loading.value) return;
loading.value = true;
const user: User = usersStore.state.user;
const pricingPkgsEnabled = configStore.state.config.pricingPackagesEnabled;
if (!pricingPkgsEnabled || !user.partner) {
setStep(UpgradeAccountStep.Options);
loading.value = false;
return;
}
let pkgAvailable = false;
try {
pkgAvailable = await payments.pricingPackageAvailable();
} catch (error) {
notify.error(error.message, null);
setStep(UpgradeAccountStep.Options);
loading.value = false;
return;
}
if (!pkgAvailable) {
setStep(UpgradeAccountStep.Options);
loading.value = false;
return;
}
setStep(UpgradeAccountStep.PricingPlan);
loading.value = false;
}
/**
* Closes upgrade account modal.
*/

View File

@ -34,6 +34,7 @@
height="48px"
:is-green="true"
:on-press="onUpgrade"
:is-disabled="loading"
/>
<div class="info-step__column__bullets">
<InfoBullet class="info-step__column__bullets__item" is-pro title="Projects" info="3 projects + more on request" />
@ -77,6 +78,7 @@ const usersStore = useUsersStore();
const notify = useNotify();
const props = defineProps<{
loading: boolean;
onUpgrade: () => void;
}>();