web/satellite/vuetify-poc: add pricing plan steps

This change adds the ability to upgrade using a custom pricing plan.

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

Change-Id: I866de25e47cb315d107201b1ccaca2cbdad6cf3c
This commit is contained in:
Wilfred Asomani 2023-09-20 11:23:54 +00:00 committed by Storj Robot
parent 31d42bb136
commit 41799ef86f
5 changed files with 367 additions and 0 deletions

View File

@ -0,0 +1,74 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="border-sm rounded-lg pa-8">
<v-row class="ma-0" justify="center" align="center">
<v-col cols="auto">
<v-badge v-if="isPartner" label="Best Value" rounded="lg" content="Best Value" color="success">
<v-btn v-if="isPartner" density="comfortable" color="success" variant="outlined" icon>
<v-icon icon="mdi-cloud-outline" />
</v-btn>
</v-badge>
<v-btn v-else density="comfortable" color="grey-lighten-1" variant="outlined" icon>
<v-icon v-if="isPro" icon="mdi-star-outline" />
<v-icon v-else icon="mdi-earth" />
</v-btn>
</v-col>
</v-row>
<div class="py-4 text-center">
<p class="font-weight-bold">{{ plan.title }}</p>
<p>{{ plan.containerSubtitle }}</p>
</div>
<div class="py-4 text-center">
<p class="mb-3">{{ plan.containerDescription }}</p>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-if="plan.containerFooterHTML" v-html="plan.containerFooterHTML" />
</div>
<v-row class="py-4" justify="center">
<v-col class="pa-0" cols="auto">
<v-btn
:variant="isFree ? 'outlined' : 'flat'"
:color="isPartner ? 'success' : isFree ? 'grey-lighten-1' : 'primary'"
@click="onActivateClick"
>
<template #append>
<v-icon icon="mdi-arrow-right" />
</template>
{{ plan.activationButtonText || ('Activate ' + plan.title) }}
</v-btn>
</v-col>
</v-row>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { VBadge, VBtn, VCol, VIcon, VRow } from 'vuetify/components';
import { PricingPlanInfo, PricingPlanType } from '@/types/common';
const props = defineProps<{
plan: PricingPlanInfo;
}>();
const emit = defineEmits<{
select: [PricingPlanInfo];
}>();
/**
* Sets the selected pricing plan and displays the pricing plan modal.
*/
function onActivateClick(): void {
emit('select', props.plan);
}
const isPartner = computed((): boolean => props.plan.type === PricingPlanType.PARTNER);
const isPro = computed((): boolean => props.plan.type === PricingPlanType.PRO);
const isFree = computed((): boolean => props.plan.type === PricingPlanType.FREE);
</script>

View File

@ -0,0 +1,85 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-row v-if="isLoading" justify="center">
<v-col cols="auto">
<v-progress-circular indeterminate />
</v-col>
</v-row>
<template v-else>
<v-row :align="smAndDown ? 'center' : 'start'" :justify="smAndDown ? 'start' : 'space-between'" :class="{'flex-column': smAndDown}">
<v-col v-for="(plan, index) in plans" :key="index" :cols="smAndDown ? 10 : 6">
<PricingPlanContainer
:plan="plan"
@select="(p) => emit('select', p)"
/>
</v-col>
</v-row>
</template>
</template>
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue';
import { useRouter } from 'vue-router';
import { VCol, VProgressCircular, VRow } from 'vuetify/components';
import { useDisplay } from 'vuetify';
import { PricingPlanInfo, PricingPlanType } from '@/types/common';
import { User } from '@/types/users';
import { useNotify } from '@/utils/hooks';
import { useUsersStore } from '@/store/modules/usersStore';
import PricingPlanContainer from '@poc/components/billing/pricingPlans/PricingPlanContainer.vue';
const usersStore = useUsersStore();
const router = useRouter();
const notify = useNotify();
const { smAndDown } = useDisplay();
const emit = defineEmits<{
select: [PricingPlanInfo];
}>();
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 = (await import('@poc/components/billing/pricingPlans/pricingPlanConfig.json')).default;
} 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>

View File

@ -0,0 +1,197 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<template v-if="!isSuccess">
<v-row class="ma-0" justify="space-between" align="center">
<v-col class="px-0" cols="auto">
<span class="font-weight-bold">Activate your plan</span>
</v-col>
<v-col class="px-0" cols="auto">
<v-btn density="compact" color="success" variant="tonal" icon>
<v-icon icon="mdi-check-outline" />
</v-btn>
</v-col>
</v-row>
<v-row class="ma-0" align="center">
<v-col class="px-0" cols="9">
<div class="pt-4">
<p class="font-weight-bold">{{ plan.title }} <span v-if="plan.activationSubtitle"> / {{ plan.activationSubtitle }}</span></p>
</div>
<div>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="plan.activationDescriptionHTML" />
</div>
</v-col>
<v-col v-if="plan.activationPriceHTML" class="px-0" cols="3">
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="font-weight-bold" v-html="plan.activationPriceHTML" />
</v-col>
</v-row>
<div v-if="!isFree" class="py-4">
<p class="text-caption">Add Card Info</p>
<StripeCardInput
ref="stripeCardInput"
class="content__bottom__card-area__input"
:on-stripe-response-callback="onCardAdded"
/>
</div>
<div class="py-4">
<v-btn
block
:color="plan.type === 'partner' ? 'success' : 'primary'"
:loading="isLoading"
@click="onActivateClick"
>
<template #prepend>
<v-icon icon="mdi-lock" />
</template>
{{ plan.activationButtonText || ('Activate ' + plan.title) }}
</v-btn>
</div>
<div class="pb-4">
<v-btn
block
variant="outlined"
color="grey-lighten-1"
:disabled="isLoading"
@click="emit('back')"
>
Back
</v-btn>
</div>
</template>
<template v-else>
<v-row class="ma-0" justify="center" align="center">
<v-col cols="auto">
<v-btn density="comfortable" color="success" variant="tonal" icon>
<v-icon icon="mdi-check-outline" />
</v-btn>
</v-col>
</v-row>
<h1 class="text-center">Success</h1>
<p class="text-center mb-4">Your plan has been successfully activated.</p>
<v-row align="center" justify="space-between" class="ma-0 mb-4 pa-2 border-sm rounded-lg">
<v-col cols="auto">
<v-icon color="success" icon="mdi-check-outline" />
</v-col>
<v-col cols="auto">
<span class="text-body-1 font-weight-bold">
{{ plan.title }}
<span v-if="plan.activationSubtitle" class="font-weight-regular"> / {{ plan.successSubtitle }}</span>
</span>
</v-col>
<v-col cols="auto">
<span style="color: var(--c-green-5);">Activated</span>
</v-col>
</v-row>
<v-btn
color="success"
block
@click="emit('close')"
>
Continue
</v-btn>
</template>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { VBtn, VCol, VIcon, VRow } from 'vuetify/components';
import { PricingPlanInfo, PricingPlanType } from '@/types/common';
import { useNotify } from '@/utils/hooks';
import { useUsersStore } from '@/store/modules/usersStore';
import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
interface StripeForm {
onSubmit(): Promise<void>;
}
const configStore = useConfigStore();
const billingStore = useBillingStore();
const usersStore = useUsersStore();
const router = useRouter();
const notify = useNotify();
const isLoading = ref<boolean>(false);
const isSuccess = ref<boolean>(false);
const stripeCardInput = ref<(typeof StripeCardInput & StripeForm) | null>(null);
const props = defineProps<{
plan: PricingPlanInfo;
}>();
const emit = defineEmits<{
back: [];
close: [];
}>();
/**
* Returns whether current plan is a free pricing plan.
*/
const isFree = computed((): boolean => {
return props.plan?.type === PricingPlanType.FREE;
});
/**
* Applies the selected pricing plan to the user.
*/
async function onActivateClick() {
if (isLoading.value || !props.plan) return;
isLoading.value = true;
if (isFree.value) {
isSuccess.value = true;
return;
}
try {
await stripeCardInput.value?.onSubmit();
} catch (error) {
notify.notifyError(error, null);
} finally {
isLoading.value = false;
}
}
/**
* Adds card after Stripe confirmation.
*/
async function onCardAdded(token: string): Promise<void> {
let action = billingStore.addCreditCard;
if (props.plan.type === PricingPlanType.PARTNER) {
action = billingStore.purchasePricingPackage;
}
try {
await action(token);
isSuccess.value = true;
// Fetch user to update paid tier status
await usersStore.getUser();
// Fetch cards to hide paid tier banner
await billingStore.getCreditCards();
} catch (error) {
notify.notifyError(error, null);
}
isLoading.value = false;
}
</script>

View File

@ -63,6 +63,14 @@
<v-window-item :value="UpgradeAccountStep.Success">
<SuccessStep @continue="model = false" />
</v-window-item>
<v-window-item :value="UpgradeAccountStep.PricingPlanSelection">
<PricingPlanSelectionStep @select="onSelectPricingPlan" />
</v-window-item>
<v-window-item v-if="plan" :value="UpgradeAccountStep.PricingPlan">
<PricingPlanStep :plan="plan" @close="model = false" @back="setStep(UpgradeAccountStep.PricingPlanSelection)" />
</v-window-item>
</v-window>
</v-card-item>
</v-card>
@ -89,6 +97,8 @@ import UpgradeOptionsStep from '@poc/components/dialogs/upgradeAccountFlow/Upgra
import AddCreditCardStep from '@poc/components/dialogs/upgradeAccountFlow/AddCreditCardStep.vue';
import AddTokensStep from '@poc/components/dialogs/upgradeAccountFlow/AddTokensStep.vue';
import SuccessStep from '@poc/components/dialogs/upgradeAccountFlow/SuccessStep.vue';
import PricingPlanSelectionStep from '@poc/components/dialogs/upgradeAccountFlow/PricingPlanSelectionStep.vue';
import PricingPlanStep from '@poc/components/dialogs/upgradeAccountFlow/PricingPlanStep.vue';
enum UpgradeAccountStep {
Info = 'infoStep',