web/satellite: see token payments history

This change lists token payment histories from storjscan/coinpayments on the "Payment Methods > Storj Tokens" page

see: https://github.com/storj/storj/issues/4941

Change-Id: I178ba7940c4cb132f05673607030725a4c4b3e1c
This commit is contained in:
Wilfred Asomani 2022-08-23 10:37:46 +00:00
parent fe3aedebe8
commit 05e57edb20
11 changed files with 607 additions and 414 deletions

View File

@ -11,7 +11,9 @@ import {
PaymentsApi,
PaymentsHistoryItem,
ProjectUsageAndCharges,
TokenAmount,
TokenDeposit,
NativePaymentHistoryItem,
Wallet,
} from '@/types/payments';
import { HttpClient } from '@/utils/httpClient';
@ -236,6 +238,43 @@ export class PaymentsHttpApi implements PaymentsApi {
return [];
}
/**
* Returns a list of native token payments.
*
* @returns list of native token payment history items
* @throws Error
*/
public async nativePaymentsHistory(): Promise<NativePaymentHistoryItem[]> {
const path = `${this.ROOT_PATH}/wallet/payments`;
const response = await this.client.get(path);
if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not list token payment history');
}
const json = await response.json();
if (!json) return [];
if (json.payments) {
return json.payments.map(item =>
new NativePaymentHistoryItem(
item.ID,
item.Wallet,
item.Type,
new TokenAmount(item.Amount.value, item.Amount.currency),
new TokenAmount(item.Received.value, item.Received.currency),
item.Status,
item.Link,
new Date(item.Timestamp),
),
);
}
return [];
}
/**
* makeTokenDeposit process coin payments.
*

View File

@ -26,11 +26,16 @@
import { Component, Vue } from 'vue-property-decorator';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import BillingHistoryHeader from '@/components/account/billing/billingTabs/BillingHistoryHeader.vue';
import BillingHistoryItem from '@/components/account/billing/billingTabs/BillingHistoryItem.vue';
import VTable from '@/components/common/VTable.vue';
const {
GET_PAYMENTS_HISTORY,
} = PAYMENTS_ACTIONS;
// @vue/component
@Component({
components: {
@ -42,6 +47,18 @@ import VTable from '@/components/common/VTable.vue';
export default class BillingArea extends Vue {
mounted(): void {
this.fetchHistory();
}
public async fetchHistory(): Promise<void> {
try {
await this.$store.dispatch(GET_PAYMENTS_HISTORY);
} catch (error) {
await this.$notify.error(error.message);
}
}
public get historyItems(): PaymentsHistoryItem[] {
return this.$store.state.paymentsModule.paymentsHistory.filter((item: PaymentsHistoryItem) => {
return item.type === PaymentsHistoryItemType.Invoice || item.type === PaymentsHistoryItemType.Charge;

View File

@ -5,42 +5,21 @@
<div class="payments-area">
<div class="payments-area__top-container">
<h1 class="payments-area__title">
Payment Methods{{ showTransactions? ' > Storj Tokens':null }}
<span class="payments-area__title__back" @click="hideTransactionsTable">Payment Methods</span>{{ showTransactions? ' > Storj Tokens':null }}
</h1>
<VButton
v-if="showTransactions"
label="Add Funds with CoinPayments"
font-size="13px"
height="40px"
width="220px"
:on-press="showAddFundsCard"
/>
</div>
<div v-if="!showTransactions" class="payments-area__container">
<add-token-card-native v-if="nativeTokenPaymentsEnabled" />
<template v-else>
<v-loader
v-if="!tokensAreLoaded"
/>
<div v-else-if="!showAddFunds">
<balance-token-card
v-for="item in mostRecentTransaction"
:key="item.id"
:v-if="tokensAreLoaded"
:billing-item="item"
:show-add-funds="showAddFunds"
@showTransactions="toggleTransactionsTable"
@toggleShowAddFunds="toggleShowAddFunds"
/>
</div>
<div v-else>
<add-token-card
:total-count="transactionCount"
@toggleShowAddFunds="toggleShowAddFunds"
@fetchHistory="addTokenHelper"
/>
</div>
</template>
<v-loader
v-if="nativePayIsLoading"
/>
<add-token-card-native
v-else-if="nativeTokenPaymentsEnabled && wallet.address"
@showTransactions="showTransactionsTable"
/>
<add-token-card
v-else
:total-count="transactionCount"
/>
<div v-for="card in creditCards" :key="card.id" class="payments-area__container__cards">
<CreditCardContainer
:credit-card="card"
@ -135,59 +114,80 @@
</div>
</div>
<div v-if="showTransactions">
<div class="payments-area__container__transactions">
<SortingHeader2
@sortFunction="sortFunction"
/>
<token-transaction-item
v-for="item in displayedHistory"
:key="item.id"
:billing-item="item"
/>
<div class="divider" />
<div class="pagination">
<div class="pagination__total">
<p>
{{ transactionCount }} transactions found
<div class="payments-area__address-card">
<div class="payments-area__address-card__left">
<canvas ref="canvas" class="payments-area__address-card__left__canvas" />
<div class="payments-area__address-card__left__balance">
<p class="payments-area__address-card__left__balance__label">
Available Balance (USD)
</p>
<p class="payments-area__address-card__left__balance__value">
{{ wallet.balance.value }}
</p>
</div>
<div class="pagination__right-container">
<div
v-if="transactionCount > 0"
class="pagination__right-container__count"
>
<span v-if="transactionCount > 10 && paginationLocation.end !== transactionCount">
{{ paginationLocation.start + 1 }} - {{ paginationLocation.end }} of {{ transactionCount }}
</span>
<span v-else>
{{ paginationLocation.start + 1 }} - {{ transactionCount }} of {{ transactionCount }}
</span>
</div>
<div
v-if="transactionCount > 10"
class="pagination__right-container__buttons"
>
<ArrowIcon
class="pagination__right-container__buttons__left"
@click="paginationController(-10)"
/>
<ArrowIcon
v-if="paginationLocation.end < transactionCount - 1"
class="pagination__right-container__buttons__right"
@click="paginationController(10)"
/>
</div>
</div>
<div class="payments-area__address-card__right">
<div class="payments-area__address-card__right__address">
<p class="payments-area__address-card__right__address__label">
Deposit Address
</p>
<p class="payments-area__address-card__right__address__value">
{{ wallet.address }}
</p>
</div>
<div class="payments-area__address-card__right__copy">
<VButton
class="modal__address__copy-button"
label="Copy Address"
width="8.6rem"
height="2.5rem"
font-size="0.9rem"
icon="copy"
:on-press="onCopyAddressClick"
/>
</div>
</div>
</div>
<div class="payments-area__transactions-area">
<h2 class="payments-area__transactions-area__title">Transactions</h2>
<v-table
class="payments-area__transactions-area__table"
items-label="transactions"
:items="displayedHistory"
:limit="pageSize"
:total-page-count="pageCount"
:total-items-count="transactionCount"
:on-page-click-callback="paginationController"
>
<template #head>
<SortingHeader2
@sortFunction="sortFunction"
/>
</template>
<template #body>
<token-transaction-item
v-for="item in displayedHistory"
:key="item.id"
:item="item"
/>
</template>
</v-table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import QRCode from 'qrcode';
import { CreditCard, PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
import {
CreditCard,
Wallet,
NativePaymentHistoryItem,
} from '@/types/payments';
import { USER_ACTIONS } from '@/store/modules/users';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { RouteConfig } from '@/router';
@ -200,12 +200,11 @@ import VLoader from '@/components/common/VLoader.vue';
import CreditCardContainer from '@/components/account/billing/billingTabs/CreditCardContainer.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import SortingHeader2 from '@/components/account/billing/depositAndBillingHistory/SortingHeader2.vue';
import BalanceTokenCard from '@/components/account/billing/paymentMethods/BalanceTokenCard.vue';
import AddTokenCard from '@/components/account/billing/paymentMethods/AddTokenCard.vue';
import AddTokenCardNative from '@/components/account/billing/paymentMethods/AddTokenCardNative.vue';
import TokenTransactionItem from '@/components/account/billing/paymentMethods/TokenTransactionItem.vue';
import VTable from '@/components/common/VTable.vue';
import ArrowIcon from '@/../static/images/common/arrowRight.svg';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import AmericanExpressIcon from '@/../static/images/payments/cardIcons/smallamericanexpress.svg';
import DinersIcon from '@/../static/images/payments/cardIcons/smalldinersclub.svg';
@ -229,15 +228,13 @@ const {
GET_CREDIT_CARDS,
REMOVE_CARD,
MAKE_CARD_DEFAULT,
GET_PAYMENTS_HISTORY,
GET_NATIVE_PAYMENTS_HISTORY,
} = PAYMENTS_ACTIONS;
const paginationStartNumber = 0;
const paginationEndNumber = 10;
// @vue/component
@Component({
components: {
VTable,
AmericanExpressIcon,
DiscoverIcon,
JCBIcon,
@ -247,14 +244,12 @@ const paginationEndNumber = 10;
VButton,
TokenTransactionItem,
SortingHeader2,
ArrowIcon,
CloseCrossIcon,
CreditCardImage,
StripeCardInput,
DinersIcon,
Trash,
CreditCardContainer,
BalanceTokenCard,
AddTokenCard,
AddTokenCardNative,
VLoader,
@ -267,14 +262,10 @@ export default class PaymentMethods extends Vue {
* controls token inputs and transaction table
*/
public showTransactions = false;
public showAddFunds = false;
public mostRecentTransaction: PaymentsHistoryItem[] = [];
public paginationLocation: {start: number, end: number} = { start: paginationStartNumber, end: paginationEndNumber };
public tokenHistory: {amount: number, start: Date, status: string,}[] = [];
public displayedHistory: Record<string, unknown>[] = [];
public displayedHistory: NativePaymentHistoryItem[] = [];
public transactionCount = 0;
public tokensAreLoaded = false;
public reloadKey = 0;
public nativePayIsLoading = true;
public pageSize = 10;
/**
* controls card inputs
@ -286,61 +277,67 @@ export default class PaymentMethods extends Vue {
public isChangeDefaultPaymentModalOpen = false;
public defaultCreditCardSelection = '';
public isRemovePaymentMethodsModalOpen = false;
public isAddCardClicked = false;
public $refs!: {
stripeCardInput: StripeCardInput & StripeForm;
canvas: HTMLCanvasElement;
};
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
public beforeMount() {
this.fetchHistory();
public mounted(): void {
this.claimWallet();
}
public addTokenHelper(): void {
this.fetchHistory();
this.toggleShowAddFunds();
private get wallet(): Wallet {
return this.$store.state.paymentsModule.wallet;
}
public async fetchHistory(): Promise<void> {
this.tokensAreLoaded = false;
public onCopyAddressClick(): void {
this.$copyText(this.wallet.address);
this.$notify.success('Address copied to your clipboard');
}
public async claimWallet(): Promise<void> {
try {
await this.$store.dispatch(GET_PAYMENTS_HISTORY);
this.fetchHelper(this.depositHistoryItems);
this.reloadKey = this.reloadKey + 1;
if (this.nativeTokenPaymentsEnabled && !this.wallet.address)
await this.$store.dispatch(PAYMENTS_ACTIONS.CLAIM_WALLET);
} catch (error) {
await this.$notify.error(error.message);
} finally {
this.nativePayIsLoading = false;
}
}
public fetchHelper(tokenArray): void {
this.mostRecentTransaction = [tokenArray[0]];
this.tokenHistory = tokenArray;
this.transactionCount = tokenArray.length;
this.displayedHistory = tokenArray.slice(0,10);
this.tokensAreLoaded = true;
this.showAddFunds = this.transactionCount <= 0;
public async fetchHistory(): Promise<void> {
this.nativePayIsLoading = true;
try {
await this.$store.dispatch(GET_NATIVE_PAYMENTS_HISTORY);
this.transactionCount = this.nativePaymentHistoryItems.length;
this.displayedHistory = this.nativePaymentHistoryItems.slice(0,this.pageSize);
} catch (error) {
await this.$notify.error(error.message);
} finally {
this.nativePayIsLoading = false;
}
}
public toggleShowAddFunds(): void {
this.showAddFunds = !this.showAddFunds;
}
public showAddFundsCard(): void {
public async hideTransactionsTable(): Promise<void> {
this.showTransactions = false;
this.showAddFunds = true;
}
public toggleTransactionsTable(): void {
this.showAddFunds = true;
this.showTransactions = !this.showTransactions;
public async showTransactionsTable(): Promise<void> {
await this.fetchHistory();
this.showTransactions = true;
await Vue.nextTick();
await this.prepQRCode();
}
/**
* Returns TokenTransactionItem item component.
*/
public get itemComponent(): typeof TokenTransactionItem {
return TokenTransactionItem;
public async prepQRCode() {
try {
await QRCode.toCanvas(this.$refs.canvas, this.wallet.address);
} catch (error) {
await this.$notify.error(error.message);
}
}
public async updatePaymentMethod(): Promise<void> {
@ -459,73 +456,51 @@ export default class PaymentMethods extends Vue {
* controls sorting the transaction table
*/
public sortFunction(key) {
this.paginationLocation = { start: 0, end: 10 };
this.displayedHistory = this.tokenHistory.slice(0,10);
switch (key) {
case 'date-ascending':
this.tokenHistory.sort((a,b) => {return a.start.getTime() - b.start.getTime();});
this.nativePaymentHistoryItems.sort((a,b) => {return a.timestamp.getTime() - b.timestamp.getTime();});
break;
case 'date-descending':
this.tokenHistory.sort((a,b) => {return b.start.getTime() - a.start.getTime();});
this.nativePaymentHistoryItems.sort((a,b) => {return b.timestamp.getTime() - a.timestamp.getTime();});
break;
case 'amount-ascending':
this.tokenHistory.sort((a,b) => {return a.amount - b.amount;});
this.nativePaymentHistoryItems.sort((a,b) => {return a.amount.value - b.amount.value;});
break;
case 'amount-descending':
this.tokenHistory.sort((a,b) => {return b.amount - a.amount;});
this.nativePaymentHistoryItems.sort((a,b) => {return b.amount.value - a.amount.value;});
break;
case 'status-ascending':
this.tokenHistory.sort((a, b) => {
this.nativePaymentHistoryItems.sort((a, b) => {
if (a.status < b.status) {return -1;}
if (a.status > b.status) {return 1;}
return 0;});
break;
case 'status-descending':
this.tokenHistory.sort((a, b) => {
this.nativePaymentHistoryItems.sort((a, b) => {
if (b.status < a.status) {return -1;}
if (b.status > a.status) {return 1;}
return 0;});
break;
}
this.displayedHistory = this.nativePaymentHistoryItems.slice(0,10);
}
/**
* controls transaction table pagination
*/
public paginationController(i): void {
let diff = this.transactionCount - this.paginationLocation.start;
if (this.paginationLocation.start + i >= 0 && this.paginationLocation.end + i <= this.transactionCount && this.paginationLocation.end !== this.transactionCount){
this.paginationLocation = {
start: this.paginationLocation.start + i,
end: this.paginationLocation.end + i,
};
} else if (this.paginationLocation.start + i < 0 ) {
this.paginationLocation = {
start: 0,
end: 10,
};
} else if (this.paginationLocation.end + i > this.transactionCount) {
this.paginationLocation = {
start: this.paginationLocation.start + i,
end: this.transactionCount,
};
} else if (this.paginationLocation.end === this.transactionCount) {
this.paginationLocation = {
start: this.paginationLocation.start + i,
end: this.transactionCount - (diff),
};
}
this.displayedHistory = this.nativePaymentHistoryItems.slice((i - 1) * this.pageSize,((i - 1) * this.pageSize) + this.pageSize);
}
this.displayedHistory = this.tokenHistory.slice(this.paginationLocation.start, this.paginationLocation.end);
public get pageCount(): number {
return Math.ceil(this.transactionCount / this.pageSize);
}
/**
* Returns deposit history items.
*/
public get depositHistoryItems(): PaymentsHistoryItem[] {
return this.$store.state.paymentsModule.paymentsHistory.filter((item: PaymentsHistoryItem) => {
return item.type === PaymentsHistoryItemType.Transaction || item.type === PaymentsHistoryItemType.DepositBonus;
});
public get nativePaymentHistoryItems(): NativePaymentHistoryItem[] {
return this.$store.state.paymentsModule.nativePaymentsHistory;
}
}
</script>
@ -841,6 +816,14 @@ $align: center;
font-family: sans-serif;
font-size: 24px;
margin: 20px 0;
&__back {
cursor: pointer;
&:hover {
color: #000000bd;
}
}
}
&__container {
@ -934,6 +917,94 @@ $align: center;
}
}
}
&__address-card {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
background: #fff;
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
border-radius: 0.6rem;
padding: 1rem 1.5rem;
font-family: 'font_regular', sans-serif;
&__left {
display: flex;
align-items: center;
gap: 1.5rem;
&__canvas {
height: 4rem !important;
width: 4rem !important;
}
&__balance {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.3rem;
&__label {
font-size: 0.9rem;
color: rgb(0 0 0 / 75%);
}
&__value {
font-family: 'font_bold', sans-serif;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@media screen and (max-width: 375px) {
width: 16rem;
}
}
}
}
&__right {
width: 60%;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.3rem;
&__address {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.3rem;
&__label {
font-size: 0.9rem;
color: rgb(0 0 0 / 75%);
}
&__value {
font-family: 'font_bold', sans-serif;
}
}
}
}
&__transactions-area {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
align-items: start;
gap: 1.5rem;
&__title {
font-family: 'font_regular', sans-serif;
font-size: 1.5rem;
line-height: 1.8rem;
}
&__table {
width: 100%;
}
}
}
@mixin reset-list {

View File

@ -2,48 +2,50 @@
// See LICENSE for copying information.
<template>
<div class="sort-header-container">
<div
class="sort-header-container__item date"
@click="sortFunction('date')"
>
<p class="sort-header-container__item__name">DATE</p>
<VerticalArrows
:is-active="arrowController.date"
:direction="dateSortDirection"
/>
</div>
<div class="sort-header-container__item transaction">
<p class="sort-header-container__item__name">TRANSACTION</p>
</div>
<div
class="sort-header-container__item amount"
@click="sortFunction('amount')"
>
<p class="sort-header-container__item__name">AMOUNT(USD)</p>
<VerticalArrows
:is-active="arrowController.amount"
:direction="amountSortDirection"
/>
</div>
<div
class="sort-header-container__item status"
@click="sortFunction('status')"
>
<p class="sort-header-container__item__name">STATUS</p>
<VerticalArrows
:is-active="arrowController.status"
:direction="statusSortDirection"
/>
</div>
<div class="sort-header-container__item details">
<p class="sort-header-container__item__name">DETAILS</p>
</div>
</div>
<fragment>
<th @click="sortFunction('date')">
<div class="th-content">
<span>DATE</span>
<VerticalArrows
:is-active="arrowController.date"
:direction="dateSortDirection"
/>
</div>
</th>
<th>
<div class="th-content">
<span>TRANSACTION</span>
</div>
</th>
<th @click="sortFunction('amount')">
<div class="th-content">
<span>AMOUNT(USD)</span>
<VerticalArrows
:is-active="arrowController.amount"
:direction="amountSortDirection"
/>
</div>
</th>
<th @click="sortFunction('status')">
<div class="th-content">
<span>STATUS</span>
<VerticalArrows
:is-active="arrowController.status"
:direction="statusSortDirection"
/>
</div>
</th>
<th class="laptop">
<div class="th-content">
<span>DETAILS</span>
</div>
</th>
</fragment>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Fragment } from 'vue-fragment';
import { SortDirection } from '@/types/common';
@ -53,6 +55,7 @@ import VerticalArrows from '@/components/common/VerticalArrows.vue';
@Component({
components: {
VerticalArrows,
Fragment,
},
})
export default class SortingHeader2 extends Vue {
@ -69,7 +72,7 @@ export default class SortingHeader2 extends Vue {
/**
* sorts table by date
*/
*/
public sortFunction(key): void {
switch (key) {
case 'date':
@ -98,51 +101,15 @@ export default class SortingHeader2 extends Vue {
</script>
<style scoped lang="scss">
.sort-header-container {
display: flex;
width: 100%;
padding: 16px 0;
.th-content {
display: flex;
text-align: left;
}
&__item {
text-align: left;
@media screen and (max-width: 1024px) and (min-width: 426px) {
&__name {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 19px;
color: #adadad;
margin: 0;
}
}
}
.date,
.amount,
.status {
display: flex;
cursor: pointer;
}
.date {
width: 15%;
}
.transaction {
width: 35%;
}
.status {
width: 15%;
}
.amount {
width: 15%;
margin: 0;
}
.details {
text-align: left;
margin: 0;
width: 20%;
.laptop {
display: none;
}
}
</style>

View File

@ -18,7 +18,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component, Vue } from 'vue-property-decorator';
import StorjLarge from '@/../static/images/billing/storj-icon-large.svg';
@ -29,8 +29,6 @@ import StorjLarge from '@/../static/images/billing/storj-icon-large.svg';
},
})
export default class AddTokenCard extends Vue {
@Prop({ default: 0 })
private readonly totalCount: number;
}
</script>
@ -72,6 +70,8 @@ export default class AddTokenCard extends Vue {
&__add-funds {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
height: 100%;
width: 100%;

View File

@ -2,64 +2,59 @@
// See LICENSE for copying information.
<template>
<div class="token">
<div v-if="wallet.address" class="token">
<div class="token__icon">
<div class="token__icon__wrapper">
<StorjLarge />
</div>
</div>
<v-loader v-if="isLoading" />
<template v-if="!isLoading && !wallet.address">
<h1 class="token__title">STORJ Token</h1>
<p class="token__info">
Deposit STORJ Token to your account and receive a 10% bonus, or $10 for every $100.
</p>
<v-button
label="Add STORJ Tokens"
width="150px"
height="40px"
<div class="token__title-area">
<div class="token__title-area__small-icon">
<StorjSmall />
</div>
<div class="token__title-area__default-wrapper">
<p class="token__title-area__default-wrapper__label">Default</p>
<VInfo>
<template #icon>
<InfoIcon />
</template>
<template #message>
<p class="token__title-area__default-wrapper__message">
If the STORJ token balance runs out, the default credit card will be charged.
<a
class="token__title-area__default-wrapper__message__link"
href=""
target="_blank"
rel="noopener noreferrer"
>
Learn More
</a>
</p>
</template>
</VInfo>
</div>
</div>
<div class="token__info-area">
<div class="token__info-area__option">
<h2 class="token__info-area__option__title">STORJ Token Deposit Address</h2>
<p class="token__info-area__option__value">{{ wallet.address }}</p>
</div>
<div class="token__info-area__option">
<h2 class="token__info-area__option__title">Total Balance</h2>
<p class="token__info-area__option__value">{{ wallet.balance.value }}</p>
</div>
</div>
<div class="token__action-area">
<VButton
class="token__action-area__history-btn"
label="See transactions"
is-transparent="true"
height="32px"
font-size="13px"
border-radius="8px"
:on-press="onAddTokensClick"
border-radius="6px"
:on-press="() => $emit('showTransactions')"
/>
</template>
<template v-if="!isLoading && wallet.address">
<div class="token__title-area">
<div class="token__title-area__small-icon">
<StorjSmall />
</div>
<div class="token__title-area__default-wrapper">
<p class="token__title-area__default-wrapper__label">Default</p>
<VInfo>
<template #icon>
<InfoIcon />
</template>
<template #message>
<p class="token__title-area__default-wrapper__message">
If the STORJ token balance runs out, the default credit card will be charged.
<a
class="token__title-area__default-wrapper__message__link"
href=""
target="_blank"
rel="noopener noreferrer"
>
Learn More
</a>
</p>
</template>
</VInfo>
</div>
</div>
<div class="token__info-area">
<div class="token__info-area__option">
<h2 class="token__info-area__option__title">STORJ Token Deposit Address</h2>
<p class="token__info-area__option__value">{{ wallet.address }}</p>
</div>
<div class="token__info-area__option">
<h2 class="token__info-area__option__title">Total Balance</h2>
<p class="token__info-area__option__value">{{ wallet.balance | centsToDollars }}</p>
</div>
</div>
<v-button
label="Add funds"
width="96px"
@ -68,14 +63,13 @@
border-radius="6px"
:on-press="onAddTokensClick"
/>
</template>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { Wallet } from '@/types/payments';
import { AnalyticsHttpApi } from '@/api/analytics';
@ -83,7 +77,6 @@ import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import VButton from '@/components/common/VButton.vue';
import VInfo from '@/components/common/VInfo.vue';
import VLoader from '@/components/common/VLoader.vue';
import InfoIcon from '@/../static/images/billing/blueInfoIcon.svg';
import StorjSmall from '@/../static/images/billing/storj-icon-small.svg';
@ -96,29 +89,12 @@ import StorjLarge from '@/../static/images/billing/storj-icon-large.svg';
StorjSmall,
StorjLarge,
VButton,
VLoader,
VInfo,
},
})
export default class AddTokenCardNative extends Vue {
public isLoading = true;
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
/**
* Mounted hook after initial render.
* Fetches wallet from backend.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_WALLET);
} catch (error) {
await this.$notify.error(error.message);
} finally {
this.isLoading = false;
}
}
/**
* Holds on add tokens button click logic.
* Triggers Add funds popup.
@ -132,9 +108,7 @@ export default class AddTokenCardNative extends Vue {
* Returns wallet from store.
*/
private get wallet(): Wallet {
// TODO: remove this when backend is ready.
return { address: 'ijefiw54et945t89459ty8e98c4jyc8489yec985yce8i59y8c598yc56', balance: 234234 };
// return this.$store.state.paymentsModule.wallet;
return this.$store.state.paymentsModule.wallet;
}
}
</script>
@ -270,6 +244,27 @@ export default class AddTokenCardNative extends Vue {
}
}
}
&__action-area {
display: flex;
justify-content: start;
align-items: center;
gap: 10px;
&__history-btn {
cursor: pointer;
padding: 0 10px;
span {
font-size: 13px;
color: #56606d;
font-family: 'font_medium', sans-serif;
line-height: 23px;
margin: 0;
white-space: nowrap;
}
}
}
}
:deep(.info__box) {

View File

@ -2,76 +2,130 @@
// See LICENSE for copying information.
<template>
<div class="container">
<div class="divider" />
<div class="container__row">
<div class="container__row__item__date-container">
<p class="container__row__item date">{{ billingItem.start.toLocaleDateString() }}</p>
<p class="container__row__item time">{{ billingItem.start.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) }}</p>
<tr @click="goToTxn">
<th class="align-left data mobile">
<div class="few-items">
<p class="array-val">
Deposit on {{ item.formattedType }}
</p>
<p class="array-val">
<span v-if="item.type === 'storjscan'">{{ item.amount.value }}</span>
<span v-else>{{ item.received.value }}</span>
</p>
<p
class="array-val" :class="{
pending_txt: item.status === 'pending',
confirmed_txt: item.status === 'confirmed',
rejected_txt: item.status === 'rejected',
}"
>
{{ item.formattedStatus }}
</p>
<p class="array-val">
{{ item.timestamp.toLocaleDateString() }}
</p>
</div>
<div class="container__row__item__description">
<p class="container__row__item__description__text">CoinPayments {{ billingItem.description.includes("Deposit")? "Deposit": "Withdrawal" }}</p>
<p class="container__row__item__description__id">{{ billingItem.id }}</p>
</div>
<p class="container__row__item amount">
<b>
<span v-if="billingItem.type === 1">
${{ billingItem.quantity.received.toFixed(2) }}
</span>
<span v-else>
${{ billingItem.quantity.total.toFixed(2) }}
</span>
</b>
</p>
<p class="container__row__item status">
<span :class="`container__row__item__circle-icon ${billingItem.status}`">
&#9679;
</span>
{{ billingItem.formattedStatus }}
</p>
<p class="container__row__item download">
<a v-if="billingItem.link" class="download-link" target="_blank" :href="billingItem.link">View On CoinPayments</a>
</p>
</div>
</div>
</th>
<fragment>
<th class="align-left data tablet-laptop">
<p>{{ item.timestamp.toLocaleDateString() }}</p>
</th>
<th class="align-left data tablet-laptop">
<p>Deposit on {{ item.formattedType }}</p>
<p class="laptop">{{ item.wallet }}</p>
</th>
<th class="align-right data tablet-laptop">
<p v-if="item.type === 'storjscan'">{{ item.amount.value }}</p>
<p v-else>{{ item.received.value }}</p>
</th>
<th class="align-left data tablet-laptop">
<div class="status">
<span
class="status__dot" :class="{
pending: item.status === 'pending',
confirmed: item.status === 'confirmed',
rejected: item.status === 'rejected'
}"
/>
<span class="status__text">{{ item.formattedStatus }}</span>
</div>
</th>
<th class="align-left data laptop">
<a
v-if="item.link" class="download-link" target="_blank"
rel="noopener noreferrer" :href="item.link"
>View on {{ item.formattedType }}</a>
</th>
</fragment>
</tr>
</template>
<script lang="ts">
import { Prop, Vue, Component } from 'vue-property-decorator';
import { Prop, Component } from 'vue-property-decorator';
import { Fragment } from 'vue-fragment';
import { PaymentsHistoryItem } from '@/types/payments';
import { NativePaymentHistoryItem } from '@/types/payments';
import Resizable from '@/components/common/Resizable.vue';
// @vue/component
@Component
export default class TokenTransactionItem extends Vue {
@Prop({ default: () => new PaymentsHistoryItem() })
private readonly billingItem: PaymentsHistoryItem;
@Component({
components: { Fragment },
})
export default class TokenTransactionItem extends Resizable {
@Prop({ default: () => new NativePaymentHistoryItem() })
private readonly item: NativePaymentHistoryItem;
public goToTxn() {
if (this.isMobile || this.isTablet)
window.open(this.item.link, '_blank', 'noreferrer');
}
}
</script>
<style scoped lang="scss">
.pending {
background: #ffa800;
}
.pending_txt {
color: #ffa800;
}
.confirmed {
background: #00ac26;
}
.confirmed_txt {
color: #00ac26;
}
.rejected {
background: #ac1a00;
}
.rejected_txt {
color: #ac1a00;
}
.divider {
height: 1px;
width: calc(100% + 30px);
background-color: #e5e7eb;
align-self: center;
.status {
display: flex;
align-items: center;
gap: 0.5rem;
&__dot {
height: 0.8rem;
width: 0.8rem;
border-radius: 100%;
}
}
.download-link {
color: #2683ff;
font-family: 'font_bold', sans-serif;
text-decoration: underline !important;
&:hover {
@ -79,69 +133,58 @@ export default class TokenTransactionItem extends Vue {
}
}
.container {
.few-items {
display: flex;
flex-direction: column;
justify-content: space-between;
}
&__row {
display: flex;
align-items: center;
width: 100%;
.array-val {
font-family: 'font_regular', sans-serif;
font-size: 0.75rem;
line-height: 1.25rem;
&__item {
font-family: sans-serif;
font-weight: 300;
font-size: 16px;
text-align: left;
margin: 30px 0;
&__description {
width: 35%;
display: flex;
flex-direction: column;
text-align: left;
&__text,
&__id {
font-family: 'font_medium', sans-serif;
}
}
&__date-container {
width: 15%;
display: flex;
flex-direction: column;
}
}
&:first-of-type {
font-family: 'font_bold', sans-serif;
font-size: 0.875rem;
margin-bottom: 3px;
}
}
.date {
font-family: 'font_bold', sans-serif;
margin: 0;
@media only screen and (max-width: 425px) {
.mobile {
display: table-cell;
}
.laptop,
.tablet-laptop {
display: none;
}
}
.time {
color: #6b7280;
margin: 0;
font-size: 14px;
@media only screen and (min-width: 426px) {
.tablet-laptop {
display: table-cell;
}
.mobile {
display: none;
}
}
.description {
font-family: 'font_medium', sans-serif;
overflow: ellipse;
@media only screen and (max-width: 1024px) and (min-width: 426px) {
.laptop {
display: none;
}
}
.status {
width: 15%;
}
@media only screen and (min-width: 1024px) {
.amount {
width: 15%;
}
.download {
text-align: left;
width: 20%;
.laptop {
display: table-cell;
}
}
</style>

View File

@ -137,9 +137,7 @@ export default class AddTokenFundsModal extends Vue {
* Returns wallet from store.
*/
private get wallet(): Wallet {
// TODO: remove this when backend is ready.
return { address: 'ijefiw54et945t89459ty8e98c4jyc8489yec985yce8i59y8c598yc56', balance: 234234 };
// return this.$store.state.paymentsModule.wallet;
return this.$store.state.paymentsModule.wallet;
}
}
</script>

View File

@ -12,6 +12,7 @@ import {
PaymentsHistoryItemType,
ProjectUsageAndCharges,
TokenDeposit,
NativePaymentHistoryItem,
Wallet,
} from '@/types/payments';
import { StoreModule } from '@/types/store';
@ -25,6 +26,7 @@ export const PAYMENTS_MUTATIONS = {
UPDATE_CARDS_SELECTION: 'UPDATE_CARDS_SELECTION',
UPDATE_CARDS_DEFAULT: 'UPDATE_CARDS_DEFAULT',
SET_PAYMENTS_HISTORY: 'SET_PAYMENTS_HISTORY',
SET_NATIVE_PAYMENTS_HISTORY: 'SET_NATIVE_PAYMENTS_HISTORY',
SET_PROJECT_USAGE_AND_CHARGES: 'SET_PROJECT_USAGE_AND_CHARGES',
SET_CURRENT_ROLLUP_PRICE: 'SET_CURRENT_ROLLUP_PRICE',
SET_PREVIOUS_ROLLUP_PRICE: 'SET_PREVIOUS_ROLLUP_PRICE',
@ -46,6 +48,7 @@ export const PAYMENTS_ACTIONS = {
MAKE_CARD_DEFAULT: 'makeCardDefault',
REMOVE_CARD: 'removeCard',
GET_PAYMENTS_HISTORY: 'getPaymentsHistory',
GET_NATIVE_PAYMENTS_HISTORY: 'getNativePaymentsHistory',
MAKE_TOKEN_DEPOSIT: 'makeTokenDeposit',
GET_PROJECT_USAGE_AND_CHARGES: 'getProjectUsageAndCharges',
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP: 'getProjectUsageAndChargesCurrentRollup',
@ -63,6 +66,7 @@ const {
UPDATE_CARDS_SELECTION,
UPDATE_CARDS_DEFAULT,
SET_PAYMENTS_HISTORY,
SET_NATIVE_PAYMENTS_HISTORY,
SET_PROJECT_USAGE_AND_CHARGES,
SET_PRICE_SUMMARY,
SET_PRICE_SUMMARY_FOR_SELECTED_PROJECT,
@ -82,6 +86,7 @@ const {
MAKE_CARD_DEFAULT,
REMOVE_CARD,
GET_PAYMENTS_HISTORY,
GET_NATIVE_PAYMENTS_HISTORY,
MAKE_TOKEN_DEPOSIT,
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP,
@ -96,6 +101,7 @@ export class PaymentsState {
public balance: AccountBalance = new AccountBalance();
public creditCards: CreditCard[] = [];
public paymentsHistory: PaymentsHistoryItem[] = [];
public nativePaymentsHistory: NativePaymentHistoryItem[] = [];
public usageAndCharges: ProjectUsageAndCharges[] = [];
public priceSummary = 0;
public priceSummaryForSelectedProject = 0;
@ -166,6 +172,9 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState,
[SET_PAYMENTS_HISTORY](state: PaymentsState, paymentsHistory: PaymentsHistoryItem[]): void {
state.paymentsHistory = paymentsHistory;
},
[SET_NATIVE_PAYMENTS_HISTORY](state: PaymentsState, paymentsHistory: NativePaymentHistoryItem[]): void {
state.nativePaymentsHistory = paymentsHistory;
},
[SET_PROJECT_USAGE_AND_CHARGES](state: PaymentsState, usageAndCharges: ProjectUsageAndCharges[]): void {
state.usageAndCharges = usageAndCharges;
},
@ -200,6 +209,7 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState,
[CLEAR](state: PaymentsState) {
state.balance = new AccountBalance();
state.paymentsHistory = [];
state.nativePaymentsHistory = [];
state.usageAndCharges = [];
state.priceSummary = 0;
state.creditCards = [];
@ -260,10 +270,15 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState,
commit(CLEAR);
},
[GET_PAYMENTS_HISTORY]: async function({ commit }: PaymentsContext): Promise<void> {
const paymentsHistory: PaymentsHistoryItem[] = await api.paymentsHistory();
const paymentsHistory = await api.paymentsHistory();
commit(SET_PAYMENTS_HISTORY, paymentsHistory);
},
[GET_NATIVE_PAYMENTS_HISTORY]: async function({ commit }: PaymentsContext): Promise<void> {
const paymentsHistory = await api.nativePaymentsHistory();
commit(SET_NATIVE_PAYMENTS_HISTORY, paymentsHistory);
},
[MAKE_TOKEN_DEPOSIT]: async function(_context: PaymentsContext, amount: number): Promise<TokenDeposit> {
return await api.makeTokenDeposit(amount);
},

View File

@ -62,6 +62,14 @@ export interface PaymentsApi {
*/
paymentsHistory(): Promise<PaymentsHistoryItem[]>;
/**
* Returns a list of invoices, transactions and all others payments history items for payment account.
*
* @returns list of payments history items
* @throws Error
*/
nativePaymentsHistory(): Promise<NativePaymentHistoryItem[]>;
/**
* Creates token transaction in CoinPayments
*
@ -361,6 +369,41 @@ export enum CouponDuration {
export class Wallet {
public constructor(
public address: string = '',
public balance: number = 0,
public balance: TokenAmount = new TokenAmount(),
) { }
}
/**
* TokenPaymentHistoryItem holds all public information about token payments history line.
*/
export class NativePaymentHistoryItem {
public constructor(
public readonly id: string = '',
public readonly wallet: string = '',
public readonly type: string = '',
public readonly amount: TokenAmount = new TokenAmount(),
public readonly received: TokenAmount = new TokenAmount(),
public readonly status: string = '',
public readonly link: string = '',
public readonly timestamp: Date = new Date(),
) { }
public get formattedStatus(): string {
return this.status.charAt(0).toUpperCase() + this.status.substring(1);
}
public get formattedType(): string {
return this.type.charAt(0).toUpperCase() + this.type.substring(1);
}
}
export class TokenAmount {
public constructor(
private readonly _value: string = '0.0',
public readonly currency: string = '',
) { }
public get value(): number {
return Number.parseFloat(this._value);
}
}

View File

@ -9,6 +9,7 @@ import {
PaymentsHistoryItem,
ProjectUsageAndCharges,
TokenDeposit,
NativePaymentHistoryItem,
Wallet,
} from '@/types/payments';
@ -54,6 +55,10 @@ export class PaymentsMock implements PaymentsApi {
return Promise.resolve([]);
}
nativePaymentsHistory(): Promise<NativePaymentHistoryItem[]> {
return Promise.resolve([]);
}
makeTokenDeposit(amount: number): Promise<TokenDeposit> {
return Promise.resolve(new TokenDeposit(amount, 'testAddress', 'testLink'));
}