mirror of https://github.com/BTCPrivate/copay.git
Merge pull request #7064 from Gamboster/feat/confirmProviderAndOthers
Feat: Confirm
This commit is contained in:
commit
8e764fbc21
|
@ -34,6 +34,7 @@ import { BackupRequestPage } from '../pages/onboarding/backup-request/backup-req
|
||||||
import { DisclaimerPage } from '../pages/onboarding/disclaimer/disclaimer';
|
import { DisclaimerPage } from '../pages/onboarding/disclaimer/disclaimer';
|
||||||
import { EmailPage } from '../pages/onboarding/email/email';
|
import { EmailPage } from '../pages/onboarding/email/email';
|
||||||
import { OnboardingPage } from '../pages/onboarding/onboarding';
|
import { OnboardingPage } from '../pages/onboarding/onboarding';
|
||||||
|
import { PayProPage } from '../pages/paypro/paypro';
|
||||||
import { TourPage } from '../pages/onboarding/tour/tour';
|
import { TourPage } from '../pages/onboarding/tour/tour';
|
||||||
import { BackupWarningPage } from '../pages/backup/backup-warning/backup-warning';
|
import { BackupWarningPage } from '../pages/backup/backup-warning/backup-warning';
|
||||||
import { BackupGamePage } from '../pages/backup/backup-game/backup-game';
|
import { BackupGamePage } from '../pages/backup/backup-game/backup-game';
|
||||||
|
@ -57,6 +58,7 @@ import { TermsOfUsePage } from '../pages/settings/about/terms-of-use/terms-of-us
|
||||||
/* Send */
|
/* Send */
|
||||||
import { AmountPage } from '../pages/send/amount/amount';
|
import { AmountPage } from '../pages/send/amount/amount';
|
||||||
import { ConfirmPage } from '../pages/send/confirm/confirm';
|
import { ConfirmPage } from '../pages/send/confirm/confirm';
|
||||||
|
import { ChooseFeeLevelPage } from '../pages/choose-fee-level/choose-fee-level';
|
||||||
|
|
||||||
/* Receive */
|
/* Receive */
|
||||||
import { CustomAmountPage } from '../pages/receive/custom-amount/custom-amount';
|
import { CustomAmountPage } from '../pages/receive/custom-amount/custom-amount';
|
||||||
|
@ -69,10 +71,13 @@ import { BwcProvider } from '../providers/bwc/bwc';
|
||||||
import { BwcErrorProvider } from '../providers/bwc-error/bwc-error';
|
import { BwcErrorProvider } from '../providers/bwc-error/bwc-error';
|
||||||
import { ConfigProvider } from '../providers/config/config';
|
import { ConfigProvider } from '../providers/config/config';
|
||||||
import { DerivationPathHelperProvider } from '../providers/derivation-path-helper/derivation-path-helper';
|
import { DerivationPathHelperProvider } from '../providers/derivation-path-helper/derivation-path-helper';
|
||||||
|
import { ExternalLinkProvider } from '../providers/external-link/external-link';
|
||||||
|
import { FeeProvider } from '../providers/fee/fee';
|
||||||
import { Filter } from '../providers/filter/filter';
|
import { Filter } from '../providers/filter/filter';
|
||||||
import { IncomingDataProvider } from '../providers/incoming-data/incoming-data';
|
import { IncomingDataProvider } from '../providers/incoming-data/incoming-data';
|
||||||
import { LanguageProvider } from '../providers/language/language';
|
import { LanguageProvider } from '../providers/language/language';
|
||||||
import { OnGoingProcess } from '../providers/on-going-process/on-going-process';
|
import { NodeWebkitProvider } from '../providers/node-webkit/node-webkit';
|
||||||
|
import { OnGoingProcessProvider } from '../providers/on-going-process/on-going-process';
|
||||||
import { PayproProvider } from '../providers/paypro/paypro';
|
import { PayproProvider } from '../providers/paypro/paypro';
|
||||||
import { PersistenceProvider, persistenceProviderFactory } from '../providers/persistence/persistence';
|
import { PersistenceProvider, persistenceProviderFactory } from '../providers/persistence/persistence';
|
||||||
import { PlatformProvider } from '../providers/platform/platform';
|
import { PlatformProvider } from '../providers/platform/platform';
|
||||||
|
@ -82,6 +87,7 @@ import { RateProvider } from '../providers/rate/rate';
|
||||||
import { ReleaseProvider } from '../providers/release/release';
|
import { ReleaseProvider } from '../providers/release/release';
|
||||||
import { ScanProvider } from '../providers/scan/scan';
|
import { ScanProvider } from '../providers/scan/scan';
|
||||||
import { TouchIdProvider } from '../providers/touchid/touchid';
|
import { TouchIdProvider } from '../providers/touchid/touchid';
|
||||||
|
import { TxConfirmNotificationProvider } from '../providers/tx-confirm-notification/tx-confirm-notification';
|
||||||
import { TxFormatProvider } from '../providers/tx-format/tx-format';
|
import { TxFormatProvider } from '../providers/tx-format/tx-format';
|
||||||
import { WalletProvider } from '../providers/wallet/wallet';
|
import { WalletProvider } from '../providers/wallet/wallet';
|
||||||
|
|
||||||
|
@ -91,6 +97,7 @@ export function createTranslateLoader(http: Http) {
|
||||||
|
|
||||||
let pages: any = [
|
let pages: any = [
|
||||||
AddPage,
|
AddPage,
|
||||||
|
ChooseFeeLevelPage,
|
||||||
CreateWalletPage,
|
CreateWalletPage,
|
||||||
CopayersPage,
|
CopayersPage,
|
||||||
ImportWalletPage,
|
ImportWalletPage,
|
||||||
|
@ -111,6 +118,7 @@ let pages: any = [
|
||||||
HomePage,
|
HomePage,
|
||||||
LockPage,
|
LockPage,
|
||||||
OnboardingPage,
|
OnboardingPage,
|
||||||
|
PayProPage,
|
||||||
PinModalPage,
|
PinModalPage,
|
||||||
ReceivePage,
|
ReceivePage,
|
||||||
SendPage,
|
SendPage,
|
||||||
|
@ -130,10 +138,13 @@ let providers: any = [
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
DerivationPathHelperProvider,
|
DerivationPathHelperProvider,
|
||||||
|
ExternalLinkProvider,
|
||||||
|
FeeProvider,
|
||||||
Filter,
|
Filter,
|
||||||
IncomingDataProvider,
|
IncomingDataProvider,
|
||||||
LanguageProvider,
|
LanguageProvider,
|
||||||
OnGoingProcess,
|
NodeWebkitProvider,
|
||||||
|
OnGoingProcessProvider,
|
||||||
PayproProvider,
|
PayproProvider,
|
||||||
PlatformProvider,
|
PlatformProvider,
|
||||||
ProfileProvider,
|
ProfileProvider,
|
||||||
|
@ -148,6 +159,7 @@ let providers: any = [
|
||||||
Toast,
|
Toast,
|
||||||
TouchID,
|
TouchID,
|
||||||
TouchIdProvider,
|
TouchIdProvider,
|
||||||
|
TxConfirmNotificationProvider,
|
||||||
TxFormatProvider,
|
TxFormatProvider,
|
||||||
WalletProvider,
|
WalletProvider,
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 40.1 (33804) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Group 2</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Icons/Transaction/Send" transform="translate(-6.000000, -8.000000)">
|
||||||
|
<g id="Group-2" transform="translate(7.000000, 7.000000)">
|
||||||
|
<g id="Icons/Send">
|
||||||
|
<g id="icons/received">
|
||||||
|
<g id="Received" transform="translate(17.244258, 17.883990) scale(-1, 1) translate(-17.244258, -17.883990) translate(0.244258, 0.883990)">
|
||||||
|
<path d="M17.3272285,33.991292 C26.4399269,33.991292 33.8272285,26.6039904 33.8272285,17.491292 C33.8272285,8.37859367 26.4399269,0.991292046 17.3272285,0.991292046 C8.21453012,0.991292046 0.82722849,8.37859367 0.82722849,17.491292 C0.82722849,26.6039904 8.21453012,33.991292 17.3272285,33.991292 Z" id="Oval-204" stroke="#BDBDBD" opacity="0.8"></path>
|
||||||
|
<path d="M11.0503675,17.6759465 L24.0056239,17.6759465 M15.816934,23.6130491 L10,17.7604241 L15.614688,12" id="Line" stroke="#BEBEBE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(17.002812, 17.806525) scale(1, -1) rotate(-90.000000) translate(-17.002812, -17.806525) "></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,8 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content padding>
|
||||||
|
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'page-choose-fee-level',
|
||||||
|
templateUrl: 'choose-fee-level.html',
|
||||||
|
})
|
||||||
|
export class ChooseFeeLevelPage {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<ion-header>
|
||||||
|
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-buttons start>
|
||||||
|
<button (click)="close()" ion-button>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content padding>
|
||||||
|
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ViewController } from 'ionic-angular';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'page-payrpo',
|
||||||
|
templateUrl: 'paypro.html',
|
||||||
|
})
|
||||||
|
export class PayProPage {
|
||||||
|
|
||||||
|
|
||||||
|
constructor(public viewCtrl: ViewController) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.viewCtrl.dismiss();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,6 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { NavController, NavParams } from 'ionic-angular';
|
import { NavController, NavParams } from 'ionic-angular';
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated class for the CustomAmountPage page.
|
|
||||||
*
|
|
||||||
* See http://ionicframework.com/docs/components/#navigation for more info
|
|
||||||
* on Ionic pages and navigation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-custom-amount',
|
selector: 'page-custom-amount',
|
||||||
|
@ -30,7 +24,7 @@ export class CustomAmountPage {
|
||||||
this.updateQrAddress();
|
this.updateQrAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateQrAddress () {
|
updateQrAddress() {
|
||||||
this.qrAddress = this.protocolHandler + ":" + this.address + "?amount=" + this.amount;
|
this.qrAddress = this.protocolHandler + ":" + this.address + "?amount=" + this.amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { NavController, Events, ActionSheetController, AlertController} from 'ionic-angular';
|
import { NavController, Events, ActionSheetController, AlertController } from 'ionic-angular';
|
||||||
|
|
||||||
//native
|
//native
|
||||||
import { SocialSharing } from '@ionic-native/social-sharing';
|
import { SocialSharing } from '@ionic-native/social-sharing';
|
||||||
|
@ -83,7 +83,7 @@ export class ReceivePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public requestSpecificAmount(): void {
|
public requestSpecificAmount(): void {
|
||||||
this.navCtrl.push(AmountPage, { address: this.address, sending: false });
|
this.navCtrl.push(AmountPage, { address: this.address, fromSend: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAddress(newAddr?: boolean): void {
|
private setAddress(newAddr?: boolean): void {
|
||||||
|
@ -143,7 +143,7 @@ export class ReceivePage {
|
||||||
buttons: [{
|
buttons: [{
|
||||||
text: 'I understand',
|
text: 'I understand',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.navCtrl.push(BackupGamePage, {walletId: this.wallet.credentials.walletId});
|
this.navCtrl.push(BackupGamePage, { walletId: this.wallet.credentials.walletId });
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,57 +10,58 @@
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-label padding>Recipient</ion-label>
|
<ion-label padding>Recipient</ion-label>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-icon name="contact" item-left></ion-icon>
|
<ion-icon name="contact" item-left></ion-icon>
|
||||||
<span>{{ address }}</span>
|
<span>{{ toAddress }}</span>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<div>
|
<div>
|
||||||
<div padding>
|
<div padding>
|
||||||
<div>
|
<div>
|
||||||
<span>Amount</span>
|
<span>Amount</span>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>{{ amount || "0.00" }}</span>
|
|
||||||
</div>
|
|
||||||
<div [hidden]="!globalResult">
|
|
||||||
= {{globalResult|| "0.00" }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="keypad">
|
<div>
|
||||||
<div class="operator-row">
|
<span>{{ amountStr || "0.00" }}</span>
|
||||||
<div class="col operator-send"
|
</div>
|
||||||
[hidden]="!allowSend" (click)="finish()">
|
<div [hidden]="!globalResult">
|
||||||
<ion-icon name="arrow-round-forward"></ion-icon>
|
= {{globalResult|| "0.00" }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col digit" (click)="pushDigit('7')">7</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('8')">8</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('9')">9</div>
|
|
||||||
<div class="col operator" (click)="pushOperator('/')">÷</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col digit" (click)="pushDigit('4')">4</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('5')">5</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('6')">6</div>
|
|
||||||
<div class="col operator" (click)="pushOperator('x')">×</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col digit" (click)="pushDigit('1')">1</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('2')">2</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('3')">3</div>
|
|
||||||
<div class="col operator" (click)="pushOperator('+')">+</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col digit" (click)="pushDigit('.')">.</div>
|
|
||||||
<div class="col digit" (click)="pushDigit('0')">0</div>
|
|
||||||
<div class="col digit" (click)="removeDigit()"><ion-icon name="backspace"></ion-icon></div>
|
|
||||||
<div class="col operator" (click)="pushOperator('-')">-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="keypad">
|
||||||
|
<div class="operator-row">
|
||||||
|
<div class="col operator-send" [hidden]="!allowSend" (click)="finish()">
|
||||||
|
<ion-icon name="arrow-round-forward"></ion-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col digit" (click)="pushDigit('7')">7</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('8')">8</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('9')">9</div>
|
||||||
|
<div class="col operator" (click)="pushOperator('/')">÷</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col digit" (click)="pushDigit('4')">4</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('5')">5</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('6')">6</div>
|
||||||
|
<div class="col operator" (click)="pushOperator('x')">×</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col digit" (click)="pushDigit('1')">1</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('2')">2</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('3')">3</div>
|
||||||
|
<div class="col operator" (click)="pushOperator('+')">+</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col digit" (click)="pushDigit('.')">.</div>
|
||||||
|
<div class="col digit" (click)="pushDigit('0')">0</div>
|
||||||
|
<div class="col digit" (click)="removeDigit()">
|
||||||
|
<ion-icon name="backspace"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="col operator" (click)="pushOperator('-')">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ion-content>
|
</ion-content>
|
|
@ -1,6 +1,18 @@
|
||||||
import { Component, HostListener } from '@angular/core';
|
import { Component, HostListener } from '@angular/core';
|
||||||
import { NavController, NavParams } from 'ionic-angular';
|
import { NavController, NavParams } from 'ionic-angular';
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
//providers
|
||||||
|
import { ProfileProvider } from '../../../providers/profile/profile';
|
||||||
|
import { ConfigProvider } from '../../../providers/config/config';
|
||||||
|
import { PlatformProvider } from '../../../providers/platform/platform';
|
||||||
|
import { NodeWebkitProvider } from '../../../providers/node-webkit/node-webkit';
|
||||||
|
import { RateProvider } from '../../../providers/rate/rate';
|
||||||
|
import { Filter } from '../../../providers/filter/filter';
|
||||||
|
import { TxFormatProvider } from '../../../providers/tx-format/tx-format';
|
||||||
|
|
||||||
|
//pages
|
||||||
import { ConfirmPage } from '../confirm/confirm';
|
import { ConfirmPage } from '../confirm/confirm';
|
||||||
import { CustomAmountPage } from '../../receive/custom-amount/custom-amount';
|
import { CustomAmountPage } from '../../receive/custom-amount/custom-amount';
|
||||||
|
|
||||||
|
@ -10,12 +22,21 @@ import { CustomAmountPage } from '../../receive/custom-amount/custom-amount';
|
||||||
})
|
})
|
||||||
export class AmountPage {
|
export class AmountPage {
|
||||||
|
|
||||||
public address: string;
|
public amountStr: string = '';
|
||||||
public amount: string;
|
|
||||||
public smallFont: boolean;
|
public smallFont: boolean;
|
||||||
public allowSend: boolean;
|
public allowSend: boolean;
|
||||||
public globalResult: string;
|
public globalResult: string;
|
||||||
public sending: boolean;
|
public fromSend: boolean;
|
||||||
|
public unit: string;
|
||||||
|
public alternativeUnit: string;
|
||||||
|
public recipientType: string;
|
||||||
|
public toAddress: string;
|
||||||
|
public name: string;
|
||||||
|
public email: string;
|
||||||
|
public color: string;
|
||||||
|
public amount: number;
|
||||||
|
public showSendMax: boolean = false;
|
||||||
|
public useSendMax: boolean;
|
||||||
|
|
||||||
private LENGTH_EXPRESSION_LIMIT = 19;
|
private LENGTH_EXPRESSION_LIMIT = 19;
|
||||||
private SMALL_FONT_SIZE_LIMIT = 10;
|
private SMALL_FONT_SIZE_LIMIT = 10;
|
||||||
|
@ -23,18 +44,135 @@ export class AmountPage {
|
||||||
private unitIndex: number = 0;
|
private unitIndex: number = 0;
|
||||||
private reNr: RegExp = /^[1234567890\.]$/;
|
private reNr: RegExp = /^[1234567890\.]$/;
|
||||||
private reOp: RegExp = /^[\*\+\-\/]$/;
|
private reOp: RegExp = /^[\*\+\-\/]$/;
|
||||||
|
private fiatCode: string;
|
||||||
|
private altUnitIndex: number = 0;
|
||||||
|
private fixedUnit: boolean;
|
||||||
|
private _id: number;
|
||||||
|
private nextStep: string;
|
||||||
|
|
||||||
constructor(public navCtrl: NavController, public navParams: NavParams) {
|
// Config Related values
|
||||||
this.amount = '';
|
public config: any;
|
||||||
|
public walletConfig: any;
|
||||||
|
public unitToSatoshi: number;
|
||||||
|
public unitDecimals: number;
|
||||||
|
public satToUnit: number;
|
||||||
|
public configFeeLevel: string;
|
||||||
|
public satToBtc: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public navCtrl: NavController,
|
||||||
|
public navParams: NavParams,
|
||||||
|
public profileProvider: ProfileProvider,
|
||||||
|
private configProvider: ConfigProvider,
|
||||||
|
private logger: Logger,
|
||||||
|
private platformProvider: PlatformProvider,
|
||||||
|
private nodeWebkitProvider: NodeWebkitProvider,
|
||||||
|
private rateProvider: RateProvider,
|
||||||
|
private filter: Filter,
|
||||||
|
private txFormatProvider: TxFormatProvider
|
||||||
|
) {
|
||||||
this.allowSend = false;
|
this.allowSend = false;
|
||||||
|
this.config = this.configProvider.get();
|
||||||
|
this.walletConfig = this.config.wallet;
|
||||||
|
this.unitToSatoshi = this.walletConfig.settings.unitToSatoshi;
|
||||||
|
this.unitDecimals = this.walletConfig.settings.unitDecimals;
|
||||||
|
this.satToUnit = 1 / this.unitToSatoshi;
|
||||||
|
this.satToBtc = 1 / 100000000;
|
||||||
|
this.configFeeLevel = this.walletConfig.settings.feeLevel ? this.walletConfig.settings.feeLevel : 'normal';
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidLoad() {
|
ionViewDidLoad() {
|
||||||
console.log('Params', this.navParams.data);
|
console.log('Params', this.navParams.data);
|
||||||
this.address = this.navParams.data.address;
|
this.toAddress = this.navParams.data.toAddress;
|
||||||
this.sending = this.navParams.data.sending;
|
this.fromSend = this.navParams.data.fromSend;
|
||||||
|
this._id = this.navParams.data.id;
|
||||||
|
this.nextStep = this.navParams.data.nextStep;
|
||||||
|
this.recipientType = this.navParams.data.recipientType || null;
|
||||||
|
this.name = this.navParams.data.name;
|
||||||
|
this.email = this.navParams.data.email;
|
||||||
|
this.color = this.navParams.data.color;
|
||||||
|
this.amount = this.navParams.data.amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ionViewDidEnter() {
|
||||||
|
this.setAvailableUnits();
|
||||||
|
this.updateUnitUI();
|
||||||
|
//this.showMenu = $ionicHistory.backView() && ($ionicHistory.backView().stateName == 'tabs.send' || $ionicHistory.backView().stateName == 'tabs.bitpayCard'); TODO
|
||||||
|
if (!this.nextStep && !this.toAddress) {
|
||||||
|
this.logger.error('Bad params at amount')
|
||||||
|
throw ('bad params');
|
||||||
|
}
|
||||||
|
// in SAT ALWAYS
|
||||||
|
if (this.amount) {
|
||||||
|
this.amountStr = ((this.amount) * this.satToUnit).toFixed(this.unitDecimals);
|
||||||
|
}
|
||||||
|
this.processAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private paste(value: string): void {
|
||||||
|
this.amountStr = value;
|
||||||
|
this.processAmount();
|
||||||
|
};
|
||||||
|
|
||||||
|
public processClipboard(): void {
|
||||||
|
if (!this.platformProvider.isNW) return;
|
||||||
|
var value = this.nodeWebkitProvider.readFromClipboard();
|
||||||
|
if (value && this.evaluate(value) > 0) this.paste(this.evaluate(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
public showSendMaxMenu(): void {
|
||||||
|
this.showSendMax = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMax(): void {
|
||||||
|
this.showSendMax = false;
|
||||||
|
this.useSendMax = true;
|
||||||
|
this.finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
public toggleAlternative(): void {
|
||||||
|
if (this.amountStr && this.isExpression(this.amountStr)) {
|
||||||
|
let amount = this.evaluate(this.format(this.amountStr));
|
||||||
|
this.globalResult = '= ' + this.processResult(amount);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public changeUnit(): void {
|
||||||
|
if (this.fixedUnit) return;
|
||||||
|
|
||||||
|
this.unitIndex++;
|
||||||
|
if (this.unitIndex >= this.availableUnits.length) this.unitIndex = 0;
|
||||||
|
|
||||||
|
|
||||||
|
if (this.availableUnits[this.unitIndex].isFiat) {
|
||||||
|
// Always return to BTC... TODO?
|
||||||
|
this.altUnitIndex = 0;
|
||||||
|
} else {
|
||||||
|
this.altUnitIndex = _.findIndex(this.availableUnits, {
|
||||||
|
isFiat: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateUnitUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
public changeAlternativeUnit(): void {
|
||||||
|
|
||||||
|
// Do nothing is fiat is not main unit
|
||||||
|
if (!this.availableUnits[this.unitIndex].isFiat) return;
|
||||||
|
|
||||||
|
var nextCoin = _.findIndex(this.availableUnits, function (x) {
|
||||||
|
if (x.isFiat) return false;
|
||||||
|
if (x.id == this.availableUnits[this.altUnitIndex].id) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nextCoin >= 0) {
|
||||||
|
this.altUnitIndex = nextCoin;
|
||||||
|
this.updateUnitUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@HostListener('document:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent) {
|
@HostListener('document:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent) {
|
||||||
if (!event.key) return;
|
if (!event.key) return;
|
||||||
if (event.which === 8) {
|
if (event.which === 8) {
|
||||||
|
@ -51,26 +189,26 @@ export class AmountPage {
|
||||||
} else if (event.keyCode === 13) this.finish();
|
} else if (event.keyCode === 13) this.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
pushDigit(digit: string) {
|
public pushDigit(digit: string): void {
|
||||||
if (this.amount && this.amount.length >= this.LENGTH_EXPRESSION_LIMIT) return;
|
if (this.amountStr && this.amountStr.length >= this.LENGTH_EXPRESSION_LIMIT) return;
|
||||||
if (this.amount.indexOf('.') > -1 && digit == '.') return;
|
if (this.amountStr.indexOf('.') > -1 && digit == '.') return;
|
||||||
// TODO: next line - Need: isFiat
|
// TODO: next line - Need: isFiat
|
||||||
//if (this.availableUnits[this.unitIndex].isFiat && this.amount.indexOf('.') > -1 && this.amount[this.amount.indexOf('.') + 2]) return;
|
//if (this.availableUnits[this.unitIndex].isFiat && this.amountStr.indexOf('.') > -1 && this.amountStr[this.amountStr.indexOf('.') + 2]) return;
|
||||||
|
|
||||||
this.amount = (this.amount + digit).replace('..', '.');
|
this.amountStr = (this.amountStr + digit).replace('..', '.');
|
||||||
this.checkFontSize();
|
this.checkFontSize();
|
||||||
this.processAmount();
|
this.processAmount();
|
||||||
};
|
};
|
||||||
|
|
||||||
removeDigit() {
|
public removeDigit(): void {
|
||||||
this.amount = (this.amount).toString().slice(0, -1);
|
this.amountStr = (this.amountStr).toString().slice(0, -1);
|
||||||
this.processAmount();
|
this.processAmount();
|
||||||
this.checkFontSize();
|
this.checkFontSize();
|
||||||
};
|
};
|
||||||
|
|
||||||
pushOperator(operator: string) {
|
public pushOperator(operator: string): void {
|
||||||
if (!this.amount || this.amount.length == 0) return;
|
if (!this.amountStr || this.amountStr.length == 0) return;
|
||||||
this.amount = this._pushOperator(this.amount, operator);
|
this.amountStr = this._pushOperator(this.amountStr, operator);
|
||||||
};
|
};
|
||||||
|
|
||||||
private _pushOperator(val: string, operator: string) {
|
private _pushOperator(val: string, operator: string) {
|
||||||
|
@ -81,27 +219,27 @@ export class AmountPage {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
isOperator(val: string) {
|
private isOperator(val: string): boolean {
|
||||||
const regex = /[\/\-\+\x\*]/;
|
const regex = /[\/\-\+\x\*]/;
|
||||||
return regex.test(val);
|
return regex.test(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
isExpression(val: string) {
|
private isExpression(val: string): boolean {
|
||||||
const regex = /^\.?\d+(\.?\d+)?([\/\-\+\*x]\d?\.?\d+)+$/;
|
const regex = /^\.?\d+(\.?\d+)?([\/\-\+\*x]\d?\.?\d+)+$/;
|
||||||
return regex.test(val);
|
return regex.test(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkFontSize() {
|
private checkFontSize(): void {
|
||||||
if (this.amount && this.amount.length >= this.SMALL_FONT_SIZE_LIMIT) this.smallFont = true;
|
if (this.amountStr && this.amountStr.length >= this.SMALL_FONT_SIZE_LIMIT) this.smallFont = true;
|
||||||
else this.smallFont = false;
|
else this.smallFont = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
processAmount() {
|
private processAmount(): void {
|
||||||
var formatedValue = this.format(this.amount);
|
var formatedValue = this.format(this.amountStr);
|
||||||
var result = this.evaluate(formatedValue);
|
var result = this.evaluate(formatedValue);
|
||||||
this.allowSend = _.isNumber(result) && +result > 0;
|
this.allowSend = _.isNumber(result) && +result > 0;
|
||||||
if (_.isNumber(result)) {
|
if (_.isNumber(result)) {
|
||||||
this.globalResult = this.isExpression(this.amount) ? '= ' + this.processResult(result) : '';
|
this.globalResult = this.isExpression(this.amountStr) ? '= ' + this.processResult(result) : '';
|
||||||
|
|
||||||
// TODO this.globalResult is always undefinded - Need: processResult()
|
// TODO this.globalResult is always undefinded - Need: processResult()
|
||||||
/* if (this.availableUnits[this.unitIndex].isFiat) {
|
/* if (this.availableUnits[this.unitIndex].isFiat) {
|
||||||
|
@ -141,30 +279,139 @@ export class AmountPage {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
processResult(val: number) {
|
private processResult(val: number): number {
|
||||||
// TODO: implement this function correctly - Need: txFormatService, isFiat, $filter
|
if (this.availableUnits[this.unitIndex].isFiat) return this.filter.formatFiatAmount(val);
|
||||||
console.log("processResult TODO");
|
else return this.txFormatProvider.formatAmount(parseInt(val.toFixed(this.unitDecimals)) * this.unitToSatoshi, true);
|
||||||
/*if (this.availableUnits[this.unitIndex].isFiat) return $filter('formatFiatAmount')(val);
|
|
||||||
else return txFormatService.formatAmount(val.toFixed(unitDecimals) * unitToSatoshi, true);*/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fromFiat(val: number) {
|
private fromFiat(val: number): number {
|
||||||
// TODO: implement next line correctly - Need: rateService
|
return parseFloat((this.rateProvider.fromFiat(val, this.fiatCode, this.availableUnits[this.altUnitIndex].id) * this.satToUnit).toFixed(this.unitDecimals));
|
||||||
//return parseFloat((rateService.fromFiat(val, fiatCode, availableUnits[altUnitIndex].id) * satToUnit).toFixed(unitDecimals));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
toFiat(val) {
|
private toFiat(val): number {
|
||||||
// TODO: implement next line correctly - Need: rateService
|
if (!this.rateProvider.getRate(this.fiatCode)) return;
|
||||||
/*if (!rateService.getRate(fiatCode)) return;
|
return parseFloat((this.rateProvider.toFiat(val * this.unitToSatoshi, this.fiatCode, this.availableUnits[this.unitIndex].id)).toFixed(2));
|
||||||
return parseFloat((rateService.toFiat(val * unitToSatoshi, fiatCode, availableUnits[unitIndex].id)).toFixed(2));*/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
finish() {
|
public finish(): void {
|
||||||
if(this.sending) {
|
|
||||||
this.navCtrl.push(ConfirmPage, {address: this.address, amount: this.globalResult});
|
let unit = this.availableUnits[this.unitIndex];
|
||||||
|
let _amount = this.evaluate(this.format(this.amountStr));
|
||||||
|
var coin = unit.id;
|
||||||
|
if (unit.isFiat) {
|
||||||
|
coin = this.availableUnits[this.altUnitIndex].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.nextStep) {
|
||||||
|
|
||||||
|
this.navCtrl.push(this.nextStep, {
|
||||||
|
id: this._id,
|
||||||
|
amount: this.useSendMax ? null : _amount,
|
||||||
|
currency: unit.id.toUpperCase(),
|
||||||
|
coin: coin,
|
||||||
|
useSendMax: this.useSendMax
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("To do");
|
let amount = _amount;
|
||||||
this.navCtrl.push(CustomAmountPage, {address: this.address, amount: this.globalResult});
|
|
||||||
|
if (unit.isFiat) {
|
||||||
|
amount = (this.fromFiat(amount) * this.unitToSatoshi).toFixed(0);
|
||||||
|
} else {
|
||||||
|
amount = (amount * this.unitToSatoshi).toFixed(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any = {
|
||||||
|
recipientType: this.recipientType,
|
||||||
|
amount: this.globalResult,
|
||||||
|
toAddress: this.toAddress,
|
||||||
|
name: this.name,
|
||||||
|
email: this.email,
|
||||||
|
color: this.color,
|
||||||
|
coin: coin,
|
||||||
|
useSendMax: this.useSendMax
|
||||||
|
}
|
||||||
|
this.navCtrl.push(ConfirmPage, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private setAvailableUnits(): void {
|
||||||
|
|
||||||
|
let hasBTCWallets = this.profileProvider.getWallets({
|
||||||
|
coin: 'btc'
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if (hasBTCWallets) {
|
||||||
|
this.availableUnits.push({
|
||||||
|
name: 'Bitcoin',
|
||||||
|
id: 'btc',
|
||||||
|
shortName: 'BTC',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasBCHWallets = this.profileProvider.getWallets({
|
||||||
|
coin: 'bch'
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if (hasBCHWallets) {
|
||||||
|
this.availableUnits.push({
|
||||||
|
name: 'Bitcoin Cash',
|
||||||
|
id: 'bch',
|
||||||
|
shortName: 'BCH',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let unitIndex = 0;
|
||||||
|
if (this.navParams.data.coin) {
|
||||||
|
var coins = this.navParams.data.coin.split(',');
|
||||||
|
var newAvailableUnits = [];
|
||||||
|
|
||||||
|
_.each(coins, (c) => {
|
||||||
|
var coin = _.find(this.availableUnits, {
|
||||||
|
id: c
|
||||||
|
});
|
||||||
|
if (!coin) {
|
||||||
|
this.logger.warn('Could not find desired coin:' + this.navParams.data.coin)
|
||||||
|
} else {
|
||||||
|
newAvailableUnits.push(coin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newAvailableUnits.length > 0) {
|
||||||
|
this.availableUnits = newAvailableUnits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// currency have preference
|
||||||
|
let fiatName;
|
||||||
|
if (this.navParams.data.currency) {
|
||||||
|
this.fiatCode = this.navParams.data.currency;
|
||||||
|
this.altUnitIndex = unitIndex
|
||||||
|
unitIndex = this.availableUnits.length;
|
||||||
|
} else {
|
||||||
|
this.fiatCode = this.config.alternativeIsoCode || 'USD';
|
||||||
|
fiatName = this.config.alternanativeName || this.fiatCode;
|
||||||
|
this.altUnitIndex = this.availableUnits.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.availableUnits.push({
|
||||||
|
name: fiatName || this.fiatCode,
|
||||||
|
// TODO
|
||||||
|
id: this.fiatCode,
|
||||||
|
shortName: this.fiatCode,
|
||||||
|
isFiat: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.navParams.data.fixedUnit) {
|
||||||
|
this.fixedUnit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateUnitUI(): void {
|
||||||
|
this.unit = this.availableUnits[this.unitIndex].shortName;
|
||||||
|
this.alternativeUnit = this.availableUnits[this.altUnitIndex].shortName;
|
||||||
|
|
||||||
|
this.processAmount();
|
||||||
|
this.logger.debug('Update unit coin @amount unit:' + this.unit + " alternativeUnit:" + this.alternativeUnit);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,54 @@
|
||||||
<ion-header>
|
<ion-header>
|
||||||
|
|
||||||
<ion-navbar>
|
<ion-navbar>
|
||||||
<ion-title>Confirm</ion-title>
|
<ion-title>{{ 'Confirm' | translate }}</ion-title>
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
|
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
|
|
||||||
<ion-content padding>
|
<ion-content>
|
||||||
<div>To: {{address}}</div>
|
<ion-list>
|
||||||
<div>Amount: {{amount}}</div>
|
<ion-item *ngIf="!criticalError">
|
||||||
|
<!--<ion-icon ios="ios-arrow-up" md="md-arrow-up" item-left></ion-icon>-->
|
||||||
|
<div class="sending-label">
|
||||||
|
<img src="assets/img/icon-tx-sent-outline.svg">
|
||||||
|
<span translate *ngIf="!tx.sendMax">Sending</span>
|
||||||
|
<span translate *ngIf="tx.sendMax">Sending maximum amount</span>
|
||||||
|
</div>
|
||||||
|
<div class="amount-label">
|
||||||
|
<div class="amount">{{tx.amountValueStr || '...'}} <span class="unit">{{tx.amountUnitStr}}</span></div>
|
||||||
|
<div class="alternative">{{tx.alternativeAmountStr || '...'}}</div>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="!criticalError">
|
||||||
|
<div [hidden]="!wallet">
|
||||||
|
<span translate>To</span>
|
||||||
|
<div *ngIf="address">
|
||||||
|
<div>{{address}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="!criticalError">
|
||||||
|
<a [hidden]="!wallet" (click)="showWalletSelector()">
|
||||||
|
<span translate>From</span>
|
||||||
|
<div class="wallet" *ngIf="wallet">
|
||||||
|
<div>{{wallet.name}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="wallet" *ngIf="!wallet">
|
||||||
|
<img src="assets/img/icon-wallet.svg"/>
|
||||||
|
<div>...</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</ion-item>
|
||||||
|
<div *ngIf="walletSelector">
|
||||||
|
<ion-list>
|
||||||
|
<ion-label>Send from</ion-label>
|
||||||
|
<ion-item *ngFor="let item of wallets" (click)="onWalletSelect(item)">
|
||||||
|
{{item.name}}
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</div>
|
||||||
|
</ion-list>
|
||||||
|
<button ion-button full (click)="approve(tx, wallet, statusChangeHandler)" [disabled]="!wallet">Send</button>
|
||||||
</ion-content>
|
</ion-content>
|
|
@ -1,5 +1,27 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { NavController, NavParams } from 'ionic-angular';
|
import { NavController, NavParams, ModalController } from 'ionic-angular';
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
import { SendPage } from '../../send/send';
|
||||||
|
import { HomePage } from '../../home/home';
|
||||||
|
import { PayProPage } from '../../paypro/paypro';
|
||||||
|
import { ChooseFeeLevelPage } from '../../choose-fee-level/choose-fee-level';
|
||||||
|
|
||||||
|
// Providers
|
||||||
|
import { ConfigProvider } from '../../../providers/config/config';
|
||||||
|
import { PlatformProvider } from '../../../providers/platform/platform';
|
||||||
|
import { ProfileProvider } from '../../../providers/profile/profile';
|
||||||
|
import { WalletProvider } from '../../../providers/wallet/wallet';
|
||||||
|
import { BwcProvider } from '../../../providers/bwc/bwc';
|
||||||
|
import { PopupProvider } from '../../../providers/popup/popup';
|
||||||
|
import { ExternalLinkProvider } from '../../../providers/external-link/external-link';
|
||||||
|
import { BwcErrorProvider } from '../../../providers/bwc-error/bwc-error';
|
||||||
|
import { OnGoingProcessProvider } from '../../../providers/on-going-process/on-going-process';
|
||||||
|
import { TxFormatProvider } from '../../../providers/tx-format/tx-format';
|
||||||
|
import { FeeProvider } from '../../../providers/fee/fee';
|
||||||
|
import { TxConfirmNotificationProvider } from '../../../providers/tx-confirm-notification/tx-confirm-notification';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-confirm',
|
selector: 'page-confirm',
|
||||||
|
@ -7,16 +29,645 @@ import { NavController, NavParams } from 'ionic-angular';
|
||||||
})
|
})
|
||||||
export class ConfirmPage {
|
export class ConfirmPage {
|
||||||
|
|
||||||
public address: string;
|
public data: any;
|
||||||
|
public toAddress: string;
|
||||||
public amount: string;
|
public amount: string;
|
||||||
|
public coin: string;
|
||||||
|
|
||||||
constructor(public navCtrl: NavController, public navParams: NavParams) {
|
public countDown = null;
|
||||||
|
public CONFIRM_LIMIT_USD: number = 20;
|
||||||
|
public FEE_TOO_HIGH_LIMIT_PER: number = 15;
|
||||||
|
|
||||||
|
public tx: any = {};
|
||||||
|
public wallet: any;
|
||||||
|
public wallets: any;
|
||||||
|
public noWalletMessage: string;
|
||||||
|
public criticalError: boolean;
|
||||||
|
public showAddress: boolean;
|
||||||
|
public walletSelectorTitle: string;
|
||||||
|
public walletSelector: boolean;
|
||||||
|
public buttonText: string;
|
||||||
|
public paymentExpired: boolean;
|
||||||
|
public remainingTimeStr: string;
|
||||||
|
public sendStatus: string;
|
||||||
|
|
||||||
|
// Config Related values
|
||||||
|
public config: any;
|
||||||
|
public walletConfig: any;
|
||||||
|
public unitToSatoshi: number;
|
||||||
|
public unitDecimals: number;
|
||||||
|
public satToUnit: number;
|
||||||
|
public configFeeLevel: string;
|
||||||
|
|
||||||
|
|
||||||
|
// Platform info
|
||||||
|
public isCordova: boolean = this.platformProvider.isCordova;
|
||||||
|
public isWindowsPhoneApp = this.platformProvider.isCordova && this.platformProvider.isWP;
|
||||||
|
|
||||||
|
//custom fee flag
|
||||||
|
public usingCustomFee: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private navCtrl: NavController,
|
||||||
|
private navParams: NavParams,
|
||||||
|
private logger: Logger,
|
||||||
|
private configProvider: ConfigProvider,
|
||||||
|
private platformProvider: PlatformProvider,
|
||||||
|
private profileProvider: ProfileProvider,
|
||||||
|
private walletProvider: WalletProvider,
|
||||||
|
private bwcProvider: BwcProvider,
|
||||||
|
private popupProvider: PopupProvider,
|
||||||
|
private externalLinkProvider: ExternalLinkProvider,
|
||||||
|
private bwcErrorProvider: BwcErrorProvider,
|
||||||
|
private onGoingProcessProvider: OnGoingProcessProvider,
|
||||||
|
private txFormatProvider: TxFormatProvider,
|
||||||
|
private feeProvider: FeeProvider,
|
||||||
|
private txConfirmNotificationProvider: TxConfirmNotificationProvider,
|
||||||
|
private modalCtrl: ModalController
|
||||||
|
) {
|
||||||
|
this.config = this.configProvider.get();
|
||||||
|
this.walletConfig = this.config.wallet;
|
||||||
|
this.unitToSatoshi = this.walletConfig.settings.unitToSatoshi;
|
||||||
|
this.unitDecimals = this.walletConfig.settings.unitDecimals;
|
||||||
|
this.satToUnit = 1 / this.unitToSatoshi;
|
||||||
|
this.configFeeLevel = this.walletConfig.settings.feeLevel ? this.walletConfig.settings.feeLevel : 'normal';
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidLoad() {
|
ionViewDidLoad() {
|
||||||
console.log('ionViewDidLoad ConfirmPage');
|
console.log('ionViewDidLoad ConfirmPage');
|
||||||
this.address = this.navParams.data.address;
|
this.data = this.navParams.data;
|
||||||
|
this.toAddress = this.navParams.data.toAddress;
|
||||||
this.amount = this.navParams.data.amount;
|
this.amount = this.navParams.data.amount;
|
||||||
|
this.coin = this.navParams.data.coin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ionViewDidEnter() {
|
||||||
|
// Setup
|
||||||
|
let B = this.coin == 'bch' ? this.bwcProvider.getBitcoreCash() : this.bwcProvider.getBitcore();
|
||||||
|
let networkName;
|
||||||
|
try {
|
||||||
|
networkName = (new B.Address(this.toAddress)).network.name;
|
||||||
|
} catch (e) {
|
||||||
|
let message = 'Copay only supports Bitcoin Cash using new version numbers addresses'; // TODO gettextCatalog
|
||||||
|
let backText = 'Go back'; // TODO gettextCatalog
|
||||||
|
let learnText = 'Learn more'; // TODO gettextCatalog
|
||||||
|
this.popupProvider.ionicConfirm(null, message, backText, learnText).then((res: boolean) => {
|
||||||
|
this.navCtrl.setRoot(SendPage);
|
||||||
|
this.navCtrl.popToRoot();
|
||||||
|
if (!res) {
|
||||||
|
let url = 'https://support.bitpay.com/hc/en-us/articles/115004671663';
|
||||||
|
this.externalLinkProvider.open(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab stateParams
|
||||||
|
let tx: any = {
|
||||||
|
amount: parseFloat(this.navParams.data.amount) * 100000000, // TODO review this line '* 100000000' convert satoshi to BTC
|
||||||
|
sendMax: this.navParams.data.useSendMax == 'true' ? true : false,
|
||||||
|
toAddress: this.navParams.data.toAddress,
|
||||||
|
description: this.navParams.data.description,
|
||||||
|
paypro: this.navParams.data.paypro,
|
||||||
|
|
||||||
|
feeLevel: this.configFeeLevel,
|
||||||
|
spendUnconfirmed: this.walletConfig.spendUnconfirmed,
|
||||||
|
|
||||||
|
// Vanity tx info (not in the real tx)
|
||||||
|
recipientType: this.navParams.data.recipientType || null,
|
||||||
|
name: this.navParams.data.name,
|
||||||
|
email: this.navParams.data.email,
|
||||||
|
color: this.navParams.data.color,
|
||||||
|
network: networkName,
|
||||||
|
coin: this.navParams.data.coin,
|
||||||
|
txp: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tx = tx;
|
||||||
|
|
||||||
|
if (tx.coin && tx.coin == 'bch') tx.feeLevel = 'normal';
|
||||||
|
|
||||||
|
this.showAddress = false;
|
||||||
|
|
||||||
|
this.walletSelectorTitle = 'Send from'; // TODO gettextCatalog
|
||||||
|
|
||||||
|
this.setWalletSelector(tx.coin, tx.network, tx.amount).then(() => {
|
||||||
|
if (this.wallets.length > 1) {
|
||||||
|
this.showWalletSelector();
|
||||||
|
} else if (this.wallets.length) {
|
||||||
|
this.setWallet(this.wallets[0], tx);
|
||||||
|
}
|
||||||
|
}).catch((err: any) => {
|
||||||
|
return this.exitWithError('Could not update wallets');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setWalletSelector(coin: string, network: string, minAmount: number): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
// no min amount? (sendMax) => look for no empty wallets
|
||||||
|
minAmount = minAmount ? minAmount : 1;
|
||||||
|
let filteredWallets: Array<any> = [];
|
||||||
|
let index: number = 0;
|
||||||
|
let walletsUpdated: number = 0;
|
||||||
|
|
||||||
|
this.wallets = this.profileProvider.getWallets({
|
||||||
|
onlyComplete: true,
|
||||||
|
network: network,
|
||||||
|
coin: coin
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.wallets || !this.wallets.length) {
|
||||||
|
this.setNoWallet('No wallets available', true); // TODO gettextCatalog
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(this.wallets, (wallet: any) => {
|
||||||
|
this.walletProvider.getStatus(wallet, {}).then((status: any) => {
|
||||||
|
walletsUpdated++;
|
||||||
|
wallet.status = status;
|
||||||
|
|
||||||
|
if (!status.availableBalanceSat)
|
||||||
|
this.logger.debug('No balance available in: ' + wallet.name);
|
||||||
|
|
||||||
|
if (status.availableBalanceSat > minAmount) {
|
||||||
|
filteredWallets.push(wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++index == this.wallets.length) {
|
||||||
|
if (!walletsUpdated)
|
||||||
|
return reject('Could not update any wallet');
|
||||||
|
|
||||||
|
if (_.isEmpty(filteredWallets)) {
|
||||||
|
this.setNoWallet('Insufficient funds', true); // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
this.wallets = _.clone(filteredWallets);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.logger.error(err);
|
||||||
|
if (++index == this.wallets.length) {
|
||||||
|
if (!walletsUpdated)
|
||||||
|
return reject('Could not update any wallet');
|
||||||
|
|
||||||
|
if (_.isEmpty(filteredWallets)) {
|
||||||
|
this.setNoWallet('Insufficient funds', true); // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
this.wallets = _.clone(filteredWallets);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setNoWallet(msg: string, criticalError?: boolean) {
|
||||||
|
this.wallet = null;
|
||||||
|
this.noWalletMessage = msg;
|
||||||
|
this.criticalError = criticalError;
|
||||||
|
this.logger.warn('Not ready to make the payment:' + msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
private exitWithError(err: any) {
|
||||||
|
this.logger.info('Error setting wallet selector:' + err);
|
||||||
|
this.popupProvider.ionicAlert("", this.bwcErrorProvider.msg(err)).then(() => { // TODO gettextCatalog
|
||||||
|
this.navCtrl.setRoot(SendPage);
|
||||||
|
this.navCtrl.popToRoot();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private showWalletSelector(): void {
|
||||||
|
this.walletSelector = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* sets a wallet on the UI, creates a TXPs for that wallet */
|
||||||
|
|
||||||
|
private setWallet(wallet: any, tx: any): void {
|
||||||
|
console.log("&&&& wallet", wallet);
|
||||||
|
console.log("&&&& tx", tx);
|
||||||
|
this.wallet = wallet;
|
||||||
|
|
||||||
|
// If select another wallet
|
||||||
|
tx.coin = wallet.coin;
|
||||||
|
tx.feeLevel = wallet.coin == 'bch' ? 'normal' : this.configFeeLevel;
|
||||||
|
this.usingCustomFee = null;
|
||||||
|
|
||||||
|
this.setButtonText(wallet.credentials.m > 1, !!tx.paypro);
|
||||||
|
|
||||||
|
if (tx.paypro)
|
||||||
|
this.paymentTimeControl(tx.paypro.expires);
|
||||||
|
|
||||||
|
this.updateTx(tx, wallet, { dryRun: true }).catch((err: any) => {
|
||||||
|
this.logger.warn(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setButtonText(isMultisig: boolean, isPayPro: boolean): void {
|
||||||
|
if (isPayPro) {
|
||||||
|
if (this.isCordova && !this.isWindowsPhoneApp) {
|
||||||
|
this.buttonText = 'Slide to pay'; // TODO gettextCatalog
|
||||||
|
} else {
|
||||||
|
this.buttonText = 'Click to pay'; // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
} else if (isMultisig) {
|
||||||
|
if (this.isCordova && !this.isWindowsPhoneApp) {
|
||||||
|
this.buttonText = 'Slide to accept'; // TODO gettextCatalog
|
||||||
|
} else {
|
||||||
|
this.buttonText = 'Click to accept'; // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.isCordova && !this.isWindowsPhoneApp) {
|
||||||
|
this.buttonText = 'Slide to send'; // TODO gettextCatalog
|
||||||
|
} else {
|
||||||
|
this.buttonText = 'Click to send'; // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private paymentTimeControl(expirationTime: number): void {
|
||||||
|
this.paymentExpired = false;
|
||||||
|
this.setExpirationTime(expirationTime);
|
||||||
|
|
||||||
|
let countDown: any = setInterval(() => {
|
||||||
|
this.setExpirationTime(expirationTime, countDown);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setExpirationTime(expirationTime: number, countDown?: any): void {
|
||||||
|
let now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
if (now > expirationTime) {
|
||||||
|
this.paymentExpired = true;
|
||||||
|
this.remainingTimeStr = 'Expired'; // TODO gettextCatalog
|
||||||
|
if (countDown) {
|
||||||
|
/* later */
|
||||||
|
clearInterval(countDown);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSecs = expirationTime - now;
|
||||||
|
let m = Math.floor(totalSecs / 60);
|
||||||
|
let s = totalSecs % 60;
|
||||||
|
this.remainingTimeStr = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTx(tx: any, wallet: any, opts: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', true);
|
||||||
|
|
||||||
|
if (opts.clearCache) {
|
||||||
|
tx.txp = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tx = tx;
|
||||||
|
let updateAmount = (): void => {
|
||||||
|
if (!tx.amount) return;
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
tx.amountStr = this.txFormatProvider.formatAmountStr(wallet.coin, tx.amount);
|
||||||
|
tx.amountValueStr = tx.amountStr.split(' ')[0];
|
||||||
|
tx.amountUnitStr = tx.amountStr.split(' ')[1];
|
||||||
|
this.txFormatProvider.formatAlternativeStr(wallet.coin, tx.amount).then((v: string) => {
|
||||||
|
tx.alternativeAmountStr = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAmount();
|
||||||
|
|
||||||
|
// End of quick refresh, before wallet is selected.
|
||||||
|
if (!wallet) {
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.feeProvider.getFeeRate(wallet.coin, tx.network, tx.feeLevel).then((feeRate: any) => {
|
||||||
|
|
||||||
|
|
||||||
|
if (!this.usingCustomFee) tx.feeRate = feeRate;
|
||||||
|
tx.feeLevelName = this.feeProvider.feeOpts[tx.feeLevel];
|
||||||
|
|
||||||
|
this.getSendMaxInfo(_.clone(tx), wallet).then((sendMaxInfo: any) => {
|
||||||
|
|
||||||
|
if (sendMaxInfo) {
|
||||||
|
|
||||||
|
this.logger.debug('Send max info', sendMaxInfo);
|
||||||
|
|
||||||
|
if (tx.sendMax && sendMaxInfo.amount == 0) {
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
this.setNoWallet('Insufficient funds'); // TODO gettextCatalog
|
||||||
|
this.popupProvider.ionicAlert('Error', 'Not enough funds for fee').then(() => {
|
||||||
|
return resolve('no_funds');
|
||||||
|
}); // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.sendMaxInfo = sendMaxInfo;
|
||||||
|
tx.amount = tx.sendMaxInfo.amount;
|
||||||
|
updateAmount();
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showSendMaxWarning(wallet, sendMaxInfo);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// txp already generated for this wallet?
|
||||||
|
if (tx.txp[wallet.id]) {
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getTxp(_.clone(tx), wallet, opts.dryRun).then((txp: any) => {
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
txp.feeStr = this.txFormatProvider.formatAmountStr(wallet.coin, txp.fee);
|
||||||
|
this.txFormatProvider.formatAlternativeStr(wallet.coin, txp.fee).then((value: string) => {
|
||||||
|
txp.alternativeFeeStr = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
let per = (txp.fee / (txp.amount + txp.fee) * 100);
|
||||||
|
txp.feeRatePerStr = per.toFixed(2) + '%';
|
||||||
|
txp.feeToHigh = per > this.FEE_TOO_HIGH_LIMIT_PER;
|
||||||
|
|
||||||
|
tx.txp[wallet.id] = txp;
|
||||||
|
this.logger.debug('Confirm. TX Fully Updated for wallet:' + wallet.id, tx);
|
||||||
|
return resolve();
|
||||||
|
}).catch((err: any) => {
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
let msg = 'Error getting SendMax information'; // TODO gettextCatalog
|
||||||
|
this.setSendError(msg);
|
||||||
|
return reject();
|
||||||
|
});
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.onGoingProcessProvider.set('calculatingFee', false);
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSendMaxInfo(tx: any, wallet: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
if (!tx.sendMax) return resolve();
|
||||||
|
|
||||||
|
//ongoingProcess.set('retrievingInputs', true);
|
||||||
|
this.walletProvider.getSendMaxInfo(wallet, {
|
||||||
|
feePerKb: tx.feeRate,
|
||||||
|
excludeUnconfirmedUtxos: !tx.spendUnconfirmed,
|
||||||
|
returnInputs: true,
|
||||||
|
}).then((res: any) => {
|
||||||
|
resolve(res);
|
||||||
|
}).catch((err: any) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSendMaxWarning(wallet: any, sendMaxInfo: any): void {
|
||||||
|
let fee = this.txFormatProvider.formatAmountStr(wallet.coin, sendMaxInfo.fee);
|
||||||
|
let msg = fee + " will be deducted for bitcoin networking fees.";
|
||||||
|
let warningMsg = this.verifyExcludedUtxos(wallet, sendMaxInfo);
|
||||||
|
|
||||||
|
if (!_.isEmpty(warningMsg))
|
||||||
|
msg += '\n' + warningMsg;
|
||||||
|
|
||||||
|
this.popupProvider.ionicAlert(null, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private verifyExcludedUtxos(wallet: any, sendMaxInfo: any): any {
|
||||||
|
let warningMsg = [];
|
||||||
|
if (sendMaxInfo.utxosBelowFee > 0) {
|
||||||
|
let amountBelowFeeStr = this.txFormatProvider.formatAmountStr(wallet.coin, sendMaxInfo.amountBelowFee);
|
||||||
|
warningMsg.push("A total of " + amountBelowFeeStr + " were excluded. These funds come from UTXOs smaller than the network fee provided.");// TODO gettextCatalog
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendMaxInfo.utxosAboveMaxSize > 0) {
|
||||||
|
let amountAboveMaxSizeStr = this.txFormatProvider.formatAmountStr(wallet.coin, sendMaxInfo.amountAboveMaxSize);
|
||||||
|
warningMsg.push("A total of " + amountAboveMaxSizeStr + " were excluded. The maximum size allowed for a transaction was exceeded.");// TODO gettextCatalog
|
||||||
|
}
|
||||||
|
return warningMsg.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
private getTxp(tx: any, wallet: any, dryRun: boolean): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
// ToDo: use a credential's (or fc's) function for this
|
||||||
|
if (tx.description && !wallet.credentials.sharedEncryptingKey) {
|
||||||
|
let msg = 'Could not add message to imported wallet without shared encrypting key'; // TODO gettextCatalog
|
||||||
|
this.logger.warn(msg);
|
||||||
|
this.setSendError(msg);
|
||||||
|
return reject(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.amount > Number.MAX_SAFE_INTEGER) {
|
||||||
|
let msg = 'Amount too big'; // TODO gettextCatalog
|
||||||
|
this.logger.warn(msg);
|
||||||
|
this.setSendError(msg);
|
||||||
|
return reject(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let txp: any = {};
|
||||||
|
|
||||||
|
txp.outputs = [{
|
||||||
|
'toAddress': tx.toAddress,
|
||||||
|
'amount': tx.amount,
|
||||||
|
'message': tx.description
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (tx.sendMaxInfo) {
|
||||||
|
txp.inputs = tx.sendMaxInfo.inputs;
|
||||||
|
txp.fee = tx.sendMaxInfo.fee;
|
||||||
|
} else {
|
||||||
|
if (this.usingCustomFee) {
|
||||||
|
txp.feePerKb = tx.feeRate;
|
||||||
|
} else txp.feeLevel = tx.feeLevel;
|
||||||
|
}
|
||||||
|
txp.feeLevel = 'normal';
|
||||||
|
txp.message = tx.description;
|
||||||
|
|
||||||
|
if (tx.paypro) {
|
||||||
|
txp.payProUrl = tx.paypro.url;
|
||||||
|
}
|
||||||
|
txp.excludeUnconfirmedUtxos = !tx.spendUnconfirmed;
|
||||||
|
txp.dryRun = dryRun;
|
||||||
|
|
||||||
|
this.walletProvider.createTx(wallet, txp).then((ctxp: any) => {
|
||||||
|
return resolve(ctxp);
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.setSendError(err);
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSendError(msg: string) {
|
||||||
|
this.sendStatus = '';
|
||||||
|
this.popupProvider.ionicAlert('Error at confirm', this.bwcErrorProvider.msg(msg)); // TODO gettextCatalog
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleAddress(): void {
|
||||||
|
this.showAddress = !this.showAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onWalletSelect(wallet: any): void {
|
||||||
|
this.setWallet(wallet, this.tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public showDescriptionPopup(tx) {
|
||||||
|
let message = 'Add description'; // TODO gettextCatalog
|
||||||
|
let opts = {
|
||||||
|
defaultText: tx.description
|
||||||
|
};
|
||||||
|
this.popupProvider.ionicPrompt(null, message, opts).then((res: string) => {
|
||||||
|
if (res) {
|
||||||
|
tx.description = res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public approve(tx: any, wallet: any, onSendStatusChange: Function): void {
|
||||||
|
|
||||||
|
if (!tx || !wallet) return;
|
||||||
|
|
||||||
|
if (this.paymentExpired) {
|
||||||
|
this.popupProvider.ionicAlert(null, 'This bitcoin payment request has expired.'); // TODO gettextCatalog
|
||||||
|
this.sendStatus = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onGoingProcessProvider.set('creatingTx', true, onSendStatusChange);
|
||||||
|
this.getTxp(_.clone(tx), wallet, false).then((txp: any) => {
|
||||||
|
this.onGoingProcessProvider.set('creatingTx', false, onSendStatusChange);
|
||||||
|
|
||||||
|
|
||||||
|
// confirm txs for more that 20usd, if not spending/touchid is enabled
|
||||||
|
let confirmTx = (): Promise<any> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.walletProvider.isEncrypted(wallet))
|
||||||
|
return resolve();
|
||||||
|
|
||||||
|
let amountUsd: number;
|
||||||
|
this.txFormatProvider.formatToUSD(wallet.coin, txp.amount).then((value: string) => {
|
||||||
|
amountUsd = parseFloat(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (amountUsd <= this.CONFIRM_LIMIT_USD)
|
||||||
|
return resolve();
|
||||||
|
|
||||||
|
let amountStr = tx.amountStr;
|
||||||
|
let name = wallet.name;
|
||||||
|
let message = 'Sending ' + amountStr + ' from your ' + name + ' wallet'; // TODO gettextCatalog
|
||||||
|
let okText = 'Confirm'; // TODO gettextCatalog
|
||||||
|
let cancelText = 'Cancel'; // TODO gettextCatalog
|
||||||
|
this.popupProvider.ionicConfirm(null, message, okText, cancelText).then((ok: boolean) => {
|
||||||
|
return resolve(!ok);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let publishAndSign = (): void => {
|
||||||
|
if (!wallet.canSign() && !wallet.isPrivKeyExternal()) {
|
||||||
|
this.logger.info('No signing proposal: No private key');
|
||||||
|
this.walletProvider.onlyPublish(wallet, txp, onSendStatusChange).catch((err: any) => {
|
||||||
|
this.setSendError(err);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.walletProvider.publishAndSign(wallet, txp, onSendStatusChange).then((txp: any) => {
|
||||||
|
|
||||||
|
if (this.config.confirmedTxsNotifications && this.config.confirmedTxsNotifications.enabled) {
|
||||||
|
this.txConfirmNotificationProvider.subscribe(wallet, {
|
||||||
|
txid: txp.txid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.onSuccessConfirm(); // TODO review this line
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.setSendError(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmTx().then((nok: boolean) => {
|
||||||
|
if (nok) {
|
||||||
|
this.sendStatus = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishAndSign();
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.logger.warn(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.logger.warn(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public statusChangeHandler(processName: string, showName: string, isOn: boolean): void {
|
||||||
|
this.logger.debug('statusChangeHandler: ', processName, showName, isOn);
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
processName === 'broadcastingTx' ||
|
||||||
|
((processName === 'signingTx') && this.wallet.m > 1) ||
|
||||||
|
(processName == 'sendingTx' && !this.wallet.canSign() && !this.wallet.isPrivKeyExternal())
|
||||||
|
) && !isOn) {
|
||||||
|
this.sendStatus = 'success';
|
||||||
|
} else if (showName) {
|
||||||
|
this.sendStatus = showName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSuccessConfirm(): void {
|
||||||
|
this.sendStatus = '';
|
||||||
|
this.navCtrl.setRoot(HomePage);
|
||||||
|
this.navCtrl.popToRoot();
|
||||||
|
this.navCtrl.parent.select(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
public openPPModal(): void {
|
||||||
|
const myModal = this.modalCtrl.create(PayProPage, {}, {
|
||||||
|
showBackdrop: true,
|
||||||
|
enableBackdropDismiss: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public chooseFeeLevel(tx: any, wallet: any): void {
|
||||||
|
|
||||||
|
if (wallet.coin == 'bch') return;
|
||||||
|
|
||||||
|
let txObject: any = {};
|
||||||
|
txObject.network = tx.network;
|
||||||
|
txObject.feeLevel = tx.feeLevel;
|
||||||
|
txObject.noSave = true;
|
||||||
|
txObject.coin = wallet.coin;
|
||||||
|
|
||||||
|
if (this.usingCustomFee) {
|
||||||
|
txObject.customFeePerKB = tx.feeRate;
|
||||||
|
txObject.feePerSatByte = tx.feeRate / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const myModal = this.modalCtrl.create(ChooseFeeLevelPage, txObject, {
|
||||||
|
showBackdrop: true,
|
||||||
|
enableBackdropDismiss: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
myModal.onDidDismiss((data: any) => {
|
||||||
|
|
||||||
|
this.logger.debug('New fee level choosen:' + data.newFeeLevel + ' was:' + tx.feeLevel);
|
||||||
|
|
||||||
|
this.usingCustomFee = data.newFeeLevel == 'custom' ? true : false;
|
||||||
|
|
||||||
|
if (tx.feeLevel == data.newFeeLevel && !this.usingCustomFee) return;
|
||||||
|
|
||||||
|
tx.feeLevel = data.newFeeLevel;
|
||||||
|
if (this.usingCustomFee) tx.feeRate = parseInt(data.customFeePerKB);
|
||||||
|
|
||||||
|
this.updateTx(tx, wallet, { clearCache: true, dryRun: true });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
|
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
|
|
||||||
<ion-content padding>
|
<ion-content padding>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-label>Recipient</ion-label>
|
<ion-label>Recipient</ion-label>
|
||||||
|
@ -16,22 +14,32 @@
|
||||||
<ion-icon class="scanner-icon" name="md-qr-scanner" item-right (click)="openScanner()"></ion-icon>
|
<ion-icon class="scanner-icon" name="md-qr-scanner" item-right (click)="openScanner()"></ion-icon>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<div *ngIf="contactsList">
|
<ion-card *ngIf="contactsList">
|
||||||
|
<ion-item>
|
||||||
|
<span translate>Contacts</span>
|
||||||
|
</ion-item>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-label>Contacts</ion-label>
|
<button *ngFor="let item of contactsList" (click)="goToAmount(item)">
|
||||||
<ion-item *ngFor="let item of contactsList" (click)="goToAmount(item)">
|
<ion-icon item-start>
|
||||||
{{item.name}}
|
<img src="assets/img/contact-placeholder.svg">
|
||||||
</ion-item>
|
</ion-icon>
|
||||||
|
<h2>{{item.name}}</h2>
|
||||||
|
</button>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</div>
|
</ion-card>
|
||||||
<div *ngIf="walletList">
|
<ion-card *ngIf="walletList">
|
||||||
|
<ion-item>
|
||||||
|
<span translate>Transfer to Wallet</span>
|
||||||
|
</ion-item>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-label>Transfer to Wallet</ion-label>
|
<button ion-item *ngFor="let item of walletList" (click)="goToAmount(item)">
|
||||||
<ion-item *ngFor="let item of walletList" (click)="goToAmount(item)">
|
<ion-icon item-start>
|
||||||
{{item.name}}
|
<img src="assets/img/icon-wallet.svg" class="icon-wallet">
|
||||||
</ion-item>
|
</ion-icon>
|
||||||
|
<h2>{{item.name}}</h2>
|
||||||
|
</button>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</div>
|
</ion-card>
|
||||||
<ion-card *ngIf="!hasWallets">
|
<ion-card *ngIf="!hasWallets">
|
||||||
<ion-card-header>
|
<ion-card-header>
|
||||||
<span translate>To get started, you'll need to create a bitcoin wallet and get some bitcoin.</span>
|
<span translate>To get started, you'll need to create a bitcoin wallet and get some bitcoin.</span>
|
||||||
|
|
|
@ -2,4 +2,10 @@ page-send {
|
||||||
.scanner-icon {
|
.scanner-icon {
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
&.icon-wallet {
|
||||||
|
width: 35px !important;
|
||||||
|
background-color: color($colors, primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ export class SendPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findContact(search: string): void {
|
public findContact(search: string): void {
|
||||||
if (this.incomingDataProvider.redir(search)) return;
|
if (this.incomingDataProvider.redir(search)) return;
|
||||||
if (!search || search.length < 2) {
|
if (!search || search.length < 2) {
|
||||||
this.updateContactsList();
|
this.updateContactsList();
|
||||||
|
@ -157,14 +157,15 @@ export class SendPage {
|
||||||
this.popupProvider.ionicAlert('Error - no address');
|
this.popupProvider.ionicAlert('Error - no address');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logger.debug('Got toAddress:' + addr + ' | ' + item.name);
|
this.logger.debug('Got address:' + addr + ' | ' + item.name);
|
||||||
this.navCtrl.push(AmountPage, {
|
this.navCtrl.push(AmountPage, {
|
||||||
recipientType: item.recipientType,
|
recipientType: item.recipientType,
|
||||||
toAddress: addr,
|
toAddress: addr,
|
||||||
toName: item.name,
|
name: item.name,
|
||||||
toEmail: item.email,
|
email: item.email,
|
||||||
toColor: item.color,
|
color: item.color,
|
||||||
coin: item.coin
|
coin: item.coin,
|
||||||
|
fromSend: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
|
|
|
@ -13,19 +13,20 @@ interface Config {
|
||||||
|
|
||||||
wallet: {
|
wallet: {
|
||||||
requiredCopayers: number;
|
requiredCopayers: number;
|
||||||
totalCopayers: number;
|
totalCopayers: number;
|
||||||
spendUnconfirmed: boolean;
|
spendUnconfirmed: boolean;
|
||||||
reconnectDelay: number;
|
reconnectDelay: number;
|
||||||
idleDurationMin: number;
|
idleDurationMin: number;
|
||||||
settings: {
|
settings: {
|
||||||
unitName: string;
|
unitName: string;
|
||||||
unitToSatoshi: number;
|
unitToSatoshi: number;
|
||||||
unitDecimals: number;
|
unitDecimals: number;
|
||||||
unitCode: string;
|
unitCode: string;
|
||||||
alternativeName: string;
|
alternativeName: string;
|
||||||
alternativeIsoCode: string;
|
alternativeIsoCode: string;
|
||||||
defaultLanguage: string;
|
defaultLanguage: string;
|
||||||
};
|
feeLevel: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
bws: {
|
bws: {
|
||||||
|
@ -112,7 +113,8 @@ const configDefault: Config = {
|
||||||
unitCode: 'btc',
|
unitCode: 'btc',
|
||||||
alternativeName: 'US Dollar',
|
alternativeName: 'US Dollar',
|
||||||
alternativeIsoCode: 'USD',
|
alternativeIsoCode: 'USD',
|
||||||
defaultLanguage: ''
|
defaultLanguage: '',
|
||||||
|
feeLevel: 'normal'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
|
||||||
|
//providers
|
||||||
|
import { PopupProvider } from '../popup/popup';
|
||||||
|
import { PlatformProvider } from '../platform/platform';
|
||||||
|
import { NodeWebkitProvider } from '../node-webkit/node-webkit';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExternalLinkProvider {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private popupProvider: PopupProvider,
|
||||||
|
private logger: Logger,
|
||||||
|
private platformProvider: PlatformProvider,
|
||||||
|
private nodeWebkitProvider: NodeWebkitProvider
|
||||||
|
) {
|
||||||
|
console.log('Hello ExternalLinkProvider Provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private restoreHandleOpenURL(old: string): void {
|
||||||
|
setTimeout(function () {
|
||||||
|
(window as any).handleOpenURL = old;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public open(url: string, optIn?: boolean, title?: string, message?: string, okText?: string, cancelText?: string) {
|
||||||
|
let old = (window as any).handleOpenURL;
|
||||||
|
|
||||||
|
(window as any).handleOpenURL = (url) => {
|
||||||
|
// Ignore external URLs
|
||||||
|
this.logger.debug('Skip: ' + url);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.platformProvider.isNW) {
|
||||||
|
this.nodeWebkitProvider.openExternalLink(url);
|
||||||
|
this.restoreHandleOpenURL(old);
|
||||||
|
} else {
|
||||||
|
if (optIn) {
|
||||||
|
let openBrowser = (res) => {
|
||||||
|
if (res) window.open(url, '_system');
|
||||||
|
this.restoreHandleOpenURL(old);
|
||||||
|
};
|
||||||
|
this.popupProvider.ionicConfirm(title, message, okText, cancelText).then((res: boolean) => {
|
||||||
|
openBrowser(res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.open(url, '_system');
|
||||||
|
this.restoreHandleOpenURL(old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
|
||||||
|
//providers
|
||||||
|
import { ConfigProvider } from '../../providers/config/config';
|
||||||
|
import { BwcProvider } from '../../providers/bwc/bwc';
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FeeProvider {
|
||||||
|
|
||||||
|
private CACHE_TIME_TS: number = 60;
|
||||||
|
// Constant fee options to translate
|
||||||
|
public feeOpts: any = {
|
||||||
|
urgent: 'Urgent', //TODO gettextcatalog
|
||||||
|
priority: 'Priority',//TODO gettextcatalog
|
||||||
|
normal: 'Normal',//TODO gettextcatalog
|
||||||
|
economy: 'Economy',//TODO gettextcatalog
|
||||||
|
superEconomy: 'Super Economy',//TODO gettextcatalog
|
||||||
|
custom: 'Custom'//TODO gettextcatalog
|
||||||
|
};
|
||||||
|
private cache: any = {
|
||||||
|
updateTs: 0,
|
||||||
|
coin: ''
|
||||||
|
}
|
||||||
|
constructor(
|
||||||
|
private configProvider: ConfigProvider,
|
||||||
|
private logger: Logger,
|
||||||
|
private bwcProvider: BwcProvider
|
||||||
|
) {
|
||||||
|
console.log('Hello FeeProvider Provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrentFeeLevel(): string {
|
||||||
|
return this.configProvider.get().wallet.settings.feeLevel || 'normal';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getFeeRate(coin: string, network: string, feeLevel: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (feeLevel == 'custom') return resolve();
|
||||||
|
network = network || 'livenet';
|
||||||
|
this.getFeeLevels(coin).then((response: any) => {
|
||||||
|
let feeLevelRate: any;
|
||||||
|
|
||||||
|
if (response.fromCache) {
|
||||||
|
feeLevelRate = _.find(response.levels[network], {
|
||||||
|
level: feeLevel
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
feeLevelRate = _.find(response[network], {
|
||||||
|
level: feeLevel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!feeLevelRate || !feeLevelRate.feePerKb) {
|
||||||
|
let msg = "Could not get dynamic fee for level: " + feeLevel; //TODO gettextcatalog
|
||||||
|
return reject(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let feeRate = feeLevelRate.feePerKb;
|
||||||
|
if (!response.fromCache) this.logger.debug('Dynamic fee: ' + feeLevel + '/' + network + ' ' + (feeLevelRate.feePerKb / 1000).toFixed() + ' SAT/B');
|
||||||
|
return resolve(feeRate);
|
||||||
|
}).catch((err) => {
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
public getCurrentFeeRate(coin: string, network: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.getFeeRate(coin, network, this.getCurrentFeeLevel()).then((data: number) => {
|
||||||
|
return resolve(data)
|
||||||
|
}).catch((err: any) => {
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public getFeeLevels(coin: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
coin = coin || 'btc';
|
||||||
|
|
||||||
|
if (this.cache.coin == coin && this.cache.updateTs > Date.now() - this.CACHE_TIME_TS * 1000) {
|
||||||
|
return resolve({ levels: this.cache.data, fromCache: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let walletClient = this.bwcProvider.getClient(null, {});
|
||||||
|
|
||||||
|
walletClient.getFeeLevels(coin, 'livenet', (errLivenet, levelsLivenet) => {
|
||||||
|
walletClient.getFeeLevels('btc', 'testnet', (errTestnet, levelsTestnet) => {
|
||||||
|
if (errLivenet || errTestnet) {
|
||||||
|
return reject('Could not get dynamic fee'); //TODO gettextcatalog
|
||||||
|
}
|
||||||
|
this.cache.updateTs = Date.now();
|
||||||
|
this.cache.coin = coin;
|
||||||
|
this.cache.data = {
|
||||||
|
'livenet': levelsLivenet,
|
||||||
|
'testnet': levelsTestnet
|
||||||
|
};
|
||||||
|
return resolve(this.cache.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -29,8 +29,6 @@ export class IncomingDataProvider {
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
private appProvider: AppProvider
|
private appProvider: AppProvider
|
||||||
) {
|
) {
|
||||||
//TODO Injecting NavController in constructor of service fails with no provider error
|
|
||||||
this.navCtrl = app.getActiveNav();
|
|
||||||
console.log('Hello IncomingDataProvider Provider');
|
console.log('Hello IncomingDataProvider Provider');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +38,8 @@ export class IncomingDataProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public redir(data: any): boolean {
|
public redir(data: any): boolean {
|
||||||
|
//TODO Injecting NavController in constructor of service fails with no provider error
|
||||||
|
this.navCtrl = this.app.getActiveNav();
|
||||||
// data extensions for Payment Protocol with non-backwards-compatible request
|
// data extensions for Payment Protocol with non-backwards-compatible request
|
||||||
if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) {
|
if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) {
|
||||||
data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, ''));
|
data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, ''));
|
||||||
|
@ -112,7 +112,8 @@ export class IncomingDataProvider {
|
||||||
|
|
||||||
// Translate address
|
// Translate address
|
||||||
this.logger.debug('address transalated to:' + addr);
|
this.logger.debug('address transalated to:' + addr);
|
||||||
this.popupProvider.ionicConfirm('Bitcoin cash Payment', 'Payment address was translated to new Bitcoin Cash address format: ' + addr, 'OK', 'Cancel').then(() => {
|
this.popupProvider.ionicConfirm('Bitcoin cash Payment', 'Payment address was translated to new Bitcoin Cash address format: ' + addr, 'OK', 'Cancel').then((res: boolean) => {
|
||||||
|
if (!res) return false;
|
||||||
|
|
||||||
let message = parsed.message;
|
let message = parsed.message;
|
||||||
let amount = parsed.amount ? parsed.amount : '';
|
let amount = parsed.amount ? parsed.amount : '';
|
||||||
|
@ -130,8 +131,6 @@ export class IncomingDataProvider {
|
||||||
} else {
|
} else {
|
||||||
this.goSend(addr, amount, message, coin);
|
this.goSend(addr, amount, message, coin);
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
// Plain URL
|
// Plain URL
|
||||||
|
@ -265,7 +264,7 @@ export class IncomingDataProvider {
|
||||||
this.navCtrl.push(SendPage, {});
|
this.navCtrl.push(SendPage, {});
|
||||||
if (amount) {
|
if (amount) {
|
||||||
this.navCtrl.push(ConfirmPage, {
|
this.navCtrl.push(ConfirmPage, {
|
||||||
toAmount: amount,
|
amount: amount,
|
||||||
toAddress: addr,
|
toAddress: addr,
|
||||||
description: message,
|
description: message,
|
||||||
coin: coin
|
coin: coin
|
||||||
|
@ -279,16 +278,17 @@ export class IncomingDataProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private goToAmountPage(toAddress: string, coin?: string) {
|
private goToAmountPage(toAddress: string, coin?: string) {
|
||||||
this.navCtrl.push(SendPage, {});
|
let fromSend = this.navCtrl.getActive().name === 'SendPage';
|
||||||
this.navCtrl.push(AmountPage, {
|
this.navCtrl.push(AmountPage, {
|
||||||
toAddress: toAddress,
|
toAddress: toAddress,
|
||||||
coin: coin
|
coin: coin,
|
||||||
|
fromSend: fromSend
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handlePayPro(payProDetails: any, coin?: string): void {
|
private handlePayPro(payProDetails: any, coin?: string): void {
|
||||||
var stateParams = {
|
var stateParams = {
|
||||||
toAmount: payProDetails.amount,
|
amount: payProDetails.amount,
|
||||||
toAddress: payProDetails.toAddress,
|
toAddress: payProDetails.toAddress,
|
||||||
description: payProDetails.memo,
|
description: payProDetails.memo,
|
||||||
paypro: payProDetails,
|
paypro: payProDetails,
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NodeWebkitProvider {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
console.log('Hello NodeWebkitProvider Provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
public readFromClipboard(): any {
|
||||||
|
let gui = require('nw.gui');
|
||||||
|
let clipboard = gui.Clipboard.get();
|
||||||
|
return clipboard.get();
|
||||||
|
};
|
||||||
|
|
||||||
|
public writeToClipboard(text): any {
|
||||||
|
let gui = require('nw.gui');
|
||||||
|
let clipboard = gui.Clipboard.get();
|
||||||
|
return clipboard.set(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
public openExternalLink(url): any {
|
||||||
|
let gui = require('nw.gui');
|
||||||
|
return gui.Shell.openExternal(url);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OnGoingProcess {
|
export class OnGoingProcessProvider {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log('Hello OnGoingProcess Provider');
|
console.log('Hello OnGoingProcessProvider Provider');
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(processName: string, isOn: boolean, customHandler?: any) {
|
public set(processName: string, isOn: boolean, customHandler?: any) {
|
||||||
console.log('TODO: OnGoingProcess set()...');
|
console.log('TODO: OnGoingProcessProvider set()...');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,14 @@ import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
|
||||||
//providers
|
//providers
|
||||||
import { ProfileProvider } from '../profile/profile';
|
import { ProfileProvider } from '../profile/profile';
|
||||||
import { OnGoingProcess } from '../on-going-process/on-going-process';
|
import { OnGoingProcessProvider } from '../on-going-process/on-going-process';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PayproProvider {
|
export class PayproProvider {
|
||||||
constructor(
|
constructor(
|
||||||
private profileProvider: ProfileProvider,
|
private profileProvider: ProfileProvider,
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
private onGoingProcess: OnGoingProcess
|
private onGoingProcessProvider: OnGoingProcessProvider
|
||||||
) {
|
) {
|
||||||
console.log('Hello PayproProvider Provider');
|
console.log('Hello PayproProvider Provider');
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,12 @@ export class PayproProvider {
|
||||||
|
|
||||||
this.logger.debug('Fetch PayPro Request...', uri);
|
this.logger.debug('Fetch PayPro Request...', uri);
|
||||||
|
|
||||||
if (!disableLoader) this.onGoingProcess.set('fetchingPayPro', true);
|
if (!disableLoader) this.onGoingProcessProvider.set('fetchingPayPro', true);
|
||||||
|
|
||||||
wallet.fetchPayPro({
|
wallet.fetchPayPro({
|
||||||
payProUrl: uri,
|
payProUrl: uri,
|
||||||
}, (err, paypro) => {
|
}, (err, paypro) => {
|
||||||
if (!disableLoader) this.onGoingProcess.set('fetchingPayPro', false);
|
if (!disableLoader) this.onGoingProcessProvider.set('fetchingPayPro', false);
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
else if (!paypro.verified) {
|
else if (!paypro.verified) {
|
||||||
this.logger.warn('Failed to verify payment protocol signatures');
|
this.logger.warn('Failed to verify payment protocol signatures');
|
||||||
|
|
|
@ -6,13 +6,23 @@ export class PopupProvider {
|
||||||
constructor(public alertCtrl: AlertController) {
|
constructor(public alertCtrl: AlertController) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ionicAlert(title: string, subTitle?: string, okText?: string): void {
|
public ionicAlert(title: string, subTitle?: string, okText?: string): Promise<any> {
|
||||||
let alert = this.alertCtrl.create({
|
return new Promise((resolve, reject) => {
|
||||||
title: title,
|
let alert = this.alertCtrl.create({
|
||||||
subTitle: subTitle,
|
title: title,
|
||||||
buttons: [okText]
|
subTitle: subTitle,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: okText,
|
||||||
|
handler: () => {
|
||||||
|
console.log('Ok clicked');
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
alert.present();
|
||||||
});
|
});
|
||||||
alert.present();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public ionicConfirm(title, message, okText, cancelText): Promise<any> {
|
public ionicConfirm(title, message, okText, cancelText): Promise<any> {
|
||||||
|
@ -25,14 +35,14 @@ export class PopupProvider {
|
||||||
text: cancelText,
|
text: cancelText,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
console.log('Disagree clicked');
|
console.log('Disagree clicked');
|
||||||
reject();
|
resolve(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: okText,
|
text: okText,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
console.log('Agree clicked');
|
console.log('Agree clicked');
|
||||||
resolve();
|
resolve(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -41,30 +51,30 @@ export class PopupProvider {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public ionicPrompt(title: string, message: string, okText?: string, cancelText?: string): Promise<any> {
|
public ionicPrompt(title: string, message: string, opts: any, okText?: string, cancelText?: string): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let prompt = this.alertCtrl.create({
|
let prompt = this.alertCtrl.create({
|
||||||
title: title,
|
title: title,
|
||||||
message: message,
|
message: message,
|
||||||
inputs: [
|
inputs: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
value: opts.defaultText,
|
||||||
placeholder: 'Title'
|
placeholder: opts.placeholder
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: cancelText,
|
text: cancelText ? cancelText : 'Cancel',
|
||||||
handler: data => {
|
handler: data => {
|
||||||
console.log('Cancel clicked');
|
console.log('Cancel clicked');
|
||||||
reject(data);
|
resolve(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: okText,
|
text: okText ? okText : 'OK',
|
||||||
handler: data => {
|
handler: data => {
|
||||||
console.log('Saved clicked');
|
console.log('Saved clicked');
|
||||||
resolve(data);
|
resolve(data[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class RateProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRates(): Promise<any> {
|
updateRates(): Promise<any> {
|
||||||
return new Promise ((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let self = this;
|
let self = this;
|
||||||
this.getBTC().then((dataBTC) => {
|
this.getBTC().then((dataBTC) => {
|
||||||
|
|
||||||
|
@ -44,23 +44,23 @@ export class RateProvider {
|
||||||
this.getBCH().then((dataBCH) => {
|
this.getBCH().then((dataBCH) => {
|
||||||
|
|
||||||
_.each(dataBCH.result, (data, paircode) => {
|
_.each(dataBCH.result, (data, paircode) => {
|
||||||
var code = paircode.substr(3,3);
|
var code = paircode.substr(3, 3);
|
||||||
var rate =data.c[0];
|
var rate = data.c[0];
|
||||||
self._ratesBCH[code] = rate;
|
self._ratesBCH[code] = rate;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._isAvailable = true;
|
this._isAvailable = true;
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((errorBCH) => {
|
.catch((errorBCH) => {
|
||||||
console.log("Error: ", errorBCH);
|
console.log("Error: ", errorBCH);
|
||||||
reject(errorBCH);
|
reject(errorBCH);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((errorBTC) => {
|
.catch((errorBTC) => {
|
||||||
console.log("Error: ", errorBTC);
|
console.log("Error: ", errorBTC);
|
||||||
reject(errorBTC);
|
reject(errorBTC);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export class RateProvider {
|
||||||
.catch((error) => console.log("Error", error));
|
.catch((error) => console.log("Error", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
getRate(code, chain) {
|
getRate(code, chain?) {
|
||||||
if (chain == 'bch')
|
if (chain == 'bch')
|
||||||
return this._ratesBCH[code];
|
return this._ratesBCH[code];
|
||||||
else
|
else
|
||||||
|
@ -107,7 +107,7 @@ export class RateProvider {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (sort) {
|
if (sort) {
|
||||||
alternatives.sort( (a, b) => {
|
alternatives.sort((a, b) => {
|
||||||
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -116,10 +116,10 @@ export class RateProvider {
|
||||||
|
|
||||||
//TODO IMPROVE WHEN AVAILABLE
|
//TODO IMPROVE WHEN AVAILABLE
|
||||||
whenAvailable() {
|
whenAvailable() {
|
||||||
return new Promise((resolve, reject)=> {
|
return new Promise((resolve, reject) => {
|
||||||
if (this._isAvailable) resolve();
|
if (this._isAvailable) resolve();
|
||||||
else {
|
else {
|
||||||
this.updateRates().then(()=>{
|
this.updateRates().then(() => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
|
||||||
|
//providers
|
||||||
|
import { PersistenceProvider } from '../persistence/persistence';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TxConfirmNotificationProvider {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private logger: Logger,
|
||||||
|
private persistenceProvider: PersistenceProvider
|
||||||
|
) {
|
||||||
|
console.log('Hello TxConfirmNotificationProvider Provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkIfEnabled(txid: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.persistenceProvider.getTxConfirmNotification(txid).then((res: any) => {
|
||||||
|
return resolve(!!res);
|
||||||
|
}).catch((err: any) => {
|
||||||
|
this.logger.error(err);
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public subscribe(client: any, opts: any): void {
|
||||||
|
client.txConfirmationSubscribe(opts, (err: any, res: any) => {
|
||||||
|
if (err) this.logger.error(err);
|
||||||
|
this.persistenceProvider.setTxConfirmNotification(opts.txid, true).catch((err: any) => {
|
||||||
|
this.logger.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public unsubscribe(client: any, txId: string): void {
|
||||||
|
client.txConfirmationUnsubscribe(txId, (err: any, res: any) => {
|
||||||
|
if (err) this.logger.error(err);
|
||||||
|
this.persistenceProvider.removeTxConfirmNotification(txId).catch((err: any) => {
|
||||||
|
this.logger.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ export class TxFormatProvider {
|
||||||
console.log('Hello TxFormatProvider Provider');
|
console.log('Hello TxFormatProvider Provider');
|
||||||
}
|
}
|
||||||
|
|
||||||
formatAmount(satoshis: number, fullPrecision?: boolean) {
|
public formatAmount(satoshis: number, fullPrecision?: boolean): number {
|
||||||
let settings = this.config.get().wallet.settings;
|
let settings = this.config.get().wallet.settings;
|
||||||
|
|
||||||
if (settings.unitCode == 'sat') return satoshis;
|
if (settings.unitCode == 'sat') return satoshis;
|
||||||
|
@ -35,45 +35,45 @@ export class TxFormatProvider {
|
||||||
return this.bwc.getUtils().formatAmount(satoshis, settings.unitCode, opts);
|
return this.bwc.getUtils().formatAmount(satoshis, settings.unitCode, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatAmountStr(coin: string, satoshis: number) {
|
public formatAmountStr(coin: string, satoshis: number): string {
|
||||||
if (isNaN(satoshis)) return;
|
if (isNaN(satoshis)) return;
|
||||||
return this.formatAmount(satoshis) + ' ' + (coin).toUpperCase();
|
return (this.formatAmount(satoshis) + ' ' + coin.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
toFiat(coin: string, satoshis: number, code: string): Promise<any> {
|
public toFiat(coin: string, satoshis: number, code: string): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isNaN(satoshis)) resolve();
|
if (isNaN(satoshis)) return resolve();
|
||||||
var v1;
|
var v1;
|
||||||
v1 = this.rate.toFiat(satoshis, code, coin);
|
v1 = this.rate.toFiat(satoshis, code, coin);
|
||||||
if (!v1) resolve(null);
|
if (!v1) return resolve(null);
|
||||||
resolve(v1.toFixed(2));
|
return resolve(v1.toFixed(2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formatToUSD(coin: string, satoshis: number) {
|
public formatToUSD(coin: string, satoshis: number): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
var v1;
|
let v1: number;
|
||||||
if (isNaN(satoshis)) resolve();
|
if (isNaN(satoshis)) return resolve();
|
||||||
v1 = this.rate.toFiat(satoshis, 'USD', coin);
|
v1 = this.rate.toFiat(satoshis, 'USD', coin);
|
||||||
if (!v1) resolve(null);
|
if (!v1) return resolve(null);
|
||||||
resolve(v1.toFixed(2));
|
return resolve(v1.toFixed(2));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
formatAlternativeStr(coin: string, satoshis: number) {
|
public formatAlternativeStr(coin: string, satoshis: number): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isNaN(satoshis)) resolve();
|
if (isNaN(satoshis)) return resolve();
|
||||||
let settings = this.config.get().wallet.settings;
|
let settings = this.config.get().wallet.settings;
|
||||||
|
|
||||||
var v1 = parseFloat((this.rate.toFiat(satoshis, settings.alternativeIsoCode, coin)).toFixed(2));
|
var v1 = parseFloat((this.rate.toFiat(satoshis, settings.alternativeIsoCode, coin)).toFixed(2));
|
||||||
var v1FormatFiat = this.filter.formatFiatAmount(v1);
|
var v1FormatFiat = this.filter.formatFiatAmount(v1);
|
||||||
if (!v1FormatFiat) resolve(null);
|
if (!v1FormatFiat) return resolve(null);
|
||||||
|
|
||||||
resolve(v1FormatFiat + ' ' + settings.alternativeIsoCode);
|
return resolve(v1FormatFiat + ' ' + settings.alternativeIsoCode);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
processTx(coin: string, tx: any) {
|
public processTx(coin: string, tx: any): any {
|
||||||
if (!tx || tx.action == 'invalid')
|
if (!tx || tx.action == 'invalid')
|
||||||
return tx;
|
return tx;
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ export class TxFormatProvider {
|
||||||
return tx;
|
return tx;
|
||||||
};
|
};
|
||||||
|
|
||||||
formatPendingTxps(txps) {
|
public formatPendingTxps(txps): any {
|
||||||
this.pendingTxProposalsCountForUs = 0;
|
this.pendingTxProposalsCountForUs = 0;
|
||||||
var now = Math.floor(Date.now() / 1000);
|
var now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ export class TxFormatProvider {
|
||||||
return txps;
|
return txps;
|
||||||
};
|
};
|
||||||
|
|
||||||
parseAmount(coin: string, amount: any, currency: string) {
|
public parseAmount(coin: string, amount: any, currency: string): any {
|
||||||
let settings = this.config.get()['wallet']['settings']; // TODO
|
let settings = this.config.get()['wallet']['settings']; // TODO
|
||||||
|
|
||||||
var satToBtc = 1 / 100000000;
|
var satToBtc = 1 / 100000000;
|
||||||
|
@ -211,7 +211,7 @@ export class TxFormatProvider {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
satToUnit(amount: any) {
|
public satToUnit(amount: any): number {
|
||||||
let settings = this.config.get()['wallet']['settings']; // TODO
|
let settings = this.config.get()['wallet']['settings']; // TODO
|
||||||
|
|
||||||
var unitToSatoshi = settings.unitToSatoshi;
|
var unitToSatoshi = settings.unitToSatoshi;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { BwcErrorProvider } from '../bwc-error/bwc-error';
|
||||||
import { RateProvider } from '../rate/rate';
|
import { RateProvider } from '../rate/rate';
|
||||||
import { Filter } from '../filter/filter';
|
import { Filter } from '../filter/filter';
|
||||||
import { PopupProvider } from '../popup/popup';
|
import { PopupProvider } from '../popup/popup';
|
||||||
import { OnGoingProcess } from '../on-going-process/on-going-process';
|
import { OnGoingProcessProvider } from '../on-going-process/on-going-process';
|
||||||
import { TouchIdProvider } from '../touchid/touchid';
|
import { TouchIdProvider } from '../touchid/touchid';
|
||||||
|
|
||||||
describe('Provider: Wallet Provider', () => {
|
describe('Provider: Wallet Provider', () => {
|
||||||
|
@ -49,7 +49,7 @@ describe('Provider: Wallet Provider', () => {
|
||||||
{ provide: RateProvider },
|
{ provide: RateProvider },
|
||||||
{ provide: Filter },
|
{ provide: Filter },
|
||||||
{ provide: PopupProvider },
|
{ provide: PopupProvider },
|
||||||
{ provide: OnGoingProcess },
|
{ provide: OnGoingProcessProvider },
|
||||||
{ provide: TouchIdProvider },
|
{ provide: TouchIdProvider },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { BwcErrorProvider } from '../bwc-error/bwc-error';
|
||||||
import { RateProvider } from '../rate/rate';
|
import { RateProvider } from '../rate/rate';
|
||||||
import { Filter } from '../filter/filter';
|
import { Filter } from '../filter/filter';
|
||||||
import { PopupProvider } from '../popup/popup';
|
import { PopupProvider } from '../popup/popup';
|
||||||
import { OnGoingProcess } from '../on-going-process/on-going-process';
|
import { OnGoingProcessProvider } from '../on-going-process/on-going-process';
|
||||||
import { TouchIdProvider } from '../touchid/touchid';
|
import { TouchIdProvider } from '../touchid/touchid';
|
||||||
|
|
||||||
import * as lodash from 'lodash';
|
import * as lodash from 'lodash';
|
||||||
|
@ -46,7 +46,7 @@ export class WalletProvider {
|
||||||
private rateProvider: RateProvider,
|
private rateProvider: RateProvider,
|
||||||
private filter: Filter,
|
private filter: Filter,
|
||||||
private popupProvider: PopupProvider,
|
private popupProvider: PopupProvider,
|
||||||
private ongoingProcess: OnGoingProcess,
|
private ongoingProcess: OnGoingProcessProvider,
|
||||||
private touchidProvider: TouchIdProvider
|
private touchidProvider: TouchIdProvider
|
||||||
) {
|
) {
|
||||||
console.log('Hello WalletService Provider');
|
console.log('Hello WalletService Provider');
|
||||||
|
@ -753,7 +753,7 @@ export class WalletProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEncrypted(wallet: any) {
|
public isEncrypted(wallet: any): boolean {
|
||||||
if (lodash.isEmpty(wallet)) return;
|
if (lodash.isEmpty(wallet)) return;
|
||||||
let isEncrypted = wallet.isPrivKeyEncrypted();
|
let isEncrypted = wallet.isPrivKeyEncrypted();
|
||||||
if (isEncrypted) this.logger.debug('Wallet is encrypted');
|
if (isEncrypted) this.logger.debug('Wallet is encrypted');
|
||||||
|
|
Loading…
Reference in New Issue