[V4] REF: lock app by fingerpint and PIN

This commit is contained in:
Gabriel Bazán 2018-01-11 18:02:38 -03:00
parent b17d74bf5b
commit 3ebaf674bb
12 changed files with 159 additions and 77 deletions

View File

@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { Platform, ModalController } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { Subscription } from 'rxjs';
//providers
import { Logger } from '@nsalaun/ng-logger';
@ -30,6 +31,8 @@ import { DisclaimerPage } from '../pages/onboarding/disclaimer/disclaimer';
export class CopayApp {
public rootPage: any;
private onResumeSubscription: Subscription;
private isModalOpen: boolean;
constructor(
private platform: Platform,
@ -68,9 +71,13 @@ export class CopayApp {
this.statusBar.styleLightContent();
this.splashScreen.hide();
}
//Check PIN or Fingerprint
this.onResumeSubscription = this.platform.resume.subscribe(() => {
this.openLockModal();
});
this.openLockModal();
// Check Profile
this.profile.loadAndBindProfile().then((profile: any) => {
this.openLockModal();
this.registerIntegrations();
if (profile) {
this.logger.info('Profile exists.');
@ -92,7 +99,12 @@ export class CopayApp {
});
}
ngOnDestroy() {
this.onResumeSubscription.unsubscribe();
}
private openLockModal(): void {
if (this.isModalOpen) return;
let config: any = this.configProvider.get();
let lockMethod = config.lock.method;
if (!lockMethod) return;
@ -101,13 +113,21 @@ export class CopayApp {
}
private openPINModal(action): void {
this.isModalOpen = true;
let modal = this.modalCtrl.create(PinModalPage, { action }, { showBackdrop: false, enableBackdropDismiss: false });
modal.present();
modal.onDidDismiss(() => {
this.isModalOpen = false;
});
}
private openFingerprintModal(): void {
this.isModalOpen = true;
let modal = this.modalCtrl.create(FingerprintModalPage, {}, { showBackdrop: false, enableBackdropDismiss: false });
modal.present();
modal.onDidDismiss(() => {
this.isModalOpen = false;
});
}
private registerIntegrations(): void {

View File

@ -1,8 +1,7 @@
<ion-header>
<ion-navbar>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-content>
<ion-item-divider class="title">Verify your identity</ion-item-divider>
<button *ngIf="showScanButton" class="scan-button" ion-item (click)="checkFingerprint()">
<ion-icon name="finger-print" item-start></ion-icon>
<span translate>Scan again</span>
</button>
</ion-content>

View File

@ -0,0 +1,8 @@
page-fingerprint {
.title {
text-align: center;
}
.scan-button {
margin-top: 20px;
}
}

View File

@ -8,13 +8,23 @@ import { TouchIdProvider } from '../../providers/touchid/touchid';
})
export class FingerprintModalPage {
public showScanButton: boolean;
constructor(
private touchid: TouchIdProvider,
private viewCtrl: ViewController
) {
this.touchid.check().then(() => {
this.viewCtrl.dismiss();
});
this.checkFingerprint();
}
public checkFingerprint(): void {
this.showScanButton = true;
this.touchid.check().then(() => {
setTimeout(() => {
this.viewCtrl.dismiss();
}, 300);
}).catch(() => {
this.showScanButton = false;
});
}
}

View File

@ -17,7 +17,7 @@
<p>Trade any leading blockchain asset for any other. Protection by Design. No Account Needed.</p>
</div>
<div class="integration-onboarding-cta">
<button ion-button no-low-fee (click)="goTo('Shift')">Start</button>
<button ion-button (click)="goTo('Shift')">Start</button>
<button ion-button clear color="light" (click)="openExternalLink('https://shapeshift.io')">Visit Shapeshift.io &rarr;</button>
</div>
</div>
@ -31,15 +31,7 @@
</div>
<ion-list class="shift">
<ion-item-divider class="help" color="light">
<div>
<span>Having problems with a ShapeShift?</span>
<a (click)="openExternalLink('https://shapeshift.zendesk.com/hc/en-us/requests/new')">
Contact the ShapeShift support team.
</a>
</div>
</ion-item-divider>
<button class="shift-btn" ion-item no-low-fee (click)="goTo('Shift')">
<button ion-item (click)="goTo('Shift')">
<img src="assets/img/shapeshift/icon-shapeshift.svg">
<span>Shift</span>
</button>

View File

@ -1,12 +1,9 @@
<ion-header>
<ion-header *ngIf="action === 'pinSetUp' || action === 'removeLock'">
<ion-navbar>
<ion-title *ngIf="!confirmingPin">Please enter your PIN</ion-title>
<ion-title *ngIf="confirmingPin">Confirm your PIN</ion-title>
<ion-buttons start>
<button ion-button *ngIf="action === 'pinSetUp' || action === 'removeLock'" (click)="goBack()">
<span ion-text color="primary" showWhen="ios" translate>Cancel</span>
<ion-icon name="md-close"></ion-icon>
<button (click)="close()" ion-button>
{{'Close' | translate}}
</button>
</ion-buttons>
</ion-navbar>
@ -14,15 +11,21 @@
</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 class="messages">
<ion-item-divider *ngIf="!disableButtons">
<span *ngIf="!confirmingPin && !incorrect">Please enter your PIN</span>
<span *ngIf="confirmingPin && !incorrect">Confirm your PIN</span>
<span class="assertive" *ngIf="incorrect" translate>Incorrect PIN, try again.</span>
</ion-item-divider>
<ion-item-divider *ngIf="disableButtons">
<span class="assertive" *ngIf="expires" translate>Try again in {{expires}}</span>
</ion-item-divider>
</div>
<div class="block-code">
<div class="circle-{{appName}}" [ngClass]="getFilledClass(1)"></div>
<div class="circle-{{appName}}" [ngClass]="getFilledClass(2)"></div>
<div class="circle-{{appName}}" [ngClass]="getFilledClass(3)"></div>
<div class="circle-{{appName}}" [ngClass]="getFilledClass(4)"></div>
<div class="circle" [ngClass]="getFilledClass(1)"></div>
<div class="circle" [ngClass]="getFilledClass(2)"></div>
<div class="circle" [ngClass]="getFilledClass(3)"></div>
<div class="circle" [ngClass]="getFilledClass(4)"></div>
</div>
<div class="block-buttons">
<div class="row">

View File

@ -1,4 +1,8 @@
page-pin {
.messages {
text-align: center;
padding-bottom: 30px;
}
.block-code {
display: flex;
justify-content: space-between;
@ -16,26 +20,17 @@ page-pin {
}
}
}
@mixin circle {
.circle {
border-radius: 50%;
box-shadow: 0 0 3px 0px #5b5b5b;
transition: background-color .2s ease-in-out;
width: 5rem;
height: 5rem;
width: 4rem;
height: 4rem;
margin: 10px;
border: 1px solid $color-primary;
max-height: 100px;
}
.circle-copay {
@include circle;
border: 1px solid #1f3598;
}
.circle-bitpay {
@include circle;
border: 1px solid #1f3598;
}
.filled-copay {
background-color: #1f3598;
}
.filled-bitpay {
background-color: #1f3598;
.filled {
background-color: $color-primary;
}
}

View File

@ -16,9 +16,9 @@ export class PinModalPage {
public firstPinEntered: string = '';
public confirmingPin: boolean = false;
public action: string = '';
public appName: string = 'copay';
public disableButtons: boolean = false;
public expires: string = '';
public incorrect: boolean;
constructor(
private navParams: NavParams,
@ -49,15 +49,15 @@ export class PinModalPage {
}
}
}
}
public goBack(): void {
public close(): void {
this.viewCtrl.dismiss();
}
public newEntry(value: string): void {
if (this.disableButtons) return;
this.incorrect = false;
this.currentPin = this.currentPin + value;
if (!this.isComplete()) return;
if (this.action === 'checkPin' || this.action === 'removeLock') this.checkIfCorrect();
@ -68,13 +68,18 @@ export class PinModalPage {
this.currentPin = '';
}
else if (this.firstPinEntered === this.currentPin) this.save();
else this.firstPinEntered = this.currentPin = '';
else {
this.firstPinEntered = this.currentPin = '';
this.incorrect = true;
this.confirmingPin = false;
}
}
}
private checkAttempts(): void {
this.currentAttempts += 1;
this.logger.info('Attempts to unlock:', this.currentAttempts);
this.incorrect = true;
if (this.currentAttempts == this.ATTEMPT_LIMIT) {
this.currentAttempts = 0;
let bannedUntil = Math.floor(Date.now() / 1000) + this.ATTEMPT_LOCK_OUT_TIME;
@ -143,7 +148,7 @@ export class PinModalPage {
}
public getFilledClass(limit): string {
return this.currentPin.length >= limit ? 'filled-' + this.appName : null;
return this.currentPin.length >= limit ? 'filled' : null;
}
private saveFailedAttempt(bannedUntil) {

View File

@ -13,4 +13,8 @@
<ion-radio (click)="select(opt.method)" value="{{opt.method}}" checked="{{opt.enabled}}" disabled="{{opt.disabled}}"></ion-radio>
</ion-item>
</ion-list>
</ion-content>
<ion-item-divider *ngIf="needsBackupMsg" text-wrap>
<span class="assertive">{{needsBackupMsg}}</span>
</ion-item-divider>
</ion-content>

View File

@ -1,44 +1,57 @@
import { Component } from '@angular/core';
import { ModalController } from 'ionic-angular';
//pages
import { PinModalPage } from '../../pin/pin';
//providers
import { ConfigProvider } from '../../../providers/config/config';
import { TouchIdProvider } from '../../../providers/touchid/touchid';
import { PinModalPage } from '../../pin/pin';
import { ProfileProvider } from '../../../providers/profile/profile';
import * as _ from 'lodash';
@Component({
selector: 'page-lock',
templateUrl: 'lock.html',
})
export class LockPage {
public options: Array<{ method: string, enabled: boolean, disabled: boolean }> = [];
public lockOptions: any;
public needsBackupMsg: string;
constructor(
private modalCtrl: ModalController,
private configProvider: ConfigProvider,
private touchid: TouchIdProvider,
private touchIdProvider: TouchIdProvider,
private profileProvider: ProfileProvider
) {
this.checkLockOptions();
}
private checkLockOptions() {
this.lockOptions = this.configProvider.get().lock;
this.options = [
{
method: 'Disabled',
enabled: !this.lockOptions.method || this.lockOptions.method == 'Disabled' ? true : false,
disabled: false
},
{
method: 'PIN',
enabled: this.lockOptions.method == 'PIN' ? true : false,
disabled: false
},
{
method: 'Fingerprint',
enabled: this.lockOptions.method == 'Fingerprint' ? true : false,
disabled: !this.touchid.isAvailable() ? true : false
}
];
this.touchIdProvider.isAvailable().then((isAvailable: boolean) => {
let needsBackup = this.needsBackup();
this.options = [
{
method: 'Disabled',
enabled: !this.lockOptions.method || this.lockOptions.method == 'Disabled' ? true : false,
disabled: false
},
{
method: 'PIN',
enabled: this.lockOptions.method == 'PIN' ? true : false,
disabled: needsBackup
},
{
method: 'Fingerprint',
enabled: this.lockOptions.method == 'Fingerprint' ? true : false,
disabled: !isAvailable || needsBackup
}
];
});
}
public select(method): void {
@ -47,7 +60,8 @@ export class LockPage {
this.openPinModal('pinSetUp');
break;
case 'Disabled':
if (this.lockOptions.method && this.lockOptions.method != 'Disabled') this.openPinModal('removeLock');
if (this.lockOptions.method && this.lockOptions.method == 'PIN') this.openPinModal('removeLock');
if (this.lockOptions.method && this.lockOptions.method == 'Fingerprint') this.removeFingerprint();
break;
case 'Fingerprint':
this.lockByFingerprint();
@ -63,8 +77,34 @@ export class LockPage {
});
}
private removeFingerprint(): void {
this.touchIdProvider.check().then(() => {
let lock = { method: 'Disabled', value: null, bannedUntil: null };
this.configProvider.set({ lock });
});
}
public lockByFingerprint(): void {
let lock = { method: 'Fingerprint', value: null, bannedUntil: null };
this.configProvider.set({ lock });
}
private needsBackup() {
let wallets = this.profileProvider.getWallets();
let singleLivenetWallet = wallets.length == 1 && wallets[0].network == 'livenet' && wallets[0].needsBackup;
let atLeastOneLivenetWallet = _.find(wallets, (w) => {
return w.network == 'livenet' && w.needsBackup;
});
if (singleLivenetWallet) {
this.needsBackupMsg = 'Backup your wallet before using this function'; //TODO gettextCatalog
return true;
} else if (atLeastOneLivenetWallet) {
this.needsBackupMsg = 'Backup all your wallets before using this function'; //TODO gettextCatalog
return true;
} else {
this.needsBackupMsg = null;
return false;
}
};
}

View File

@ -71,10 +71,14 @@
</button>
<button ion-item (click)="openLockPage()">
<ion-icon name="ios-lock-outline" item-start></ion-icon>
<ion-icon *ngIf="lockMethod != 'Disabled'" name="ios-lock-outline" item-start></ion-icon>
<ion-icon *ngIf="lockMethod == 'Disabled'" name="ios-unlock-outline" item-start></ion-icon>
<ion-label>
{{'Lock' | translate}}
</ion-label>
<ion-note item-end>
{{lockMethod}}
</ion-note>
</button>
<ion-item-divider *ngIf="walletsBtc.length" color="light">{{'Bitcoin Wallets' | translate}}</ion-item-divider>

View File

@ -38,6 +38,7 @@ export class SettingsPage {
public selectedAlternative: any;
public isCordova: boolean;
public isWindowsPhoneApp: boolean;
public lockMethod: string;
constructor(
private navCtrl: NavController,
@ -73,6 +74,7 @@ export class SettingsPage {
name: this.config.wallet.settings.alternativeName,
isoCode: this.config.wallet.settings.alternativeIsoCode
}
this.lockMethod = this.config.lock.method;
}
public openBitcoinCashPage(): void {