mirror of https://github.com/BTCPrivate/copay.git
[V4] REF: lock app by fingerpint and PIN
This commit is contained in:
parent
b17d74bf5b
commit
3ebaf674bb
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
page-fingerprint {
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
.scan-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 →</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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue