From cc47c1fe8a058b9a62548057d650f2a1ce05cbd2 Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Fri, 5 Jan 2018 16:30:55 -0300 Subject: [PATCH] Refactor: tx-details --- src/assets/img/icon-broadcasted.svg | 17 ++ src/assets/img/icon-card.svg | 15 + src/assets/img/icon-warning.png | Bin 0 -> 1065 bytes .../feedback/send-feedback/send-feedback.ts | 2 +- src/pages/tx-details/tx-details.html | 161 ++++++++--- src/pages/tx-details/tx-details.scss | 137 ++++++++- src/pages/tx-details/tx-details.ts | 272 ++++++++++++++++-- src/providers/wallet/wallet.ts | 9 +- src/theme/global.scss | 40 +-- 9 files changed, 540 insertions(+), 113 deletions(-) create mode 100644 src/assets/img/icon-broadcasted.svg create mode 100644 src/assets/img/icon-card.svg create mode 100644 src/assets/img/icon-warning.png diff --git a/src/assets/img/icon-broadcasted.svg b/src/assets/img/icon-broadcasted.svg new file mode 100644 index 000000000..766957c73 --- /dev/null +++ b/src/assets/img/icon-broadcasted.svg @@ -0,0 +1,17 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/img/icon-card.svg b/src/assets/img/icon-card.svg new file mode 100644 index 000000000..873f912f6 --- /dev/null +++ b/src/assets/img/icon-card.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/assets/img/icon-warning.png b/src/assets/img/icon-warning.png new file mode 100644 index 0000000000000000000000000000000000000000..fa33cfcdcef991bcddc957a1e00ac99a50191af5 GIT binary patch literal 1065 zcmV+^1lIeBP)N>j4+O<*wwLn}PG+IdIJ!YmGJ_KHRnOfu`ozwp8B+VEhPh`Up}RGhmK2avEvC?{15Xc0HT} ze*X%nl!3CvnNcvu8aa(JAisIFe{XDUD!4OYfQcUg9T0BK9Bbr|+Xw@pL|6@XKRN`Q z`Wo;83r%qCa;%X%v~`SuF$Nquraf z#^TY(Q=78D_*nJWeTj$O8N#*$L-_TNIG}yt^cct?mp$xV4Fi5>G8oA`*bDr594@qW z24Z1>=7>OG*Srb_Ipng3y*Fz>e*479y6=hI5$sHF=>RUC0Ayg{S?C(v zaL*aInj?o?_OO>4)S~9~81M!w&^qpgdo@_kTdyoboh8U7lR_u$LOtqUOIf zAivzx|7@~7(G$PB8JNCMcl1}Gl?P=3Sv^IH%I=5$m{5T+l zMcow&1Hhmn0{2@Mew8EZ^(#(;T7R9tbO^vF7FU)tz=!u(E(-*W>n-Zks;gPP~8b^dw7FZ!9_CJ52`UVmeKK|dj;*7 - + -
- - Received +
+ + + + {{'Sent' | translate}} + {{'Received' | translate}} + {{'Moved' | translate}}
-
- - Sent +
+ + Sending + Receiving
-
- - Moved +
+
{{btx.amountValueStr}} + {{btx.amountUnitStr}} +
+
+ {{btx.alternativeAmountStr}} + + ... + + {{rate| currency:'':2}} {{alternativeIsoCode}} ({{rateDate | amDateFormat:'MM/DD/YYYY HH:mm a'}}) + + +
+ -
{{tx.amount | satToUnit: wallet.coin}}
-
{{tx.amount | satToFiat: wallet.coin}}
+ + To + + + +
+ {{ btx.addressTo }}" + {{ toName }} +
+
- - - -

To

-

{{wallet.name || '...'}}

-

{{tx.addressTo || '...'}}

+ From + To +
+ + + + +
{{wallet.name}}
+
- - - - -

From

-

{{wallet.name || '...'}}

-
- - - {{'Date' | translate}} + + Created by - {{tx.time * 1000 | amDateFormat:'MM/DD/YYYY HH:mm a'}} + {{btx.creatorName}} - - - + + {{'Fee' | translate}} + {{btx.feeStr || '...'}} + + + {{btx.feeFiatStr || '...'}}  + - {{btx.feeRateStr}} of the transaction + + + + + + + This transaction could take a long time to confirm or could be dropped due to the low fees set by the sender + + + + + + This transaction amount is too small compared to current Bitcoin network fees. Spending these funds will need a Bitcoin network + fee cost comparable to the funds itself. + + Learn more + + {{'Confirmations' | translate}} - {{confirmations || 'Unconfirmed' | translate}} + + Unconfirmed + + + {{btx.confirmations}} + + + {{btx.safeConfirmed}} + + + + {{'Notify me if confirmed' | translate}} + + + +
+ {{'Timeline' | translate}} +
+
+
+
!
+ +
{{actionList.length - i}}
+
+ + + + +
+
+
+ - + \ No newline at end of file diff --git a/src/pages/tx-details/tx-details.scss b/src/pages/tx-details/tx-details.scss index e40e50e43..ce20ab7a5 100644 --- a/src/pages/tx-details/tx-details.scss +++ b/src/pages/tx-details/tx-details.scss @@ -1,21 +1,132 @@ page-tx-details { - .action { + $item-border-color: #EFEFEF; + $item-lateral-padding: 20px; + $v-success-color: #1abb9b; + $v-icon-border-radius: 3px; + $item-label-color: #6C6C6E; + + .sending-label { display: flex; - height: 4rem; align-items: center; - margin-top: 1.5rem; - margin-bottom: 1.5rem; + .sending-img { + margin-right: 2rem; + } + span { + font-size: 1.8rem; + } } - img { - margin-right: 15px; + .amount-label { + padding-top: 2rem; + .amount { + font-size: 3.5rem; + .unit { + font-weight: 100; + } + } + .alternative { + margin-top: 1rem; + color: color($global-colors, light-grey); + } } - .amount-str { - font-size: 30px; - margin-bottom: .5rem; + + .payment-proposal-to, .wallet { + display: flex; + align-items: center; + margin-top: 1rem; + .icon-bitcoin { + margin-right: 1rem; + width: 37px; + height: 37px; + } } - .amount-alt-str { - font-size: 16px; - margin-left: 2px; - margin-bottom: 1.5rem; + + .timeline-item { + border: 0; + &:nth-child(2) { + border-top: 1px solid $item-border-color; + } + &:last-child { + border-bottom: 1px solid $item-border-color; + } + padding: 17px $item-lateral-padding; + + > * { + padding-right: 0; + } + } + + .timeline-content { + display: flex; + align-items: center; + + &-icon { + display: flex; + align-items: center; + min-width: 40px; + padding-right: 1rem; + padding-left: 1rem; + margin-right: .5rem; + position: relative; + + &::before { + content: ''; + background: #e8e8e8; + height: 100px; + width: 1px; + position: absolute; + left: 50%; + -webkit-transform: translateX(-50%) translateY(-100%); + transform: translateX(-50%) translateY(-100%); + top: 0; + } + + &::after { + content: ''; + background: #e8e8e8; + height: 100px; + width: 1px; + position: absolute; + left: 50%; + -webkit-transform: translateX(-50%) translateY(100%); + transform: translateX(-50%) translateY(100%); + bottom: 0; + } + + > div { + border: 3px solid $v-success-color; + border-radius: $v-icon-border-radius; + display: flex; + height: 26px; + width: 26px; + font-size: 13px; + align-items: center; + justify-content: center; + font-weight: 600; + vertical-align: middle; + color: $v-success-color; + z-index: 10; + + &.rejected { + background: #E15061; + border-color: #E15061; + color: #FFFFFF; + font-family: "Roboto-Bold"; + font-size: 19px; + } + } + } + + &-label { + flex-grow: 1; + + > .action { + font-size: 16px; + } + + > .name { + color: #aaa; + font-size: 14px; + } + } } } diff --git a/src/pages/tx-details/tx-details.ts b/src/pages/tx-details/tx-details.ts index 653d7b4a7..32efea80f 100644 --- a/src/pages/tx-details/tx-details.ts +++ b/src/pages/tx-details/tx-details.ts @@ -1,11 +1,17 @@ import { Component } from "@angular/core"; -import { NavParams } from 'ionic-angular'; +import { NavController, NavParams, Events } from 'ionic-angular'; import { Logger } from '@nsalaun/ng-logger'; +import * as _ from "lodash"; // Providers +import { AddressBookProvider } from '../../providers/address-book/address-book'; +import { ConfigProvider } from '../../providers/config/config'; import { ExternalLinkProvider } from '../../providers/external-link/external-link'; import { OnGoingProcessProvider } from "../../providers/on-going-process/on-going-process"; +import { PopupProvider } from '../../providers/popup/popup'; import { ProfileProvider } from '../../providers/profile/profile'; +import { TxConfirmNotificationProvider } from '../../providers/tx-confirm-notification/tx-confirm-notification'; +import { TxFormatProvider } from '../../providers/tx-format/tx-format'; import { WalletProvider } from '../../providers/wallet/wallet'; @Component({ @@ -13,49 +19,253 @@ import { WalletProvider } from '../../providers/wallet/wallet'; templateUrl: 'tx-details.html' }) export class TxDetailsPage { - public title: string; + private txId: string; + private config: any; + private blockexplorerUrl: string; + public wallet: any; - public tx: any; - public confirmations: string; + public btx: any; + public actionList: Array; + public isShared: boolean; + public title: string; + public alternativeIsoCode: string; + public rateDate: any; + public rate: any; + public txNotification: any; + public color: string; + public copayerId: string; + public txsUnsubscribedForNotifications: boolean; + public toName: string; constructor( - private navParams: NavParams, - private walletProvider: WalletProvider, - private profileProvider: ProfileProvider, + private addressBookProvider: AddressBookProvider, + private configProvider: ConfigProvider, + private events: Events, private externalLinkProvider: ExternalLinkProvider, + private logger: Logger, + private navCtrl: NavController, + private navParams: NavParams, private onGoingProcess: OnGoingProcessProvider, - private logger: Logger + private popupProvider: PopupProvider, + private profileProvider: ProfileProvider, + private txConfirmNotificationProvider: TxConfirmNotificationProvider, + private txFormatProvider: TxFormatProvider, + private walletProvider: WalletProvider ) { + this.config = this.configProvider.get(); + + this.txId = this.navParams.data.txid; + this.title = 'Transaction'; // Todo: gettextCatalog this.wallet = this.profileProvider.getWallet(this.navParams.data.walletId); - this.tx = {}; - this.confirmations = null; + this.color = this.wallet.color; + this.copayerId = this.wallet.credentials.copayerId; + this.isShared = this.wallet.credentials.n > 1; + this.txsUnsubscribedForNotifications = this.config.confirmedTxsNotifications ? !this.config.confirmedTxsNotifications.enabled : true; + + if (this.wallet.coin == 'bch') { + this.blockexplorerUrl = 'bch-insight.bitpay.com'; + } else { + this.blockexplorerUrl = 'insight.bitpay.com'; + } + + this.txConfirmNotificationProvider.checkIfEnabled(this.txId).then((res: any) => { + this.txNotification = { + value: res + }; + }); + + this.updateTx(); } - ionViewDidLoad() { - const txid = this.navParams.data.txid; + ionViewWillEnter() { + this.events.subscribe('bwsEvent', (walletId: string, type: string, n: any) => { + if (type == 'NewBlock' && n && n.data && n.data.network == 'livenet') this.updateTxDebounced({ hideLoading: true }); + }); + } - this.onGoingProcess.set('loadingTxInfo', true); - this.walletProvider.getTx(this.wallet, txid).then((tx) => { - this.onGoingProcess.set('loadingTxInfo', false); - this.tx = tx; - if (this.tx.action == 'sent') this.title = 'Sent Funds'; - if (this.tx.action == 'received') this.title = 'Received Funds'; - if (this.tx.action == 'moved') this.title = 'Moved Funds'; + ionViewWillLeave() { + this.events.unsubscribe('bwsEvent'); + } - if (this.tx.safeConfirmed) this.confirmations = this.tx.safeConfirmed; - else if (this.tx.confirmations > 6) this.confirmations = '6+'; - }).catch((err) => { + public readMore(): void { + let url = 'https://github.com/bitpay/copay/wiki/COPAY---FAQ#amount-too-low-to-spend'; + let optIn = true; + let title = null; + let message = 'Read more in our Wiki'; // Todo: gettextCatalog + let okText = 'Open'; // Todo: gettextCatalog + let cancelText = 'Go Back'; // Todo: gettextCatalog + this.externalLinkProvider.open(url, optIn, title, message, okText, cancelText); + } + + private updateMemo(): void { + this.walletProvider.getTxNote(this.wallet, this.btx.txid).then((note: string) => { + if (!note) return; + this.btx.note = note; + }).catch((err: any) => { + this.logger.warn('Could not fetch transaction note: ' + err); + return; + }); + } + + private initActionList(): void { + this.actionList = []; + if (this.btx.action != 'sent' || !this.isShared) return; + + var actionDescriptions = { + created: 'Proposal Created', // Todo: gettextCatalog + accept: 'Accepted', // Todo: gettextCatalog + reject: 'Rejected', // Todo: gettextCatalog + broadcasted: 'Broadcasted' // Todo: gettextCatalog + }; + + this.actionList.push({ + type: 'created', + time: this.btx.createdOn, + description: actionDescriptions.created, + by: this.btx.creatorName + }); + + _.each(this.btx.actions, (action: any) => { + this.actionList.push({ + type: action.type, + time: action.createdOn, + description: actionDescriptions[action.type], + by: action.copayerName + }); + }); + + this.actionList.push({ + type: 'broadcasted', + time: this.btx.time, + description: actionDescriptions.broadcasted, + }); + + setTimeout(() => { + this.actionList.reverse(); + }, 10); + } + + private updateTxDebounced = _.debounce(this.updateTx, 1000); + + private updateTx(opts?: any): void { + opts = opts ? opts : {}; + if (!opts.hideLoading) this.onGoingProcess.set('loadingTxInfo', true); + this.walletProvider.getTx(this.wallet, this.txId).then((tx: any) => { + if (!opts.hideLoading) this.onGoingProcess.set('loadingTxInfo', false); + + this.btx = this.txFormatProvider.processTx(this.wallet.coin, tx); + let v: string = this.txFormatProvider.formatAlternativeStr(this.wallet.coin, tx.fees); + this.btx.feeFiatStr = v; + this.btx.feeRateStr = (this.btx.fees / (this.btx.amount + this.btx.fees) * 100).toFixed(2) + '%'; + + if (this.btx.action != 'invalid') { + if (this.btx.action == 'sent') this.title = 'Sent Funds'; // Todo: gettextCatalog + if (this.btx.action == 'received') this.title = 'Received Funds'; // Todo: gettextCatalog + if (this.btx.action == 'moved') this.title = 'Moved Funds'; // Todo: gettextCatalog + } + + this.updateMemo(); + this.initActionList(); + this.getFiatRate(); + this.contact(); + + this.walletProvider.getLowAmount(this.wallet).then((amount: number) => { + this.btx.lowAmount = tx.amount < amount; + }).catch((err: any) => { + this.logger.warn('Error getting low amounts: ' + err); + return; + }); + }).catch((err: any) => { + if (!opts.hideLoading) this.onGoingProcess.set('loadingTxInfo', false); + this.logger.warn('Error getting transaction: ' + err); + this.navCtrl.pop(); + return this.popupProvider.ionicAlert('Error', 'Transaction not available at this time'); // Todo: gettextCatalog + }); + } + + public showCommentPopup(): void { + let opts: any = {}; + if (this.btx.message) { + opts.defaultText = this.btx.message; + } + if (this.btx.note && this.btx.note.body) opts.defaultText = this.btx.note.body; + + this.popupProvider.ionicPrompt(this.wallet.name, 'Memo', opts).then((text: string) => { // Todo: gettextCatalog + if (typeof text == "undefined") return; + + this.btx.note = { + body: text + }; + this.logger.debug('Saving memo'); + + let args = { + txid: this.btx.txid, + body: text + }; + + this.walletProvider.editTxNote(this.wallet, args).then((res: any) => { + this.logger.info('Tx Note edited: ', res); + }).catch((err: any) => { + this.logger.debug('Could not save tx comment ' + err); + }); + }); + } + + public viewOnBlockchain(): void { + let btx = this.btx; + let url = 'https://' + (this.getShortNetworkName() == 'test' ? 'test-' : '') + this.blockexplorerUrl + '/tx/' + btx.txid; + let optIn = true; + let title = null; + let message = 'View Transaction on Insight'; // Todo: gettextCatalog + let okText = 'Open Insight'; // Todo: gettextCatalog + let cancelText = 'Go Back'; // Todo: gettextCatalog + this.externalLinkProvider.open(url, optIn, title, message, okText, cancelText); + } + + public getShortNetworkName(): string { + let n: string = this.wallet.credentials.network; + return n.substring(0, 4); + } + + private getFiatRate(): void { + this.alternativeIsoCode = this.wallet.status.alternativeIsoCode; + this.wallet.getFiatRate({ + code: this.alternativeIsoCode, + ts: this.btx.time * 1000 + }, (err, res) => { + if (err) { + this.logger.debug('Could not get historic rate'); + return; + } + if (res && res.rate) { + this.rateDate = res.fetchedOn; + this.rate = res.rate; + } + }); + } + + public txConfirmNotificationChange(): void { + if (this.txNotification.value) { + this.txConfirmNotificationProvider.subscribe(this.wallet, { + txid: this.txId + }); + } else { + this.txConfirmNotificationProvider.unsubscribe(this.wallet, this.txId); + } + } + + private contact(): void { + let addr = this.btx.addressTo; + this.addressBookProvider.get(addr).then((ab: any) => { + if (ab) { + let name = _.isObject(ab) ? ab.name : ab; + this.toName = name; + } else { + this.toName = addr; + } + }).catch((err: any) => { this.logger.warn(err); }); } - addMemo() { - return; - } - - viewOnBlockchain() { - const prefix = this.wallet.coin === 'bch' ? 'bch-' : this.wallet.network === 'testnet' ? 'test-' : ''; - const url = 'https://' + prefix + 'insight.bitpay.com/tx/' + this.tx.txid; - this.externalLinkProvider.open(url); - } } diff --git a/src/providers/wallet/wallet.ts b/src/providers/wallet/wallet.ts index af29d81cb..6b1c51d82 100644 --- a/src/providers/wallet/wallet.ts +++ b/src/providers/wallet/wallet.ts @@ -677,10 +677,11 @@ export class WalletProvider { // Approx utxo amount, from which the uxto is economically redeemable public getMinFee(wallet: any, nbOutputs?: number): Promise { return new Promise((resolve, reject) => { - this.feeProvider.getFeeLevels(wallet.coin).then((data) => { - let lowLevelRate = (lodash.find(data.levels[wallet.network], { - level: 'normal', - }).feePerKb / 1000).toFixed(0); + this.feeProvider.getFeeLevels(wallet.coin).then((data: any) => { + let normalLevelRate: any = lodash.find(data.levels[wallet.network], (level: any) => { + return level.level === 'normal'; + }); + let lowLevelRate: string = (normalLevelRate.feePerKb / 1000).toFixed(0); let size = this.getEstimatedTxSize(wallet, nbOutputs); return resolve(size * parseInt(lowLevelRate)); }).catch((err) => { diff --git a/src/theme/global.scss b/src/theme/global.scss index bc55d0143..64257bdbf 100644 --- a/src/theme/global.scss +++ b/src/theme/global.scss @@ -120,6 +120,14 @@ $global-colors: ( } } +.gravatar { + min-width: 3rem; + min-height: 3rem; +} + + +// Utilities + .assertive, a.assertive { color: color($global-colors, assertive); } @@ -132,31 +140,13 @@ $global-colors: ( font-weight: 700; } -.gravatar { - min-width: 3rem; - min-height: 3rem; +.ellipsis { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -/* - * Wallet selector - */ - -@mixin wallets-list { - height: 3.5rem; - width: 3.5rem; - content: " "; - position: absolute; - top: 6px; - border-radius: $icon-border-radius; +.pointer { + cursor: pointer; } - -.wallet-livenet::before { - @include wallets-list; - background: color($colors, primary) url('../assets/img/icon-wallet.svg') no-repeat 0px 0px; -} - -.wallet-testnet::before { - @include wallets-list; - background: color($colors, primary) url('../assets/img/icon-wallet-testnet.svg') no-repeat 0px 0px; -} -