Merge pull request #6858 from gabrielbazan7/feat/lockByPin

Lock by PIN and Fingerprint
This commit is contained in:
Javier Donadío 2017-10-13 15:59:32 -03:00 committed by GitHub
commit 0e428744f2
15 changed files with 435 additions and 41 deletions

View File

@ -1,17 +1,22 @@
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Platform, ModalController } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
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;
@ -22,7 +27,9 @@ export class CopayApp {
private splashScreen: SplashScreen,
private logger: Logger,
private app: AppProvider,
private profile: ProfileProvider
private profile: ProfileProvider,
private config: ConfigProvider,
private modalCtrl: ModalController
) {
this.initializeApp();
@ -44,6 +51,7 @@ export class CopayApp {
this.profile.get().then((profile: any) => {
if (profile) {
this.logger.info('Profile read. Go to HomePage.');
this.openLockModal();
this.rootPage = TabsPage;
} else {
// TODO: go to onboarding page
@ -53,4 +61,22 @@ export class CopayApp {
});
});
}
openLockModal() {
let config = this.config.get();
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();
}
}

View File

@ -46,6 +46,9 @@ 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';
/* Send */
@ -92,8 +95,11 @@ let pages: any = [
CopayApp,
DisclaimerPage,
EmailPage,
FingerprintModalPage,
HomePage,
LockPage,
OnboardingPage,
PinModalPage,
ReceivePage,
SendPage,
ScanPage,

View File

@ -0,0 +1,8 @@
<ion-header>
<ion-navbar>
</ion-navbar>
</ion-header>
<ion-content padding>
</ion-content>

View File

View File

@ -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();
});
}
}

50
src/pages/pin/pin.html Normal file
View File

@ -0,0 +1,50 @@
<ion-header>
<ion-navbar>
<ion-buttons start>
<button *ngIf="action === 'pinSetUp' || action === 'removeLock'" (click)="goBack()" ion-button icon-only>
<ion-icon name="arrow-back"></ion-icon>
</button>
</ion-buttons>
<ion-title *ngIf="!confirmingPin">Please enter your PIN</ion-title>
<ion-title *ngIf="confirmingPin">Confirm your PIN</ion-title>
</ion-navbar>
</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>
<div class="circle-{{appName}}" [ngClass]="getFilledClass(3)"></div>
<div class="circle-{{appName}}" [ngClass]="getFilledClass(4)"></div>
</div>
<div class="block-buttons">
<div class="row">
<div (click)="newEntry('1')">1</div>
<div (click)="newEntry('2')">2</div>
<div (click)="newEntry('3')">3</div>
</div>
<div class="row">
<div (click)="newEntry('4')">4</div>
<div (click)="newEntry('5')">5</div>
<div (click)="newEntry('6')">6</div>
</div>
<div class="row">
<div (click)="newEntry('7')">7</div>
<div (click)="newEntry('8')">8</div>
<div (click)="newEntry('9')">9</div>
</div>
<div class="row">
<div></div>
<div (click)="newEntry('0')">0</div>
<div (click)="delete()">
<ion-icon name="arrow-round-back"></ion-icon>
</div>
</div>
</div>
</ion-content>

41
src/pages/pin/pin.scss Normal file
View File

@ -0,0 +1,41 @@
page-pin {
.block-code {
display: flex;
justify-content: space-between;
max-width: 300px;
margin: auto;
}
.block-buttons {
.row {
font-size: 1.7rem;
cursor: pointer;
display: flex;
justify-content: space-around;
div {
padding: 40px;
}
}
}
@mixin circle {
border-radius: 50%;
box-shadow: 0 0 3px 0px #5b5b5b;
transition: background-color .2s ease-in-out;
width: 5rem;
height: 5rem;
margin: 10px;
}
.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;
}
}

155
src/pages/pin/pin.ts Normal file
View File

@ -0,0 +1,155 @@
import { Component } from '@angular/core';
import { NavController, NavParams, ViewController } from 'ionic-angular';
import { ConfigProvider } from '../../providers/config/config';
import { Logger } from '@nsalaun/ng-logger';
@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(
private navCtrl: NavController,
private navParams: NavParams,
private config: ConfigProvider,
private logger: Logger,
private viewCtrl: ViewController
) {
switch (this.navParams.get('action')) {
case 'checkPin':
this.action = 'checkPin';
break;
case 'pinSetUp':
this.action = 'pinSetUp';
break;
case 'removeLock':
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 {
this.navCtrl.pop();
}
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();
if (this.action === 'pinSetUp') {
if (!this.confirmingPin) {
this.confirmingPin = true;
this.firstPinEntered = this.currentPin;
this.currentPin = '';
}
else if (this.firstPinEntered === this.currentPin) this.save();
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 };
this.config.set({ lock });
this.viewCtrl.dismiss();
}
checkIfCorrect(): void {
let config = this.config.get();
let pinValue = config['lock'] && config['lock']['value'];
if (pinValue == this.currentPin) {
if (this.action === 'removeLock') {
let lock = { method: 'Disabled', value: null, bannedUntil: null };
this.config.set({ lock });
this.viewCtrl.dismiss();
}
if (this.action === 'checkPin') this.viewCtrl.dismiss();
}
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 });
}
}

View File

@ -0,0 +1,20 @@
<ion-header>
<ion-navbar>
<ion-title>Lock</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list radio-group>
<ion-list-header>
Startup lock by
</ion-list-header>
<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}}" disabled="{{opt.disabled}}"></ion-radio>
</ion-item>
</ion-list>
</ion-content>

View File

@ -0,0 +1,66 @@
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({
selector: 'page-lock',
templateUrl: 'lock.html',
})
export class LockPage {
public options: Array<{ method: string, enabled: boolean, disabled: boolean }> = [];
public lockOptions: Object;
constructor(
private modalCtrl: ModalController,
private config: ConfigProvider,
private touchid: TouchIdProvider,
) {
this.lockOptions = this.config.get()['lock'];
this.options = [
{
method: 'Disabled',
enabled: 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
}
];
}
select(method): void {
switch (method) {
case 'PIN':
this.openPinModal('pinSetUp');
break;
case 'Disabled':
this.openPinModal('removeLock');
break;
case 'Fingerprint':
this.lockByFingerprint();
break;
}
}
openPinModal(action) {
let modal = this.modalCtrl.create(PinModalPage, { action });
modal.present();
}
lockByFingerprint() {
let lock = { method: 'Fingerprint', value: null, bannedUntil: null };
this.config.set({ lock });
}
}

View File

@ -57,18 +57,12 @@
<ion-option>Urgent</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-item (click)="openLockPage()">
<ion-icon name="lock" item-start></ion-icon>
<ion-label>
Lock
</ion-label>
<ion-select>
<ion-option value="">Disabled</ion-option>
<ion-option value="pin">Lock by PIN</ion-option>
<ion-option value="finger">Lock by fingerprint</ion-option>
</ion-select>
</ion-item>
<ion-item-divider color="light">Wallets &amp; integrations</ion-item-divider>
<ion-item>
<ion-icon name="folder" item-start></ion-icon>
@ -81,13 +75,13 @@
<ion-item-divider color="light">More</ion-item-divider>
<ion-item (click)="openAdvancedPage()">
<ion-icon name="hammer" item-start></ion-icon>
Advanced
</ion-item>
<ion-icon name="hammer" item-start></ion-icon>
Advanced
</ion-item>
<ion-item (click)="openAboutPage()">
<ion-icon name="apps" item-start></ion-icon>
About {{appName}}
</ion-item>
</ion-list>
</ion-content>
</ion-content>

View File

@ -5,7 +5,7 @@ import { AppProvider } from '../../providers/app/app';
import { LanguageProvider } from '../../providers/language/language';
import { RateProvider } from '../../providers/rate/rate';
import { AltCurrencyPage } from './alt-currency/alt-currency';
import { LockPage } from './lock/lock';
import { AboutPage } from './about/about';
import { AdvancedPage } from './advanced/advanced';
@ -55,4 +55,8 @@ export class SettingsPage {
this.navCtrl.push(AboutPage);
}
openLockPage() {
this.navCtrl.push(LockPage);
}
}

View File

@ -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);
@ -75,6 +78,6 @@ export class AppProvider {
getInfo() {
return this.http.get(this.jsonPath)
.map((res:Response) => res.json());
.map((res: Response) => res.json());
}
}

View File

@ -123,7 +123,6 @@ export class ConfigProvider {
if (_.isString(newOpts)) {
newOpts = JSON.parse(newOpts);
}
_.merge(config, this.configCache, newOpts);
this.configCache = config;
this.persistence.storeConfig(this.configCache).then(() => {

View File

@ -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();
});
}
});