diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 99c972a56..ccfe45423 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -7,13 +7,16 @@ import { Logger } from '@nsalaun/ng-logger'; import { AppProvider } from '../providers/app/app'; import { ProfileProvider } from '../providers/profile/profile'; import { ConfigProvider } from '../providers/config/config'; +import { TouchIdProvider } from '../providers/touchid/touchid'; import { TabsPage } from '../pages/tabs/tabs'; import { OnboardingPage } from '../pages/onboarding/onboarding'; import { PinModalPage } from '../pages/pin/pin'; +import { FingerprintModalPage } from '../pages/fingerprint/fingerprint'; @Component({ - templateUrl: 'app.html' + templateUrl: 'app.html', + providers: [TouchIdProvider] }) export class CopayApp { rootPage: any; @@ -61,13 +64,19 @@ export class CopayApp { openLockModal() { let config = this.config.get(); - let lockMethod = config['lock'] && config['lock']['method']; - if (!config['lock']['method']) return; - if (config['lock']['method'] == 'PIN') this.openPINModal('checkPin'); + let lockMethod = config['lock']['method']; + if (!lockMethod) return; + if (lockMethod == 'PIN') this.openPINModal('checkPin'); + if (lockMethod == 'Fingerprint') this.openFingerprintModal(); } openPINModal(action) { let modal = this.modalCtrl.create(PinModalPage, { action }, { showBackdrop: false, enableBackdropDismiss: false }); modal.present(); } + + openFingerprintModal() { + let modal = this.modalCtrl.create(FingerprintModalPage, {}, { showBackdrop: false, enableBackdropDismiss: false }); + modal.present(); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 57aabcfd5..b55ae66f1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -42,6 +42,7 @@ import { SettingsPage } from '../pages/settings/settings'; import { AboutPage } from '../pages/settings/about/about'; import { AdvancedPage } from '../pages/settings/advanced/advanced'; import { AltCurrencyPage } from '../pages/settings/alt-currency/alt-currency'; +import { FingerprintModalPage } from '../pages/fingerprint/fingerprint'; import { LockPage } from '../pages/settings/lock/lock'; import { PinModalPage } from '../pages/pin/pin'; import { TermsOfUsePage } from '../pages/settings/about/terms-of-use/terms-of-use'; @@ -85,6 +86,7 @@ let pages: any = [ CopayApp, DisclaimerPage, EmailPage, + FingerprintModalPage, HomePage, LockPage, OnboardingPage, diff --git a/src/pages/fingerprint/fingerprint.html b/src/pages/fingerprint/fingerprint.html new file mode 100644 index 000000000..a449cb91f --- /dev/null +++ b/src/pages/fingerprint/fingerprint.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pages/fingerprint/fingerprint.scss b/src/pages/fingerprint/fingerprint.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/fingerprint/fingerprint.ts b/src/pages/fingerprint/fingerprint.ts new file mode 100644 index 000000000..bfcbc8cd8 --- /dev/null +++ b/src/pages/fingerprint/fingerprint.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { NavController, NavParams, ViewController } from 'ionic-angular'; +import { TouchIdProvider } from '../../providers/touchid/touchid'; + +@Component({ + selector: 'page-fingerprint', + templateUrl: 'fingerprint.html', +}) +export class FingerprintModalPage { + + constructor( + private touchid: TouchIdProvider, + private viewCtrl: ViewController + ) { + touchid.check().then(() => { + this.viewCtrl.dismiss(); + }); + } + +} diff --git a/src/pages/pin/pin.html b/src/pages/pin/pin.html index da3f88781..589a5cf0a 100644 --- a/src/pages/pin/pin.html +++ b/src/pages/pin/pin.html @@ -2,7 +2,7 @@ - @@ -13,6 +13,10 @@ +
+
Incorrect PIN, try again.
+ +
diff --git a/src/pages/pin/pin.scss b/src/pages/pin/pin.scss index bb4ee9289..f429419f1 100644 --- a/src/pages/pin/pin.scss +++ b/src/pages/pin/pin.scss @@ -5,7 +5,7 @@ page-pin { max-width: 300px; margin: auto; } - .block-buttons{ + .block-buttons { .row { font-size: 1.7rem; cursor: pointer; diff --git a/src/pages/pin/pin.ts b/src/pages/pin/pin.ts index a7e1191e8..3241edaa6 100644 --- a/src/pages/pin/pin.ts +++ b/src/pages/pin/pin.ts @@ -3,26 +3,29 @@ import { NavController, NavParams, ViewController } from 'ionic-angular'; import { ConfigProvider } from '../../providers/config/config'; import { Logger } from '@nsalaun/ng-logger'; -import * as _ from 'lodash'; - @Component({ selector: 'page-pin', templateUrl: 'pin.html', }) export class PinModalPage { + private ATTEMPT_LIMIT: number = 3; + private ATTEMPT_LOCK_OUT_TIME: number = 5 * 60; + public currentAttempts: number = 0; public currentPin: string = ''; public firstPinEntered: string = ''; public confirmingPin: boolean = false; public action: string = ''; public appName: string = 'copay'; + public disableButtons: boolean = false; + public expires: string = ''; constructor( - public navCtrl: NavController, - public navParams: NavParams, + private navCtrl: NavController, + private navParams: NavParams, private config: ConfigProvider, private logger: Logger, - public viewCtrl: ViewController + private viewCtrl: ViewController ) { switch (this.navParams.get('action')) { @@ -36,6 +39,18 @@ export class PinModalPage { this.action = 'removeLock' } + if (this.action === 'checkPin' || this.action === 'removeLock') { + let config = this.config.get(); + let bannedUntil = config['lock']['bannedUntil']; + if (bannedUntil) { + let now = Math.floor(Date.now() / 1000); + if (now < bannedUntil) { + this.disableButtons = true; + this.lockTimeControl(bannedUntil); + } + } + } + } goBack(): void { @@ -43,6 +58,7 @@ export class PinModalPage { } newEntry(value: string): void { + if (this.disableButtons) return; this.currentPin = this.currentPin + value; if (!this.isComplete()) return; if (this.action === 'checkPin' || this.action === 'removeLock') this.checkIfCorrect(); @@ -53,20 +69,56 @@ export class PinModalPage { this.currentPin = ''; } else if (this.firstPinEntered === this.currentPin) this.save(); - else { - this.firstPinEntered = this.currentPin = ''; - } + else this.firstPinEntered = this.currentPin = ''; } } + checkAttempts(): void { + this.currentAttempts += 1; + this.logger.info('Attempts to unlock:', this.currentAttempts); + if (this.currentAttempts == this.ATTEMPT_LIMIT) { + this.currentAttempts = 0; + let bannedUntil = Math.floor(Date.now() / 1000) + this.ATTEMPT_LOCK_OUT_TIME; + this.saveFailedAttempt(bannedUntil); + this.lockTimeControl(bannedUntil); + } + } + + lockTimeControl(bannedUntil): void { + this.setExpirationTime(bannedUntil, null); + var countDown = setInterval(() => { + this.setExpirationTime(bannedUntil, countDown); + }, 1000); + } + + setExpirationTime(bannedUntil, countDown) { + let now = Math.floor(Date.now() / 1000); + if (now > bannedUntil) { + if (countDown) this.reset(countDown); + } else { + this.disableButtons = true; + let totalSecs = bannedUntil - now; + let m = Math.floor(totalSecs / 60); + let s = totalSecs % 60; + this.expires = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2); + } + } + + reset(countDown) { + this.expires = this.disableButtons = null; + this.currentPin = this.firstPinEntered = ''; + clearInterval(countDown); + } + delete(): void { + if (this.disableButtons) return; this.currentPin = this.currentPin.substring(0, this.currentPin.length - 1); } isComplete(): boolean { if (this.currentPin.length < 4) return false; else return true; - }; + } save(): void { let lock = { method: 'PIN', value: this.currentPin, bannedUntil: null }; @@ -85,11 +137,19 @@ export class PinModalPage { } if (this.action === 'checkPin') this.viewCtrl.dismiss(); } - else this.currentPin = ''; + else { + this.currentPin = ''; + this.checkAttempts(); + } } getFilledClass(limit): string { return this.currentPin.length >= limit ? 'filled-' + this.appName : null; } + saveFailedAttempt(bannedUntil) { + let lock = { bannedUntil: bannedUntil }; + this.config.set({ lock }); + } + } diff --git a/src/pages/settings/lock/lock.html b/src/pages/settings/lock/lock.html index 59198d87c..837276acc 100644 --- a/src/pages/settings/lock/lock.html +++ b/src/pages/settings/lock/lock.html @@ -14,7 +14,7 @@ {{opt.method}} - + \ No newline at end of file diff --git a/src/pages/settings/lock/lock.ts b/src/pages/settings/lock/lock.ts index ee4082aad..f90b9b9a4 100644 --- a/src/pages/settings/lock/lock.ts +++ b/src/pages/settings/lock/lock.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { NavController, NavParams, ModalController } from 'ionic-angular'; import { ConfigProvider } from '../../../providers/config/config'; - +import { TouchIdProvider } from '../../../providers/touchid/touchid'; import { PinModalPage } from '../../pin/pin'; @Component({ @@ -9,27 +9,37 @@ import { PinModalPage } from '../../pin/pin'; templateUrl: 'lock.html', }) export class LockPage { - public options: Array<{ method: string, enabled: boolean }> = []; + public options: Array<{ method: string, enabled: boolean, disabled: boolean }> = []; public lockOptions: Object; constructor( private modalCtrl: ModalController, - private config: ConfigProvider + private config: ConfigProvider, + private touchid: TouchIdProvider, ) { this.lockOptions = this.config.get()['lock']; this.options = [ { method: 'Disabled', - enabled: this.lockOptions['method'] == 'Disabled' ? true : false + enabled: this.lockOptions['method'] == 'Disabled' ? true : false, + disabled: false }, { method: 'PIN', - enabled: this.lockOptions['method'] == 'PIN' ? true : false + enabled: this.lockOptions['method'] == 'PIN' ? true : false, + disabled: false }, + { + method: 'Fingerprint', + enabled: this.lockOptions['method'] == 'Fingerprint' ? true : false, + disabled: !this.touchid.isAvailable() ? true : false + } ]; } + + select(method): void { switch (method) { case 'PIN': @@ -37,6 +47,10 @@ export class LockPage { break; case 'Disabled': this.openPinModal('removeLock'); + break; + case 'Fingerprint': + this.lockByFingerprint(); + break; } } @@ -44,4 +58,9 @@ export class LockPage { let modal = this.modalCtrl.create(PinModalPage, { action }); modal.present(); } + + lockByFingerprint() { + let lock = { method: 'Fingerprint', value: null, bannedUntil: null }; + this.config.set({ lock }); + } } diff --git a/src/providers/app/app.ts b/src/providers/app/app.ts index d3d8b527b..a95404b39 100644 --- a/src/providers/app/app.ts +++ b/src/providers/app/app.ts @@ -5,6 +5,7 @@ import 'rxjs/add/operator/map'; import { LanguageProvider } from '../../providers/language/language'; import { ConfigProvider } from '../../providers/config/config'; +import { TouchIdProvider } from '../../providers/touchid/touchid'; interface App { WindowsStoreDisplayName: string; @@ -51,7 +52,8 @@ export class AppProvider { public http: Http, private logger: Logger, private language: LanguageProvider, - private config: ConfigProvider + private config: ConfigProvider, + private touchid: TouchIdProvider ) { this.logger.info('AppProvider initialized.'); } @@ -62,6 +64,7 @@ export class AppProvider { // storage -> config -> language -> unit -> app // Everything ok this.language.init(config); + this.touchid.init(); this.getInfo().subscribe((info) => { this.info = info; resolve(true); diff --git a/src/providers/touchid/touchid.ts b/src/providers/touchid/touchid.ts index e0f31a851..9c265b338 100644 --- a/src/providers/touchid/touchid.ts +++ b/src/providers/touchid/touchid.ts @@ -7,13 +7,15 @@ import { AndroidFingerprintAuth } from '@ionic-native/android-fingerprint-auth'; @Injectable() export class TouchIdProvider { - private _isAvailable: any = false; + private _isAvailable: boolean = false; - constructor(private touchId: TouchID, private androidFingerprintAuth: AndroidFingerprintAuth, private platform: PlatformProvider) { - this.checkDeviceFingerprint(); - } + constructor( + private touchId: TouchID, + private androidFingerprintAuth: AndroidFingerprintAuth, + private platform: PlatformProvider + ) { } - checkDeviceFingerprint() { + init() { if (this.platform.isAndroid) this.checkAndroid(); if (this.platform.isIOS) this.checkIOS(); } @@ -21,7 +23,7 @@ export class TouchIdProvider { checkIOS() { this.touchId.isAvailable() .then( - res => this._isAvailable = 'IOS', + res => this._isAvailable = true, err => console.log("Fingerprint is not available") ); } @@ -30,7 +32,7 @@ export class TouchIdProvider { this.androidFingerprintAuth.isAvailable() .then( res => { - if (res.isAvailable) this._isAvailable = 'ANDROID' + if (res.isAvailable) this._isAvailable = true else console.log("Fingerprint is not available") }); } @@ -39,8 +41,8 @@ export class TouchIdProvider { return new Promise((resolve, reject) => { this.touchId.verifyFingerprint('Scan your fingerprint please') .then( - res => resolve(true), - err => reject(false) + res => resolve(), + err => reject() ); }); } @@ -51,19 +53,19 @@ export class TouchIdProvider { .then(result => { if (result.withFingerprint) { console.log('Successfully authenticated with fingerprint.'); - resolve(true); + resolve(); } else if (result.withBackup) { console.log('Successfully authenticated with backup password!'); - resolve(true); + resolve(); } else console.log('Didn\'t authenticate!'); }) .catch(error => { if (error === this.androidFingerprintAuth.ERRORS.FINGERPRINT_CANCELLED) { console.log('Fingerprint authentication cancelled'); - reject(false); + reject(); } else { console.error(error); - resolve(false); + resolve(); } }); }); @@ -76,22 +78,22 @@ export class TouchIdProvider { check(): Promise { return new Promise((resolve, reject) => { if (!this.isAvailable()) reject(); - if (this.isAvailable() == 'IOS') { + if (this.platform.isIOS) { this.verifyIOSFingerprint() - .then((success) => { - resolve(success); + .then(() => { + resolve(); }) - .catch(err => { - reject(err); + .catch(() => { + reject(); }); } - if (this.isAvailable() == 'ANDROID') { + if (this.platform.isAndroid) { this.verifyAndroidFingerprint() - .then((success) => { - resolve(success); + .then(() => { + resolve(); }) - .catch(err => { - reject(err); + .catch(() => { + reject(); }); } });