web/satellite: billing banners (#3649)
This commit is contained in:
parent
0bf7ac5b20
commit
f83837bb03
@ -34,9 +34,6 @@ export default class AccountArea extends Vue {}
|
||||
.account-area-container {
|
||||
padding: 0 39px 0 55px;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
height: 90vh;
|
||||
|
||||
&__navigation {
|
||||
position: absolute;
|
||||
|
@ -295,8 +295,6 @@ export default class ProfileArea extends Vue {
|
||||
&__button-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
margin-top: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<template>
|
||||
<div class="account-billing-area">
|
||||
<div class="account-billing-area__notification-container">
|
||||
<div class="account-billing-area__notification-container" v-if="hasNoCreditCard">
|
||||
<div class="account-billing-area__notification-container__negative-balance" v-if="isBalanceNegative">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="10" fill="#EB5757"/>
|
||||
@ -53,8 +53,8 @@ export default class BillingArea extends Vue {
|
||||
// If balance is lower - yellow notification should appear.
|
||||
private readonly CRITICAL_AMOUNT: number = 1000;
|
||||
|
||||
public beforeDestroy() {
|
||||
this.$store.dispatch(CLEAR_PAYMENT_INFO);
|
||||
public get hasNoCreditCard(): boolean {
|
||||
return this.$store.state.paymentsModule.creditCards.length === 0;
|
||||
}
|
||||
|
||||
public get isBalanceNegative(): boolean {
|
||||
@ -69,8 +69,10 @@ export default class BillingArea extends Vue {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.account-billing-area {
|
||||
padding-bottom: 35px;
|
||||
|
||||
&__notification-container {
|
||||
margin-top: 35px;
|
||||
|
||||
&__negative-balance,
|
||||
&__low-balance {
|
||||
|
@ -4,7 +4,12 @@
|
||||
<template>
|
||||
<div class="payment-methods-area">
|
||||
<div class="payment-methods-area__top-container">
|
||||
<h1 class="payment-methods-area__title text">Payment Methods</h1>
|
||||
<div>
|
||||
<h1 class="payment-methods-area__title text">Payment Methods</h1>
|
||||
<h2 v-if="isBonusInfoShown" class="payment-methods-area__bonus-info">
|
||||
You have a chance to get bonus credits!
|
||||
</h2>
|
||||
</div>
|
||||
<div class="payment-methods-area__button-area">
|
||||
<div class="payment-methods-area__button-area__default-buttons" v-if="isDefaultState">
|
||||
<VButton
|
||||
@ -128,6 +133,10 @@ export default class PaymentMethods extends Vue {
|
||||
return this.areaState === PaymentMethodsBlockState.ADDING_CARD;
|
||||
}
|
||||
|
||||
public get isBonusInfoShown(): boolean {
|
||||
return this.$store.state.paymentsModule.creditCards.length === 0;
|
||||
}
|
||||
|
||||
public onChangeTokenValue(value: number): void {
|
||||
this.tokenDepositValue = value;
|
||||
}
|
||||
@ -155,7 +164,7 @@ export default class PaymentMethods extends Vue {
|
||||
*/
|
||||
public async onConfirmAddSTORJ(): Promise<void> {
|
||||
if (this.tokenDepositValue >= this.MAX_TOKEN_AMOUNT_IN_DOLLARS || this.tokenDepositValue === 0) {
|
||||
await this.$notify.error('Deposit amount must be more than 0 and less then 1000000');
|
||||
await this.$notify.error('Deposit amount must be more than 0 and less than 1000000');
|
||||
this.tokenDepositValue = this.DEFAULT_TOKEN_DEPOSIT_VALUE;
|
||||
this.areaState = PaymentMethodsBlockState.DEFAULT;
|
||||
|
||||
@ -256,6 +265,13 @@ export default class PaymentMethods extends Vue {
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
&__bonus-info {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
color: #7889a1;
|
||||
}
|
||||
|
||||
&__button-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
68
web/satellite/src/components/common/VBanner.vue
Normal file
68
web/satellite/src/components/common/VBanner.vue
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="banner">
|
||||
<NotificationSvg />
|
||||
<div class="column">
|
||||
<p class="banner__text">{{ text }}</p>
|
||||
<p class="banner__additional-text">{{ additionalText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import NotificationSvg from '@/../static/images/notifications/notification.svg';
|
||||
|
||||
/**
|
||||
* VBanner is custom banner on top of all pages in Dashboard
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
NotificationSvg,
|
||||
},
|
||||
})
|
||||
export default class VBanner extends Vue {
|
||||
@Prop({default: ''})
|
||||
private readonly text: string;
|
||||
@Prop({default: ''})
|
||||
private readonly additionalText: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 20px 20px 20px 20px;
|
||||
border-radius: 12px;
|
||||
background-color: #d0e3fe;
|
||||
margin: 32px 65px 32px 55px;
|
||||
|
||||
&__text {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 19px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__additional-text {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
margin: 3px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #717e92;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: 0 17px;
|
||||
}
|
||||
</style>
|
@ -19,7 +19,7 @@
|
||||
<div class="header-default-state" v-if="isDefaultState">
|
||||
<VButton
|
||||
class="button"
|
||||
label="+Add"
|
||||
label="+ Add"
|
||||
width="122px"
|
||||
height="48px"
|
||||
:on-press="onAddUsersClick"
|
||||
|
@ -19,14 +19,14 @@
|
||||
:on-item-click="onMemberClick"
|
||||
/>
|
||||
</div>
|
||||
<VPagination
|
||||
</div>
|
||||
<VPagination
|
||||
v-if="totalPageCount > 1"
|
||||
class="pagination-area"
|
||||
ref="pagination"
|
||||
:total-page-count="totalPageCount"
|
||||
:on-page-click-callback="onPageClick"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
<div class="team-area__empty-search-result-area" v-if="isEmptySearchResultShown">
|
||||
<h1 class="team-area__empty-search-result-area__title">No results found</h1>
|
||||
<EmptySearchResultIcon class="team-area__empty-search-result-area__image"/>
|
||||
@ -170,14 +170,12 @@ export default class ProjectMembersArea extends Vue {
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-height: 84vh;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
flex-direction: column;
|
||||
height: 49.4vh;
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,7 +200,9 @@ export default class ProjectMembersArea extends Vue {
|
||||
}
|
||||
|
||||
.pagination-area {
|
||||
margin-top: 50px;
|
||||
margin-left: -25px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
@ -211,17 +211,4 @@ export default class ProjectMembersArea extends Vue {
|
||||
padding: 40px 40px 55px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 800px) {
|
||||
|
||||
.team-area {
|
||||
|
||||
&__container {
|
||||
|
||||
&__content {
|
||||
height: 41.5vh !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -74,7 +74,8 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
||||
state: new PaymentsState(),
|
||||
mutations: {
|
||||
[SET_BALANCE](state: PaymentsState, balance: number): void {
|
||||
state.balance = balance;
|
||||
// we need -1 multiplication because negative balance from server is credits
|
||||
state.balance = balance * -1;
|
||||
},
|
||||
[SET_CREDIT_CARDS](state: PaymentsState, creditCards: CreditCard[]): void {
|
||||
state.creditCards = creditCards;
|
||||
|
@ -11,7 +11,14 @@
|
||||
<div class="dashboard-container__wrap__column">
|
||||
<DashboardHeader/>
|
||||
<div class="dashboard-container__main-area">
|
||||
<router-view/>
|
||||
<VBanner
|
||||
v-if="isBannerShown"
|
||||
text="You have no payment method added."
|
||||
additional-text="To start work with your account please add Credit Card or add $50.00 or more worth of STORJ tokens to your balance."
|
||||
/>
|
||||
<div class="dashboard-container__main-area__content">
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -21,6 +28,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import VBanner from '@/components/common/VBanner.vue';
|
||||
import DashboardHeader from '@/components/header/HeaderArea.vue';
|
||||
import NavigationArea from '@/components/navigation/NavigationArea.vue';
|
||||
|
||||
@ -55,6 +63,7 @@ const {
|
||||
components: {
|
||||
NavigationArea,
|
||||
DashboardHeader,
|
||||
VBanner,
|
||||
},
|
||||
})
|
||||
export default class DashboardArea extends Vue {
|
||||
@ -113,6 +122,7 @@ export default class DashboardArea extends Vue {
|
||||
}
|
||||
|
||||
const selectedProjectId: string | null = LocalData.getSelectedProjectId();
|
||||
|
||||
if (selectedProjectId) {
|
||||
await this.$store.dispatch(PROJECTS_ACTIONS.SELECT, selectedProjectId);
|
||||
} else {
|
||||
@ -148,6 +158,10 @@ export default class DashboardArea extends Vue {
|
||||
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
|
||||
}
|
||||
|
||||
public get isBannerShown(): boolean {
|
||||
return this.$store.state.paymentsModule.creditCards.length === 0;
|
||||
}
|
||||
|
||||
public get isLoading(): boolean {
|
||||
return this.$store.state.appStateModule.appState.fetchState === AppState.LOADING;
|
||||
}
|
||||
@ -156,13 +170,13 @@ export default class DashboardArea extends Vue {
|
||||
* This method checks if current route is available when user has no created projects
|
||||
*/
|
||||
private isRouteAccessibleWithoutProject(): boolean {
|
||||
const awailableRoutes = [
|
||||
const availableRoutes = [
|
||||
RouteConfig.Account.with(RouteConfig.Billing).path,
|
||||
RouteConfig.Account.with(RouteConfig.Profile).path,
|
||||
RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails).path,
|
||||
];
|
||||
|
||||
return awailableRoutes.includes(this.$router.currentRoute.path.toLowerCase());
|
||||
return availableRoutes.includes(this.$router.currentRoute.path.toLowerCase());
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -191,7 +205,29 @@ export default class DashboardArea extends Vue {
|
||||
&__main-area {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: calc(100vh - 50px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 900px) {
|
||||
|
||||
.dashboard-container__main-area__content {
|
||||
height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 700px) {
|
||||
|
||||
.dashboard-container__main-area__content {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 500px) {
|
||||
|
||||
.dashboard-container__main-area__content {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="10" fill="#2683FF"/>
|
||||
<path d="M18.1489 17.043H21.9149V28H18.1489V17.043ZM20 12C20.5816 12 21.0567 12.1823 21.4255 12.5468C21.8085 12.8979 22 13.357 22 13.9241C22 14.4776 21.8085 14.9367 21.4255 15.3013C21.0567 15.6658 20.5816 15.8481 20 15.8481C19.4184 15.8481 18.9362 15.6658 18.5532 15.3013C18.1844 14.9367 18 14.4776 18 13.9241C18 13.357 18.1844 12.8979 18.5532 12.5468C18.9362 12.1823 19.4184 12 20 12Z" fill="#F5F6FA"/>
|
||||
</svg>
|
After Width: | Height: | Size: 568 B |
47
web/satellite/tests/unit/mock/api/payments.ts
Normal file
47
web/satellite/tests/unit/mock/api/payments.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge, TokenDeposit } from '@/types/payments';
|
||||
|
||||
/**
|
||||
* Mock for PaymentsApi
|
||||
*/
|
||||
export class PaymentsMock implements PaymentsApi {
|
||||
private tokenDeposit: TokenDeposit;
|
||||
|
||||
setupAccount(): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
getBalance(): Promise<number> {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
projectsCharges(): Promise<ProjectCharge[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
addCreditCard(token: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
removeCreditCard(cardId: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
listCreditCards(): Promise<CreditCard[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
makeCreditCardDefault(cardId: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
billingHistory(): Promise<BillingHistoryItem[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
makeTokenDeposit(amount: number): Promise<TokenDeposit> {
|
||||
return Promise.resolve(this.tokenDeposit);
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ exports[`Team HeaderArea renders correctly 1`] = `
|
||||
<div class="team-header-container__wrapper">
|
||||
<vheader-stub placeholder="Team Members" search="function () { [native code] }">
|
||||
<div class="header-default-state">
|
||||
<vbutton-stub label="+Add" width="122px" height="48px" onpress="function () { [native code] }" class="button"></vbutton-stub>
|
||||
<vbutton-stub label="+ Add" width="122px" height="48px" onpress="function () { [native code] }" class="button"></vbutton-stub>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
@ -86,7 +86,7 @@ exports[`Team HeaderArea renders correctly with opened Add team member popup 1`]
|
||||
<div class="team-header-container__wrapper">
|
||||
<vheader-stub placeholder="Team Members" search="function () { [native code] }">
|
||||
<div class="header-default-state">
|
||||
<vbutton-stub label="+Add" width="122px" height="48px" onpress="function () { [native code] }" class="button"></vbutton-stub>
|
||||
<vbutton-stub label="+ Add" width="122px" height="48px" onpress="function () { [native code] }" class="button"></vbutton-stub>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
|
@ -6,6 +6,7 @@ exports[`ProjectMembersArea.vue empty search result area render correctly 1`] =
|
||||
<headerarea-stub headerstate="0" selectedprojectmemberscount="0"></headerarea-stub>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<div class="team-area__empty-search-result-area">
|
||||
<h1 class="team-area__empty-search-result-area__title">No results found</h1>
|
||||
<emptysearchresulticon-stub class="team-area__empty-search-result-area__image"></emptysearchresulticon-stub>
|
||||
@ -19,6 +20,7 @@ exports[`ProjectMembersArea.vue renders correctly 1`] = `
|
||||
<headerarea-stub headerstate="0" selectedprojectmemberscount="0"></headerarea-stub>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<div class="team-area__empty-search-result-area">
|
||||
<h1 class="team-area__empty-search-result-area__title">No results found</h1>
|
||||
<emptysearchresulticon-stub class="team-area__empty-search-result-area__image"></emptysearchresulticon-stub>
|
||||
@ -38,8 +40,8 @@ exports[`ProjectMembersArea.vue team area renders correctly 1`] = `
|
||||
this._init(options);
|
||||
}" onitemclick="function () { [native code] }" dataset="[object Object]"></vlist-stub>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
@ -8,6 +8,7 @@ import { makeApiKeysModule } from '@/store/modules/apiKeys';
|
||||
import { appStateModule } from '@/store/modules/appState';
|
||||
import { makeBucketsModule } from '@/store/modules/buckets';
|
||||
import { makeNotificationsModule } from '@/store/modules/notifications';
|
||||
import { makePaymentsModule } from '@/store/modules/payments';
|
||||
import { makeProjectMembersModule } from '@/store/modules/projectMembers';
|
||||
import { makeProjectsModule } from '@/store/modules/projects';
|
||||
import { makeUsageModule } from '@/store/modules/usage';
|
||||
@ -21,6 +22,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
|
||||
import { ApiKeysMock } from '../mock/api/apiKeys';
|
||||
import { BucketsMock } from '../mock/api/buckets';
|
||||
import { PaymentsMock } from '../mock/api/payments';
|
||||
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
|
||||
import { ProjectsApiMock } from '../mock/api/projects';
|
||||
import { ProjectUsageMock } from '../mock/api/usage';
|
||||
@ -42,6 +44,7 @@ const teamMembersModule = makeProjectMembersModule(new ProjectMembersApiMock());
|
||||
const bucketsModule = makeBucketsModule(new BucketsMock());
|
||||
const usageModule = makeUsageModule(new ProjectUsageMock());
|
||||
const notificationsModule = makeNotificationsModule();
|
||||
const paymentsModule = makePaymentsModule(new PaymentsMock());
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
@ -53,6 +56,7 @@ const store = new Vuex.Store({
|
||||
projectsModule,
|
||||
appStateModule,
|
||||
teamMembersModule,
|
||||
paymentsModule,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -8,7 +8,10 @@ exports[`Dashboard renders correctly when data is loaded 1`] = `
|
||||
<div class="dashboard-container__wrap__column">
|
||||
<dashboardheader-stub></dashboardheader-stub>
|
||||
<div class="dashboard-container__main-area">
|
||||
<router-view-stub name="default"></router-view-stub>
|
||||
<vbanner-stub text="You have no payment method added." additionaltext="To start work with your account please add Credit Card or add $50.00 or more worth of STORJ tokens to your balance."></vbanner-stub>
|
||||
<div class="dashboard-container__main-area__content">
|
||||
<router-view-stub name="default"></router-view-stub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user