web/satellite: extend low token balance banner use-case

Updated condition on when this banner should be shown.
Also, added this banner to project dashboard and billing pages.

Issue:
https://github.com/storj/storj/issues/6356
https://github.com/storj/storj/issues/6368

Change-Id: I2f8f587a3c75508df0a9a6e84e1684b3c3904aa7
This commit is contained in:
Vitalii 2023-10-20 15:00:06 +03:00 committed by Storj Robot
parent 4cbdc0342a
commit 40e43826a9
12 changed files with 186 additions and 210 deletions

View File

@ -53,6 +53,7 @@ type FrontendConfig struct {
BillingFeaturesEnabled bool `json:"billingFeaturesEnabled"`
UnregisteredInviteEmailsEnabled bool `json:"unregisteredInviteEmailsEnabled"`
FreeTierInvitesEnabled bool `json:"freeTierInvitesEnabled"`
UserBalanceForUpgrade int64 `json:"userBalanceForUpgrade"`
}
// Satellites is a configuration value that contains a list of satellite names and addresses.

View File

@ -753,6 +753,7 @@ func (server *Server) frontendConfigHandler(w http.ResponseWriter, r *http.Reque
BillingFeaturesEnabled: server.config.BillingFeaturesEnabled,
UnregisteredInviteEmailsEnabled: server.config.UnregisteredInviteEmailsEnabled,
FreeTierInvitesEnabled: server.config.FreeTierInvitesEnabled,
UserBalanceForUpgrade: server.config.UserBalanceForUpgrade,
}
err := json.NewEncoder(w).Encode(&cfg)

View File

@ -2,60 +2,80 @@
// See LICENSE for copying information.
<template>
<div class="account-billing-area">
<div class="account-billing-area__header__div">
<div class="account-billing-area__title">
<h1 class="account-billing-area__title__text">Billing</h1>
<div ref="content" class="account-billing-area">
<div class="account-billing-area__wrap">
<div class="account-billing-area__wrap__title">
<h1 class="account-billing-area__wrap__title__text">Billing</h1>
</div>
<div class="account-billing-area__header">
<v-banner
v-if="isLowBalance && content"
class="account-billing-area__wrap__low-balance"
message="Your STORJ Token balance is low. Deposit more STORJ tokens or add a credit card to avoid interruptions in service."
link-text="Deposit tokens"
severity="warning"
:dashboard-ref="content"
:on-link-click="onAddTokensClick"
/>
<div class="account-billing-area__wrap__header">
<div
:class="`account-billing-area__header__tab first-header-tab ${routeHas('overview') ? 'selected-tab' : ''}`"
:class="`account-billing-area__wrap__header__tab first-header-tab ${routeHas('overview') ? 'selected-tab' : ''}`"
@click="routeToOverview"
>
<p>Overview</p>
</div>
<div
:class="`account-billing-area__header__tab ${routeHas('methods') ? 'selected-tab' : ''}`"
:class="`account-billing-area__wrap__header__tab ${routeHas('methods') ? 'selected-tab' : ''}`"
@click="routeToPaymentMethods"
>
<p>Payment Methods</p>
</div>
<div
:class="`account-billing-area__header__tab ${routeHas('history') ? 'selected-tab' : ''}`"
:class="`account-billing-area__wrap__header__tab ${routeHas('history') ? 'selected-tab' : ''}`"
@click="routeToBillingHistory"
>
<p>Billing History</p>
</div>
<div
:class="`account-billing-area__header__tab last-header-tab ${routeHas('coupons') ? 'selected-tab' : ''}`"
:class="`account-billing-area__wrap__header__tab last-header-tab ${routeHas('coupons') ? 'selected-tab' : ''}`"
@click="routeToCoupons"
>
<p>Coupons</p>
</div>
</div>
<div class="account-billing-area__divider" />
<div class="account-billing-area__wrap__divider" />
</div>
<router-view />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { RouteConfig } from '@/types/router';
import { APP_STATE_DROPDOWNS } from '@/utils/constants/appStatePopUps';
import { APP_STATE_DROPDOWNS, MODALS } from '@/utils/constants/appStatePopUps';
import { NavigationLink } from '@/types/navigation';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useBillingStore } from '@/store/modules/billingStore';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useConfigStore } from '@/store/modules/configStore';
import { useLowTokenBalance } from '@/composables/useLowTokenBalance';
import VBanner from '@/components/common/VBanner.vue';
const analyticsStore = useAnalyticsStore();
const appStore = useAppStore();
const billingStore = useBillingStore();
const configStore = useConfigStore();
const notify = useNotify();
const router = useRouter();
const route = useRoute();
const isLowBalance = useLowTokenBalance();
const content = ref<HTMLElement | null>(null);
/**
* Indicates if free credits dropdown shown.
@ -89,6 +109,15 @@ const baseAccountRoute = computed((): NavigationLink => {
return RouteConfig.Account;
});
/**
* Holds on add tokens button click logic.
* Triggers Add funds popup.
*/
function onAddTokensClick(): void {
analyticsStore.eventTriggered(AnalyticsEvent.ADD_FUNDS_CLICKED);
appStore.updateActiveModal(MODALS.addTokenFunds);
}
/**
* Whether current route name contains term.
*/
@ -139,93 +168,33 @@ function routeToCoupons(): void {
router.push(couponsPath);
}
}
onMounted(async () => {
if (!configStore.state.config.nativeTokenPaymentsEnabled) {
return;
}
try {
await Promise.all([
billingStore.getBalance(),
billingStore.getCreditCards(),
billingStore.getNativePaymentsHistory(),
]);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.BILLING_AREA);
}
});
</script>
<style scoped lang="scss">
.label-header {
display: none;
}
.selected-tab {
border-bottom: 5px solid black;
}
.credit-history {
.account-billing-area {
padding-bottom: 40px;
&__coupon-modal-wrapper {
background: #1b2533c7 75%;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1000;
}
&__coupon-modal {
width: 741px;
height: 298px;
background: #fff;
border-radius: 8px;
margin: 15% auto;
position: relative;
&__header-wrapper {
display: flex;
justify-content: space-between;
}
&__header {
font-family: 'font_bold', sans-serif;
font-style: normal;
font-weight: bold;
font-size: 16px;
line-height: 148.31%;
margin: 30px 0 10px;
display: inline-block;
}
&__input-wrapper {
position: relative;
width: 85%;
margin: 0 auto;
.headerless-input::placeholder {
color: #384b65;
opacity: 0.4;
position: relative;
left: 20px;
}
}
&__claim-button {
position: absolute;
bottom: 11px;
right: 10px;
}
&__apply-button {
width: 85%;
height: 44px;
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
bottom: 50px;
background: #93a1af;
}
&__icon {
position: absolute;
top: 90px;
z-index: 1;
left: 20px;
}
}
}
.selected-tab {
border-bottom: 5px solid black;
}
.account-billing-area {
padding-bottom: 40px;
&__wrap {
&__title {
padding-top: 20px;
@ -235,9 +204,8 @@ function routeToCoupons(): void {
}
}
&__divider {
width: 100%;
border-bottom: 1px solid #dadfe7;
&__low-balance {
margin-top: 25px;
}
&__header {
@ -277,107 +245,37 @@ function routeToCoupons(): void {
}
}
&__title-area {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20px 0;
&__balance-area {
display: flex;
align-items: center;
justify-content: space-between;
font-family: 'font_regular', sans-serif;
&__tokens-area {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
color: #768394;
font-size: 16px;
line-height: 19px;
&__label {
margin-right: 10px;
white-space: nowrap;
}
}
&__free-credits {
display: flex;
align-items: center;
position: relative;
cursor: default;
margin-right: 50px;
color: #768394;
font-size: 16px;
line-height: 19px;
&__label {
margin-right: 10px;
white-space: nowrap;
}
}
}
}
&__notification-container {
margin-top: 20px;
&__negative-balance,
&__low-balance {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 20px;
border-radius: 12px;
&__text {
font-family: 'font_medium', sans-serif;
margin: 0 17px;
font-size: 14px;
font-weight: 500;
line-height: 19px;
}
}
&__negative-balance {
background-color: #ffd4d2;
}
&__low-balance {
background-color: #fcf8e3;
}
&__divider {
width: 100%;
border-bottom: 1px solid #dadfe7;
}
}
}
.custom-position {
margin: 30px 0 20px;
}
@media only screen and (width <= 625px) {
.icon {
min-width: 14px;
margin-left: 10px;
}
.account-billing-area {
@media only screen and (width <= 625px) {
.account-billing-area__header__div {
&__wrap {
margin-right: -24px;
margin-left: -24px;
}
.account-billing-area__title {
margin-left: 24px;
}
&__title {
margin-left: 24px;
}
.first-header-tab {
margin-left: 24px;
}
.last-header-tab {
margin-right: 24px;
&__low-balance {
margin: 25px 24px 0;
}
}
}
.first-header-tab {
margin-left: 24px;
}
.last-header-tab {
margin-right: 24px;
}
}
</style>

View File

@ -6,9 +6,9 @@
<template #content>
<div class="add-tokens">
<p class="add-tokens__info">
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
Send more than {{ amountForUpgrade }} 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
class="add-tokens__info__link"
href="https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212"
@ -72,6 +72,7 @@ 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 { centsToDollars, microDollarsToCents } from '@/utils/strings';
import UpgradeAccountWrapper from '@/components/modals/upgradeAccountFlow/UpgradeAccountWrapper.vue';
import VButton from '@/components/common/VButton.vue';
@ -94,6 +95,12 @@ const canvas = ref<HTMLCanvasElement>();
const intervalID = ref<NodeJS.Timer>();
const viewState = ref<ViewState>(ViewState.Default);
const amountForUpgrade = computed<string>(() => {
const balanceForUpgrade = configStore.state.config.userBalanceForUpgrade;
return centsToDollars(microDollarsToCents(balanceForUpgrade));
});
/**
* Returns wallet from store.
*/

View File

@ -24,11 +24,9 @@
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';

View File

@ -5,7 +5,7 @@
<UpgradeAccountWrapper title="Upgrade to Pro">
<template #content>
<p class="options-info">
Add a credit card to activate your Pro Account, or deposit more than $10 in STORJ tokens to upgrade
Add a credit card to activate your Pro Account, or deposit more than {{ amountForUpgrade }} in STORJ tokens to upgrade
and get 10% bonus on your STORJ tokens deposit.
</p>
<div class="options-buttons">
@ -36,13 +36,19 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useUsersStore } from '@/store/modules/usersStore';
import { useNotify } from '@/utils/hooks';
import { useConfigStore } from '@/store/modules/configStore';
import { centsToDollars, microDollarsToCents } from '@/utils/strings';
import UpgradeAccountWrapper from '@/components/modals/upgradeAccountFlow/UpgradeAccountWrapper.vue';
import VButton from '@/components/common/VButton.vue';
const usersStore = useUsersStore();
const configStore = useConfigStore();
const notify = useNotify();
const props = defineProps<{
@ -50,6 +56,12 @@ const props = defineProps<{
onAddCard: () => void;
onAddTokens: () => Promise<void>;
}>();
const amountForUpgrade = computed<string>(() => {
const balanceForUpgrade = configStore.state.config.userBalanceForUpgrade;
return centsToDollars(microDollarsToCents(balanceForUpgrade));
});
</script>
<style scoped lang="scss">

View File

@ -2,7 +2,16 @@
// See LICENSE for copying information.
<template>
<div class="project-dashboard">
<div ref="content" class="project-dashboard">
<v-banner
v-if="isLowBalance && content && billingEnabled"
class="project-dashboard__low-balance"
message="Your STORJ Token balance is low. Deposit more STORJ tokens or add a credit card to avoid interruptions in service."
link-text="Go to billing"
severity="warning"
:dashboard-ref="content"
:on-link-click="redirectToBillingOverview"
/>
<div class="project-dashboard__heading">
<h1 class="project-dashboard__heading__title" aria-roledescription="title">{{ selectedProject.name }}</h1>
<project-ownership-tag :role="(selectedProject.ownerId === user.id) ? ProjectRole.Owner : ProjectRole.Member" />
@ -196,6 +205,8 @@ import { ProjectMembersPage, ProjectRole } from '@/types/projectMembers';
import { AccessGrantsPage } from '@/types/accessGrants';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useCreateProjectClickHandler } from '@/composables/useCreateProjectClickHandler';
import { AccountBalance, CreditCard } from '@/types/payments';
import { useLowTokenBalance } from '@/composables/useLowTokenBalance';
import VLoader from '@/components/common/VLoader.vue';
import InfoContainer from '@/components/project/dashboard/InfoContainer.vue';
@ -209,6 +220,7 @@ import BucketsTable from '@/components/objects/BucketsTable.vue';
import EncryptionBanner from '@/components/objects/EncryptionBanner.vue';
import ProjectOwnershipTag from '@/components/project/ProjectOwnershipTag.vue';
import LimitsArea from '@/components/project/dashboard/LimitsArea.vue';
import VBanner from '@/components/common/VBanner.vue';
import NewProjectIcon from '@/../static/images/project/newProject.svg';
import InfoIcon from '@/../static/images/project/infoIcon.svg';
@ -228,11 +240,13 @@ const pmStore = useProjectMembersStore();
const agStore = useAccessGrantsStore();
const { handleCreateProjectClick } = useCreateProjectClickHandler();
const isLowBalance = useLowTokenBalance();
const notify = useNotify();
const router = useRouter();
const now = new Date().toLocaleDateString('en-US');
const content = ref<HTMLElement | null>(null);
const isDataFetching = ref<boolean>(true);
const areBucketsFetching = ref<boolean>(true);
const isServerSideEncryptionBannerHidden = ref<boolean>(true);
@ -373,6 +387,13 @@ const bucketsCount = computed((): number => {
return bucketsStore.state.page.totalCount;
});
/**
* Redirects to Billing Page Overview tab.
*/
function redirectToBillingOverview(): void {
router.push(RouteConfig.Account.with(RouteConfig.Billing.with(RouteConfig.BillingOverview)).path);
}
/**
* Hides server-side encryption banner.
*/
@ -481,13 +502,21 @@ onMounted(async (): Promise<void> => {
appStore.toggleHasJustLoggedIn();
}
const promises: Promise<void | ProjectMembersPage | AccessGrantsPage>[] = [
let promises: Promise<void | ProjectMembersPage | AccessGrantsPage | AccountBalance | CreditCard[]>[] = [
projectsStore.getDailyProjectData({ since: past, before: now }),
pmStore.getProjectMembers(FIRST_PAGE, projectID),
agStore.getAccessGrants(FIRST_PAGE, projectID),
];
if (billingEnabled.value) promises.push(billingStore.getCoupon());
if (billingEnabled.value) {
promises = [
...promises,
billingStore.getBalance(),
billingStore.getCreditCards(),
billingStore.getNativePaymentsHistory(),
billingStore.getCoupon(),
];
}
await Promise.all(promises);
} catch (error) {
@ -530,6 +559,10 @@ onBeforeUnmount((): void => {
font-family: 'font_regular', sans-serif;
padding-bottom: 55px;
&__low-balance {
margin-bottom: 20px;
}
&__heading {
display: flex;
gap: 10px;

View File

@ -0,0 +1,26 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { computed } from 'vue';
import { microDollarsToCents } from '@/utils/strings';
import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useUsersStore } from '@/store/modules/usersStore';
export function useLowTokenBalance() {
const userStore = useUsersStore();
const configStore = useConfigStore();
const billingStore = useBillingStore();
return computed<boolean>(() => {
const notEnoughBalance = billingStore.state.nativePaymentsHistory.length > 0 &&
billingStore.state.balance.sum < microDollarsToCents(configStore.state.config.userBalanceForUpgrade);
return (
userStore.state.user.paidTier && !billingStore.state.creditCards.length && notEnoughBalance
) || (
billingStore.state.creditCards.length > 0 && billingStore.state.balance.sum > 0 && notEnoughBalance
);
});
}

View File

@ -50,6 +50,7 @@ export class FrontendConfig {
billingFeaturesEnabled: boolean;
unregisteredInviteEmailsEnabled: boolean;
freeTierInvitesEnabled: boolean;
userBalanceForUpgrade: number;
}
export class MultiCaptchaConfig {

View File

@ -89,6 +89,14 @@ export function centsToDollars(cents: number) {
return formatPrice(decimalShift(cents.toString(), 2));
}
/**
* microDollarsToCents converts micro dollars to cents.
* @param microDollars - the micro dollars value
*/
export function microDollarsToCents(microDollars: number): number {
return microDollars / 10000;
}
/**
* bytesToBase10String Converts bytes to base-10 types.
* @param amountInBytes

View File

@ -51,19 +51,20 @@ import { useUsersStore } from '@/store/modules/usersStore';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { RouteConfig } from '@/types/router';
import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useLowTokenBalance } from '@/composables/useLowTokenBalance';
import VBanner from '@/components/common/VBanner.vue';
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
const router = useRouter();
const billingStore = useBillingStore();
const usersStore = useUsersStore();
const appStore = useAppStore();
const configStore = useConfigStore();
const isLowBalance = useLowTokenBalance();
const props = defineProps<{
parentRef: HTMLElement;
}>();
@ -87,15 +88,6 @@ const isAccountWarned = computed((): boolean => {
return usersStore.state.user.freezeStatus.warned;
});
/**
* Indicates if low STORJ token balance banner is shown.
*/
const isLowBalance = computed((): boolean => {
return !billingStore.state.creditCards.length &&
billingStore.state.nativePaymentsHistory.length > 0 &&
billingStore.state.balance.sum < billingStore.state.projectCharges.getPrice();
});
/* whether the paid tier banner should be shown */
const isPaidTierBannerShown = computed((): boolean => {
return !usersStore.state.user.paidTier

View File

@ -158,7 +158,6 @@ onMounted(async () => {
try {
await Promise.all([
billingStore.getBalance(),
billingStore.getProjectUsageAndChargesCurrentRollup(),
billingStore.getCreditCards(),
billingStore.getNativePaymentsHistory(),
]);