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:
parent
f3dbeed239
commit
a5c1d9aa19
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user