web/satellite: credit history page implemented

WHAT:
credit history page implemented.
can be visited by clicking specific button in a free credits dropdown.

WHY:
UI didn't display remaining coupon value.
coupons and referral items (in future) are displayed in the same place.

Change-Id: I495fd7a99f2ea5117152aaf8f495bd5322f02588
This commit is contained in:
VitaliiShpital 2020-06-10 14:42:44 +03:00 committed by Vitalii Shpital
parent 2c3fe5597d
commit 8ecf01ece8
25 changed files with 731 additions and 23 deletions

View File

@ -309,15 +309,27 @@ func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billi
remaining = 0
}
var couponStatus string
switch coupon.Status {
case 0:
couponStatus = "Active"
case 1:
couponStatus = "Used"
default:
couponStatus = "Expired"
}
billingHistory = append(billingHistory,
&BillingHistoryItem{
ID: coupon.ID.String(),
Description: coupon.Description,
Amount: coupon.Amount,
Remaining: remaining,
Status: "Added as Free Credits",
Status: couponStatus,
Link: "",
Start: coupon.Created,
End: coupon.ExpirationDate(),
Type: Coupon,
},
)

View File

@ -11,7 +11,7 @@ import {
TokenDeposit,
} from '@/types/payments';
import { HttpClient } from '@/utils/httpClient';
import { toUnixTimestamp } from '@/utils/time';
import { Time } from '@/utils/time';
/**
* PaymentsHttpApi is a http implementation of Payments API.
@ -71,8 +71,8 @@ export class PaymentsHttpApi implements PaymentsApi {
* projectsUsageAndCharges returns usage and how much money current user will be charged for each project which he owns.
*/
public async projectsUsageAndCharges(start: Date, end: Date): Promise<ProjectUsageAndCharges[]> {
const since = toUnixTimestamp(start).toString();
const before = toUnixTimestamp(end).toString();
const since = Time.toUnixTimestamp(start).toString();
const before = Time.toUnixTimestamp(end).toString();
const path = `${this.ROOT_PATH}/account/charges?from=${since}&to=${before}`;
const response = await this.client.get(path);
@ -211,7 +211,6 @@ export class PaymentsHttpApi implements PaymentsApi {
}
const paymentsHistoryItems = await response.json();
if (paymentsHistoryItems) {
return paymentsHistoryItems.map(item =>
new PaymentsHistoryItem(
@ -223,7 +222,9 @@ export class PaymentsHttpApi implements PaymentsApi {
item.link,
new Date(item.start),
new Date(item.end),
item.type),
item.type,
item.remaining,
),
);
}

View File

@ -20,9 +20,17 @@
</div>
<div class="account-billing-area__title-area" v-if="userHasOwnProject" :class="{ 'custom-position': hasNoCreditCard && (isBalanceLow || isBalanceNegative) }">
<div class="account-billing-area__title-area__balance-area">
<span class="account-billing-area__title-area__balance-area__free-credits">
Free Credits: {{ balance.freeCredits | centsToDollars }}
</span>
<div @click.stop="toggleDropdown" class="account-billing-area__title-area__balance-area__free-credits">
<span class="account-billing-area__title-area__balance-area__free-credits__amount">
Free Credits: {{ balance.freeCredits | centsToDollars }}
</span>
<HideIcon v-if="isCreditsDropdownShown"/>
<ExpandIcon v-else/>
<CreditsDropdown
v-show="isCreditsDropdownShown"
@close="closeDropdown"
/>
</div>
<span class="account-billing-area__title-area__balance-area__tokens" :style="{ color: balanceColor }">
STORJ Balance: {{ balance.coins | centsToDollars }}
</span>
@ -41,10 +49,13 @@ import { Component, Vue } from 'vue-property-decorator';
import PeriodSelection from '@/components/account/billing/depositAndBillingHistory/PeriodSelection.vue';
import SmallDepositHistory from '@/components/account/billing/depositAndBillingHistory/SmallDepositHistory.vue';
import EstimatedCostsAndCredits from '@/components/account/billing/estimatedCostsAndCredits/EstimatedCostsAndCredits.vue';
import CreditsDropdown from '@/components/account/billing/freeCredits/CreditsDropdown.vue';
import PaymentMethods from '@/components/account/billing/paymentMethods/PaymentMethods.vue';
import VDatepicker from '@/components/common/VDatePicker.vue';
import DatePickerIcon from '@/../static/images/account/billing/datePicker.svg';
import ExpandIcon from '@/../static/images/account/billing/expand.svg';
import HideIcon from '@/../static/images/account/billing/hide.svg';
import LowBalanceIcon from '@/../static/images/account/billing/lowBalance.svg';
import NegativeBalanceIcon from '@/../static/images/account/billing/negativeBalance.svg';
@ -64,9 +75,14 @@ import { ProjectOwning } from '@/utils/projectOwning';
DatePickerIcon,
LowBalanceIcon,
NegativeBalanceIcon,
CreditsDropdown,
ExpandIcon,
HideIcon,
},
})
export default class BillingArea extends Vue {
public isCreditsDropdownShown: boolean = false;
/**
* Mounted lifecycle hook before initial render.
* Fetches billing history and project limits.
@ -145,6 +161,20 @@ export default class BillingArea extends Vue {
public get userHasOwnProject(): boolean {
return new ProjectOwning(this.$store).userHasOwnProject();
}
/**
* Toggles free credits dropdown visibility.
*/
public toggleDropdown(): void {
this.isCreditsDropdownShown = !this.isCreditsDropdownShown;
}
/**
* Closes free credits dropdown.
*/
public closeDropdown(): void {
this.isCreditsDropdownShown = false;
}
}
</script>
@ -164,15 +194,24 @@ export default class BillingArea extends Vue {
justify-content: space-between;
font-family: 'font_regular', sans-serif;
&__free-credits,
&__tokens {
font-size: 16px;
line-height: 19px;
}
&__free-credits {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
margin-right: 50px;
color: #768394;
&__amount {
margin-right: 10px;
font-size: 16px;
line-height: 19px;
}
}
}
}

View File

@ -51,7 +51,7 @@ export default class DetailedHistory extends Vue {
}
return this.$store.state.paymentsModule.paymentsHistory.filter((item: PaymentsHistoryItem) => {
return item.type === PaymentsHistoryItemType.Transaction || item.type === PaymentsHistoryItemType.Coupon;
return item.type === PaymentsHistoryItemType.Transaction || item.type === PaymentsHistoryItemType.DepositBonus;
});
}

View File

@ -11,7 +11,7 @@
<ExpandIcon v-if="!isDropdownShown"/>
<HideIcon v-else/>
</div>
<div class="period-selection__dropdown" v-if="isDropdownShown" v-click-outside="close">
<div class="period-selection__dropdown" v-show="isDropdownShown" v-click-outside="close">
<div
class="period-selection__dropdown__item"
v-for="(option, index) in periodOptions"

View File

@ -44,7 +44,7 @@ export default class SmallDepositHistory extends Vue {
*/
public get depositHistoryItems(): PaymentsHistoryItem[] {
return this.$store.state.paymentsModule.paymentsHistory.filter((item: PaymentsHistoryItem) => {
return item.type === PaymentsHistoryItemType.Transaction || item.type === PaymentsHistoryItemType.Coupon;
return item.type === PaymentsHistoryItemType.Transaction || item.type === PaymentsHistoryItemType.DepositBonus;
}).slice(0, 3);
}
}

View File

@ -62,7 +62,7 @@ import { Project } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { toUnixTimestamp } from '@/utils/time';
import { Time } from '@/utils/time';
@Component({
components: {
@ -149,8 +149,8 @@ export default class UsageAndChargesItem extends Vue {
url.pathname = 'usage-report';
url.searchParams.append('projectID', projectID);
url.searchParams.append('since', toUnixTimestamp(startDate).toString());
url.searchParams.append('before', toUnixTimestamp(endDate).toString());
url.searchParams.append('since', Time.toUnixTimestamp(startDate).toString());
url.searchParams.append('before', Time.toUnixTimestamp(endDate).toString());
this.$segment.track(SegmentEvent.REPORT_DOWNLOADED, {
start_date: startDate,

View File

@ -0,0 +1,66 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="credits-dropdown" v-click-outside="closeDropdown">
<div @click="redirect" class="credits-dropdown__link-container">
<span class="credits-dropdown__link-container__link">Credits History</span>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { RouteConfig } from '@/router';
@Component
export default class CreditsDropdown extends Vue {
/**
* Holds logic to redirect user to credit history page.
*/
public redirect(): void {
this.$router.push(RouteConfig.Account.with(RouteConfig.CreditsHistory).path);
}
/**
* Closes dropdown.
*/
public closeDropdown(): void {
this.$emit('close');
}
}
</script>
<style scoped lang="scss">
.credits-dropdown {
z-index: 120;
position: absolute;
left: 0;
top: 35px;
background-color: #fff;
border-radius: 6px;
border: 1px solid #c5cbdb;
box-shadow: 0 8px 34px rgba(161, 173, 185, 0.41);
width: 220px;
&__link-container {
width: calc(100% - 30px);
height: 50px;
padding: 0 15px;
display: flex;
align-items: center;
border-radius: 6px;
&:hover {
background-color: #f5f5f7;
}
&__link {
font-size: 14px;
line-height: 19px;
color: #7e8b9c;
}
}
}
</style>

View File

@ -0,0 +1,170 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="credit-history">
<div class="credit-history__back-area" @click="onBackToBillingClick">
<BackImage/>
<p class="credit-history__back-area__title">Back to Billing</p>
</div>
<h1 class="credit-history__title">Free Credits</h1>
<div class="credit-history__content">
<h1 class="credit-history__content__sum">{{ remainingSum | centsToDollars }}</h1>
<span class="credit-history__content__info">Available credits since last bill</span>
<span class="credit-history__content__details">DETAILS</span>
<SortingHeader/>
<CreditsItem
v-for="(item, index) in historyItems"
:key="index"
:credits-item="item"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import CreditsItem from '@/components/account/billing/freeCredits/CreditsItem.vue';
import SortingHeader from '@/components/account/billing/freeCredits/SortingHeader.vue';
import BackImage from '@/../static/images/account/billing/back.svg';
import { RouteConfig } from '@/router';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
@Component({
components: {
CreditsItem,
BackImage,
SortingHeader,
},
})
export default class CreditsHistory extends Vue {
/**
* Returns list of free credit history items.
*/
public get historyItems(): PaymentsHistoryItem[] {
return this.$store.state.paymentsModule.paymentsHistory.filter((item: PaymentsHistoryItem) => {
return item.type === PaymentsHistoryItemType.Coupon;
});
}
/**
* Returns remaining sum of items.
*/
public get remainingSum(): number {
const remainingAmounts: number[] = this.historyItems.map((item: PaymentsHistoryItem) => item.remaining);
return remainingAmounts.reduce((accumulator, current) => accumulator + current);
}
/**
* Replaces location to root billing route.
*/
public onBackToBillingClick(): void {
this.$router.push(RouteConfig.Billing.path);
}
}
</script>
<style scoped lang="scss">
p,
h1 {
margin: 0;
}
.credit-history {
margin-top: 27px;
padding: 0 0 80px 0;
background-color: #f5f6fa;
font-family: 'font_regular', sans-serif;
&__back-area {
display: flex;
align-items: center;
cursor: pointer;
width: 184px;
margin-bottom: 32px;
&__title {
font-family: 'font_medium', sans-serif;
font-weight: 500;
font-size: 16px;
line-height: 21px;
color: #768394;
white-space: nowrap;
margin-left: 15px;
}
&:hover {
.credit-history__back-area__title {
color: #2683ff;
}
.back-button-svg-path {
fill: #2683ff;
}
}
}
&__title {
font-family: 'font_bold', sans-serif;
font-size: 22px;
line-height: 27px;
color: #384b65;
margin-bottom: 20px;
}
&__content {
background-color: #fff;
padding: 40px 40px 30px 40px;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: flex-start;
&__sum {
font-family: 'font_bold', sans-serif;
font-size: 36px;
line-height: 53px;
color: #384b65;
}
&__info {
font-size: 16px;
line-height: 24px;
color: #909090;
margin-bottom: 35px;
}
&__details {
width: 100%;
text-align: left;
font-weight: 500;
font-size: 16px;
line-height: 23px;
letter-spacing: 0.04em;
color: #919191;
padding-bottom: 22px;
border-bottom: 1px solid #c7cdd2;
margin-bottom: 75px;
}
}
}
::-webkit-scrollbar,
::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb {
width: 0;
}
@media (max-height: 1000px) and (max-width: 1230px) {
.credit-history {
overflow-y: scroll;
height: 65vh;
}
}
</style>

View File

@ -0,0 +1,75 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="container">
<p class="container__item">{{ creditType }}</p>
<p class="container__item">{{ creditsItem.status }}</p>
<p class="container__item">{{ memoryAmount }} GB ({{ creditsItem.amount | centsToDollars }})</p>
<p class="container__item available">{{ creditsItem.remaining | centsToDollars }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import PaymentsHistoryItemDate from '@/components/account/billing/depositAndBillingHistory/PaymentsHistoryItemDate.vue';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
@Component({
components: {
PaymentsHistoryItemDate,
},
})
export default class CreditsItem extends Vue {
@Prop({default: () => new PaymentsHistoryItem()})
private readonly creditsItem: PaymentsHistoryItem;
/**
* Return credit type string depending on item type.
*/
public get creditType(): string {
const trial = 'Trial Credit';
const referral = 'Referral Credit';
if (this.creditsItem.type === PaymentsHistoryItemType.Coupon) {
return trial;
}
return referral;
}
/**
* Returns memory amount depending on item's money amount.
*/
public get memoryAmount(): number {
const gbPrice: number = 5.5; // in cents.
return Math.floor(this.creditsItem.amount / gbPrice);
}
}
</script>
<style scoped lang="scss">
.container {
display: flex;
align-items: center;
width: 100%;
&__item {
min-width: 28%;
font-family: 'font_regular', sans-serif;
text-align: left;
margin: 10px 0;
font-size: 14px;
line-height: 19px;
color: #354049;
}
}
.available {
min-width: 16%;
text-align: right;
}
</style>

View File

@ -0,0 +1,53 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="sort-header-container">
<div class="sort-header-container__item">
<p class="sort-header-container__item__name">CREDIT TYPE</p>
</div>
<div class="sort-header-container__item">
<p class="sort-header-container__item__name">STATUS</p>
</div>
<div class="sort-header-container__item">
<p class="sort-header-container__item__name">EARNED AMOUNT</p>
</div>
<div class="sort-header-container__item available">
<p class="sort-header-container__item__name">AVAILABLE</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SortingHeader extends Vue {}
</script>
<style scoped lang="scss">
.sort-header-container {
display: flex;
width: 100%;
&__item {
text-align: left;
min-width: 28%;
&__name {
font-family: 'font_regular', sans-serif;
font-weight: 500;
font-size: 14px;
line-height: 19px;
letter-spacing: 0.03em;
color: #adadad;
margin: 0;
}
}
}
.available {
min-width: 16%;
text-align: right;
}
</style>

View File

@ -7,6 +7,7 @@ import Router, { RouteRecord } from 'vue-router';
import AccountArea from '@/components/account/AccountArea.vue';
import AccountBilling from '@/components/account/billing/BillingArea.vue';
import DetailedHistory from '@/components/account/billing/depositAndBillingHistory/DetailedHistory.vue';
import CreditsHistory from '@/components/account/billing/freeCredits/CreditsHistory.vue';
import SettingsArea from '@/components/account/SettingsArea.vue';
import ApiKeysArea from '@/components/apiKeys/ApiKeysArea.vue';
import Page404 from '@/components/errors/Page404.vue';
@ -43,6 +44,7 @@ export abstract class RouteConfig {
public static Billing = new NavigationLink('billing', 'Billing');
public static BillingHistory = new NavigationLink('billing-history', 'Billing History');
public static DepositHistory = new NavigationLink('deposit-history', 'Deposit History');
public static CreditsHistory = new NavigationLink('credits-history', 'Credits History');
// TODO: disabled until implementation
// public static Referral = new NavigationLink('referral', 'Referral');
@ -57,6 +59,7 @@ export const notProjectRelatedRoutes = [
RouteConfig.Billing.name,
RouteConfig.BillingHistory.name,
RouteConfig.DepositHistory.name,
RouteConfig.CreditsHistory.name,
RouteConfig.Settings.name,
// RouteConfig.Referral.name,
];
@ -111,6 +114,11 @@ export const router = new Router({
name: RouteConfig.DepositHistory.name,
component: DetailedHistory,
},
{
path: RouteConfig.CreditsHistory.path,
name: RouteConfig.CreditsHistory.name,
component: CreditsHistory,
},
// {
// path: RouteConfig.Referral.path,
// name: RouteConfig.Referral.name,

View File

@ -114,6 +114,7 @@ export class PaymentsHistoryItem {
public readonly start: Date = new Date(),
public readonly end: Date = new Date(),
public readonly type: PaymentsHistoryItemType = PaymentsHistoryItemType.Invoice,
public readonly remaining: number = 0,
) {}
public get quantity(): Amount {
@ -155,6 +156,8 @@ export enum PaymentsHistoryItemType {
Charge = 2,
// Coupon is a promotional coupon item.
Coupon = 3,
// DepositBonus is a 10% bonus for using Coinpayments transactions.
DepositBonus = 4,
}
/**

View File

@ -1,10 +1,15 @@
// Copyright (C) 2019 Storj Labs, Inc.
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* toUnixTimestamp converts Date to unix timestamp.
* @param time
* Time holds methods to operate over timestamps.
*/
export function toUnixTimestamp(time: Date): number {
return Math.floor(time.getTime() / 1000);
export class Time {
/**
* toUnixTimestamp converts Date to unix timestamp.
* @param time
*/
public static toUnixTimestamp(time: Date): number {
return Math.floor(time.getTime() / 1000);
}
}

View File

@ -0,0 +1,3 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.372773 0.338888C0.869804 -0.112963 1.67565 -0.112963 2.17268 0.338888L7 4.72741L11.8273 0.338888C12.3243 -0.112963 13.1302 -0.112963 13.6272 0.338888C14.1243 0.790739 14.1243 1.52333 13.6272 1.97519L7 8L0.372773 1.97519C-0.124258 1.52333 -0.124258 0.790739 0.372773 0.338888Z" fill="#768394"/>
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="14px" height="8px" viewBox="0 0 14 8" enable-background="new 0 0 14 8" xml:space="preserve">
<image id="image0" width="14" height="8" x="0" y="0" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAICAMAAAD+zz7+AAAABGdBTUEAALGPC/xhBQAAACBjSFJN AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAXVBMVEX////+//+0u8X8/f2n sLt2g5Srs779/v74+fqbpLGep7P6+vv19veUnqvv8PKMl6WPmafx8vTm6eyDj566wcn+/v6xuMKG kqHp6+6HkqF3g5TBx86krLh/i5vO0tgN414OAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAAYAAAAGAA 8GtCzwAAAAd0SU1FB+QGCQ4gEt/qMf4AAABSSURBVAjXPYxZDoAgEEMHtC4I7oqy3f+YMiH4vvqS tkSMkFLQT9MCXV9tGJFRUzFtclaA0Wzzwk3urxvRfgBn/rluwD70wvky8g6WQkz1MsXwAXmBAxlp 0FISAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIwLTA2LTA5VDE0OjMyOjE4KzAwOjAwc98QGgAAACV0 RVh0ZGF0ZTptb2RpZnkAMjAyMC0wNi0wOVQxNDozMjoxOCswMDowMAKCqKYAAAAASUVORK5CYII="/>
</svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@ -8,7 +8,14 @@ exports[`PeriodSelection renders correctly 1`] = `
</div>
<expandicon-stub></expandicon-stub>
</div>
<!---->
<div class="period-selection__dropdown" style="display: none;">
<div class="period-selection__dropdown__item">
<selectedicon-stub class="selected-image"></selectedicon-stub> <span class="period-selection__dropdown__item__label">Current Billing Period</span>
</div>
<div class="period-selection__dropdown__item">
<!----> <span class="period-selection__dropdown__item__label">Previous Billing Period</span></div>
<div class="period-selection__dropdown__link-container"><span class="period-selection__dropdown__link-container__link">Billing History</span></div>
</div>
</div>
`;
@ -21,7 +28,7 @@ exports[`PeriodSelection renders correctly with dropdown 1`] = `
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6272 7.66111C13.1302 8.11296 12.3243 8.11296 11.8273 7.66111L7 3.27259L2.17268 7.66111C1.67565 8.11296 0.869804 8.11296 0.372774 7.66111C-0.124258 7.20926 -0.124258 6.47666 0.372774 6.02481L7 -6.11959e-07L13.6272 6.02481C14.1243 6.47667 14.1243 7.20926 13.6272 7.66111Z" fill="#2683FF"></path>
</svg>
</div>
<div class="period-selection__dropdown">
<div class="period-selection__dropdown" style="">
<div class="period-selection__dropdown__item"><svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="selected-image">
<path d="M0.495399 5.75514C-0.165133 5.12956 -0.165133 4.1153 0.495399 3.48973C1.15593 2.86415 2.22687 2.86415 2.8874 3.48973L6.87406 7.26541C7.53459 7.89098 7.53459 8.90524 6.87406 9.53082C6.21353 10.1564 5.14259 10.1564 4.48206 9.53082L0.495399 5.75514Z" fill="#2683FF"></path>
<path d="M6.87406 9.53082C6.21353 10.1564 5.14259 10.1564 4.48206 9.53082C3.82153 8.90524 3.82153 7.89098 4.48206 7.26541L11.6581 0.469182C12.3186 -0.156394 13.3895 -0.156394 14.0501 0.469182C14.7106 1.09476 14.7106 2.10902 14.0501 2.73459L6.87406 9.53082Z" fill="#2683FF"></path>

View File

@ -0,0 +1,57 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import sinon from 'sinon';
import { VNode } from 'vue';
import { DirectiveBinding } from 'vue/types/options';
import CreditsDropdown from '@/components/account/billing/freeCredits/CreditsDropdown.vue';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
let clickOutsideEvent: EventListener;
localVue.directive('click-outside', {
bind: function (el: HTMLElement, binding: DirectiveBinding, vnode: VNode) {
clickOutsideEvent = function(event: Event): void {
if (el === event.target) {
return;
}
if (vnode.context) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', clickOutsideEvent);
},
unbind: function(): void {
document.body.removeEventListener('click', clickOutsideEvent);
},
});
describe('CreditsDropdown', (): void => {
it('renders correctly', (): void => {
const wrapper = mount(CreditsDropdown, {
localVue,
});
expect(wrapper).toMatchSnapshot();
});
it('clicks work correctly', async (): Promise<void> => {
const clickSpy = sinon.spy();
const wrapper = mount(CreditsDropdown, {
localVue,
methods: {
redirect: clickSpy,
},
});
await wrapper.find('.credits-dropdown__link-container').trigger('click');
expect(clickSpy.callCount).toBe(1);
});
});

View File

@ -0,0 +1,67 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import sinon from 'sinon';
import Vuex from 'vuex';
import CreditsHistory from '@/components/account/billing/freeCredits/CreditsHistory.vue';
import { PaymentsHttpApi } from '@/api/payments';
import { router } from '@/router';
import { makePaymentsModule, PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { makeProjectsModule, PROJECTS_MUTATIONS } from '@/store/modules/projects';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
import { Project } from '@/types/projects';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ProjectsApiMock } from '../../../mock/api/projects';
const localVue = createLocalVue();
const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const paymentsApi = new PaymentsHttpApi();
const paymentsModule = makePaymentsModule(paymentsApi);
const itemInvoice = new PaymentsHistoryItem('testId', 'Invoice', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Invoice);
const itemCharge = new PaymentsHistoryItem('testId1', 'Charge', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Charge);
const itemTransaction = new PaymentsHistoryItem('testId2', 'Transaction', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Transaction);
const coupon = new PaymentsHistoryItem('testId', 'desc', 275, 0, 'test', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 275);
const coupon1 = new PaymentsHistoryItem('testId', 'desc', 500, 0, 'test', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 300);
const project = new Project('id', 'projectName', 'projectDescription', 'test', 'testOwnerId', false);
const clickSpy = sinon.spy();
localVue.use(Vuex);
localVue.filter('centsToDollars', (cents: number): string => {
return `$${(cents / 100).toFixed(2)}`;
});
const store = new Vuex.Store({ modules: { paymentsModule, projectsModule }});
store.commit(PROJECTS_MUTATIONS.SET_PROJECTS, [project]);
store.commit(PROJECTS_MUTATIONS.SELECT_PROJECT, project.id);
store.commit(PAYMENTS_MUTATIONS.SET_PAYMENTS_HISTORY, [itemInvoice, itemCharge, itemTransaction, coupon, coupon1]);
describe('CreditsHistory', (): void => {
it('renders correctly', (): void => {
const wrapper = shallowMount(CreditsHistory, {
localVue,
store,
router,
});
expect(wrapper).toMatchSnapshot();
});
it('click on back works correctly', async (): Promise<void> => {
const wrapper = shallowMount(CreditsHistory, {
localVue,
store,
router,
methods: {
onBackToBillingClick: clickSpy,
},
});
await wrapper.find('.credit-history__back-area').trigger('click');
expect(clickSpy.callCount).toBe(1);
});
});

View File

@ -0,0 +1,51 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import CreditsItem from '@/components/account/billing/freeCredits/CreditsItem.vue';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
const couponActive = new PaymentsHistoryItem('testId', 'desc', 275, 0, 'Active', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 275);
const couponExpired = new PaymentsHistoryItem('testId', 'desc', 275, 0, 'Expired', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 0);
const couponUsed = new PaymentsHistoryItem('testId', 'desc', 500, 0, 'Used', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 0);
localVue.filter('centsToDollars', (cents: number): string => {
return `$${(cents / 100).toFixed(2)}`;
});
describe('CreditsItem', (): void => {
it('renders correctly if not expired', (): void => {
const wrapper = mount(CreditsItem, {
localVue,
propsData: {
creditsItem: couponActive,
},
});
expect(wrapper).toMatchSnapshot();
});
it('renders correctly if expired', (): void => {
const wrapper = mount(CreditsItem, {
localVue,
propsData: {
creditsItem: couponExpired,
},
});
expect(wrapper).toMatchSnapshot();
});
it('renders correctly if used', (): void => {
const wrapper = mount(CreditsItem, {
localVue,
propsData: {
creditsItem: couponUsed,
},
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,18 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import SortingHeader from '@/components/account/billing/freeCredits/SortingHeader.vue';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
describe('SortingHeader', (): void => {
it('renders correctly', (): void => {
const wrapper = mount(SortingHeader, {
localVue,
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditsDropdown renders correctly 1`] = `
<div class="credits-dropdown">
<div class="credits-dropdown__link-container"><span class="credits-dropdown__link-container__link">Credits History</span></div>
</div>
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditsHistory renders correctly 1`] = `
<div class="credit-history">
<div class="credit-history__back-area">
<backimage-stub></backimage-stub>
<p class="credit-history__back-area__title">Back to Billing</p>
</div>
<h1 class="credit-history__title">Free Credits</h1>
<div class="credit-history__content">
<h1 class="credit-history__content__sum">$5.75</h1> <span class="credit-history__content__info">Available credits since last bill</span> <span class="credit-history__content__details">DETAILS</span>
<sortingheader-stub></sortingheader-stub>
<creditsitem-stub creditsitem="[object Object]"></creditsitem-stub>
<creditsitem-stub creditsitem="[object Object]"></creditsitem-stub>
</div>
</div>
`;

View File

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditsItem renders correctly if expired 1`] = `
<div class="container">
<p class="container__item">Trial Credit</p>
<p class="container__item">Expired</p>
<p class="container__item">50 GB ($2.75)</p>
<p class="container__item available">$0.00</p>
</div>
`;
exports[`CreditsItem renders correctly if not expired 1`] = `
<div class="container">
<p class="container__item">Trial Credit</p>
<p class="container__item">Active</p>
<p class="container__item">50 GB ($2.75)</p>
<p class="container__item available">$2.75</p>
</div>
`;
exports[`CreditsItem renders correctly if used 1`] = `
<div class="container">
<p class="container__item">Trial Credit</p>
<p class="container__item">Used</p>
<p class="container__item">90 GB ($5.00)</p>
<p class="container__item available">$0.00</p>
</div>
`;

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SortingHeader renders correctly 1`] = `
<div class="sort-header-container">
<div class="sort-header-container__item">
<p class="sort-header-container__item__name">CREDIT TYPE</p>
</div>
<div class="sort-header-container__item">
<p class="sort-header-container__item__name">STATUS</p>
</div>
<div class="sort-header-container__item">
<p class="sort-header-container__item__name">EARNED AMOUNT</p>
</div>
<div class="sort-header-container__item available">
<p class="sort-header-container__item__name">AVAILABLE</p>
</div>
</div>
`;