diff --git a/src/js/controllers/advancedSettings.js b/src/js/controllers/advancedSettings.js index c5cd65103..5b213b053 100644 --- a/src/js/controllers/advancedSettings.js +++ b/src/js/controllers/advancedSettings.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('advancedSettingsController', function($scope, $rootScope, $log, $window, lodash, configService, uxLanguage, platformInfo, profileService, feeService, storageService, $ionicHistory, $timeout, $ionicScrollDelegate) { +angular.module('copayApp.controllers').controller('advancedSettingsController', function($scope, $log, configService) { var updateConfig = function() { var config = configService.getSync(); @@ -11,7 +11,6 @@ angular.module('copayApp.controllers').controller('advancedSettingsController', $scope.recentTransactionsEnabled = { value: config.recentTransactions.enabled }; - $scope.hideNextSteps = { value: config.hideNextSteps.enabled }; @@ -31,7 +30,7 @@ angular.module('copayApp.controllers').controller('advancedSettingsController', $scope.nextStepsChange = function() { var opts = { hideNextSteps: { - enabled: $scope.hideNextSteps.value + enabled: $scope.hideNextSteps.value }, }; configService.set(opts, function(err) { diff --git a/src/js/controllers/lock.js b/src/js/controllers/lock.js new file mode 100644 index 000000000..13c9fccb7 --- /dev/null +++ b/src/js/controllers/lock.js @@ -0,0 +1,119 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('lockController', function($state, $scope, $timeout, $log, configService, popupService, gettextCatalog, appConfigService, fingerprintService, profileService, lodash) { + + function init() { + var config = configService.getSync(); + $scope.locking = config.lock.method != 'pin'; + + $scope.options = [ + { + method: 'none', + label: gettextCatalog.getString('Disabled'), + value: config.lock.method == '', + }, + { + method: 'pin', + label: gettextCatalog.getString('Enable PIN'), + value: config.lock.method == 'pin', + needsBackup: null, + }, + ]; + + if (fingerprintService.isAvailable()) { + $scope.options.push({ + method: 'fingerprint', + label: gettextCatalog.getString('Enable Fingerprint'), + value: config.lock.method == 'fingerprint', + needsBackup: null, + }); + } + + $scope.currentOption = lodash.find($scope.options, 'value'); + processWallets(); + }; + + $scope.$on("$ionicView.beforeEnter", function(event) { + init(); + }); + + function processWallets() { + var wallets = profileService.getWallets(); + var singleLivenetWallet = wallets.length == 1 && wallets[0].network == 'livenet' && wallets[0].needsBackup; + var atLeastOneLivenetWallet = lodash.any(wallets, function(w) { + return w.network == 'livenet' && w.needsBackup; + }); + + if (singleLivenetWallet) { + $scope.errorMsg = gettextCatalog.getString('Backup your wallet before using this function'); + disableOptsUntilBackup(); + } else if (atLeastOneLivenetWallet) { + $scope.errorMsg = gettextCatalog.getString('Backup all livenet wallets before using this function'); + disableOptsUntilBackup(); + } else { + enableOptsAfterBackup(); + $scope.errorMsg = null; + } + + function enableOptsAfterBackup() { + $scope.options[1].needsBackup = false; + if ($scope.options[2]) $scope.options[2].needsBackup = false; + }; + + function disableOptsUntilBackup() { + $scope.options[1].needsBackup = true; + if ($scope.options[2]) $scope.options[2].needsBackup = true; + }; + + $timeout(function() { + $scope.$apply(); + }); + }; + + $scope.select = function(method) { + if (method == 'none') + saveConfig(); + else if (method == 'fingerprint') { + var config = configService.getSync(); + if (config.lock.method == 'pin') { + askForDisablePin(function(disablePin) { + if (disablePin) saveConfig('fingerprint'); + else init(); + }); + } else saveConfig('fingerprint'); + } else if (method == 'pin') { + $state.transitionTo('tabs.lock.pin', { + fromSettings: true, + locking: $scope.locking + }); + } + $timeout(function() { + $scope.$apply(); + }); + }; + + function askForDisablePin(cb) { + var message = gettextCatalog.getString('{{appName}} is protected by Pin. Are you sure you want to disable it?', { + appName: appConfigService.nameCase + }); + var okText = gettextCatalog.getString('Continue'); + var cancelText = gettextCatalog.getString('Cancel'); + popupService.showConfirm(null, message, okText, cancelText, function(ok) { + if (!ok) return cb(false); + return cb(true); + }); + }; + + function saveConfig(method) { + var opts = { + lock: { + method: method || '', + value: '', + } + }; + + configService.set(opts, function(err) { + if (err) $log.debug(err); + }); + }; +}); diff --git a/src/js/controllers/lockedView.js b/src/js/controllers/lockedView.js new file mode 100644 index 000000000..0842c5cf6 --- /dev/null +++ b/src/js/controllers/lockedView.js @@ -0,0 +1,17 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('lockedViewController', function($state, $scope, $ionicHistory, fingerprintService, appConfigService, gettextCatalog) { + $scope.$on("$ionicView.beforeEnter", function(event) { + $scope.title = appConfigService.nameCase + ' ' + gettextCatalog.getString('is locked'); + $scope.appName = appConfigService.name; + }); + + $scope.requestFingerprint = function() { + fingerprintService.check('unlockingApp', function(err) { + if (err) return; + $state.transitionTo('tabs.home').then(function() { + $ionicHistory.clearHistory(); + }); + }); + }; +}); diff --git a/src/js/controllers/pin.js b/src/js/controllers/pin.js new file mode 100644 index 000000000..94dd15d40 --- /dev/null +++ b/src/js/controllers/pin.js @@ -0,0 +1,179 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('pinController', function($state, $interval, $stateParams, $ionicHistory, $timeout, $scope, $log, configService, appConfigService) { + var ATTEMPT_LIMIT = 3; + var ATTEMPT_LOCK_OUT_TIME = 5 * 60; + + $scope.$on("$ionicView.beforeEnter", function(event) { + $scope.currentPin = $scope.confirmPin = ''; + $scope.fromSettings = $stateParams.fromSettings == 'true' ? true : false; + $scope.locking = $stateParams.locking == 'true' ? true : false; + $scope.match = $scope.error = $scope.disableButtons = false; + $scope.currentAttempts = 0; + $scope.appName = appConfigService.name; + }); + + $scope.$on("$ionicView.enter", function(event) { + configService.whenAvailable(function(config) { + $scope.bannedUntil = config.lock.bannedUntil || null; + if ($scope.bannedUntil) { + var now = Math.floor(Date.now() / 1000); + if (now < $scope.bannedUntil) { + $scope.error = $scope.disableButtons = true; + lockTimeControl($scope.bannedUntil); + } + } + }); + }); + + function checkAttempts() { + $scope.currentAttempts += 1; + $log.debug('Attempts to unlock:', $scope.currentAttempts); + if ($scope.currentAttempts === ATTEMPT_LIMIT) { + $scope.currentAttempts = 0; + var limitTime = Math.floor(Date.now() / 1000) + ATTEMPT_LOCK_OUT_TIME; + var config = configService.getSync(); + var opts = { + lock: { + method: 'pin', + value: config.lock.value, + bannedUntil: limitTime, + attempts: config.lock.attempts + 1, + } + }; + + configService.set(opts, function(err) { + if (err) $log.debug(err); + lockTimeControl(limitTime); + }); + } + }; + + function lockTimeControl(limitTime) { + $scope.limitTimeExpired = false; + setExpirationTime(); + + var countDown = $interval(function() { + setExpirationTime(); + }, 1000); + + function setExpirationTime() { + var now = Math.floor(Date.now() / 1000); + if (now > limitTime) { + $scope.limitTimeExpired = true; + if (countDown) reset(); + } else { + $scope.disableButtons = true; + var totalSecs = limitTime - now; + var m = Math.floor(totalSecs / 60); + var s = totalSecs % 60; + $scope.expires = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2); + } + }; + + function reset() { + $scope.expires = $scope.error = $scope.disableButtons = null; + $scope.currentPin = $scope.confirmPin = ''; + $interval.cancel(countDown); + $timeout(function() { + $scope.$apply(); + }); + return; + }; + }; + + $scope.getFilledClass = function(limit) { + return $scope.currentPin.length >= limit ? 'filled-' + $scope.appName : null; + }; + + $scope.delete = function() { + if ($scope.disableButtons) return; + if ($scope.currentPin.length > 0) { + $scope.currentPin = $scope.currentPin.substring(0, $scope.currentPin.length - 1); + $scope.error = false; + $scope.updatePin(); + } + }; + + $scope.isComplete = function() { + if ($scope.currentPin.length < 4) return false; + else return true; + }; + + $scope.updatePin = function(value) { + if ($scope.disableButtons) return; + $scope.error = false; + if (value && !$scope.isComplete()) { + $scope.currentPin = $scope.currentPin + value; + $timeout(function() { + $scope.$apply(); + }); + } + $scope.save(); + }; + + $scope.save = function() { + if (!$scope.isComplete()) return; + var config = configService.getSync(); + $scope.match = config.lock && config.lock.method == 'pin' && config.lock.value == $scope.currentPin ? true : false; + if (!$scope.locking) { + if ($scope.match) { + if ($scope.fromSettings) saveSettings(); + else { + saveSettings('pin', $scope.currentPin); + $scope.error = false; + } + } else { + $timeout(function() { + $scope.confirmPin = $scope.currentPin = ''; + $scope.error = true; + }, 200); + checkAttempts(); + } + } else { + processCodes(); + } + }; + + function processCodes() { + if (!$scope.confirmPin) { + $timeout(function() { + $scope.confirmPin = $scope.currentPin; + $scope.currentPin = ''; + }, 200); + } else { + if ($scope.confirmPin == $scope.currentPin) + saveSettings('pin', $scope.confirmPin); + else { + $scope.confirmPin = $scope.currentPin = ''; + $scope.error = true; + } + } + $timeout(function() { + $scope.$apply(); + }); + }; + + function saveSettings(method, value) { + var config = configService.getSync(); + var opts = { + lock: { + method: method || '', + value: value || '', + bannedUntil: null, + attempts: config.lock.attempts + 1, + } + }; + + configService.set(opts, function(err) { + if (err) $log.debug(err); + $scope.close(); + }); + }; + + $scope.close = function(delay) { + $timeout(function() { + $ionicHistory.viewHistory().backView ? $ionicHistory.goBack() : $state.go('tabs.home'); + }, delay || 1); + }; +}); diff --git a/src/js/controllers/tab-settings.js b/src/js/controllers/tab-settings.js index ca350bc51..80152ca61 100644 --- a/src/js/controllers/tab-settings.js +++ b/src/js/controllers/tab-settings.js @@ -51,7 +51,12 @@ angular.module('copayApp.controllers').controller('tabSettingsController', funct $scope.$on("$ionicView.beforeEnter", function(event, data) { $scope.isCordova = platformInfo.isCordova; + $scope.isDevel = platformInfo.isDevel; $scope.appName = appConfigService.nameCase; + configService.whenAvailable(function(config) { + $scope.locked = config.lock && config.lock.method != '' ? true : false; + $scope.method = config.lock && config.lock.method != '' ? config.lock.method.charAt(0).toUpperCase() + config.lock.method.slice(1) : gettextCatalog.getString('Disabled'); + }); }); $scope.$on("$ionicView.enter", function(event, data) { diff --git a/src/js/routes.js b/src/js/routes.js index 4fc657bd4..6b6f4625d 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -119,6 +119,30 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } }) + /* + * + * Pin + * + */ + + .state('pin', { + url: '/pin/', + controller: 'pinController', + templateUrl: 'views/pin.html', + }) + + /* + * + * Locked + * + */ + + .state('lockedView', { + url: '/lockedView/', + controller: 'lockedViewController', + templateUrl: 'views/lockedView.html', + }) + /* * * URI @@ -439,6 +463,25 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } } }) + .state('tabs.lock', { + url: '/lock', + views: { + 'tab-settings@tabs': { + controller: 'lockController', + templateUrl: 'views/lock.html', + } + } + }) + .state('tabs.lock.pin', { + url: '/pin/:fromSettings/:locking', + views: { + 'tab-settings@tabs': { + controller: 'pinController', + templateUrl: 'views/pin.html', + cache: false + } + } + }) /* * @@ -1091,7 +1134,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } }); }) - .run(function($rootScope, $state, $location, $log, $timeout, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService, /* plugins START HERE => */ coinbaseService, glideraService, amazonService, bitpayCardService) { + .run(function($rootScope, $state, $location, $log, $timeout, startupService, fingerprintService, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService, configService, /* plugins START HERE => */ coinbaseService, glideraService, amazonService, bitpayCardService) { uxLanguage.init(); @@ -1153,7 +1196,28 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr }); $ionicPlatform.on('resume', function() { - // Nothing to do + if (platformInfo.isCordova || platformInfo.isDevel) { + configService.whenAvailable(function(config) { + var nextView; + var lock = config.lock; + if (lock && lock.method == 'fingerprint' && fingerprintService.isAvailable()) { + fingerprintService.check('unlockingApp', function(err) { + if (err) nextView = 'lockedView'; + else if ($ionicHistory.currentStateName() == 'lockedView') nextView = 'tabs.home'; + else nextView = $ionicHistory.currentStateName(); + goTo(nextView); + }); + } else if (lock && lock.method == 'pin') { + goTo('pin'); + } + + function goTo(nextView) { + $state.transitionTo(nextView).then(function() { + if (nextView == 'lockedView') $ionicHistory.clearHistory(); + }); + }; + }); + } }); $ionicPlatform.on('menubutton', function() { @@ -1196,10 +1260,27 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr disableAnimate: true, historyRoot: true }); - $state.transitionTo('tabs.home').then(function() { - // Clear history - $ionicHistory.clearHistory(); - }); + if (platformInfo.isCordova || platformInfo.isDevel) { + startupService.ready(); + configService.whenAvailable(function(config) { + var lock = config.lock; + if (fingerprintService.isAvailable() && lock && lock.method == 'fingerprint') { + fingerprintService.check('unlockingApp', function(err) { + if (err) goTo('lockedView'); + else goTo('tabs.home'); + }); + } else if (lock && lock.method == 'pin') { + goTo('pin'); + } else + goTo('tabs.home'); + + function goTo(nextView) { + $state.transitionTo(nextView).then(function() { + $ionicHistory.clearHistory(); + }); + }; + }); + } }); } diff --git a/src/js/services/configService.js b/src/js/services/configService.js index 747554ca1..3d979af7f 100644 --- a/src/js/services/configService.js +++ b/src/js/services/configService.js @@ -53,6 +53,13 @@ angular.module('copayApp.services').factory('configService', function(storageSer } }, + lock: { + method: '', + value: '', + bannedUntil: null, + attempts: null, + }, + // External services recentTransactions: { enabled: true, diff --git a/src/js/services/fingerprintService.js b/src/js/services/fingerprintService.js index 39e937360..86488a96a 100644 --- a/src/js/services/fingerprintService.js +++ b/src/js/services/fingerprintService.js @@ -14,7 +14,7 @@ angular.module('copayApp.services').factory('fingerprintService', function($log, function(msg) { FingerprintAuth.isAvailable(function(result) { - if (result.isAvailable) + if (result.isAvailable) _isAvailable = 'ANDROID'; }, function() { @@ -71,6 +71,7 @@ angular.module('copayApp.services').factory('fingerprintService', function($log, var isNeeded = function(client) { if (!_isAvailable) return false; + if (client === 'unlockingApp') return true; var config = configService.getSync(); config.touchIdFor = config.touchIdFor || {}; @@ -84,7 +85,7 @@ angular.module('copayApp.services').factory('fingerprintService', function($log, root.check = function(client, cb) { if (isNeeded(client)) { - $log.debug('FingerPrint Service:', _isAvailable); + $log.debug('FingerPrint Service:', _isAvailable); if (_isAvailable == 'IOS') return requestTouchId(cb); else diff --git a/src/sass/views/lockedView.scss b/src/sass/views/lockedView.scss new file mode 100644 index 000000000..23fed7a83 --- /dev/null +++ b/src/sass/views/lockedView.scss @@ -0,0 +1,42 @@ +#locked-view { + @mixin img-frame { + height: 60px; + width: 60px; + box-shadow: none; + margin: auto; + } + .img-container-copay { + padding: 20%; + @media(min-width: 480px) { + max-height: 150px; + } + .big-icon-svg { + > .bg { + @include img-frame; + background-image: url("../img/icon-fingerprint-copay.svg"); + } + } + } + .img-container-bitpay { + padding: 20%; + @media(min-width: 480px) { + max-height: 150px; + } + .big-icon-svg { + > .bg { + @include img-frame; + background-image: url("../img/icon-fingerprint-bitpay.svg"); + } + } + } + .comments { + text-align: center; + .header { + font-size: 20px; + } + .text-content { + width: 90%; + margin: 5% auto; + } + } +} diff --git a/src/sass/views/pin.scss b/src/sass/views/pin.scss new file mode 100644 index 000000000..3d695f185 --- /dev/null +++ b/src/sass/views/pin.scss @@ -0,0 +1,95 @@ +#pin { + background-color: #FAFAFA; + .bar.bar-clear { + background-color: transparent; + border: none; + .back-button .icon:before { + color: #2d3f50; + } + } + .content { + text-align: center; + width: 100%; + height: 100%; + .app-icon { + margin-top: -55px; + .big-icon-svg { + > .bg { + background-image: url("../img/app/icon.png"); + height: 60px; + width: 60px; + margin: auto; + } + } + } + .block-text { + align-items: center; + background-color: #F1F1F1; + height: 30%; + border-bottom: 1px solid #c5c5c5; + .message { + margin: auto; + } + span { + width: 60%; + margin: 10% auto; + } + @media(min-width: 480px) { + span { + font-size: 30px; + width: 90%; + } + } + } + .block-code { + width: 50%; + margin: auto; + @media(min-width: 480px) { + width: 25%; + } + } + .block-buttons { + .col { + padding: 5%; + } + color: $v-dark-gray; + font-size: 1.7rem; + font-family: $v-font-family-light; + cursor: pointer; + position: absolute; + bottom: 3%; + left: 5%; + width: 90%; + @media(min-width: 480px) { + left: 15%; + width: 70%; + max-height: 55%; + } + } + } + @mixin circle { + border-radius: 50%; + box-shadow: 0 0 3px 0px #5b5b5b; + transition: background-color .2s ease-in-out; + padding: 7%; + margin: 5%; + } + .circle-copay { + @include circle; + border: 1px solid $v-accent-color; + } + .circle-bitpay { + @include circle; + border: 1px solid $v-primary-color; + } + .filled-copay { + background-color: $v-accent-color; + } + .filled-bitpay { + background-color: #1f3598; + } + .error { + color: #f13333; + max-width: 70%; + } +} diff --git a/src/sass/views/views.scss b/src/sass/views/views.scss index a4648928f..bd8520c01 100644 --- a/src/sass/views/views.scss +++ b/src/sass/views/views.scss @@ -46,3 +46,5 @@ @import "includes/accountSelector"; @import "integrations/integrations"; @import "custom-amount"; +@import "pin"; +@import "lockedView"; diff --git a/www/img/icon-fingerprint-bitpay.svg b/www/img/icon-fingerprint-bitpay.svg new file mode 100644 index 000000000..edf9f95da --- /dev/null +++ b/www/img/icon-fingerprint-bitpay.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/img/icon-fingerprint-copay.svg b/www/img/icon-fingerprint-copay.svg new file mode 100644 index 000000000..dcad0d889 --- /dev/null +++ b/www/img/icon-fingerprint-copay.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/views/advancedSettings.html b/www/views/advancedSettings.html index 9905fdec2..7181505f8 100644 --- a/www/views/advancedSettings.html +++ b/www/views/advancedSettings.html @@ -13,7 +13,7 @@
If enabled, wallets will also try to spend unconfirmed funds. This option may cause transaction delays.
- +
diff --git a/www/views/lock.html b/www/views/lock.html new file mode 100644 index 000000000..d0e49e15e --- /dev/null +++ b/www/views/lock.html @@ -0,0 +1,17 @@ + + + {{'Lock App' | translate}} + + + + + + + {{opt.label}} + + +
+ {{errorMsg}} +
+
+
diff --git a/www/views/lockedView.html b/www/views/lockedView.html new file mode 100644 index 000000000..98ca5a986 --- /dev/null +++ b/www/views/lockedView.html @@ -0,0 +1,21 @@ + + + {{title}} + + + +
+
+
+
+
+
+
+
One-touch Sign In
+
Please place your fingertip on the scanner to verify your identity
+
+ +
+
diff --git a/www/views/pin.html b/www/views/pin.html new file mode 100644 index 000000000..34816f2a2 --- /dev/null +++ b/www/views/pin.html @@ -0,0 +1,73 @@ + + + + + +
+
+
Please enter your PIN
+
Confirm your PIN
+
+
Incorrect PIN, try again.
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
2
+
+
+
3
+
+
+
+
+
4
+
+
+
5
+
+
+
6
+
+
+
+
+
7
+
+
+
8
+
+
+
9
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
diff --git a/www/views/tab-home.html b/www/views/tab-home.html index 5e1b1a99c..6d2ec9247 100644 --- a/www/views/tab-home.html +++ b/www/views/tab-home.html @@ -91,7 +91,7 @@ {{wallet.status.totalBalanceStr ? wallet.status.totalBalanceStr : ( wallet.cachedBalance ? wallet.cachedBalance + (wallet.cachedBalanceUpdatedOn ? ' · ' + ( wallet.cachedBalanceUpdatedOn * 1000 | amTimeAgo) : '') : '' ) }} - + [Balance Hidden] {{wallet.m}}-of-{{wallet.n}} diff --git a/www/views/tab-settings.html b/www/views/tab-settings.html index b12ab2b5d..e0bbe0e0e 100644 --- a/www/views/tab-settings.html +++ b/www/views/tab-settings.html @@ -89,6 +89,16 @@ + + + + {{'Lock App' | translate}} + + {{method}} + + + +
{{'Wallets & Integrations' | translate}}