web/satellite: added Upgrade to Paid Tier banner

Added new info banner to show user their used and total storage values with a button to upgrade to Paid Tier with auto limit increase

Change-Id: I827818dcb5179358df246218a47feb61bc1a1bac
This commit is contained in:
Vitalii Shpital 2021-06-30 19:47:47 +03:00
parent 1230cde317
commit bab43af6ce
14 changed files with 164 additions and 183 deletions

View File

@ -264,6 +264,7 @@ func (a *Auth) GetAccount(w http.ResponseWriter, r *http.Request) {
CompanyName string `json:"companyName"`
EmployeeCount string `json:"employeeCount"`
HaveSalesContact bool `json:"haveSalesContact"`
PaidTier bool `json:"paidTier"`
}
auth, err := console.GetAuth(ctx)
@ -283,6 +284,7 @@ func (a *Auth) GetAccount(w http.ResponseWriter, r *http.Request) {
user.Position = auth.User.Position
user.EmployeeCount = auth.User.EmployeeCount
user.HaveSalesContact = auth.User.HaveSalesContact
user.PaidTier = auth.User.PaidTier
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&user)

View File

@ -93,6 +93,7 @@
"declaration-block-trailing-semicolon": "always",
"declaration-colon-space-before": "never",
"declaration-colon-space-after": "always",
"declaration-colon-newline-after": null,
"number-leading-zero": "always",
"function-url-quotes": "always",
"font-family-name-quotes": "always-unless-keyword",

View File

@ -135,6 +135,7 @@ export class AuthHttpApi {
userResponse.partnerId,
userResponse.password,
userResponse.projectLimit,
userResponse.paidTier,
);
}

View File

@ -3,7 +3,6 @@
<template>
<div class="account-billing-area">
<InfoBar/>
<div class="account-billing-area__notification-container" v-if="hasNoCreditCard">
<div class="account-billing-area__notification-container__negative-balance" v-if="isBalanceNegative">
<NegativeBalanceIcon/>
@ -62,7 +61,6 @@ import SmallDepositHistory from '@/components/account/billing/depositAndBillingH
import EstimatedCostsAndCredits from '@/components/account/billing/estimatedCostsAndCredits/EstimatedCostsAndCredits.vue';
import CreditsHistory from '@/components/account/billing/freeCredits/CreditsHistory.vue';
import HistoryDropdown from '@/components/account/billing/HistoryDropdown.vue';
import InfoBar from '@/components/account/billing/InfoBar.vue';
import PaymentMethods from '@/components/account/billing/paymentMethods/PaymentMethods.vue';
import VLoader from '@/components/common/VLoader.vue';
@ -90,7 +88,6 @@ import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
ExpandIcon,
HideIcon,
CreditsHistory,
InfoBar,
VLoader,
},
})
@ -330,7 +327,7 @@ export default class BillingArea extends Vue {
}
&__notification-container {
margin-top: 60px;
margin-top: 20px;
&__negative-balance,
&__low-balance {

View File

@ -1,174 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="billing-info-bar">
<div class="billing-info-bar__info">
<VLoader class="billing-info-loader" v-if="isDataFetching" width="20px" height="20px"/>
<b v-else class="billing-info-bar__info__storage-value">{{ storageRemaining }}</b>
<span class="billing-info-bar__info__storage-label">of Storage Remaining</span>
<VLoader class="billing-info-loader" v-if="isDataFetching" width="20px" height="20px"/>
<b v-else class="billing-info-bar__info__bandwidth-value">{{ bandwidthRemaining }}</b>
<span class="billing-info-bar__info__bandwidth-label">of Bandwidth Remaining</span>
<router-link class="billing-info-bar__info__button" :to="projectDashboardPath">Details</router-link>
</div>
<a
class="billing-info-bar__link"
:href="projectLimitsIncreaseRequestURL"
target="_blank"
rel="noopener noreferrer"
>
Request Limit Increase ->
</a>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VLoader from '@/components/common/VLoader.vue';
import { RouteConfig } from '@/router';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { Size } from '@/utils/bytesSize';
import { MetaUtils } from '@/utils/meta';
/**
* VBanner is common banner for needed pages
*/
@Component({
components: {
VLoader,
},
})
export default class InfoBar extends Vue {
public readonly projectDashboardPath: string = RouteConfig.ProjectDashboard.path;
public isDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches project limits.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
this.isDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Returns formatted string of remaining storage.
*/
public get storageRemaining(): string {
const storageUsed = this.$store.state.projectsModule.currentLimits.storageUsed;
const storageLimit = this.$store.state.projectsModule.currentLimits.storageLimit;
const difference = storageLimit - storageUsed;
if (difference < 0) {
return '0 Bytes';
}
const remaining = new Size(difference, 2);
return `${remaining.formattedBytes}${remaining.label}`;
}
/**
* Returns formatted string of remaining bandwidth.
*/
public get bandwidthRemaining(): string {
const bandwidthUsed = this.$store.state.projectsModule.currentLimits.bandwidthUsed;
const bandwidthLimit = this.$store.state.projectsModule.currentLimits.bandwidthLimit;
const difference = bandwidthLimit - bandwidthUsed;
if (difference < 0) {
return '0 Bytes';
}
const remaining = new Size(difference, 2);
return `${remaining.formattedBytes}${remaining.label}`;
}
/**
* Returns project limits increase request url from config.
*/
public get projectLimitsIncreaseRequestURL(): string {
return MetaUtils.getMetaContent('project-limits-increase-request-url');
}
}
</script>
<style scoped lang="scss">
.billing-info-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #dce5f2;
width: calc(100% - 60px);
padding: 10px 30px;
font-family: 'font_regular', sans-serif;
color: #2b3543;
&__info {
display: flex;
align-items: center;
&__storage-value,
&__storage-label,
&__bandwidth-value,
&__bandwidth-label {
margin-right: 5px;
font-size: 14px;
line-height: 17px;
white-space: nowrap;
}
&__button {
padding: 5px 10px;
margin-left: 15px;
background-color: #fff;
opacity: 0.8;
border-radius: 6px;
font-size: 12px;
line-height: 15px;
letter-spacing: 0.02em;
color: rgba(43, 53, 67, 0.5);
}
}
&__link {
font-size: 14px;
line-height: 17px;
font-family: 'font_medium', sans-serif;
color: #2683ff;
}
}
.billing-info-loader {
margin-right: 5px;
}
@media screen and (max-width: 768px) {
.billing-info-bar {
&__info {
&__storage-value,
&__storage-label,
&__bandwidth-value,
&__bandwidth-label {
white-space: unset;
}
}
}
}
</style>

View File

@ -52,5 +52,4 @@ export default class SortingHeader extends Vue {}
}
}
}
</style>

View File

@ -0,0 +1,118 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pt-bar">
<p class="pt-bar__info">
You are currently using
<VLoader
v-if="isDataFetching"
class="pt-bar__info__loader"
is-white="true"
width="20px"
height="20px"
/>
<b class="pt-bar__info__bold" v-else>{{storageUsed}}</b>
storage out of the
<b class="pt-bar__info__bold">150GB</b>
included in the free account.
</p>
<p class="pt-bar__functional">
Upload up to 75TB.
<b class="pt-bar__info__bold upgrade">Upgrade now.</b>
</p>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VLoader from '@/components/common/VLoader.vue';
import { PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { ProjectLimits } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
@Component({
components: {
VLoader,
},
})
export default class PaidTierBar extends Vue {
/**
* Mounted lifecycle hook after initial render.
* Fetches total limits.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_TOTAL_LIMITS);
} catch (error) {
await this.$notify.error(error.message);
}
await this.$store.commit(PAYMENTS_MUTATIONS.TOGGLE_PAID_TIER_BANNER_TO_LOADED);
}
/**
* Indicates if total limits data is fetching.
*/
public get isDataFetching(): boolean {
return this.$store.state.paymentsModule.isPaidTierBarLoading;
}
/**
* Returns formatted string of used storage.
*/
public get storageUsed(): string {
if (this.totalLimits.storageUsed === 0) {
return '0';
}
const used = new Size(this.totalLimits.storageUsed, 0);
return `${used.formattedBytes}${used.label}`;
}
/**
* Returns total limits from store.
*/
private get totalLimits(): ProjectLimits {
return this.$store.state.projectsModule.totalLimits;
}
}
</script>
<style scoped lang="scss">
.pt-bar {
font-family: 'font_regular', sans-serif;
display: flex;
align-items: center;
justify-content: space-between;
background: #0047ff;
font-size: 14px;
line-height: 18px;
color: #eee;
padding: 5px 30px;
&__info,
&__functional {
display: flex;
align-items: center;
white-space: nowrap;
&__bold {
margin: 0 5px;
font-family: 'font_bold', sans-serif;
}
&__loader {
margin: 0 5px;
}
}
}
.upgrade {
cursor: pointer;
}
</style>

View File

@ -20,12 +20,11 @@ import StripeCardInput from '@/components/account/billing/paymentMethods/StripeC
import { RouteConfig } from '@/router';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { USER_ACTIONS } from '@/store/modules/users';
const {
ADD_CREDIT_CARD,
GET_CREDIT_CARDS,
GET_BALANCE,
} = PAYMENTS_ACTIONS;
interface StripeForm {
@ -52,6 +51,9 @@ export default class AddCardForm extends Vue {
try {
await this.$store.dispatch(ADD_CREDIT_CARD, token);
// We fetch User one more time to update their Paid Tier status.
await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
await this.$notify.error(error.message);

View File

@ -30,7 +30,8 @@ import ProjectUsage from '@/components/project/usage/ProjectUsage.vue';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PAYMENTS_ACTIONS, PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
import { MetaUtils } from '@/utils/meta';
@ -60,6 +61,10 @@ export default class ProjectDashboard extends Vue {
const FIRST_PAGE = 1;
try {
await this.$store.commit(PAYMENTS_MUTATIONS.TOGGLE_PAID_TIER_BANNER_TO_LOADING);
await this.$store.dispatch(PROJECTS_ACTIONS.GET_TOTAL_LIMITS);
await this.$store.commit(PAYMENTS_MUTATIONS.TOGGLE_PAID_TIER_BANNER_TO_LOADED);
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, FIRST_PAGE);
this.areBucketsFetching = false;

View File

@ -27,6 +27,8 @@ export const PAYMENTS_MUTATIONS = {
SET_PREVIOUS_ROLLUP_PRICE: 'SET_PREVIOUS_ROLLUP_PRICE',
SET_PRICE_SUMMARY: 'SET_PRICE_SUMMARY',
SET_PRICE_SUMMARY_FOR_SELECTED_PROJECT: 'SET_PRICE_SUMMARY_FOR_SELECTED_PROJECT',
TOGGLE_PAID_TIER_BANNER_TO_LOADING: 'TOGGLE_PAID_TIER_BANNER_TO_LOADING',
TOGGLE_PAID_TIER_BANNER_TO_LOADED: 'TOGGLE_PAID_TIER_BANNER_TO_LOADED',
};
export const PAYMENTS_ACTIONS = {
@ -57,6 +59,8 @@ const {
SET_PROJECT_USAGE_AND_CHARGES,
SET_PRICE_SUMMARY,
SET_PRICE_SUMMARY_FOR_SELECTED_PROJECT,
TOGGLE_PAID_TIER_BANNER_TO_LOADING,
TOGGLE_PAID_TIER_BANNER_TO_LOADED,
} = PAYMENTS_MUTATIONS;
const {
@ -87,6 +91,7 @@ export class PaymentsState {
public priceSummaryForSelectedProject: number = 0;
public startDate: Date = new Date();
public endDate: Date = new Date();
public isPaidTierBarLoading: boolean = true;
}
/**
@ -165,6 +170,12 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
state.priceSummaryForSelectedProject = usageAndChargesForSelectedProject.summary();
},
[TOGGLE_PAID_TIER_BANNER_TO_LOADING](state: PaymentsState): void {
state.isPaidTierBarLoading = true;
},
[TOGGLE_PAID_TIER_BANNER_TO_LOADED](state: PaymentsState): void {
state.isPaidTierBarLoading = false;
},
[CLEAR](state: PaymentsState) {
state.balance = new AccountBalance();
state.paymentsHistory = [];

View File

@ -54,6 +54,7 @@ export function makeUsersModule(api: UsersApi): StoreModule<User> {
}
state.projectLimit = user.projectLimit;
state.paidTier = user.paidTier;
},
[CLEAR](state: User): void {

View File

@ -34,6 +34,7 @@ export class User {
public partnerId: string = '',
public password: string = '',
public projectLimit: number = 0,
public paidTier: boolean = false,
public isProfessional: boolean = false,
public position: string = '',
public companyName: string = '',

View File

@ -15,6 +15,7 @@
</p>
</div>
<div v-if="!isLoading" class="dashboard__wrap">
<PaidTierBar v-if="!isPaidTierStatus && !isOnboardingTour"/>
<DashboardHeader/>
<div class="dashboard__wrap__main-area">
<NavigationArea class="regular-navigation"/>
@ -29,6 +30,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import PaidTierBar from '@/components/account/billing/paidTier/PaidTierBar.vue';
import DashboardHeader from '@/components/header/HeaderArea.vue';
import NavigationArea from '@/components/navigation/NavigationArea.vue';
@ -48,7 +50,6 @@ import { MetaUtils } from '@/utils/meta';
const {
SETUP_ACCOUNT,
GET_BALANCE,
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
} = PAYMENTS_ACTIONS;
@ -57,6 +58,7 @@ const {
NavigationArea,
DashboardHeader,
LoaderImage,
PaidTierBar,
},
})
export default class DashboardArea extends Vue {
@ -124,6 +126,20 @@ export default class DashboardArea extends Vue {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
}
/**
* Returns user's paid tier status from store.
*/
public get isPaidTierStatus(): boolean {
return this.$store.state.usersModule.paidTier;
}
/**
* Indicates if current route is onboarding tour.
*/
public get isOnboardingTour(): boolean {
return this.$route.path.includes(RouteConfig.OnboardingTour.path);
}
/**
* Returns satellite name from store (config).
*/

View File

@ -5,6 +5,7 @@ exports[`Dashboard renders correctly when data is loaded 1`] = `
<!---->
<!---->
<div class="dashboard__wrap">
<!---->
<dashboardheader-stub></dashboardheader-stub>
<div class="dashboard__wrap__main-area">
<navigationarea-stub class="regular-navigation"></navigationarea-stub>