diff --git a/package.json b/package.json index 1f0269347..56d794c7e 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@ionic/core": "^6.0.0", "@ionic/pwa-elements": "3.1.1", "@levminer/speakeasy": "^1.3.1", + "@metamask/detect-provider": "^1.2.0", "@ngx-translate/core": "13.0.0", "@ngx-translate/http-loader": "6.0.0", "@sentry/angular": "^6.16.1", diff --git a/src/app/pages/wallet/send/component.ts b/src/app/pages/wallet/send/component.ts index f4dab3bfb..d324e28f9 100644 --- a/src/app/pages/wallet/send/component.ts +++ b/src/app/pages/wallet/send/component.ts @@ -1,8 +1,8 @@ import {environment} from '@env/environment'; -import {Component} from '@angular/core'; +import {Component, OnDestroy} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; -import {ModalController, NavController} from '@ionic/angular'; +import {LoadingController, ModalController, NavController} from '@ionic/angular'; import {BarcodeScanner} from '@ionic-native/barcode-scanner/ngx'; import {UtilsHelper} from '@helpers/utils'; import {FormValidationHelper} from '@helpers/validation-form'; @@ -18,442 +18,491 @@ import {ClipboardService} from '@services/clipboard'; import {LanguageService} from '@services/languages'; import {ThemeService} from '@services/theme'; import {HighFeeModalComponent} from '@pages/wallet/high-fee-modal/high-fee.component'; +import {MetamaskService} from '@services/metamask.service'; const logContent = (data) => Object.assign({service: 'app:wallet:send'}, data); @Component({ - selector: 'app-wallet-send', - templateUrl: 'index.html', - styleUrls: ['style.scss'] + selector: 'app-wallet-send', + templateUrl: 'index.html', + styleUrls: ['style.scss'] }) -export class WalletSendComponent { - - public formObj: FormGroup; - public selectedWallet: WalletModel; - public currency: CurrencyModel; - public status: StatusModel; - public coinCode: string; - public minWithdrawAmount: string; - public noResult: boolean; - public isReady: boolean; - public fiatAmount: string; - public type: string; // fiat/crypto - private readonly defaultType: any; - public selectedTheme: string; - - constructor( - private hardwareService: HardwareService, - private modalCtrl: ModalController, - private utilsHelper: UtilsHelper, - public route: ActivatedRoute, - public formBuilder: FormBuilder, - private navController: NavController, - private appState: AppState, - private barcodeScanner: BarcodeScanner, - private formValidationHelper: FormValidationHelper, - private langService: LanguageService, - private clipboardService: ClipboardService, - private themeService: ThemeService, - ) { - this.noResult = false; - this.defaultType = 'fiat'; - this.isReady = false; - this.fiatAmount = '0.00'; - this.themeService.theme.subscribe(theme => this.selectedTheme = theme); - } - - ionViewWillEnter(): void { - this.selectedTheme = this.themeService.get(); - this.coinCode = this.route.snapshot.queryParams.coinCode || environment.defaultWallet; - this.type = this.route.snapshot.queryParams.type || this.defaultType; - this.init(); - } - - ionViewDidLeave(): void { - } - - /** - * Subscribe Events - */ - async init() { - const [appCurrency, appStatus] = await this.appState.getMultipleState(['appCurrency', 'appStatus']); - this.currency = appCurrency; - this.status = appStatus; - } - - /** - * changeType function - */ - changeType(type: string) { - this.type = type; - } - - /** - * onSelectedWallet function - */ - onSelectedWallet(wallet: any) { - - this.selectedWallet = null; - this.isReady = false; - this.fiatAmount = '0.00'; - - if (this.formObj) { - this.formObj = null; +export class WalletSendComponent implements OnDestroy{ + + public formObj: FormGroup; + public selectedWallet: WalletModel; + public currency: CurrencyModel; + public status: StatusModel; + public coinCode: string; + public minWithdrawAmount: string; + public noResult: boolean; + public isReady: boolean; + public fiatAmount: string; + public type: string; // fiat/crypto + private readonly defaultType: any; + public selectedTheme: string; + public isWeb: boolean; + + constructor( + private hardwareService: HardwareService, + private modalCtrl: ModalController, + private utilsHelper: UtilsHelper, + public route: ActivatedRoute, + public formBuilder: FormBuilder, + private navController: NavController, + private appState: AppState, + private barcodeScanner: BarcodeScanner, + private formValidationHelper: FormValidationHelper, + private langService: LanguageService, + private clipboardService: ClipboardService, + private themeService: ThemeService, + private metamaskService: MetamaskService, + private loadingController: LoadingController + ) { + this.noResult = false; + this.defaultType = 'fiat'; + this.isReady = false; + this.fiatAmount = '0.00'; + this.themeService.theme.subscribe(theme => this.selectedTheme = theme); } - if (!this.utilsHelper.objectHasValue(wallet)) { - this.noResult = true; - this.isReady = true; - return; + ionViewWillEnter(): void { + this.isWeb = Capacitor.getPlatform() === 'web'; + this.selectedTheme = this.themeService.get(); + this.coinCode = this.route.snapshot.queryParams.coinCode || environment.defaultWallet; + this.type = this.route.snapshot.queryParams.type || this.defaultType; + this.init(); } - this.noResult = false; - this.selectedWallet = wallet; - this.initForm(); - } - - /** - * initForm function - */ - initForm() { - - const form: any = { - amount: ['', Validators.compose([ - this.formValidationHelper.withdrawValidAmount.bind(this), - Validators.required - ])] - }; - - if (this.selectedWallet.coin.code !== 'GBP') { - form.toAddress = ['', Validators.compose([ - this.formValidationHelper.ValidAddress.bind(this), - Validators.pattern(this.utilsHelper.regex.address), - Validators.required - ])]; + ionViewDidLeave(): void { } - if (this.selectedWallet.coin.code === 'GBP') { - form.extra = this.formBuilder.group({ - sortCode: ['', Validators.compose([ - Validators.required, - Validators.pattern(this.utilsHelper.regex.sortCode), - ])], - accountNumber: ['', Validators.compose([ - Validators.required, - Validators.pattern(this.utilsHelper.regex.accountNumber), - ])] - }); + /** + * Subscribe Events + */ + async init() { + const [appCurrency, appStatus] = await this.appState.getMultipleState(['appCurrency', 'appStatus']); + this.currency = appCurrency; + this.status = appStatus; } - if (this.selectedWallet.coin.code === 'XRP') { - form.extra = this.formBuilder.group({ - tag: [''] - }); + /** + * changeType function + */ + changeType(type: string) { + this.type = type; } - this.formObj = this.formBuilder.group(form); - this.isReady = true; - } + /** + * onSelectedWallet function + */ + onSelectedWallet(wallet: any) { + + this.selectedWallet = null; + this.isReady = false; + this.fiatAmount = '0.00'; - /** - * showMaxAmount function - */ - showMaxAmount(): boolean { + if (this.formObj) { + this.formObj = null; + } + + if (!this.utilsHelper.objectHasValue(wallet)) { + this.noResult = true; + this.isReady = true; + return; + } - if (!this.utilsHelper.objectHasValue(this.selectedWallet) || !this.utilsHelper.objectHasValue(this.status)) { - return false; + this.noResult = false; + this.selectedWallet = wallet; + this.initForm(); } - const balanceDjs = new Decimal(this.selectedWallet.balance || 0); - - this.minWithdrawAmount = this.status.coins[this.selectedWallet.coin.code] ? - this.status.coins[this.selectedWallet.coin.code].min_transfer : '0'; - - return balanceDjs.gt(0) && balanceDjs.gte(this.minWithdrawAmount); - } - - /** - * getBalance function - */ - getBalance(rate: string, balance: string, coinCode: string): string { - return this.utilsHelper.alignBalanceToStableCoin(rate, balance, coinCode, this.currency.symbol); - } - - /** - * showMaxAmount function - */ - calcFiatAmount(value: number | string) { - - this.fiatAmount = value ? this.utilsHelper.alignBalanceToStableCoin(this.selectedWallet.rate, value.toString(), this.selectedWallet.coin.code, this.currency.symbol) : '0.00'; - this.formObj.controls.amount.markAsTouched(); - this.formObj.updateValueAndValidity(); - } - - /** - * setMaxAmount function - */ - setMaxAmount() { - this.formObj.controls.amount.patchValue(this.selectedWallet.balance); - this.formObj.controls.amount.markAsTouched(); - this.formObj.get('amount').updateValueAndValidity(); - this.formObj.updateValueAndValidity(); - this.showMaxAmount(); - this.calcFiatAmount(this.selectedWallet.balance); - } - - /** - * askScan function - */ - askScan() { - - if (Capacitor.getPlatform() !== 'web') { - this.scan(); - } else { - this.scanQrCodeWeb(); + /** + * initForm function + */ + initForm() { + + const form: any = { + amount: ['', Validators.compose([ + this.formValidationHelper.withdrawValidAmount.bind(this), + Validators.required + ])] + }; + + if (this.selectedWallet.coin.code !== 'GBP') { + form.toAddress = ['', Validators.compose([ + this.formValidationHelper.ValidAddress.bind(this), + Validators.pattern(this.utilsHelper.regex.address), + Validators.required + ])]; + } + + if (this.selectedWallet.coin.code === 'GBP') { + form.extra = this.formBuilder.group({ + sortCode: ['', Validators.compose([ + Validators.required, + Validators.pattern(this.utilsHelper.regex.sortCode), + ])], + accountNumber: ['', Validators.compose([ + Validators.required, + Validators.pattern(this.utilsHelper.regex.accountNumber), + ])] + }); + } + + if (this.selectedWallet.coin.code === 'XRP') { + form.extra = this.formBuilder.group({ + tag: [''] + }); + } + + this.formObj = this.formBuilder.group(form); + this.isReady = true; } - } - - /** - * scan function - */ - scan() { - - this.hardwareService.toggle(true); - - this.barcodeScanner.scan({ - preferFrontCamera: true, - showFlipCameraButton: true, - showTorchButton: true, - prompt: this.langService.getTranslate('CAMERA.message'), // Android - resultDisplayDuration: 0 - }).then((barcodeData) => { - - if (barcodeData && barcodeData.text && barcodeData.format === 'QR_CODE') { - this.stripeAddress(barcodeData.text); - } else if (barcodeData && barcodeData.cancelled) { - return; - } - }, (error) => { - - logger.error(logContent(Object.assign({}, { - info: 'Error scan qrcode', - err: error - }))); - - }); - } - - /** - * stripeAddress function - */ - stripeAddress(barcodeData: string) { - - if (!this.utilsHelper.stringHasValue(barcodeData)) { - return; + + /** + * showMaxAmount function + */ + showMaxAmount(): boolean { + + if (!this.utilsHelper.objectHasValue(this.selectedWallet) || !this.utilsHelper.objectHasValue(this.status)) { + return false; + } + + const balanceDjs = new Decimal(this.selectedWallet.balance || 0); + + this.minWithdrawAmount = this.status.coins[this.selectedWallet.coin.code] ? + this.status.coins[this.selectedWallet.coin.code].min_transfer : '0'; + + return balanceDjs.gt(0) && balanceDjs.gte(this.minWithdrawAmount); } - try { + /** + * getBalance function + */ + getBalance(rate: string, balance: string, coinCode: string): string { + return this.utilsHelper.alignBalanceToStableCoin(rate, balance, coinCode, this.currency.symbol); + } - let {address} = this.utilsHelper.amonLib.URI.parse(barcodeData); + /** + * showMaxAmount function + */ + calcFiatAmount(value: number | string) { - if (address && address.includes('?dt=')) { - const result = this.utilsHelper.amonLib.coins('XRP').parseTag(address); - address = result.address; + this.fiatAmount = value ? this.utilsHelper.alignBalanceToStableCoin(this.selectedWallet.rate, value.toString(), this.selectedWallet.coin.code, this.currency.symbol) : '0.00'; + this.formObj.controls.amount.markAsTouched(); + this.formObj.updateValueAndValidity(); + } - this.formObj.controls.extra.get('tag').patchValue(result.tag); - this.formObj.controls.extra.get('tag').markAsTouched(); - this.formObj.controls.extra.get('tag').updateValueAndValidity(); - } + /** + * setMaxAmount function + */ + setMaxAmount() { + this.formObj.controls.amount.patchValue(this.selectedWallet.balance); + this.formObj.controls.amount.markAsTouched(); + this.formObj.get('amount').updateValueAndValidity(); + this.formObj.updateValueAndValidity(); + this.showMaxAmount(); + this.calcFiatAmount(this.selectedWallet.balance); + } - this.formObj.controls.toAddress.patchValue(address); - this.formObj.controls.toAddress.markAsTouched(); - this.formObj.updateValueAndValidity(); + /** + * askScan function + */ + askScan() { - } catch (error) { + if (Capacitor.getPlatform() !== 'web') { + this.scan(); + } else { + this.scanQrCodeWeb(); + } + } - logger.warn(logContent(Object.assign({}, { - info: 'Error stripe address', - err: error - }))); + /** + * scan function + */ + scan() { + + this.hardwareService.toggle(true); + + this.barcodeScanner.scan({ + preferFrontCamera: true, + showFlipCameraButton: true, + showTorchButton: true, + prompt: this.langService.getTranslate('CAMERA.message'), // Android + resultDisplayDuration: 0 + }).then((barcodeData) => { + + if (barcodeData && barcodeData.text && barcodeData.format === 'QR_CODE') { + this.stripeAddress(barcodeData.text); + } else if (barcodeData && barcodeData.cancelled) { + return; + } + }, (error) => { + + logger.error(logContent(Object.assign({}, { + info: 'Error scan qrcode', + err: error + }))); + + }); } - } - /** - * submit function - */ - async reviewPayment() { + /** + * stripeAddress function + */ + stripeAddress(barcodeData: string) { - const rawForm = this.formObj.getRawValue(); + if (!this.utilsHelper.stringHasValue(barcodeData)) { + return; + } - const summaryModal = await this.modalCtrl.create({ - id: 'summary', - component: SummaryModalComponent, - componentProps: { - payment: rawForm, - wallet: this.selectedWallet - } - }); + try { - summaryModal.onWillDismiss().then((params) => { - if (params.data && params.data.status && params.data.txId) { - this.formObj.reset(); - this.goToTransactions(params.data.txId); - } - }); + let {address} = this.utilsHelper.amonLib.URI.parse(barcodeData); - await summaryModal.present(); - } + if (address && address.includes('?dt=')) { + const result = this.utilsHelper.amonLib.coins('XRP').parseTag(address); + address = result.address; - /** - * scanQrCode Function - */ - async scanQrCodeWeb() { + this.formObj.controls.extra.get('tag').patchValue(result.tag); + this.formObj.controls.extra.get('tag').markAsTouched(); + this.formObj.controls.extra.get('tag').updateValueAndValidity(); + } - this.hardwareService.toggle(true); + this.formObj.controls.toAddress.patchValue(address); + this.formObj.controls.toAddress.markAsTouched(); + this.formObj.updateValueAndValidity(); - const qrcodeScanner = await this.modalCtrl.create({ - id: 'qrcode-scanner', - component: QrcodeScannerComponent, - componentProps: {} - }); + } catch (error) { - qrcodeScanner.onDidDismiss().then((params) => { + logger.warn(logContent(Object.assign({}, { + info: 'Error stripe address', + err: error + }))); + } + } - if (params.data && params.data.address) { - this.stripeAddress(params.data.address); - } - }); + /** + * submit function + */ + async reviewPayment() { + + const rawForm = this.formObj.getRawValue(); + + const summaryModal = await this.modalCtrl.create({ + id: 'summary', + component: SummaryModalComponent, + componentProps: { + payment: rawForm, + wallet: this.selectedWallet + } + }); + + summaryModal.onWillDismiss().then((params) => { + if (params.data && params.data.status && params.data.txId) { + this.formObj.reset(); + this.goToTransactions(params.data.txId); + } + }); + + await summaryModal.present(); + } - await qrcodeScanner.present(); - } + /** + * scanQrCode Function + */ + async scanQrCodeWeb() { - /** - * onPasteAddress Function - */ - async onPaste(field: string, extra: boolean) { + this.hardwareService.toggle(true); - try { + const qrcodeScanner = await this.modalCtrl.create({ + id: 'qrcode-scanner', + component: QrcodeScannerComponent, + componentProps: {} + }); - const clipboard = await this.clipboardService.read(); + qrcodeScanner.onDidDismiss().then((params) => { - if (clipboard && clipboard.type === 'text/plain' && this.utilsHelper.stringHasValue(clipboard.value)) { + if (params.data && params.data.address) { + this.stripeAddress(params.data.address); + } + }); - const value = this.parseString(clipboard.value); + await qrcodeScanner.present(); + } - if (extra) { - this.formObj.controls.extra.get(field).patchValue(value); - this.formObj.controls.extra.get(field).markAsTouched(); - this.formObj.controls.extra.get(field).updateValueAndValidity(); - } else { - this.formObj.controls[field].patchValue(value); - this.formObj.controls[field].markAsTouched(); - this.formObj.controls[field].updateValueAndValidity(); - } + /** + * onPasteAddress Function + */ + async onPaste(field: string, extra: boolean) { + + try { - } + const clipboard = await this.clipboardService.read(); - } catch (error) { - logger.warn(logContent(Object.assign({}, { - info: 'Error onPaste address', - err: error - }))); + if (clipboard && clipboard.type === 'text/plain' && this.utilsHelper.stringHasValue(clipboard.value)) { + + const value = this.parseString(clipboard.value); + + if (extra) { + this.formObj.controls.extra.get(field).patchValue(value); + this.formObj.controls.extra.get(field).markAsTouched(); + this.formObj.controls.extra.get(field).updateValueAndValidity(); + } else { + this.formObj.controls[field].patchValue(value); + this.formObj.controls[field].markAsTouched(); + this.formObj.controls[field].updateValueAndValidity(); + } + + } + + } catch (error) { + logger.warn(logContent(Object.assign({}, { + info: 'Error onPaste address', + err: error + }))); + } } - } - /** - * parseString Function - */ - parseString(value?: string) { + /** + * parseString Function + */ + parseString(value?: string) { - if (this.utilsHelper.stringHasValue(value)) { - return value.replace(/-/g, '').replace(/ /g, ''); + if (this.utilsHelper.stringHasValue(value)) { + return value.replace(/-/g, '').replace(/ /g, ''); + } + + return value; } - return value; - } + /** + * getCoinIcon function + */ + getIcon(icon: string): string { - /** - * getCoinIcon function - */ - getIcon(icon: string): string { + if (this.selectedTheme === 'dark') { + return `${icon}-dark`; + } - if (this.selectedTheme === 'dark') { - return `${icon}-dark`; + return icon; } - return icon; - } + /** + * submit Function + */ + async submit() { - /** - * submit Function - */ - async submit() { + const oracleRates = await this.appState.getNewState('oracleRates'); - const oracleRates = await this.appState.getNewState('oracleRates'); + if ( + this.utilsHelper.objectHasValue(this.selectedWallet) && + !this.utilsHelper.isFiat(this.selectedWallet.coin.code) && + this.utilsHelper.objectHasValue(oracleRates) && + this.utilsHelper.objectHasValue(this.status) + ) { - if ( - this.utilsHelper.objectHasValue(this.selectedWallet) && - !this.utilsHelper.isFiat(this.selectedWallet.coin.code) && - this.utilsHelper.objectHasValue(oracleRates) && - this.utilsHelper.objectHasValue(this.status) - ) { + try { + const eurRate = oracleRates[this.selectedWallet.coin.code].EUR.rate; + const eurFeesDc = new Decimal(eurRate || 0).mul(this.status.coins[this.selectedWallet.coin.code].fee_withdraw || 0); - try { - const eurRate = oracleRates[this.selectedWallet.coin.code].EUR.rate; - const eurFeesDc = new Decimal(eurRate || 0).mul(this.status.coins[this.selectedWallet.coin.code].fee_withdraw || 0); + if (eurFeesDc.gt(environment.blockchainHighFee)) { + return this.openHighFeeModal(); + } + + } catch (error) { + logger.error(logContent(Object.assign({}, { + info: 'Error check high fee', + err: error + }))); + } - if (eurFeesDc.gt(environment.blockchainHighFee)) { - return this.openHighFeeModal(); } - } catch (error) { - logger.error(logContent(Object.assign({}, { - info: 'Error check high fee', - err: error - }))); - } - } + return this.reviewPayment(); + } - return this.reviewPayment(); + /** + * openHighFeeModal function + */ + async openHighFeeModal() { - } + const highFeeModal = await this.modalCtrl.create({ + id: 'high-fee-modal', + component: HighFeeModalComponent, + cssClass: 'modal-mini tier', + componentProps: {} + }); - /** - * openHighFeeModal function - */ - async openHighFeeModal() { + highFeeModal.onDidDismiss().then(() => { + this.reviewPayment(); + }); - const highFeeModal = await this.modalCtrl.create({ - id: 'high-fee-modal', - component: HighFeeModalComponent, - cssClass: 'modal-mini tier', - componentProps: {} - }); + await highFeeModal.present(); + } - highFeeModal.onDidDismiss().then(() => { - this.reviewPayment(); - }); + /** + * Get Network function + */ + getNetwork(selectedWallet: any): string { - await highFeeModal.present(); - } + return this.utilsHelper.getNetwork(selectedWallet); + } - /** - * Get Network function - */ - getNetwork(selectedWallet: any): string { + /** + * goToTransactions function + */ + goToTransactions(txId: string) { - return this.utilsHelper.getNetwork(selectedWallet); - } + this.navController.navigateForward('/auth/wallet/transactions', { + queryParams: {coinCode: this.selectedWallet.coin.code, txId} + }); + } - /** - * goToTransactions function - */ - goToTransactions(txId: string) { + /** + * useMetamask function + */ + useMetamask = async () => { + + MetamaskService.account$.subscribe(account => { + this.formObj.controls.toAddress.patchValue(account); + this.formObj.controls.toAddress.markAsTouched(); + this.formObj.updateValueAndValidity(); + }, error => { + console.log('error: ', error); + }); + + MetamaskService.chainId$ + .subscribe(_ => { + }, error => { + console.log('error chain id: ', error); + this.formObj.controls.toAddress.patchValue(''); + this.formObj.controls.toAddress.markAsTouched(); + this.formObj.updateValueAndValidity(); + }); + + const topLoading = await this.loadingController.getTop(); + if(topLoading) { + await this.loadingController.dismiss(); + } + const loader = await this.loadingController.create(this.utilsHelper.loaderOption()); + await loader.present(); + this.metamaskService.useMetaMask() + .subscribe(async (address) => { + this.formObj.controls.toAddress.patchValue(address); + this.formObj.controls.toAddress.markAsTouched(); + this.formObj.updateValueAndValidity(); + await loader.dismiss(); + }, async (error) => { + console.log('error from component: ', error); + await loader.dismiss(); + }); + }; - this.navController.navigateForward('/auth/wallet/transactions', { - queryParams: {coinCode: this.selectedWallet.coin.code, txId} - }); - } + ngOnDestroy() { + this.metamaskService.removeListeners(); + } } diff --git a/src/app/pages/wallet/send/index.html b/src/app/pages/wallet/send/index.html index 54e8a8eaf..4e0e1266c 100644 --- a/src/app/pages/wallet/send/index.html +++ b/src/app/pages/wallet/send/index.html @@ -1,151 +1,162 @@ - - - - - + + + + + - -
- {{ 'WALLET.title.send' | translate }} -
-
- - - - - - -
- - - - - - - -
- - -
-
- - - - - - - {{'WALLET.form.sort_code' | translate}} - - - - - - - - {{'WALLET.form.account_number' | translate}} - - - - - - - - - {{(selectedWallet?.isFiat) ? 'IBAN' : 'WALLET.form.address' | translate}} - {{getNetwork(selectedWallet)}} - - - - - - - - - - - - Tag {{'INFO.optional' | translate}} - - - - - - - - {{'WALLET.form.amount' | translate}} - + +
+ {{ 'WALLET.title.send' | translate }} +
+
+ + + + + + +
+ + + + + + + +
+ + +
+
+ + + + + + + {{'WALLET.form.sort_code' | translate}} + + + + + + + + {{'WALLET.form.account_number' | translate}} + + + + + + + + + {{(selectedWallet?.isFiat) ? 'IBAN' : 'WALLET.form.address' | translate}} + {{getNetwork(selectedWallet)}} + + + + + + + + + + + + + + + + + + Tag {{'INFO.optional' | translate}} + + + + + + + + {{'WALLET.form.amount' | translate}} + {{currency?.symbol_native}} {{fiatAmount | number}} - - - - - - - - - {{'BUTTON.maxAmount' | translate}} - - - - -
-
-
-
- - - {{'BUTTON.reviewPayment' | translate}} - - -
- - -

-
- -
-
-
- - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + {{'BUTTON.maxAmount' | translate}} +
+ + + +
+
+
+
+ + + {{'BUTTON.reviewPayment' | translate}} + + +
+ + +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
diff --git a/src/app/pages/wallet/send/style.scss b/src/app/pages/wallet/send/style.scss index ccfe96a7f..1ba805ec4 100644 --- a/src/app/pages/wallet/send/style.scss +++ b/src/app/pages/wallet/send/style.scss @@ -155,13 +155,12 @@ span.fiat_amount { } } + ion-button.btn_qrcode { - position: absolute; - right: 12px; - top: 20px; + --padding-start: 0; + --padding-end: 0; width: 32px; height: 32px; - z-index: 9; --background: #eee; --background-focused: #eee; --background-hover: #eee; @@ -183,7 +182,26 @@ ion-button.btn_qrcode { } } -ion-button.qrcode_above { +ion-row.actions { + position: absolute; + right: 0; + top: 20px; + z-index: 9; +} + +ion-icon.metamask { + width: 32px; + height: 32px; + margin-left: 12px; + cursor: pointer; +} + +ion-row.up-input { + top: 0; + margin-top: 0; +} + +ion-icon.qrcode_above { top: 0; margin-top: 0; } diff --git a/src/app/services/metamask.service.spec.ts b/src/app/services/metamask.service.spec.ts new file mode 100644 index 000000000..966b615a9 --- /dev/null +++ b/src/app/services/metamask.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MetamaskService } from './metamask.service'; + +describe('MetamaskService', () => { + let service: MetamaskService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MetamaskService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/metamask.service.ts b/src/app/services/metamask.service.ts new file mode 100644 index 000000000..c905cf255 --- /dev/null +++ b/src/app/services/metamask.service.ts @@ -0,0 +1,118 @@ +import {Injectable} from '@angular/core'; +import detectEthereumProvider from '@metamask/detect-provider'; +import {from, of, Subject, throwError} from 'rxjs'; +import {catchError, switchMap} from 'rxjs/operators'; +import {environment} from '@env/environment'; +import {ToastService} from '@services/toast'; +import {LanguageService} from '@services/languages'; + +const EthMainnet = '0x1'; + +@Injectable({ + providedIn: 'root' +}) +export class MetamaskService { + + public ethProvider; + public currentAccount; + public chainId; + static account$: Subject = new Subject(); + static chainId$: Subject = new Subject(); + + constructor(private toastService: ToastService, private langService: LanguageService) { + } + + async detectEthProvider() { + this.ethProvider = await detectEthereumProvider(); + if (!this.ethProvider) { + throw new Error('install_meta_mask'); + } else if (this.ethProvider && this.ethProvider !== (window as any).ethereum) { + throw new Error('you_have_multiple_wallets_installed'); + } + return this.ethProvider; + } + + async requestChainId() { + this.chainId = await this.ethProvider.request({method: 'eth_chainId'}); + console.log('chain id: ', this.chainId); + await this.listenChainChanges(); + if (this.chainId !== EthMainnet) { + // throw new Error('please_select_main_net_in_meta_mask'); + return this.requestChangeNetwork(); + } + return this.chainId; + } + + async requestChangeNetwork() { + await (window.ethereum as any).request({ + method: 'wallet_switchEthereumChain', + params: [{chainId: EthMainnet}] + }); + } + + // this is not read you shoukd have a solution + async handleChainChanged(_chainId) { + console.log('chain changed: ', _chainId); + this.chainId = _chainId; + if (this.chainId !== EthMainnet) { + await (window.ethereum as any).request({ + method: 'wallet_switchEthereumChain', + params: [{chainId: EthMainnet}] + }); + // return this.requestChangeNetwork(); + // MetamaskService.chainId$.error('please_select_main_net_in_meta_mask'); + } + } + + async listenChainChanges() { + this.ethProvider.on('chainChanged', this.handleChainChanged); + } + + async requestAccounts() { + const accounts = await this.ethProvider + .request({method: 'eth_accounts'}); + await this.listenAccountChanged(); + return accounts; + } + + handleAccountsChanged(accounts: string[]) { + if (environment.production && this.chainId !== EthMainnet) { + MetamaskService.chainId$.error('please_select_main_net_in_meta_mask'); + return; + } + MetamaskService.account$.next(accounts && accounts.length ? accounts[0] : null); + } + + async listenAccountChanged() { + this.ethProvider.on('accountsChanged', this.handleAccountsChanged); + } + + + public useMetaMask() { + return from(this.detectEthProvider()) + .pipe( + switchMap(_ => from(this.requestChainId())), + switchMap(__ => from(this.requestAccounts())), + switchMap(accounts => accounts && accounts?.length ? of(accounts) : this.ethProvider.request({method: 'eth_requestAccounts'})), + switchMap((accounts: any) => of(accounts && accounts?.length ? accounts[0] : null)), + catchError(error => { + console.log('error: ', error); + if (error && error.code === 4001) { + this.toastService.responseError(this.langService.getTranslate('ERRORS.metamask_user_reject')); + } else if (error && error.code === -32002) { + this.toastService.responseError(this.langService.getTranslate('ERRORS.metamask_already_have_pending_request')); + } else { + this.toastService.responseError(this.langService.getTranslate('ERRORS.metamask_not_installed')); + } + return throwError(error); + })); + }; + + + removeListeners() { + if (this.ethProvider) { + this.ethProvider.removeListener('accountsChanged', this.handleChainChanged); + this.ethProvider.removeListener('chainChanged', this.handleChainChanged); + } + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 497c1ebd2..20bb0c7ab 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -832,7 +832,10 @@ "invalidPhoneNumber": "mobile number can't be the same", "newPasswordDifferentFromOld": "new password must be different from the old one", "doesMatchPassword": "password doesn't match", - "required": "field required" + "required": "field required", + "metamask_not_installed": "please install the metamask extension", + "metamask_user_reject": "Please try again and follow the steps to connect to your wallet", + "metamask_already_have_pending_request": "You've already a pending request, please open metamask and follow the steps" }, "SERVER_ERRORS": { "unknown_error": "Error.", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 2e4f4bb0e..695cbbf9e 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -832,7 +832,10 @@ "invalidPhoneNumber": "el numero no puede ser el mismo", "newPasswordDifferentFromOld": "la nueva contraseña debe ser diferente a la anterior", "doesMatchPassword": "la contraseña no coincide", - "required": "campo obligatorio" + "required": "campo obligatorio", + "metamask_not_installed": "instale la extensión metamask", + "metamask_user_reject": "Vuelva a intentarlo y siga los pasos para conectarse a su billetera", + "metamask_already_have_pending_request": "Ya tiene una solicitud pendiente, abra metamask y siga los pasos" }, "SERVER_ERRORS": { "unknown_error": "Error.", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index 66a6614da..28787b7f1 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -832,7 +832,10 @@ "invalidPhoneNumber": "le numéro de mobile doit être différent", "newPasswordDifferentFromOld": "le nouveau mot de passe doit être différent de l'ancien", "doesMatchPassword": "les mots de passe ne correspondent pas", - "required": "champ requis" + "required": "champ requis", + "metamask_not_installed": "installer l'extension metamask", + "metamask_user_reject": "Please try again and follow the steps to connect in your wallet", + "metamask_already_have_pending_request": "Vous avez déjà une demande en attente, veuillez ouvrir metamask et suivre les étapes" }, "SERVER_ERRORS": { "unknown_error": "Erreur inconnue.", diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 840d62a51..62404fd91 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -832,7 +832,10 @@ "invalidPhoneNumber": "numero di telefono non può essere uguale", "newPasswordDifferentFromOld": "la nuova password deve essere diversa da quella precedente", "doesMatchPassword": "le password sono diverse", - "required": "campo obligatorio" + "required": "campo obligatorio", + "metamask_not_installed": "installa l'estensione di metamask", + "metamask_user_reject": "riprova e segui i passaggi per connetterti al tuo portafoglio", + "metamask_already_have_pending_request": "Hai già una richiesta in sospeso, apri metamask e segui i passaggi" }, "SERVER_ERRORS": { "unknown_error": "Errore.", diff --git a/src/assets/i18n/pt.json b/src/assets/i18n/pt.json index 8354ea513..d4f983b3a 100644 --- a/src/assets/i18n/pt.json +++ b/src/assets/i18n/pt.json @@ -832,7 +832,10 @@ "invalidPhoneNumber": "o número do celular não pode ser o mesmo", "newPasswordDifferentFromOld": "a nova senha deve ser diferente da antiga", "doesMatchPassword": "a senha não coincide", - "required": "preenchimento obrigatório" + "required": "preenchimento obrigatório", + "metamask_not_installed": "instale a extensão metamask", + "metamask_user_reject": "Tente novamente e siga as etapas para se conectar à sua carteira", + "metamask_already_have_pending_request": "Você já tem uma solicitação pendente, abra a metamask e siga as etapas" }, "SERVER_ERRORS": { "unknown_error": "Erro.", diff --git a/src/assets/ionic-icons/metamask.svg b/src/assets/ionic-icons/metamask.svg new file mode 100644 index 000000000..003b12e24 --- /dev/null +++ b/src/assets/ionic-icons/metamask.svg @@ -0,0 +1 @@ +metamask \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 0e51c5e65..0149c7f12 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -9,5 +9,6 @@ "type": "image/png" }], "background_color": "#000", - "theme_color": "#000" + "theme_color": "#000", + "content_security_policy": "default-src 'none' ; object-src 'self' ; frame-ancestors 'none' ; frame-src https://intercom-sheets.com https://www.googletagmanager.com https://www.facebook.com https://cardframe.isaac.tribepayments.com 'unsafe-inline' ; connect-src 'self' https://amon.tech https://wallet.amon.tech https://api.amon.tech https://rates.amon.tech https://wa.appsflyer.com https://wa.onelink.me https://websdk.appsflyer.com https://www.gstatic.com https://firebase.googleapis.com https://firebaseinstallations.googleapis.com https://www.facebook.com https://connect.facebook.net https://stats.g.doubleclick.net https://www.googletagmanager.com https://www.google-analytics.com https://www.google.com https://min-api.cryptocompare.com https://images.cryptocompare.com https://maps.google.com https://maps.googleapis.com https://www.redditstatic.com https://analytics.tiktok.com https://www.google.ad https://www.google.ae https://www.google.com.af https://www.google.com.ag https://www.google.com.ai https://www.google.al https://www.google.am https://www.google.co.ao https://www.google.com.ar https://www.google.as https://www.google.at https://www.google.com.au https://www.google.az https://www.google.ba https://www.google.com.bd https://www.google.be https://www.google.bf https://www.google.bg https://www.google.com.bh https://www.google.bi https://www.google.bj https://www.google.com.bn https://www.google.com.bo https://www.google.com.br https://www.google.bs https://www.google.bt https://www.google.co.bw https://www.google.by https://www.google.com.bz https://www.google.ca https://www.google.cd https://www.google.cf https://www.google.cg https://www.google.ch https://www.google.ci https://www.google.co.ck https://www.google.cl https://www.google.cm https://www.google.cn https://www.google.com.co https://www.google.co.cr https://www.google.com.cu https://www.google.cv https://www.google.com.cy https://www.google.cz https://www.google.de https://www.google.dj https://www.google.dk https://www.google.dm https://www.google.com.do https://www.google.dz https://www.google.com.ec https://www.google.ee https://www.google.com.eg https://www.google.es https://www.google.com.et https://www.google.fi https://www.google.com.fj https://www.google.fm https://www.google.fr https://www.google.ga https://www.google.ge https://www.google.gg https://www.google.com.gh https://www.google.com.gi https://www.google.gl https://www.google.gm https://www.google.gr https://www.google.com.gt https://www.google.gy https://www.google.com.hk https://www.google.hn https://www.google.hr https://www.google.ht https://www.google.hu https://www.google.co.id https://www.google.ie https://www.google.co.il https://www.google.im https://www.google.co.in https://www.google.iq https://www.google.is https://www.google.it https://www.google.je https://www.google.com.jm https://www.google.jo https://www.google.co.jp https://www.google.co.ke https://www.google.com.kh https://www.google.ki https://www.google.kg https://www.google.co.kr https://www.google.com.kw https://www.google.kz https://www.google.la https://www.google.com.lb https://www.google.li https://www.google.lk https://www.google.co.ls https://www.google.lt https://www.google.lu https://www.google.lv https://www.google.com.ly https://www.google.co.ma https://www.google.md https://www.google.me https://www.google.mg https://www.google.mk https://www.google.ml https://www.google.com.mm https://www.google.mn https://www.google.ms https://www.google.com.mt https://www.google.mu https://www.google.mv https://www.google.mw https://www.google.com.mx https://www.google.com.my https://www.google.co.mz https://www.google.com.na https://www.google.com.ng https://www.google.com.ni https://www.google.ne https://www.google.nl https://www.google.no https://www.google.com.np https://www.google.nr https://www.google.nu https://www.google.co.nz https://www.google.com.om https://www.google.com.pa https://www.google.com.pe https://www.google.com.pg https://www.google.com.ph https://www.google.com.pk https://www.google.pl https://www.google.pn https://www.google.com.pr https://www.google.ps https://www.google.pt https://www.google.com.py https://www.google.com.qa https://www.google.ro https://www.google.ru https://www.google.rw https://www.google.com.sa https://www.google.com.sb https://www.google.sc https://www.google.se https://www.google.com.sg https://www.google.sh https://www.google.si https://www.google.sk https://www.google.com.sl https://www.google.sn https://www.google.so https://www.google.sm https://www.google.sr https://www.google.st https://www.google.com.sv https://www.google.td https://www.google.tg https://www.google.co.th https://www.google.com.tj https://www.google.tl https://www.google.tm https://www.google.tn https://www.google.to https://www.google.com.tr https://www.google.tt https://www.google.com.tw https://www.google.co.tz https://www.google.com.ua https://www.google.co.ug https://www.google.co.uk https://www.google.com.uy https://www.google.co.uz https://www.google.com.vc https://www.google.co.ve https://www.google.vg https://www.google.co.vi https://www.google.com.vn https://www.google.vu https://www.google.ws https://www.google.rs https://www.google.co.za https://www.google.co.zm https://www.google.co.zw https://www.google.cat wss://*.intercom.io https://fonts.googleapis.com https://*.intercom.io *.intercomcdn.com *.intercomusercontent.com https://amon-tech.zendesk.com https://static.zdassets.com https://ekr.zdassets.com https://api.exchangeratesapi.io https://upload.wikimedia.org https://sentry.io https://*.ingest.sentry.io https://api.coingecko.com https://assets.coingecko.com https://api.celsius.network https://amon.sjv.io/cur/14124 https://amon.sjv.io/xur/14124 https://amon.sjv.io/ur/14124 https://amon.sjv.io/xconv/26957/14124 https://amon.sjv.io/xconv/26958/14124 https://amon.sjv.io/xconv/26960/14124 https://rawjeansadvertising.com https://utt.impactcdn.com/A2917494-d314-405d-94f7-8fcafac303fc1.js https://*.cloudfront.net/* https://cloudfront.net/* *.cloudfront.net/* https://onesignal.com https://cdn.onesignal.com https://img.onesignal.com ; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.intercom.io *.intercomcdn.com https://utt.impactcdn.com https://js.intercomcdn.com https://utt.impactcdn.com/A2917494-d314-405d-94f7-8fcafac303fc1.js https://websdk.appsflyer.com https://www.gstatic.com https://connect.facebook.net https://www.googletagmanager.com https://www.google-analytics.com https://ssl.google-analytics.com https://maps.google.com https://maps.googleapis.com https://analytics.tiktok.com https://static.zdassets.com https://assets.coingecko.com https://onesignal.com https://cdn.onesignal.com ; style-src 'self' https://www.googletagmanager.com https://fonts.googleapis.com 'unsafe-inline' https://onesignal.com https://cdn.onesignal.com ; font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com *.intercomcdn.com ; img-src 'self' https: blob: data: maps.gstatic.com *.googleapis.com *.ggpht https://assets.coingecko.com https://www.facebook.com https://www.googletagmanager.com https://www.google-analytics.com *.intercomassets.com *.intercomusercontent.com *.intercomcdn.io *.intercom.io https://*.intercom-attachments-1.com https://*.intercom-attachments-2.com https://*.intercom-attachments-3.com https://*.intercom-attachments-4.com https://*.intercom-attachments-5.com https://*.intercom-attachments-6.com https://*.intercom-attachments-7.com https://*.intercom-attachments-8.com https://*.intercom-attachments-9.com ; media-src 'self' *.intercomcdn.com ; form-action 'self' https://intercom-zendesk-plugin-api.getalvis.com https://www.facebook.com https://intercom.help *.intercom.io ; base-uri 'self' ; manifest-src 'self' ; worker-src 'self' https://wallet.amon.tech ; upgrade-insecure-requests ;" }