web/satellite: info bars for accounts with no paywall

WHAT:
info bars for accounts with no paywall implemented, USR-976

WHY:
we should notify users with no paywall that available coupon value is running low or coupon is used

Change-Id: I1a84afce890515b3aaedf1f0b8d359499af05471
This commit is contained in:
VitaliiShpital 2020-08-04 20:36:49 +03:00 committed by Vitalii Shpital
parent db57d76ee9
commit e5012fcb3d
13 changed files with 255 additions and 21 deletions

View File

@ -269,7 +269,7 @@ export class PaymentsHttpApi implements PaymentsApi {
throw new ErrorUnauthorized();
}
throw new Error('can not process coin payment');
throw new Error('can not get paywall status');
}
return await response.json();

View File

@ -180,11 +180,3 @@ a {
height: calc(100vh - 70px);
}
}
@media screen and (max-height: 700px) {
.navigation-area {
padding: 20px 0;
height: calc(100vh - 40px);
}
}

View File

@ -0,0 +1,53 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template src="./noPaywallInfoBar.html"/>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { RouteConfig } from '@/router';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
@Component
export default class NoPaywallInfoBar extends Vue {
private readonly ONE_QUARTER = 25; // in percents.
public readonly billingPath: string = RouteConfig.Account.with(RouteConfig.Billing).path;
/**
* Indicates if default message is shown.
*/
public get isDefaultMessage(): boolean {
return this.promotionalCoupon.remainingAmountPercentage() > this.ONE_QUARTER;
}
/**
* Indicates if warning message is shown.
*/
public get isWarningMessage(): boolean {
const remaining = this.promotionalCoupon.remainingAmountPercentage();
return remaining > 0 && remaining <= this.ONE_QUARTER;
}
/**
* Indicates if error message is shown.
*/
public get isErrorMessage(): boolean {
return this.promotionalCoupon.remainingAmountPercentage() === 0;
}
/**
* Returns promotional coupon.
*/
private get promotionalCoupon(): PaymentsHistoryItem {
const coupons: PaymentsHistoryItem[] = this.$store.state.paymentsModule.paymentsHistory.filter((item: PaymentsHistoryItem) => {
return item.type === PaymentsHistoryItemType.Coupon;
});
return coupons[coupons.length - 1];
}
}
</script>
<style scoped lang="scss" src="./noPaywallInfoBar.scss"/>

View File

@ -0,0 +1,18 @@
<!--Copyright (C) 2020 Storj Labs, Inc.-->
<!--See LICENSE for copying information.-->
<div class="no-paywall-bar" :class="{ blue: isDefaultMessage, orange: isWarningMessage, red: isErrorMessage }">
<p class="no-paywall-bar__message" v-if="isDefaultMessage">
<b class="no-paywall-bar__message__bold">Try Storj with 50 GB Free.</b>
<span>Add a payment method to keep experiencing Storj after your trial.</span>
</p>
<p class="no-paywall-bar__message" v-if="isWarningMessage">
<b class="no-paywall-bar__message__bold">Your 50 GB trial is running out soon!</b>
<span>Add a payment method to keep experiencing Storj.</span>
</p>
<p class="no-paywall-bar__message" v-if="isErrorMessage">
<b class="no-paywall-bar__message__bold">Your 50 GB trial has run out!</b>
<span>Add a payment method to keep experiencing Storj.</span>
</p>
<router-link class="no-paywall-bar__link" :to="billingPath">Add a Payment Method -></router-link>
</div>

View File

@ -0,0 +1,39 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
.no-paywall-bar {
width: calc(100% - 60px);
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
font-family: 'font_regular', sans-serif;
&__message,
&__link {
font-weight: normal;
font-size: 14px;
line-height: 12px;
color: #fff;
&__bold {
margin-right: 5px;
}
}
&__link {
font-family: 'font_medium', sans-serif;
}
}
.blue {
background-color: #2582ff;
}
.orange {
background-color: #e67c00;
}
.red {
background-color: #e43e3e;
}

View File

@ -65,7 +65,6 @@ import CloseImage from '@/../static/images/onboardingTour/close.svg';
import { RouteConfig } from '@/router';
import { TourState } from '@/utils/constants/onboardingTourEnums';
import { MetaUtils } from '@/utils/meta';
@Component({
components: {
@ -123,7 +122,7 @@ export default class OnboardingTourArea extends Vue {
* Indicates if paywall is enabled.
*/
public get isPaywallEnabled(): boolean {
return this.$store.state.paymentsModule.paywallEnabled;
return this.$store.state.paymentsModule.isPaywallEnabled;
}
/**

View File

@ -88,7 +88,7 @@ export class PaymentsState {
public priceSummary: number = 0;
public startDate: Date = new Date();
public endDate: Date = new Date();
public paywallEnabled: boolean = true;
public isPaywallEnabled: boolean = true;
}
/**
@ -154,7 +154,7 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
state.priceSummary = usageItemSummaries.reduce((accumulator, current) => accumulator + current);
},
[SET_PAYWALL_ENABLED_STATUS](state: PaymentsState, paywallEnabledStatus: boolean): void {
state.paywallEnabled = paywallEnabledStatus;
state.isPaywallEnabled = paywallEnabledStatus;
},
[CLEAR](state: PaymentsState) {
state.balance = new AccountBalance();
@ -164,7 +164,7 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
state.creditCards = [];
state.startDate = new Date();
state.endDate = new Date();
state.paywallEnabled = true;
state.isPaywallEnabled = true;
},
},
actions: {

View File

@ -110,7 +110,9 @@ export class PaymentAmountOption {
) {}
}
// BillingHistoryItem holds all public information about billing history line.
/**
* PaymentsHistoryItem holds all public information about payments history line.
*/
export class PaymentsHistoryItem {
public constructor(
public readonly id: string = '',
@ -137,6 +139,17 @@ export class PaymentsHistoryItem {
return this.status.charAt(0).toUpperCase() + this.status.substring(1);
}
/**
* RemainingAmountPercentage will return remaining amount of item in percentage.
*/
public remainingAmountPercentage(): number {
if (this.amount === 0) {
return 0;
}
return this.remaining / this.amount * 100;
}
private amountDollars(amount): number {
return amount / 100;
}

View File

@ -6,11 +6,12 @@
<div v-if="isLoading" class="loading-overlay active">
<img class="loading-image" src="@/../static/images/register/Loading.gif" alt="Company logo loading gif">
</div>
<div v-else class="dashboard-container__wrap">
<NoPaywallInfoBar v-if="isNoPaywallInfoBarShown && !isLoading"/>
<div v-if="!isLoading" class="dashboard-container__wrap">
<NavigationArea class="regular-navigation"/>
<div class="dashboard-container__wrap__column">
<DashboardHeader/>
<div class="dashboard-container__main-area">
<div class="dashboard-container__main-area" :class="{ extended: isNoPaywallInfoBarShown }">
<div class="dashboard-container__main-area__bar-area">
<VInfoBar
v-if="isInfoBarShown"
@ -38,6 +39,7 @@ import { Component, Vue } from 'vue-property-decorator';
import VInfoBar from '@/components/common/VInfoBar.vue';
import DashboardHeader from '@/components/header/HeaderArea.vue';
import NavigationArea from '@/components/navigation/NavigationArea.vue';
import NoPaywallInfoBar from '@/components/noPaywallInfoBar/NoPaywallInfoBar.vue';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { RouteConfig } from '@/router';
@ -72,6 +74,7 @@ const {
NavigationArea,
DashboardHeader,
VInfoBar,
NoPaywallInfoBar,
},
})
export default class DashboardArea extends Vue {
@ -203,6 +206,17 @@ export default class DashboardArea extends Vue {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
}
/**
* Indicates if no paywall info bar is shown.
*/
public get isNoPaywallInfoBarShown(): boolean {
const isOnboardingTour: boolean = this.$route.name === RouteConfig.OnboardingTour.name;
return !this.isPaywallEnabled && !isOnboardingTour &&
this.$store.state.paymentsModule.balance.coins === 0 &&
this.$store.state.paymentsModule.creditCards.length === 0;
}
/**
* Indicates if info bar is shown.
*/
@ -252,6 +266,13 @@ export default class DashboardArea extends Vue {
public get isLoading(): boolean {
return this.$store.state.appStateModule.appState.fetchState === AppState.LOADING;
}
/**
* Indicates if paywall is enabled.
*/
private get isPaywallEnabled(): boolean {
return this.$store.state.paymentsModule.isPaywallEnabled;
}
}
</script>
@ -294,6 +315,10 @@ export default class DashboardArea extends Vue {
}
}
.extended {
height: calc(100vh - 90px);
}
@media screen and (max-width: 1280px) {
.regular-navigation {

View File

@ -45,6 +45,8 @@ class NotificatorPlugin {
const notificationsPlugin = new NotificatorPlugin();
localVue.use(notificationsPlugin);
const ANIMATION_COMPLETE_TIME = 600;
describe('PaymentMethods', () => {
it('renders correctly without card', () => {
const wrapper = mount(PaymentMethods, {
@ -72,8 +74,8 @@ describe('PaymentMethods', () => {
setTimeout(() => {
expect(wrapper).toMatchSnapshot();
done();
}, 500);
}, 500);
}, ANIMATION_COMPLETE_TIME);
}, ANIMATION_COMPLETE_TIME);
});
});
@ -94,8 +96,8 @@ describe('PaymentMethods', () => {
setTimeout(() => {
expect(wrapper).toMatchSnapshot();
done();
}, 500);
}, 500);
}, ANIMATION_COMPLETE_TIME);
}, ANIMATION_COMPLETE_TIME);
});
});

View File

@ -0,0 +1,64 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import NoPaywallInfoBar from '@/components/noPaywallInfoBar/NoPaywallInfoBar.vue';
import { router } from '@/router';
import { makePaymentsModule, PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { PaymentsHistoryItem, PaymentsHistoryItemType, ProjectUsageAndCharges } from '@/types/payments';
import { createLocalVue, mount } from '@vue/test-utils';
import { PaymentsMock } from '../mock/api/payments';
const localVue = createLocalVue();
const paymentsModule = makePaymentsModule(new PaymentsMock());
const { SET_PAYMENTS_HISTORY } = PAYMENTS_MUTATIONS;
const store = new Vuex.Store({
modules: {
paymentsModule,
},
});
localVue.use(Vuex);
describe('NoPaywallInfoBar.vue', () => {
it('renders correctly with less than 75% usage', () => {
const coupon: PaymentsHistoryItem = new PaymentsHistoryItem('id', 'coupon', 300, 300, 'test', '', new Date(), new Date(), PaymentsHistoryItemType.Coupon, 275);
store.commit(SET_PAYMENTS_HISTORY, [coupon]);
const wrapper = mount(NoPaywallInfoBar, {
store,
localVue,
router,
});
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with more than 75% usage', () => {
const coupon: PaymentsHistoryItem = new PaymentsHistoryItem('id', 'coupon', 300, 300, 'test', '', new Date(), new Date(), PaymentsHistoryItemType.Coupon, 75);
store.commit(SET_PAYMENTS_HISTORY, [coupon]);
const wrapper = mount(NoPaywallInfoBar, {
store,
localVue,
router,
});
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with used coupon', () => {
const coupon: PaymentsHistoryItem = new PaymentsHistoryItem('id', 'coupon', 300, 300, 'test', '', new Date(), new Date(), PaymentsHistoryItemType.Coupon, 0);
store.commit(SET_PAYMENTS_HISTORY, [coupon]);
const wrapper = mount(NoPaywallInfoBar, {
store,
localVue,
router,
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -2,6 +2,8 @@
exports[`Dashboard renders correctly when data is loaded 1`] = `
<div class="dashboard-container">
<!---->
<!---->
<div class="dashboard-container__wrap">
<navigationarea-stub class="regular-navigation"></navigationarea-stub>
<div class="dashboard-container__wrap__column">
@ -22,5 +24,7 @@ exports[`Dashboard renders correctly when data is loaded 1`] = `
exports[`Dashboard renders correctly when data is loading 1`] = `
<div class="dashboard-container">
<div class="loading-overlay active"><img src="@/../static/images/register/Loading.gif" alt="Company logo loading gif" class="loading-image"></div>
<!---->
<!---->
</div>
`;

View File

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoPaywallInfoBar.vue renders correctly with less than 75% usage 1`] = `
<div class="no-paywall-bar blue">
<p class="no-paywall-bar__message"><b class="no-paywall-bar__message__bold">Try Storj with 50 GB Free.</b> <span>Add a payment method to keep experiencing Storj after your trial.</span></p>
<!---->
<!----> <a href="/account/billing" class="no-paywall-bar__link">Add a Payment Method -&gt;</a>
</div>
`;
exports[`NoPaywallInfoBar.vue renders correctly with more than 75% usage 1`] = `
<div class="no-paywall-bar orange">
<!---->
<p class="no-paywall-bar__message"><b class="no-paywall-bar__message__bold">Your 50 GB trial is running out soon!</b> <span>Add a payment method to keep experiencing Storj.</span></p>
<!----> <a href="/account/billing" class="no-paywall-bar__link">Add a Payment Method -&gt;</a>
</div>
`;
exports[`NoPaywallInfoBar.vue renders correctly with used coupon 1`] = `
<div class="no-paywall-bar red">
<!---->
<!---->
<p class="no-paywall-bar__message"><b class="no-paywall-bar__message__bold">Your 50 GB trial has run out!</b> <span>Add a payment method to keep experiencing Storj.</span></p> <a href="/account/billing" class="no-paywall-bar__link">Add a Payment Method -&gt;</a>
</div>
`;