mirror of https://github.com/BTCPrivate/copay.git
adding lock by fingerprint and some fixes to lock by PIN
This commit is contained in:
parent
6a0555773f
commit
ac97a79066
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
|
||||
</ion-content>
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<ion-navbar>
|
||||
<ion-buttons start>
|
||||
<button *ngIf="action === 'pinSetUp'" (click)="goBack()" ion-button icon-only>
|
||||
<button *ngIf="action === 'pinSetUp' || action === 'removeLock'" (click)="goBack()" ion-button icon-only>
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
|
@ -13,6 +13,10 @@
|
|||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<div *ngIf="disableButtons">
|
||||
<div *ngIf="!expires" translate>Incorrect PIN, try again.</div>
|
||||
<time *ngIf="expires" translate>Try again in {{expires}}</time>
|
||||
</div>
|
||||
<div class="block-code">
|
||||
<div class="circle-{{appName}}" [ngClass]="getFilledClass(1)"></div>
|
||||
<div class="circle-{{appName}}" [ngClass]="getFilledClass(2)"></div>
|
||||
|
|
|
@ -5,7 +5,7 @@ page-pin {
|
|||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.block-buttons{
|
||||
.block-buttons {
|
||||
.row {
|
||||
font-size: 1.7rem;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<ion-item *ngFor="let opt of options">
|
||||
<ion-label>{{opt.method}}</ion-label>
|
||||
<ion-radio (click)="select(opt.method)" value="{{opt.method}}" checked="{{opt.enabled}}"></ion-radio>
|
||||
<ion-radio (click)="select(opt.method)" value="{{opt.method}}" checked="{{opt.enabled}}" disabled="{{opt.disabled}}"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<any> {
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue