-
+
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 6e2aa8d37..2ea850c5b 100644
--- a/src/js/controllers/tab-send.js
+++ b/src/js/controllers/tab-send.js
@@ -101,12 +101,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 bad6f9c09..2730f327c 100644
--- a/src/sass/ionic.scss
+++ b/src/sass/ionic.scss
@@ -1,13 +1,28 @@
-/* Set ionic variables */
-$font-family-sans-serif: "Roboto", sans-serif;
-$font-family-light-sans-serif: "Roboto-Light", sans-serif-light;
+/* constants */
$royal: #1e3186;
-$soft-blue: rgb(100,124,232);
-$base-background-color: #f5f5f5;
+$soft-blue: #647ce8;
+$subtle-gray: #f5f5f5;
+$roboto: "Roboto", sans-serif;
+$roboto-light: "Roboto-Light", sans-serif-light;
-/* Ionic Workaround */
+/* Set ionic variables */
+$font-family-sans-serif: $roboto;
+$font-family-light-sans-serif: $roboto-light;
+$base-background-color: $subtle-gray;
+$item-default-active-bg: $subtle-gray;
+$ios-transition-duration: 200ms;
+
+/* Ionic Workarounds */
+// Please include a description of the problem solved by the workaround.
+
+// 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/main.scss b/src/sass/main.scss
index 7ceb64a70..c41b47da3 100644
--- a/src/sass/main.scss
+++ b/src/sass/main.scss
@@ -438,363 +438,6 @@ ul.wallet-selection.wallets {
}
// General purpose
-.dn {
- display: none;
-}
-
-.dni {
- display: none !important;
-}
-
-.pr {
- position: relative;
-}
-
-.pa {
- position: absolute;
-}
-
-.m0 {
- margin: 0;
-}
-
-.p0i {
- padding: 0 !important;
-}
-
-.db {
- display: block;
-}
-
-.dib {
- display: inline-block;
-}
-
-.size-10 {
- font-size: 10px;
-}
-
-.size-12 {
- font-size: 12px;
-}
-
-.size-14 {
- font-size: 14px;
-}
-
-.size-16 {
- font-size: 16px;
-}
-
-.size-18 {
- font-size: 18px;
-}
-
-.size-21 {
- font-size: 21px;
-}
-
-.size-24 {
- font-size: 24px;
-}
-
-.size-28 {
- font-size: 28px;
-}
-
-.size-32 {
- font-size: 32px;
-}
-
-.size-36 {
- font-size: 36px;
-}
-
-.size-42 {
- font-size: 42px;
-}
-
-.size-48 {
- font-size: 48px;
-}
-
-.size-60 {
- font-size: 60px;
-}
-
-.size-72 {
- font-size: 72px;
-}
-
-.m5 {
- margin: 5px;
-}
-
-.m5t {
- margin-top: 5px;
-}
-
-.m8t {
- margin-top: 8px;
-}
-
-.m5b {
- margin-bottom: 5px;
-}
-
-.m5r {
- margin-right: 5px;
-}
-
-.m10 {
- margin: 10px;
-}
-
-.m10b {
- margin-bottom: 10px;
-}
-
-.m3t {
- margin-top: 3px;
-}
-
-.m10t {
- margin-top: 10px;
-}
-
-.m15b {
- margin-bottom: 15px;
-}
-
-.m15r {
- margin-right: 15px;
-}
-
-.m20b {
- margin-bottom: 20px;
-}
-
-.m30b {
- margin-bottom: 30px;
-}
-
-.m40b {
- margin-bottom: 40px;
-}
-
-.m50b {
- margin-bottom: 50px;
-}
-
-.m10r {
- margin-right: 10px;
-}
-
-.m40r {
- margin-right: 40px;
-}
-
-.m55r {
- margin-right: 55px;
-}
-
-.m25r {
- margin-right: 25px;
-}
-
-.m10l {
- margin-left: 10px;
-}
-
-.m5l {
- margin-left: 5px;
-}
-
-.m15l {
- margin-left: 15px;
-}
-
-.m15t {
- margin-top: 15px;
-}
-
-.m20r {
- margin-right: 20px;
-}
-
-.m20t {
- margin-top: 20px;
-}
-
-.m20ti {
- margin-top: 20px !important;
-}
-
-.m20tp {
- margin-top: 20%;
-}
-
-.m30tp {
- margin-top: 30%;
-}
-
-.m15 {
- margin: 15px;
-}
-
-.m15h {
- margin: 0 15px;
-}
-
-.p10t {
- padding-top: 10px;
-}
-
-.p10h {
- padding-right: 10px;
- padding-left: 10px;
-}
-
-.p15h {
- padding: 0 15px;
-}
-
-.p0r {
- padding-right: 0;
-}
-
-.p70r {
- padding-right: 70px;
-}
-
-.p70l {
- padding-left: 70px;
-}
-
-.p5h {
- padding: 0 5px;
-}
-
-.p20h {
- padding: 0 20px;
-}
-
-.p20v {
- padding: 20px 0;
-}
-
-.p20b {
- padding-bottom: 20px;
-}
-
-.p25b {
- padding-bottom: 25px;
-}
-
-.p25l {
- padding-left: 25px;
-}
-
-.p15l {
- padding-left: 15px;
-}
-
-.p15 {
- padding: 15px;
-}
-
-.p20 {
- padding: 20px;
-}
-
-.p15t {
- padding-top: 15px;
-}
-
-.p20t {
- padding-top: 20px;
-}
-
-.p50t {
- padding-top: 50px;
-}
-
-.p10 {
- padding: 10px;
-}
-
-.p10i {
- padding: 10px !important;
-}
-
-.p10b {
- padding-bottom: 10px;
-}
-
-.p45t {
- padding-top: 45px;
-}
-
-.p60t {
- padding-top: 60px;
-}
-
-.p60b {
- padding-bottom: 60px;
-}
-
-.m60t {
- margin-top: 60px;
-}
-
-.p45li {
- padding-left: 45px !important;
-}
-
-.m30v {
- margin: 30px 0;
-}
-
-.m15v {
- margin: 15px 0;
-}
-
-.m10h {
- margin: 0 10px;
-}
-
-.m10v {
- margin: 10px 0;
-}
-
-.m20v {
- margin: 20px 0;
-}
-
-.m30v {
- margin: 30px 0;
-}
-
-.m30a {
- margin: 30px auto;
-}
-
-.m-negative-l {
- margin-left: -0.9375rem;
-}
-
-.br100 {
- border-radius: 100% !important;
-}
-
-.lh {
- line-height: 0;
-}
-
-.lh140 {
- line-height: 140%;
-}
-
.oh {
overflow: hidden;
}
@@ -994,6 +637,7 @@ input[type=number] {
@import "views/confirm";
@import "views/tab-home";
@import "views/tab-receive";
+@import "views/tab-scan";
@import "views/tab-send";
@import "views/tab-settings";
@import "views/walletDetails";
diff --git a/src/sass/views/tab-home.scss b/src/sass/views/tab-home.scss
index 9566fff16..6ac2ab547 100644
--- a/src/sass/views/tab-home.scss
+++ b/src/sass/views/tab-home.scss
@@ -1,12 +1,14 @@
#tab-home {
.icon-create-wallet {
- background-image: url("../img/icon-bitcoin.svg");
+ background-image: url("../img/icon-wallet.svg");
+ background-color: #4A90E2; // default wallet color
}
.icon-buy-bitcoin {
- background-image: url("../img/icon-gift.svg");
+ background-image: url("../img/icon-bitcoin.svg");
}
.icon-bitpay-card {
- background-image: url("../img/icon-bitpay.svg");
+ background-image: url("../img/icon-card.svg");
+ background-color: #1e3186;
}
.icon-gift {
background-image: url("../img/icon-gift.svg");
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;
+}
diff --git a/webkitbuilds/.desktop b/webkitbuilds/.desktop
index ef36f1003..b886061ba 100644
--- a/webkitbuilds/.desktop
+++ b/webkitbuilds/.desktop
@@ -1,9 +1,9 @@
[Desktop Entry]
Type=Application
-Version=0.13.0
-Name=BitPay
-Comment=The BitPay Bitcoin Wallet
-Exec=bitpay
+Version=0.6.0
+Name=BitPay Wallet
+Comment=Secure Bitcoin Storage
+Exec=wallet
Icon=icon-256.png
Terminal=false
Categories=Finance
diff --git a/webkitbuilds/setup-win.iss b/webkitbuilds/setup-win.iss
index cb7008351..75890c496 100755
--- a/webkitbuilds/setup-win.iss
+++ b/webkitbuilds/setup-win.iss
@@ -1,8 +1,8 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-#define MyAppName "bitpay"
-#define MyAppVersion "0.13.0"
+#define MyAppName "wallet"
+#define MyAppVersion "0.6.0"
#define MyAppPublisher "BitPay"
#define MyAppURL "https://bitpay.com"
#define MyAppExeName "*NAMECASENOSPACE.exe"
@@ -18,7 +18,7 @@ AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
-OutputBaseFilename=BitPay-win
+OutputBaseFilename=BitPay Wallet-win
OutputDir=./
Compression=lzma
SolidCompression=yes
@@ -32,8 +32,8 @@ Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
-Source: "BitPay\win64\bitpay.exe"; DestDir: "{app}"; Flags: ignoreversion
-Source: "BitPay\win64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "BitPay Wallet\win64\wallet.exe"; DestDir: "{app}"; Flags: ignoreversion
+Source: "BitPay Wallet\win64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "../public/img/icons/favicon.ico"; DestDir: "{app}"; DestName: "icon.ico"; Flags: ignoreversion
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
@@ -50,8 +50,8 @@ Root: HKCR; Subkey: "bitcoin"; ValueType: "string"; ValueName: "URL Protocol"; V
Root: HKCR; Subkey: "bitcoin\DefaultIcon"; ValueType: "string"; ValueData: "{app}\{#MyAppExeName},0"
Root: HKCR; Subkey: "bitcoin\shell\open\command"; ValueType: "string"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
-Root: HKCR; Subkey: "bitpay"; ValueType: "string"; ValueData: "URL:BitPay Custom Protocol"; Flags: uninsdeletekey
-Root: HKCR; Subkey: "bitpay"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""
-Root: HKCR; Subkey: "bitpay\DefaultIcon"; ValueType: "string"; ValueData: "{app}\{#MyAppExeName},0"
-Root: HKCR; Subkey: "bitpay\shell\open\command"; ValueType: "string"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
+Root: HKCR; Subkey: "wallet"; ValueType: "string"; ValueData: "URL:BitPay Wallet Custom Protocol"; Flags: uninsdeletekey
+Root: HKCR; Subkey: "wallet"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""
+Root: HKCR; Subkey: "wallet\DefaultIcon"; ValueType: "string"; ValueData: "{app}\{#MyAppExeName},0"
+Root: HKCR; Subkey: "wallet\shell\open\command"; ValueType: "string"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""