web/satellite: token payments logic (#3581)

This commit is contained in:
Nikolay Yurchenko 2019-11-25 14:59:41 +02:00 committed by GitHub
parent 1aa2bc0a83
commit 8234e24d13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 416 additions and 118 deletions

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge } from '@/types/payments';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge, TokenDeposit } from '@/types/payments';
import { HttpClient } from '@/utils/httpClient';
/**
@ -194,6 +194,7 @@ export class PaymentsHttpApi implements PaymentsApi {
item.id,
item.description,
item.amount,
item.tokenReceived,
item.status,
item.link,
new Date(item.start),
@ -204,4 +205,26 @@ export class PaymentsHttpApi implements PaymentsApi {
return [];
}
/**
* makeTokenDeposit process coin payments
* @param amount
* @throws Error
*/
public async makeTokenDeposit(amount: number): Promise<TokenDeposit> {
const path = `${this.ROOT_PATH}/tokens/deposit`;
const response = await this.client.post(path, JSON.stringify({ amount }));
if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not process coin payment');
}
const result = await response.json();
return new TokenDeposit(result.amount, result.address);
}
}

View File

@ -30,7 +30,7 @@
import { Component, Vue } from 'vue-property-decorator';
import AccountBalance from '@/components/account/billing/balance/AccountBalance.vue';
import DepositAndBilling from '@/components/account/billing/depositAndBilling/DepositAndBilling.vue';
import DepositAndBilling from '@/components/account/billing/billingHistory/DepositAndBilling.vue';
import MonthlyBillingSummary from '@/components/account/billing/monthlySummary/MonthlyBillingSummary.vue';
import PaymentMethods from '@/components/account/billing/paymentMethods/PaymentMethods.vue';

View File

@ -31,8 +31,8 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BillingItem from '@/components/account/billing/depositAndBilling/BillingItem.vue';
import SortingHeader from '@/components/account/billing/depositAndBilling/SortingHeader.vue';
import BillingItem from '@/components/account/billing/billingHistory/BillingItem.vue';
import SortingHeader from '@/components/account/billing/billingHistory/SortingHeader.vue';
import VPagination from '@/components/common/VPagination.vue';
import { RouteConfig } from '@/router';

View File

@ -0,0 +1,96 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="countdown-container">
<div v-if="isExpired">{{date}}</div>
<div class="row" v-else>
<p>Expires in </p>
<p class="digit margin">{{ hours | leadingZero }}</p>
<p>:</p>
<p class="digit">{{ minutes | leadingZero }}</p>
<p>:</p>
<p class="digit">{{ seconds | leadingZero }}</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { BillingHistoryItemType } from '@/types/payments';
@Component
export default class BillingHistoryDate extends Vue {
@Prop({default: () => new Date()})
private readonly expiration: Date;
@Prop({default: () => new Date()})
private readonly start: Date;
@Prop({default: 0})
private readonly type: BillingHistoryItemType;
private readonly expirationTimeInSeconds: number;
private nowInSeconds = Math.trunc(new Date().getTime() / 1000);
private intervalID: number;
public isExpired: boolean;
public constructor() {
super();
this.expirationTimeInSeconds = Math.trunc(new Date(this.expiration).getTime() / 1000);
this.isExpired = (this.expirationTimeInSeconds - this.nowInSeconds) < 0;
this.ready();
}
public get date(): string {
if (this.type === BillingHistoryItemType.Transaction) {
return this.start.toLocaleDateString();
}
return `${this.start.toLocaleDateString()} - ${this.expiration.toLocaleDateString()}`;
}
public get seconds(): number {
return (this.expirationTimeInSeconds - this.nowInSeconds) % 60;
}
public get minutes(): number {
return Math.trunc((this.expirationTimeInSeconds - this.nowInSeconds) / 60) % 60;
}
public get hours(): number {
return Math.trunc((this.expirationTimeInSeconds - this.nowInSeconds) / 3600) % 24;
}
private ready(): void {
this.intervalID = window.setInterval(() => {
if ((this.expirationTimeInSeconds - this.nowInSeconds) < 0) {
this.isExpired = true;
clearInterval(this.intervalID);
return;
}
this.nowInSeconds = Math.trunc(new Date().getTime() / 1000);
}, 1000);
}
}
</script>
<style scoped lang="scss">
.digit {
font-family: 'font_bold', sans-serif;
}
.margin {
margin-left: 5px;
}
.row {
display: flex;
align-items: center;
justify-content: flex-start;
}
</style>

View File

@ -0,0 +1,103 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="container">
<BillingHistoryItemDate
class="container__item"
:start="billingItem.start"
:expiration="billingItem.end"
:type="billingItem.type"
/>
<p class="container__item description">{{billingItem.description}}</p>
<p class="container__item status">{{billingItem.formattedStatus}}</p>
<p class="container__item amount">
<b>
{{billingItem.quantity.currency}}
<span v-if="billingItem.type === 1">
{{billingItem.quantity.received}}
</span>
<span v-else>
{{billingItem.quantity.total}}
</span>
</b>
<span v-if="billingItem.type === 1">
of {{billingItem.quantity.total}}
</span>
</p>
<p class="container__item download" v-html="billingItem.downloadLinkHtml()"></p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import BillingHistoryItemDate from '@/components/account/billing/billingHistory/BillingHistoryItemDate.vue';
import { BillingHistoryItem } from '@/types/payments';
@Component({
components: {
BillingHistoryItemDate,
},
})
export default class BillingItem extends Vue {
@Prop({default: () => new BillingHistoryItem()})
private readonly billingItem: BillingHistoryItem;
}
</script>
<style scoped lang="scss">
.download-link {
color: #2683ff;
font-family: 'font_bold', sans-serif;
&:hover {
color: #0059d0;
}
}
.container {
display: flex;
padding: 0 30px;
align-items: center;
width: calc(100% - 60px);
border-top: 1px solid rgba(169, 181, 193, 0.3);
&__item {
width: 20%;
font-family: 'font_medium', sans-serif;
font-size: 16px;
text-align: left;
color: #61666b;
}
}
.description {
width: 31%;
}
.status {
width: 12%;
}
.amount {
width: 27%;
margin: 0;
}
.download {
margin: 0;
text-align: right;
min-width: 142px;
width: 10%;
}
.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 175px;
}
</style>

View File

@ -19,8 +19,8 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BillingItem from '@/components/account/billing/depositAndBilling/BillingItem.vue';
import SortingHeader from '@/components/account/billing/depositAndBilling/SortingHeader.vue';
import BillingItem from '@/components/account/billing/billingHistory/BillingItem.vue';
import SortingHeader from '@/components/account/billing/billingHistory/SortingHeader.vue';
import { RouteConfig } from '@/router';
import { BillingHistoryItem } from '@/types/payments';

View File

@ -3,10 +3,10 @@
<template>
<div class="sort-header-container">
<div class="sort-header-container__item">
<div class="sort-header-container__item date">
<p class="sort-header-container__item__name">Date</p>
</div>
<div class="sort-header-container__item">
<div class="sort-header-container__item description">
<p class="sort-header-container__item__name">Description</p>
</div>
<div class="sort-header-container__item status">
@ -35,8 +35,7 @@ export default class SortingHeader extends Vue {}
&__item {
text-align: left;
width: 35%;
margin-right: 10px;
width: 20%;
&__name {
font-family: 'font_medium', sans-serif;
@ -47,17 +46,22 @@ export default class SortingHeader extends Vue {}
}
}
.description {
width: 31%;
}
.status {
width: 15%;
width: 12%;
}
.amount {
width: 15%;
width: 27%;
margin: 0;
}
.download {
margin: 0;
min-width: 130px;
width: 10%;
}
</style>

View File

@ -1,67 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="container">
<p class="container__item">{{billingItem.date()}}</p>
<p class="container__item">{{billingItem.description}}</p>
<p class="container__item status">{{billingItem.status}}</p>
<p class="container__item amount"><b>{{billingItem.amountDollars()}}</b></p>
<p class="container__item download" v-html="billingItem.downloadLinkHtml()"></p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { BillingHistoryItem } from '@/types/payments';
@Component
export default class BillingItem extends Vue {
@Prop({default: new BillingHistoryItem()})
private readonly billingItem: BillingHistoryItem;
}
</script>
<style scoped lang="scss">
.download-link {
color: #2683ff;
font-family: 'font_bold', sans-serif;
&:hover {
color: #0059d0;
}
}
.container {
display: flex;
padding: 0 30px;
align-items: center;
width: calc(100% - 60px);
border-top: 1px solid rgba(169, 181, 193, 0.3);
&__item {
width: 35%;
font-family: 'font_medium', sans-serif;
font-size: 16px;
text-align: left;
margin-right: 10px;
color: #61666b;
}
}
.status {
width: 15%;
}
.amount {
width: 15%;
margin: 0;
}
.download {
margin: 0;
text-align: right;
min-width: 142px;
}
</style>

View File

@ -30,7 +30,7 @@
<div class="payment-methods-area__adding-container storj" v-if="isAddingStorjState">
<div class="storj-container">
<p class="storj-container__label">Deposit STORJ Tokens via Coin Payments</p>
<StorjInput class="form"/>
<TokenDepositSelection class="form" @onChangeTokenValue="onChangeTokenValue"/>
</div>
<VButton
label="Continue to Coin Payments"
@ -41,9 +41,9 @@
</div>
<div class="payment-methods-area__adding-container card" v-if="isAddingCardState">
<p class="payment-methods-area__adding-container__label">Add Credit or Debit Card</p>
<StripeInput
<StripeCardInput
class="payment-methods-area__adding-container__stripe"
ref="stripeInput"
ref="stripeCardInput"
:on-stripe-response-callback="addCard"
/>
<VButton
@ -67,8 +67,8 @@
import { Component, Vue } from 'vue-property-decorator';
import CardComponent from '@/components/account/billing/paymentMethods/CardComponent.vue';
import StorjInput from '@/components/account/billing/paymentMethods/StorjInput.vue';
import StripeInput from '@/components/account/billing/paymentMethods/StripeInput.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import TokenDepositSelection from '@/components/account/billing/paymentMethods/TokenDepositSelection.vue';
import VButton from '@/components/common/VButton.vue';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
@ -79,6 +79,8 @@ import { PaymentMethodsBlockState } from '@/utils/constants/billingEnums';
const {
ADD_CREDIT_CARD,
GET_CREDIT_CARDS,
MAKE_TOKEN_DEPOSIT,
GET_BILLING_HISTORY,
} = PAYMENTS_ACTIONS;
interface StripeForm {
@ -89,13 +91,16 @@ interface StripeForm {
components: {
VButton,
CardComponent,
StorjInput,
StripeInput,
TokenDepositSelection,
StripeCardInput,
},
})
export default class PaymentMethods extends Vue {
private areaState: number = PaymentMethodsBlockState.DEFAULT;
private isLoading: boolean = false;
private readonly DEFAULT_TOKEN_DEPOSIT_VALUE = 20;
private readonly MAX_TOKEN_AMOUNT_IN_DOLLARS = 1000000;
private tokenDepositValue: number = this.DEFAULT_TOKEN_DEPOSIT_VALUE;
public mounted() {
try {
@ -106,7 +111,7 @@ export default class PaymentMethods extends Vue {
}
public $refs!: {
stripeInput: StripeInput & StripeForm;
stripeCardInput: StripeCardInput & StripeForm;
};
public get creditCards(): CreditCard[] {
@ -123,6 +128,10 @@ export default class PaymentMethods extends Vue {
return this.areaState === PaymentMethodsBlockState.ADDING_CARD;
}
public onChangeTokenValue(value: number): void {
this.tokenDepositValue = value;
}
public onAddSTORJ(): void {
this.areaState = PaymentMethodsBlockState.ADDING_STORJ;
@ -135,16 +144,43 @@ export default class PaymentMethods extends Vue {
}
public onCancel(): void {
this.areaState = PaymentMethodsBlockState.DEFAULT;
this.tokenDepositValue = this.DEFAULT_TOKEN_DEPOSIT_VALUE;
return;
}
public onConfirmAddSTORJ(): void {
/**
* onConfirmAddSTORJ checks if amount is valid and if so process token
* payment and return state to default
*/
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');
this.tokenDepositValue = this.DEFAULT_TOKEN_DEPOSIT_VALUE;
this.areaState = PaymentMethodsBlockState.DEFAULT;
return;
}
try {
const tokenResponse = await this.$store.dispatch(MAKE_TOKEN_DEPOSIT, this.tokenDepositValue * 100);
await this.$notify.success(`Successfully created new deposit transaction! \nAddress:${tokenResponse.address} \nAmount:${tokenResponse.amount}`);
} catch (error) {
await this.$notify.error(error.message);
}
this.tokenDepositValue = this.DEFAULT_TOKEN_DEPOSIT_VALUE;
try {
await this.$store.dispatch(GET_BILLING_HISTORY);
} catch (error) {
await this.$notify.error(error.message);
}
this.areaState = PaymentMethodsBlockState.DEFAULT;
}
public async onConfirmAddStripe(): Promise<void> {
await this.$refs.stripeInput.onSubmit();
await this.$refs.stripeCardInput.onSubmit();
}
public async addCard(token: string) {

View File

@ -17,9 +17,9 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
import { MetaUtils } from '@/utils/meta';
// StripeInput encapsulates Stripe add card addition logic
// StripeCardInput encapsulates Stripe add card addition logic
@Component
export default class StripeInput extends Vue {
export default class StripeCardInput extends Vue {
@Prop({default: () => console.error('onStripeResponse is not reinitialized')})
private readonly onStripeResponseCallback: (result: any) => void;

View File

@ -17,8 +17,8 @@
class="options-container__item"
v-for="option in paymentOptions"
:key="option.label"
@click.prevent="select(option)">
@click.prevent.stop="select(option)"
>
<div class="options-container__item__svg" v-if="option.value === current.value">
<svg width="15" height="13" viewBox="0 0 15 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0928 3.02746C14.6603 2.4239 14.631 1.4746 14.0275 0.907152C13.4239 0.339699 12.4746 0.368972 11.9072 0.972536L14.0928 3.02746ZM4.53846 11L3.44613 12.028C3.72968 12.3293 4.12509 12.5001 4.53884 12.5C4.95258 12.4999 5.34791 12.3289 5.63131 12.0275L4.53846 11ZM3.09234 7.27469C2.52458 6.67141 1.57527 6.64261 0.971991 7.21036C0.36871 7.77812 0.339911 8.72743 0.907664 9.33071L3.09234 7.27469ZM11.9072 0.972536L3.44561 9.97254L5.63131 12.0275L14.0928 3.02746L11.9072 0.972536ZM5.6308 9.97199L3.09234 7.27469L0.907664 9.33071L3.44613 12.028L5.6308 9.97199Z" fill="#2683FF"/>
@ -30,7 +30,13 @@
</div>
</div>
<label class="label" v-if="isCustomAmount">
<input class="custom-input" type="number" placeholder="Enter Amount" v-model="customAmount">
<input
v-number
class="custom-input"
placeholder="Enter Amount"
v-model="customAmount"
@input="onCustomAmountChange"
>
<div class="input-svg" @click="toggleCustomAmount">
<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="#2683FF"/>
@ -47,33 +53,64 @@ import { PaymentAmountOption } from '@/types/payments';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
@Component
export default class StorjInput extends Vue {
export default class TokenDepositSelection extends Vue {
/**
* Set of default payment options
*/
public paymentOptions: PaymentAmountOption[] = [
new PaymentAmountOption(20, `US $20 (+5 Bonus)`),
new PaymentAmountOption(5, `US $5`),
new PaymentAmountOption(10, `US $10 (+2 Bonus)`),
new PaymentAmountOption(100, `US $100 (+20 Bonus)`),
new PaymentAmountOption(1000, `US $1000 (+200 Bonus)`),
new PaymentAmountOption(20, `USD $20`),
new PaymentAmountOption(5, `USD $5`),
new PaymentAmountOption(10, `USD $10`),
new PaymentAmountOption(100, `USD $100`),
new PaymentAmountOption(1000, `USD $1000`),
];
public current: PaymentAmountOption = new PaymentAmountOption(20, `US $20 (+$5 Bonus)`);
public customAmount: number = 0;
public current: PaymentAmountOption = this.paymentOptions[0];
public customAmount: string = '';
public isCustomAmount = false;
/**
* isSelectionShown flag that indicate is token amount selection shown
*/
public get isSelectionShown(): boolean {
return this.$store.state.appStateModule.appState.isPaymentSelectionShown;
}
/**
* toggleSelection toggles token amount selection
*/
public toggleSelection(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_PAYMENT_SELECTION);
}
public toggleCustomAmount(): void {
this.isCustomAmount = !this.isCustomAmount;
/**
* onCustomAmountChange input event handle that emits value to parent component
*/
public onCustomAmountChange(): void {
this.$emit('onChangeTokenValue', parseInt(this.customAmount, 10));
}
public select(value: PaymentAmountOption): void {
this.current = value;
/**
* toggleCustomAmount toggles custom amount input and changes token value in parent
*/
public toggleCustomAmount(): void {
this.isCustomAmount = !this.isCustomAmount;
if (this.isCustomAmount) {
this.$emit('onChangeTokenValue', 0);
return;
}
this.$emit('onChangeTokenValue', this.paymentOptions[0].value);
}
/**
* select standard value from list and emits it value to parent component
*/
public select(option: PaymentAmountOption): void {
this.current = option;
this.$emit('onChangeTokenValue', option.value);
this.toggleSelection();
}
}
@ -91,6 +128,7 @@ export default class StorjInput extends Vue {
font-size: 16px;
line-height: 28px;
color: #354049;
-moz-appearance: textfield;
}
.custom-input::-webkit-inner-spin-button,

View File

@ -39,6 +39,34 @@ Vue.directive('click-outside', {
},
});
/**
* number directive allow user to type only numbers in input
*/
Vue.directive('number', {
bind (el: HTMLElement) {
el.addEventListener('keydown', (e: KeyboardEvent) => {
const keyCode = parseInt(e.key);
if (!isNaN(keyCode) || e.key === 'Delete' || e.key === 'Backspace') {
return;
}
e.preventDefault();
});
},
});
/**
* leadingZero adds zero to the start of single digit number
*/
Vue.filter('leadingZero', function (value: number): string {
if (value <= 9) {
return `0${value}`;
}
return `${value}`;
});
/**
* centsToDollars is a Vue filter that converts amount of cents in dollars string.
*/

View File

@ -6,7 +6,7 @@ import Router, { RouteRecord } from 'vue-router';
import AccountArea from '@/components/account/AccountArea.vue';
import AccountBilling from '@/components/account/billing/BillingArea.vue';
import BillingHistory from '@/components/account/billing/depositAndBilling/BillingHistory.vue';
import BillingHistory from '@/components/account/billing/billingHistory/BillingHistory.vue';
import ProfileArea from '@/components/account/ProfileArea.vue';
import ApiKeysArea from '@/components/apiKeys/ApiKeysArea.vue';
import BucketArea from '@/components/buckets/BucketArea.vue';

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge } from '@/types/payments';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge, TokenDeposit } from '@/types/payments';
const PAYMENTS_MUTATIONS = {
SET_BALANCE: 'SET_BALANCE',
@ -25,6 +25,7 @@ export const PAYMENTS_ACTIONS = {
MAKE_CARD_DEFAULT: 'makeCardDefault',
REMOVE_CARD: 'removeCard',
GET_BILLING_HISTORY: 'getBillingHistory',
MAKE_TOKEN_DEPOSIT: 'makeTokenDeposit',
GET_PROJECT_CHARGES: 'getProjectCharges',
};
@ -49,6 +50,7 @@ const {
MAKE_CARD_DEFAULT,
REMOVE_CARD,
GET_BILLING_HISTORY,
MAKE_TOKEN_DEPOSIT,
GET_PROJECT_CHARGES,
} = PAYMENTS_ACTIONS;
@ -157,6 +159,9 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
commit(SET_BILLING_HISTORY, billingHistory);
},
[MAKE_TOKEN_DEPOSIT]: async function({commit}: any, amount: number): Promise<TokenDeposit> {
return await api.makeTokenDeposit(amount);
},
[GET_PROJECT_CHARGES]: async function({commit}: any): Promise<void> {
const charges: ProjectCharge[] = await api.projectsCharges();

View File

@ -61,6 +61,14 @@ export interface PaymentsApi {
* @throws Error
*/
billingHistory(): Promise<BillingHistoryItem[]>;
/**
* Creates token transaction in CoinPayments
*
* @param amount
* @throws Error
*/
makeTokenDeposit(amount: number): Promise<TokenDeposit>;
}
export class CreditCard {
@ -89,29 +97,38 @@ export class BillingHistoryItem {
public readonly id: string = '',
public readonly description: string = '',
public readonly amount: number = 0,
public readonly received: number = 0,
public readonly status: string = '',
public readonly link: string = '',
public readonly start: Date = new Date(),
public readonly end: Date = new Date(),
public readonly type: BillingHistoryItemType = 0,
public readonly type: BillingHistoryItemType = BillingHistoryItemType.Invoice,
) {}
public date(): string {
if (this.type) {
return this.start.toLocaleDateString();
public get quantity(): Amount {
if (this.type === BillingHistoryItemType.Invoice) {
return new Amount('$', this.amountDollars(this.amount));
}
return `${this.start.toLocaleDateString()} - ${this.end.toLocaleDateString()}`;
return new Amount('$', this.amountDollars(this.amount), this.amountDollars(this.received));
}
public amountDollars(): string {
return `$${this.amount / 100}`;
public get formattedStatus(): string {
return this.status.charAt(0).toUpperCase() + this.status.substring(1);
}
private amountDollars(amount): number {
return amount / 100;
}
public downloadLinkHtml(): string {
const downloadLabel = this.type === 1 ? 'EtherScan' : 'PDF';
if (!this.link) {
return '';
}
return `<a class="download-link" href="${this.link}">${downloadLabel}</a>`;
const downloadLabel = this.type === BillingHistoryItemType.Transaction ? 'Checkout' : 'PDF';
return `<a class="download-link" target="_blank" href="${this.link}">${downloadLabel}</a>`;
}
}
@ -123,6 +140,20 @@ export enum BillingHistoryItemType {
Transaction = 1,
}
// TokenDeposit holds public information about token deposit
export class TokenDeposit {
constructor(public amount: number, public address: string) {}
}
// Amount holds information for displaying billing item payment
class Amount {
public constructor(
public currency: string = '',
public total: number = 0,
public received: number = 0,
) {}
}
/**
* ProjectCharge shows how much money current project will charge in the end of the month.
*/

View File

@ -1,6 +1,7 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
// TODO: move functions to Validator class
export function validateEmail(email: string): boolean {
const rgx = /.*@.*\..*$/;

View File

@ -4,7 +4,7 @@
import {
validateEmail,
validatePassword,
} from '@/utils/validation';
} from '@/utils/validation';
describe('validation', () => {
it('validatePassword regex works correctly', () => {