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:
parent
db57d76ee9
commit
e5012fcb3d
@ -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();
|
||||
|
@ -180,11 +180,3 @@ a {
|
||||
height: calc(100vh - 70px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 700px) {
|
||||
|
||||
.navigation-area {
|
||||
padding: 20px 0;
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
}
|
||||
|
@ -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"/>
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
64
web/satellite/tests/unit/views/NoPaywallInfoBar.spec.ts
Normal file
64
web/satellite/tests/unit/views/NoPaywallInfoBar.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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>
|
||||
`;
|
||||
|
@ -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 -></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 -></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 -></a>
|
||||
</div>
|
||||
`;
|
Loading…
Reference in New Issue
Block a user