-
+
directive will have its own
-navigation history that also transitions its views in and out.
--->
-
+
@@ -13,12 +8,8 @@ navigation history that also transitions its views in and out.
-
-
-
-
-
-
+
+
diff --git a/src/js/controllers/tab-scan.js b/src/js/controllers/tab-scan.js
new file mode 100644
index 000000000..3f444ac3c
--- /dev/null
+++ b/src/js/controllers/tab-scan.js
@@ -0,0 +1,45 @@
+'use strict';
+
+angular.module('copayApp.controllers').controller('tabScanController', function($scope, $log, $timeout, scannerService, incomingData) {
+
+ $scope.$on("$ionicView.beforeEnter", function() {
+ $log.debug('Preparing to display available controls.');
+ var capabilities = scannerService.getCapabilities();
+ $scope.canEnableLight = capabilities.canEnableLight;
+ $scope.canChangeCamera = capabilities.canChangeCamera;
+ });
+
+ $scope.$on("$ionicView.afterEnter", function() {
+ scannerService.activate(function(){
+ scannerService.scan(function(err, contents){
+ if(err){
+ $log.debug('Scan canceled.');
+ } else {
+ incomingData.redir(contents);
+ }
+ });
+ });
+ });
+ $scope.$on("$ionicView.afterLeave", function() {
+ scannerService.deactivate();
+ });
+
+ $scope.toggleLight = function(){
+ scannerService.toggleLight(function(lightEnabled){
+ $scope.lightActive = lightEnabled;
+ $scope.$apply();
+ });
+ };
+
+ $scope.toggleCamera = function(){
+ $scope.cameraToggleActive = true;
+ scannerService.toggleCamera(function(status){
+ // (a short delay for the user to see the visual feedback)
+ $timeout(function(){
+ $scope.cameraToggleActive = false;
+ $log.debug('Camera toggle control deactivated.');
+ }, 200);
+ });
+ };
+
+});
diff --git a/src/js/controllers/tab-send.js b/src/js/controllers/tab-send.js
index c3583249a..c2f884f92 100644
--- a/src/js/controllers/tab-send.js
+++ b/src/js/controllers/tab-send.js
@@ -97,12 +97,6 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
});
};
- $scope.onQrCodeScanned = function(data) {
- if (!incomingData.redir(data)) {
- popupService.showAlert(null, gettextCatalog.getString('Invalid data'));
- }
- };
-
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$scope.formData = {
search: null
diff --git a/src/js/routes.js b/src/js/routes.js
index f522e5689..788f41b7b 100644
--- a/src/js/routes.js
+++ b/src/js/routes.js
@@ -208,6 +208,15 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
}
}
})
+ .state('tabs.scan', {
+ url: '/scan',
+ views: {
+ 'tab-scan': {
+ controller: 'tabScanController',
+ templateUrl: 'views/tab-scan.html',
+ }
+ }
+ })
.state('tabs.send', {
url: '/send',
views: {
@@ -875,7 +884,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
}
});
})
- .run(function($rootScope, $state, $location, $log, $timeout, $ionicHistory, $ionicPlatform, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService) {
+ .run(function($rootScope, $state, $location, $log, $timeout, $ionicHistory, $ionicPlatform, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService) {
uxLanguage.init();
openURLService.init();
@@ -982,7 +991,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr
} else {
profileService.storeProfileIfDirty();
$log.debug('Profile loaded ... Starting UX.');
-
+ scannerService.gentleInitialize();
$state.go('tabs.home');
}
});
diff --git a/src/js/services/scannerService.js b/src/js/services/scannerService.js
new file mode 100644
index 000000000..b46e5e604
--- /dev/null
+++ b/src/js/services/scannerService.js
@@ -0,0 +1,188 @@
+'use strict';
+
+angular.module('copayApp.services').service('scannerService', function($log, $timeout, platformInfo) {
+
+ var isDesktop = !platformInfo.isCordova;
+ var QRScanner = window.QRScanner;
+ var lightEnabled = false;
+ var backCamera = true; // the plugin defaults to the back camera
+
+ // Initalize known capabilities
+ var hasPermission = isDesktop? true: false;
+ var canEnableLight = false;
+ var canChangeCamera = false;
+
+ function _checkCapabilities(status){
+ $log.debug('scannerService is reviewing platform capabilities...');
+ // Permission can be assumed on the desktop builds
+ hasPermission = (isDesktop || status.authorized)? true: false;
+ canEnableLight = status.canEnableLight? true : false;
+ canChangeCamera = status.canChangeCamera? true : false;
+ function orIsNot(bool){
+ return bool? '' : 'not ';
+ }
+ $log.debug('A light is ' + orIsNot(canEnableLight) + 'available on this platform.');
+ $log.debug('A second camera is ' + orIsNot(canChangeCamera) + 'available on this platform.');
+ }
+
+ /**
+ * Immediately return known capabilities of the current platform.
+ */
+ this.getCapabilities = function(){
+ return {
+ hasPermission: hasPermission,
+ canEnableLight: canEnableLight,
+ canChangeCamera: canChangeCamera
+ }
+ }
+
+ /**
+ * If camera access has been granted, pre-initialize the QRScanner. This method
+ * can be safely called before the scanner is visible to improve perceived
+ * scanner loading times.
+ *
+ * The `status` of QRScanner is returned to the callback.
+ */
+ this.gentleInitialize = function(callback) {
+ $log.debug('Trying to pre-initialize QRScanner.');
+ if(!isDesktop){
+ QRScanner.getStatus(function(status){
+ _checkCapabilities(status);
+ if(status.authorized){
+ $log.debug('Camera permission already granted.');
+ _initalize();
+ } else {
+ $log.debug('QRScanner not authorized, waiting to initalize.');
+ if(typeof callback === "function"){
+ callback && callback(status);
+ }
+ }
+ });
+ } else {
+ $log.debug('Camera permission assumed on desktop.');
+ _initalize();
+ }
+ function _initalize(){
+ $log.debug('Preparing scanner...');
+ QRScanner.prepare(function(err, status){
+ if(err){
+ $log.error(err);
+ }
+ _checkCapabilities(status);
+ callback && callback(status);
+ });
+ }
+ };
+
+ var nextHide = null;
+ var nextDestroy = null;
+ var hideAfterSeconds = 15;
+ var destroyAfterSeconds = 5 * 60;
+
+ /**
+ * (Re)activate the QRScanner, and cancel the timeouts if present.
+ *
+ * The `status` of QRScanner is passed to the callback when activation
+ * is complete.
+ */
+ this.activate = function(callback) {
+ $log.debug('Activating scanner...');
+ QRScanner.show(function(status){
+ _checkCapabilities(status);
+ callback(status);
+ });
+ if(nextHide !== null){
+ $timeout.cancel(nextHide);
+ nextHide = null;
+ }
+ if(nextDestroy !== null){
+ $timeout.cancel(nextDestroy);
+ nextDestroy = null;
+ }
+ };
+
+ /**
+ * Start a new scan.
+ *
+ * The callback receives: (err, contents)
+ */
+ this.scan = function(callback) {
+ $log.debug('Scanning...');
+ QRScanner.scan(callback);
+ };
+
+ /**
+ * Deactivate the QRScanner. To balance user-perceived performance and power
+ * consumption, this kicks off a countdown which will "sleep" the scanner
+ * after a certain amount of time.
+ *
+ * The `status` of QRScanner is passed to the callback when deactivation
+ * is complete.
+ */
+ this.deactivate = function(callback) {
+ $log.debug('Deactivating scanner...');
+ QRScanner.cancelScan();
+ nextHide = $timeout(_hide, hideAfterSeconds * 1000);
+ nextDestroy = $timeout(_destroy, destroyAfterSeconds * 1000);
+ };
+
+ // Natively hide the QRScanner's preview
+ // On mobile platforms, this can reduce GPU/power usage
+ // On desktop, this fully turns off the camera (and any associated privacy lights)
+ function _hide(){
+ $log.debug('Scanner not in use for ' + hideAfterSeconds + ' seconds, hiding...');
+ QRScanner.hide();
+ }
+
+ // Reduce QRScanner power/processing consumption by the maximum amount
+ function _destroy(){
+ $log.debug('Scanner not in use for ' + destroyAfterSeconds + ' seconds, destroying...');
+ QRScanner.destroy();
+ }
+
+ /**
+ * Toggle the device light (if available).
+ *
+ * The callback receives a boolean which is `true` if the light is enabled.
+ */
+ this.toggleLight = function(callback) {
+ $log.debug('Toggling light...');
+ if(lightEnabled){
+ QRScanner.disableLight(_handleResponse);
+ } else {
+ QRScanner.enableLight(_handleResponse);
+ }
+ function _handleResponse(err, status){
+ if(err){
+ $log.error(err);
+ } else {
+ lightEnabled = status.lightEnabled;
+ var state = lightEnabled? 'enabled' : 'disabled';
+ $log.debug('Light ' + state + '.');
+ }
+ callback(lightEnabled);
+ }
+ };
+
+ /**
+ * Switch cameras (if a second camera is available).
+ *
+ * The `status` of QRScanner is passed to the callback when activation
+ * is complete.
+ */
+ this.toggleCamera = function(callback) {
+ var nextCamera = backCamera? 1 : 0;
+ function cameraToString(index){
+ return index === 1? 'front' : 'back'; // front = 1, back = 0
+ };
+ $log.debug('Toggling to the ' + cameraToString(nextCamera) + ' camera...');
+ QRScanner.useCamera(nextCamera, function(err, status){
+ if(err){
+ $log.error(err);
+ }
+ backCamera = status.currentCamera === 1? false : true;
+ $log.debug('Camera toggled. Now using the ' + cameraToString(backCamera) + ' camera.');
+ callback(status);
+ });
+ };
+});
diff --git a/src/sass/ionic.scss b/src/sass/ionic.scss
index 0244562b7..2730f327c 100644
--- a/src/sass/ionic.scss
+++ b/src/sass/ionic.scss
@@ -19,5 +19,10 @@ $ios-transition-duration: 200ms;
// class to dynamically hide the ion-nav-bar for v1 Amazon flow
ion-nav-bar.hide { display: block !important; }
+// the ion tabs element never needs it's own background (backgrounds are
+// rendered by the tabs), and the default background would cover the scanner
+ion-tabs.ion-tabs-transparent {
+ background: none transparent;
+}
@import "../../bower_components/ionic/scss/ionic";
diff --git a/src/sass/views/tab-scan.scss b/src/sass/views/tab-scan.scss
new file mode 100644
index 000000000..36d42ca2b
--- /dev/null
+++ b/src/sass/views/tab-scan.scss
@@ -0,0 +1,54 @@
+#tab-scan {
+ // view background is transparent to show video preview
+ background: none transparent;
+ .scanner-controls {
+ width: 100%;
+ text-align: center;
+ bottom: 0;
+ position: absolute;
+ }
+ .guides {
+ display: flex;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+ top: 0;
+ left: 0;
+ }
+ .qr-scan-guides {
+ width: 60%;
+ max-width: 400px;
+ margin-bottom: 8em;
+ }
+ .icon-flash, .icon-camera-toggle {
+ border-radius: 50%;
+ width: 4em;
+ height: 4em;
+ background-color: rgba(13, 13, 13, 0.79);
+ background-repeat: no-repeat;
+ background-clip: padding-box;
+ background-size: 100%;
+ display: inline-block;
+ margin: 2em 1em;
+ cursor: pointer;
+ // hover for desktop only
+ body:not(.platform-cordova) &:hover {
+ background-color: rgba(31, 40, 78, 0.79);
+ }
+ &.active, &:active {
+ background-color: rgba(100, 124, 232, 0.79);
+ }
+ }
+ .icon-flash {
+ background-image: url("../img/icon-flash.svg");
+ }
+ .icon-camera-toggle {
+ background-image: url("../img/icon-camera-toggle.svg");
+ }
+}
+
+#cordova-plugin-qrscanner-still, #cordova-plugin-qrscanner-video-preview {
+ background-color: #060d2d !important;
+}