web/satellite/vuetify-poc: add card and token option steps

This change adds the option to upgrade using credit card or tokens.

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

Change-Id: Ic0141c49ec4cf6311d381c4941cfa95371d62e94
This commit is contained in:
Wilfred Asomani 2023-09-20 11:22:44 +00:00
parent f3dbeed239
commit a5c1d9aa19
5 changed files with 471 additions and 0 deletions

View File

@ -0,0 +1,131 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<p class="pb-4">
By saving your card information, you allow Storj to charge your card for future payments in accordance with
the terms.
</p>
<v-divider />
<div class="py-4">
<StripeCardInput
ref="stripeCardInput"
:on-stripe-response-callback="addCardToDB"
/>
</div>
<div class="pt-4">
<v-row justify="center" class="mx-0 pb-3">
<v-col class="pl-0">
<v-btn
block
variant="outlined"
color="grey-lighten-1"
:disabled="loading"
@click="emit('back')"
>
Back
</v-btn>
</v-col>
<v-col class="px-0">
<v-btn
block
color="success"
:loading="loading"
@click="onSaveCardClick"
>
<template #prepend>
<v-icon icon="mdi-lock" />
</template>
Save card
</v-btn>
</v-col>
</v-row>
<p class="mt-1 text-caption text-center">Your information is secured with 128-bit SSL & AES-256 encryption.</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { VDivider, VBtn, VIcon, VCol, VRow } from 'vuetify/components';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { RouteConfig } from '@/types/router';
import { useNotify } from '@/utils/hooks';
import { useBillingStore } from '@/store/modules/billingStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
interface StripeForm {
onSubmit(): Promise<void>;
}
const analyticsStore = useAnalyticsStore();
const usersStore = useUsersStore();
const billingStore = useBillingStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const router = useRouter();
const route = useRoute();
const emit = defineEmits<{
success: [];
back: [];
}>();
const loading = ref<boolean>(false);
const stripeCardInput = ref<typeof StripeCardInput & StripeForm | null>(null);
/**
* Provides card information to Stripe.
*/
async function onSaveCardClick(): Promise<void> {
if (loading.value || !stripeCardInput.value) return;
loading.value = true;
try {
await stripeCardInput.value.onSubmit();
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.UPGRADE_ACCOUNT_MODAL);
}
loading.value = false;
}
/**
* Adds card after Stripe confirmation.
*
* @param token from Stripe
*/
async function addCardToDB(token: string): Promise<void> {
loading.value = true;
try {
await billingStore.addCreditCard(token);
notify.success('Card successfully added');
// We fetch User one more time to update their Paid Tier status.
await usersStore.getUser();
if (route.path.includes(RouteConfig.ProjectDashboard.name.toLowerCase())) {
await projectsStore.getProjectLimits(projectsStore.state.selectedProject.id);
}
if (route.path.includes(RouteConfig.Billing.path)) {
await billingStore.getCreditCards();
}
analyticsStore.eventTriggered(AnalyticsEvent.MODAL_ADD_CARD);
emit('success');
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.UPGRADE_ACCOUNT_MODAL);
}
loading.value = false;
}
</script>

View File

@ -0,0 +1,205 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<p class="pb-4">
Send more than $10 in STORJ Tokens to the following deposit address to upgrade to a Pro account.
Your account will be upgraded after your transaction receives {{ neededConfirmations }} confirmations.
If your account is not automatically upgraded, please fill out this
<a
style="color: var(--c-blue-3);"
href="https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212"
target="_blank"
rel="noopener noreferrer"
>limit increase request form</a>.
</p>
<v-row class="pb-4 ma-0" justify="center">
<v-col cols="auto">
<canvas ref="canvas" />
</v-col>
</v-row>
<p class="text-caption font-weight-bold">
Deposit Address
<v-tooltip max-width="200px" location="top">
<template #activator="{ props }">
<v-btn v-bind="props" density="compact" variant="plain" color="grey" icon>
<v-icon icon="mdi-information-outline" />
</v-btn>
</template>
<p>
This is a Storj deposit address generated just for you.
<a
style="color: var(--c-white);"
href=""
target="_blank"
rel="noopener noreferrer"
>
Learn more
</a>
</p>
</v-tooltip>
</p>
<v-row justify="space-between" align="center" class="ma-0 mb-4 border-sm rounded-lg">
<v-col class="pa-0 pl-3">
<p>{{ wallet.address }}</p>
</v-col>
<v-col class="pa-2 pr-3" cols="auto">
<v-btn
density="compact"
@click="onCopyAddressClick"
>
<template #prepend>
<v-icon icon="mdi-content-copy" />
</template>
Copy
</v-btn>
</v-col>
</v-row>
<v-divider />
<AddTokensStepBanner
:is-default="viewState === ViewState.Default"
:is-pending="viewState === ViewState.Pending"
:is-success="viewState === ViewState.Success"
:pending-payments="pendingPayments"
/>
<v-btn
v-if="viewState !== ViewState.Success"
class="mt-3"
block
variant="outlined"
color="grey-lighten-1"
@click="emit('back')"
>
Back
</v-btn>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import QRCode from 'qrcode';
import { VTooltip, VBtn, VIcon, VCol, VRow, VDivider } from 'vuetify/components';
import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useNotify } from '@/utils/hooks';
import { PaymentStatus, PaymentWithConfirmations, Wallet } from '@/types/payments';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useUsersStore } from '@/store/modules/usersStore';
import AddTokensStepBanner from '@poc/components/dialogs/upgradeAccountFlow/AddTokensStepBanner.vue';
enum ViewState {
Default,
Pending,
Success,
}
const configStore = useConfigStore();
const billingStore = useBillingStore();
const usersStore = useUsersStore();
const notify = useNotify();
const canvas = ref<HTMLCanvasElement>();
const intervalID = ref<NodeJS.Timer>();
const viewState = ref<ViewState>(ViewState.Default);
const emit = defineEmits<{
back: [];
}>();
/**
* Returns wallet from store.
*/
const wallet = computed((): Wallet => {
return billingStore.state.wallet as Wallet;
});
/**
* Returns needed transaction confirmations from config store.
*/
const neededConfirmations = computed((): number => {
return configStore.state.config.neededTransactionConfirmations;
});
/**
* Returns pending payments from store.
*/
const pendingPayments = computed((): PaymentWithConfirmations[] => {
return billingStore.state.pendingPaymentsWithConfirmations;
});
/**
* Copies address to user's clipboard.
*/
function onCopyAddressClick(): void {
navigator.clipboard.writeText(wallet.value.address);
notify.success('Address copied to your clipboard');
}
/**
* Sets current view state depending on payment statuses.
*/
function setViewState(): void {
switch (true) {
case pendingPayments.value.some(p => p.status === PaymentStatus.Pending):
viewState.value = ViewState.Pending;
break;
case pendingPayments.value.some(p => p.status === PaymentStatus.Confirmed):
viewState.value = ViewState.Success;
break;
default:
viewState.value = ViewState.Default;
}
}
watch(() => pendingPayments.value, async () => {
setViewState();
if (viewState.value !== ViewState.Success) {
return;
}
clearInterval(intervalID.value);
billingStore.clearPendingPayments();
// fetch User to update their Paid Tier status.
await usersStore.getUser();
}, { deep: true });
/**
* Mounted lifecycle hook after initial render.
* Renders QR code.
*/
onMounted(async (): Promise<void> => {
setViewState();
intervalID.value = setInterval(async () => {
try {
await billingStore.getPaymentsWithConfirmations();
} catch { /* empty */ }
}, 20000); // get payments every 20 seconds.
if (!canvas.value) {
return;
}
try {
await QRCode.toCanvas(canvas.value, wallet.value.address, { width: 124 });
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.UPGRADE_ACCOUNT_MODAL);
}
});
onBeforeUnmount(() => {
clearInterval(intervalID.value);
if (viewState.value === ViewState.Success) {
billingStore.clearPendingPayments();
}
});
</script>

View File

@ -0,0 +1,90 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-alert
class="mt-3"
density="compact"
variant="tonal"
:type="isSuccess ? 'success' : 'warning'"
>
<template #prepend>
<v-icon v-if="isDefault" icon="mdi-information-outline" />
<v-icon v-if="isPending" icon="mdi-clock-outline" />
<v-icon v-if="isSuccess" icon="mdi-check-circle-outline" />
</template>
<template #text>
<p v-if="isDefault">
<span class="font-weight-bold d-block">Send only STORJ Tokens to this deposit address.</span>
<span>Sending anything else may result in the loss of your deposit.</span>
</p>
<div v-if="isPending">
<p class="banner__message">
<b>{{ stillPendingTransactions.length }} transaction{{ stillPendingTransactions.length > 1 ? 's' : '' }} pending...</b>
{{ txWithLeastConfirmations.confirmations }} of {{ neededConfirmations }} confirmations
</p>
</div>
<div v-if="isSuccess" class="banner__row">
<p class="banner__message">
Successful deposit of {{ totalValueCounter('tokenValue') }} STORJ tokens.
You received an additional bonus of {{ totalValueCounter('bonusTokens') }} STORJ tokens.
</p>
</div>
</template>
</v-alert>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { VAlert, VIcon } from 'vuetify/components';
import { PaymentStatus, PaymentWithConfirmations } from '@/types/payments';
import { useConfigStore } from '@/store/modules/configStore';
const configStore = useConfigStore();
const props = defineProps<{
isDefault: boolean
isPending: boolean
isSuccess: boolean
pendingPayments: PaymentWithConfirmations[]
}>();
/**
* Returns an array of still pending transactions to correctly display confirmations count.
*/
const stillPendingTransactions = computed((): PaymentWithConfirmations[] => {
return props.pendingPayments.filter(p => p.status === PaymentStatus.Pending);
});
/**
* Returns transaction with the least confirmations count.
*/
const txWithLeastConfirmations = computed((): PaymentWithConfirmations => {
return stillPendingTransactions.value.reduce((minTx: PaymentWithConfirmations, currentTx: PaymentWithConfirmations) => {
if (currentTx.confirmations < minTx.confirmations) {
return currentTx;
}
return minTx;
}, props.pendingPayments[0]);
});
/**
* Returns needed confirmations count for each transaction from config store.
*/
const neededConfirmations = computed((): number => {
return configStore.state.config.neededTransactionConfirmations;
});
/**
* Calculates total count of provided payment field from the list (i.e. tokenValue or bonusTokens).
*/
function totalValueCounter(field: keyof PaymentWithConfirmations): string {
return props.pendingPayments.reduce((acc: number, curr: PaymentWithConfirmations) => {
return acc + (curr[field] as number);
}, 0).toLocaleString(undefined, { maximumFractionDigits: 2 });
}
</script>

View File

@ -0,0 +1,25 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<p class="pb-4">Your Pro Account has been successfully activated.</p>
<v-btn
block
color="success"
@click="emit('continue')"
>
<template #append>
<v-icon icon="mdi-arrow-right" />
</template>
Continue
</v-btn>
</template>
<script setup lang="ts">
import { VBtn, VIcon } from 'vuetify/components';
const emit = defineEmits<{
continue: [];
}>();
</script>

View File

@ -46,6 +46,23 @@
@add-tokens="onAddTokens"
/>
</v-window-item>
<v-window-item :value="UpgradeAccountStep.AddCC">
<AddCreditCardStep
@success="() => setStep(UpgradeAccountStep.Success)"
@back="() => setStep(UpgradeAccountStep.Options)"
/>
</v-window-item>
<v-window-item :value="UpgradeAccountStep.AddTokens">
<AddTokensStep
@back="() => setStep(UpgradeAccountStep.Options)"
/>
</v-window-item>
<v-window-item :value="UpgradeAccountStep.Success">
<SuccessStep @continue="model = false" />
</v-window-item>
</v-window>
</v-card-item>
</v-card>
@ -69,6 +86,9 @@ import { PricingPlanInfo } from '@/types/common';
import UpgradeInfoStep from '@poc/components/dialogs/upgradeAccountFlow/UpgradeInfoStep.vue';
import UpgradeOptionsStep from '@poc/components/dialogs/upgradeAccountFlow/UpgradeOptionsStep.vue';
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';
enum UpgradeAccountStep {
Info = 'infoStep',