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:
parent
31d42bb136
commit
41799ef86f
@ -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>
|
@ -0,0 +1 @@
|
||||
{}
|
@ -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>
|
@ -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>
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user