feat(scan): create Scan tab and scannerService, implement QRScanner and controls

This commit is contained in:
Jason Dreyzehner 2016-09-30 21:35:23 -04:00
parent ceac7c2c2b
commit f901c95f67
13 changed files with 393 additions and 23 deletions

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="QR-scanner-guides" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 271 236" style="enable-background:new 0 0 271 236;" xml:space="preserve">
<style type="text/css">
.st0{filter:url(#filter-2);}
.st1{fill:none;stroke:#647CE8;stroke-width:2;}
.st2{fill:#FFFFFF;fill-opacity:0.3;}
</style>
<filter filterUnits="objectBoundingBox" height="200%" id="filter-2" width="200%" x="-50%" y="-50%">
<feMorphology in="SourceAlpha" operator="dilate" radius="1" result="shadowSpreadOuter1"></feMorphology>
<feOffset dx="0" dy="0" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
<feMorphology in="SourceAlpha" operator="erode" radius="1" result="shadowInner"></feMorphology>
<feOffset dx="0" dy="0" in="shadowInner" result="shadowInner"></feOffset>
<feComposite in="shadowOffsetOuter1" in2="shadowInner" operator="out" result="shadowOffsetOuter1"></feComposite>
<feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="6"></feGaussianBlur>
<feColorMatrix in="shadowBlurOuter1" type="matrix" values="0 0 0 0 0.392156863 0 0 0 0 0.48627451 0 0 0 0 0.909803922 0 0 0 1 0">
</feColorMatrix>
</filter>
<g id="scan-line">
<g class="st0">
<line x1="11" y1="111" x2="260" y2="111"/>
</g>
<g>
<path id="line" class="st1" d="M11,111h249"/>
</g>
</g>
<path id="top-left" class="st2" d="M28,10h53.1c2.8,0,5-2.2,5-5s-2.2-5-5-5H23c-2.8,0-5,2.2-5,5v58c0,2.8,2.2,5,5,5s5-2.2,5-5V10z"
/>
<path id="bottom-left" class="st2" d="M28,224.7v-53.1c0-2.8-2.2-5-5-5s-5,2.2-5,5v58.1c0,2.8,2.2,5,5,5h58c2.8,0,5-2.2,5-5
c0-2.8-2.2-5-5-5H28z"/>
<path id="top-right" class="st2" d="M244,10v53.1c0,2.8,2.2,5,5,5s5-2.2,5-5V5c0-2.8-2.2-5-5-5h-58c-2.8,0-5,2.2-5,5s2.2,5,5,5H244z
"/>
<path id="bottom-right" class="st2" d="M244.1,225.6H191c-2.8,0-5,2.2-5,5c0,2.8,2.2,5,5,5h58.1c2.8,0,5-2.2,5-5v-58
c0-2.8-2.2-5-5-5s-5,2.2-5,5V225.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="camera-toggle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.8;fill:none;stroke:#FFFFFF;}
.st1{opacity:0.386;fill:#FFFFFF;enable-background:new ;}
</style>
<path id="top-arrow-line" class="st0" d="M10.5,18.1c0-4.7,4.3-8.5,9.5-8.5c3.7,0,6.9,1.9,8.5,4.7"/>
<path id="bottom-arrow-line" class="st0" d="M29.5,21c0,4.7-4.3,8.5-9.5,8.5c-3.7,0-6.9-1.9-8.5-4.7"/>
<polyline id="top-arrow-head" class="st0" points="28.9,11.1 28.4,14.1 25,13.7 "/>
<polyline id="bottom-arrow-head" class="st0" points="11,27.8 11.4,24.8 14.8,25.2 "/>
<path id="camera" class="st1" d="M24.6,21.9c0,0.8-0.7,1.5-1.5,1.5H17c-0.8,0-1.5-0.7-1.5-1.5v-3.5c0-0.8,0.7-1.5,1.5-1.5h1l0.8-1.1
h2.5l0.8,1.1h1c0.8,0,1.5,0.7,1.5,1.5V21.9z M20.1,21.9c1.1,0,2.1-0.8,2.1-1.9s-0.9-1.9-2.1-1.9C18.9,18.1,18,19,18,20
S18.9,21.9,20.1,21.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Flash_1_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.8;fill:#FFFFFF;enable-background:new ;}
</style>
<polygon id="bolt" class="st0" points="15,21.4 19.6,21.4 18,30 25,18.6 20.4,18.6 22,10 "/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

View File

@ -5,7 +5,7 @@
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" opacity="0.9">
<g id="Icons/Tabs/Scan/2" transform="translate(1.000000, -6.000000)" stroke-width="1.5" stroke="#FFFFFF">
<g id="Icons/Tabs/Scan/2" transform="translate(1.000000, -6.000000)" stroke-width="1.5" stroke="#647CE8">
<g id="scan" transform="translate(-1.000000, 6.000000)">
<g id="Scan">
<path d="M10.4803945,1.62179474 L6.04824505,1.62179474 M5.74546723,1.62179474 L5.74546723,5.91363686" id="Line-Copy-2" stroke-linejoin="round"></path>
@ -17,4 +17,4 @@
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,22 @@
<ion-view id="tab-scan">
<ion-nav-bar class="bar-royal">
<ion-nav-title>{{'Scan' | translate}}</ion-nav-title>
</ion-nav-bar>
<ion-content scroll="false">
<div class="guides">
<img class="qr-scan-guides" src="img/bitpay-wallet-qr-scan-guides.svg">
</div>
<div class="scanner-controls">
<a ng-click="toggleLight()" ng-show="canEnableLight">
<i class="icon">
<div class="icon-flash" ng-class="{'active': lightActive}"></div>
</i>
</a>
<a ng-click="toggleCamera()" ng-show="canChangeCamera">
<i class="icon">
<div class="icon-camera-toggle" ng-class="{'active': cameraToggleActive}"></div>
</i>
</a>
</div>
</ion-content>
</ion-view>

View File

@ -8,7 +8,7 @@
<label class="item item-input bitcoin-address">
<i class="icon ion-social-bitcoin placeholder-icon"></i>
<div class="qr-scan-icon" ng-style="{'width': '100%'}">
<qr-scanner class="qr-icon size-24" ng-style="{'top': '3px'}" on-scan="onQrCodeScanned(data)"></qr-scanner>
<a ui-sref="tabs.scan" ng-style="{'top': '6px'}"><i class="icon ion-qr-scanner"></i></a>
<input type="text"
placeholder="{{'Search or enter bitcoin address' | translate}}"
ng-model="formData.search"

View File

@ -1,9 +1,4 @@
<!--
Create tabs with an icon and label, using the tabs-positive style.
Each tab's child <ion-nav-view> directive will have its own
navigation history that also transitions its views in and out.
-->
<ion-tabs class="tabs-icon-top tabs-color-active-positive" ng-class="{'tabs-item-hide': hideTabs}">
<ion-tabs class="tabs-icon-top tabs-color-active-positive ion-tabs-transparent" ng-class="{'tabs-item-hide': hideTabs}">
<ion-tab title="Home" icon-off="ico-home" icon-on="ico-home-selected" ui-sref="tabs.home">
<ion-nav-view name="tab-home"></ion-nav-view>
@ -13,12 +8,8 @@ navigation history that also transitions its views in and out.
<ion-nav-view name="tab-receive"></ion-nav-view>
</ion-tab>
<!-- this actually NEVER gets rendered -->
<qr-scanner class="qr-icon size-24" style="display:none" set-fn="setScanFn(theScanFn)" on-scan="onScan(data)"></qr-scanner>
<ion-tab title="Scan" icon-off="ico-scan" ng-click="scan()" >
<ion-tab title="Scan" icon-off="ico-scan" icon-on="ico-scan-selected" ui-sref="tabs.scan">
<ion-nav-view name="tab-scan"></ion-nav-view>
</ion-tab>
<ion-tab title="Send" icon-off="ico-send" icon-on="ico-send-selected" ui-sref="tabs.send">

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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";

View File

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