diff --git a/.gitignore b/.gitignore index fb26abc57..7f0f7a730 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,14 @@ version.js android/package android/*.apk +android/*.keystore + +mobile/*.keystore +mobile/assets/www +mobile/bin/* +mobile/gen/* +mobile/cordova/* +mobile/CordovaLib/* coverage/ diff --git a/README.md b/README.md index 598627475..9d7d1e3bd 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,32 @@ One solution is to use Copay with a Python version manager for 2.6. # Development +## Android APK + +System Requirements + +* Download [Android SDK](http://developer.android.com/sdk/index.html) +* Download and install [Crosswalk 8](https://crosswalk-project.org/#documentation/getting_started) (Use Linux setup for OSX) + +Add to your ~/.bash_profile or ~/.bashrc + +``` +export CROSSWALK="" +``` + +To build the APK run the script: + +``` +sh android/build.sh [-d] +``` +- The -d flag will package the apk in debug mode, allowing [remote debugging chrome](https://developer.chrome.com/devtools/docs/remote-debugging) +- The APK file is in **android/Copay_VERSION_arm.apk** + +To install the APK in your device run: + +``` +adb install -r Copay_VERSION_arm.apk +``` ## Google Chrome Extension diff --git a/TODO.md b/TODO.md index 209b292e1..bc9a34c43 100644 --- a/TODO.md +++ b/TODO.md @@ -30,33 +30,6 @@ It was developed to be run on OSX. The outputs are copied to the dist directory DMG is created with hdiutil EXE is created with makensis (brew install makensis) -## Android APK - -System Requirements - -* Download [Android SDK](http://developer.android.com/sdk/index.html) -* Download and install [Crosswalk](https://crosswalk-project.org/#documentation/getting_started) (Use Linux setup for OSX) - -Add to your ~/.bash_profile or ~/.bashrc - -``` -export CROSSWALK="" -``` - -To build the APK run the script: - -``` -sh android/build.sh [-d] -``` -- The -d flag will package the apk in debug mode, allowing [remote debugging chrome](https://developer.chrome.com/devtools/docs/remote-debugging) -- The APK file is in **android/Copay_VERSION_arm.apk** - -To install the APK in your device run: - -``` -adb install -r Copay_VERSION_arm.apk -``` - # Development diff --git a/android/build.sh b/android/build.sh index 111389d45..313aa0320 100644 --- a/android/build.sh +++ b/android/build.sh @@ -26,6 +26,16 @@ then DEBUG="--enable-remote-debugging" fi +if [[ $# -eq 1 && ! $1 = "-d" ]] +then + if [ ! -f $BUILDDIR/copay.keystore ] + then + echo "${OpenColor}${Red}* Can't build production app without a keystore${CloseColor}" + exit 1 + fi + PRODUCTION="--keystore-path=$BUILDDIR/copay.keystore --keystore-alias=copay_play --keystore-passcode=$1" +fi + # Move to the build directory cd $BUILDDIR @@ -48,13 +58,13 @@ checkOK echo "${OpenColor}${Green}* Copying all app files...${CloseColor}" sed "s/APP_VERSION/$VERSION/g" manifest.json > $APPDIR/manifest.json cd $BUILDDIR/.. -cp -af {css,font,img,js,lib,sound,config.js,version.js,index.html,./android/icon.png} $APPDIR +cp -af {css,font,img,js,lib,sound,config.js,version.js,index.html,./android/icon.png,./android/logo.png} $APPDIR checkOK # Building the APK echo "${OpenColor}${Green}* Building APK file...${CloseColor}" cd $CROSSWALK -python make_apk.py --manifest=$APPDIR/manifest.json --target-dir=$BUILDDIR --arch=arm $DEBUG +python make_apk.py --manifest=$APPDIR/manifest.json --package=com.bitpay.copay --arch=arm --target-dir=$BUILDDIR $DEBUG $PRODUCTION checkOK cd $BUILDDIR diff --git a/android/icon.png b/android/icon.png index 2a2a9bda9..84f38c3b6 100644 Binary files a/android/icon.png and b/android/icon.png differ diff --git a/android/logo.png b/android/logo.png new file mode 100644 index 000000000..1a324a9af Binary files /dev/null and b/android/logo.png differ diff --git a/android/manifest.json b/android/manifest.json index 88f9b2025..ff5336880 100644 --- a/android/manifest.json +++ b/android/manifest.json @@ -1,12 +1,21 @@ { "name": "Copay", "version": "APP_VERSION", - "app": { - "launch":{ - "local_path": "index.html" - } + "start_url": "index.html", + "permissions": [ + "Vibration" + ], + "xwalk_launch_screen": { + "ready_when": "complete", + "default": { + "background_color": "#2C3E50", + "image": "logo.png" + } }, "icons": { "128": "icon.png" - } + }, + "xwalk_hosts": [ + "https://*" + ] } diff --git a/bower.json b/bower.json index fb1e4c22a..cf1a4f301 100644 --- a/bower.json +++ b/bower.json @@ -18,7 +18,7 @@ "sjcl": "1.0.0", "file-saver": "*", "qrcode-decoder-js": "*", - "bitcore": "0.1.25", + "bitcore": "0.1.34", "angular-moment": "~0.7.1", "socket.io-client": ">=1.0.0", "mousetrap": "1.4.6" diff --git a/img/notification.png b/img/notification.png new file mode 100644 index 000000000..70b0843c0 Binary files /dev/null and b/img/notification.png differ diff --git a/js/app.js b/js/app.js index 01c36efb6..cf5656438 100644 --- a/js/app.js +++ b/js/app.js @@ -33,6 +33,13 @@ var copayApp = window.copayApp = angular.module('copayApp', [ 'copayApp.directives', ]); +copayApp.config(function($sceDelegateProvider) { + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'mailto:**' + ]); +}); + angular.module('copayApp.filters', []); angular.module('copayApp.services', []); angular.module('copayApp.controllers', []); diff --git a/js/controllers/import.js b/js/controllers/import.js index 0193aef5e..901945d5c 100644 --- a/js/controllers/import.js +++ b/js/controllers/import.js @@ -17,9 +17,9 @@ angular.module('copayApp.controllers').controller('ImportController', updateStatus('Importing wallet - Setting things up...'); var w, errMsg; + // try to import encrypted wallet with passphrase try { w = walletFactory.import(encryptedObj, passphrase); - } catch (e) { errMsg = e.message; } @@ -31,12 +31,14 @@ angular.module('copayApp.controllers').controller('ImportController', return; } + // if wallet was never used, we're done if (!w.isReady()) { $rootScope.wallet = w; controllerUtils.startNetwork($rootScope.wallet, $scope); return; } + // if it was used, we need to scan for indices w.updateIndexes(function(err) { updateStatus('Importing wallet - We are almost there...'); if (err) { diff --git a/js/controllers/send.js b/js/controllers/send.js index 8c77f960a..5dc4565ba 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -32,6 +32,7 @@ angular.module('copayApp.controllers').controller('SendController', // Detect protocol $scope.isHttp = ($window.location.protocol.indexOf('http') === 0); + $scope.isCordova = typeof(window.cordova) != 'undefined'; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL; @@ -72,8 +73,8 @@ angular.module('copayApp.controllers').controller('SendController', }); // reset fields - $scope.address = $scope.amount = $scope.comment = null; - form.address.$pristine = form.amount.$pristine = form.comment.$pristine = true; + $scope.address = $scope.amount = $scope.commentText = null; + form.address.$pristine = form.amount.$pristine = true; }; // QR code Scanner @@ -194,6 +195,25 @@ angular.module('copayApp.controllers').controller('SendController', }, 500); }; + $scope.scannerIntent = function() { + cordova.plugins.barcodeScanner.scan( + function onSuccess(result) { + if (result.cancelled) return; + + var bip21 = copay.Structure.parseBitcoinURI(result.text); + $scope.address = bip21.address; + + if (bip21.amount) { + $scope.amount = bip21.amount * bitcore.util.COIN * satToUnit; + } + + $rootScope.$digest(); + }, + function onError(error) { + alert('Scanning error'); + }); + } + $scope.toggleAddressBookEntry = function(key) { var w = $rootScope.wallet; w.toggleAddressBookEntry(key); diff --git a/js/controllers/sidebar.js b/js/controllers/sidebar.js index 45fdcde1e..79b8ad859 100644 --- a/js/controllers/sidebar.js +++ b/js/controllers/sidebar.js @@ -18,7 +18,7 @@ angular.module('copayApp.controllers').controller('SidebarController', 'icon': 'fi-arrow-right', 'link': 'send' }, { - 'title': 'More...', + 'title': 'More', 'icon': 'fi-download', 'link': 'backup' }]; diff --git a/js/directives.js b/js/directives.js index 48457bf2f..f14b477e7 100644 --- a/js/directives.js +++ b/js/directives.js @@ -156,63 +156,55 @@ angular.module('copayApp.directives') restrict: 'EACM', require: 'ngModel', link: function(scope, element, attrs) { - var strength = { - messages: ['very weak', 'weak', 'weak', 'medium', 'strong'], - colors: ['#c0392b', '#e74c3c', '#d35400', '#f39c12', '#27ae60'], - mesureStrength: function(p) { - var force = 0; - var regex = /[$-/:-?{-~!"^_`\[\]]/g; - var lowerLetters = /[a-z]+/.test(p); - var upperLetters = /[A-Z]+/.test(p); - var numbers = /[0-9]+/.test(p); - var symbols = regex.test(p); - var flags = [lowerLetters, upperLetters, numbers, symbols]; - var passedMatches = flags.filter(function(el) { - return !!el; - }).length; - force = 2 * p.length + (p.length >= 10 ? 1 : 0); - force += passedMatches * 10; + var MIN_LENGTH = 8; + var MESSAGES = ['Very Weak', 'Very Weak', 'Weak', 'Medium', 'Strong', 'Very Strong']; + var COLOR = ['#dd514c', '#dd514c', '#faa732', '#faa732', '#5eb95e', '#5eb95e']; - // penality (short password) - force = (p.length <= 6) ? Math.min(force, 10) : force; - - // penality (poor variety of characters) - force = (passedMatches == 1) ? Math.min(force, 10) : force; - force = (passedMatches == 2) ? Math.min(force, 20) : force; - force = (passedMatches == 3) ? Math.min(force, 40) : force; - return force; - }, - getColor: function(s) { - var idx = 0; - - if (s <= 10) { - idx = 0; - } else if (s <= 20) { - idx = 1; - } else if (s <= 30) { - idx = 2; - } else if (s <= 40) { - idx = 3; + function evaluateMeter(password) { + var passwordStrength = 0; + var text; + if (password.length > 0) passwordStrength = 1; + if (password.length >= MIN_LENGTH) { + if ((password.match(/[a-z]/)) && (password.match(/[A-Z]/))) { + passwordStrength++; } else { - idx = 4; + text = ', add mixed case'; } - - return { - idx: idx + 1, - col: this.colors[idx], - message: this.messages[idx] - }; + if (password.match(/\d+/)) { + passwordStrength++; + } else { + if (!text) text = ', add numerals'; + } + if (password.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/)) { + passwordStrength++; + } else { + if (!text) text = ', add punctuation'; + } + if (password.length > 12) { + passwordStrength++; + } else { + if (!text) text = ', add characters'; + } + } else { + text = ', that\'s short'; } - }; + if (!text) text = ''; + + return { + strength: passwordStrength, + message: MESSAGES[passwordStrength] + text, + color: COLOR[passwordStrength] + } + } scope.$watch(attrs.ngModel, function(newValue, oldValue) { if (newValue && newValue !== '') { - var c = strength.getColor(strength.mesureStrength(newValue)); + var info = evaluateMeter(newValue); element.css({ - 'border-color': c.col + 'border-color': info.color }); - scope[attrs.checkStrength] = c.message; + scope[attrs.checkStrength] = info.message; } }); } diff --git a/js/filters.js b/js/filters.js index 42848fe4c..76efd6313 100644 --- a/js/filters.js +++ b/js/filters.js @@ -20,14 +20,18 @@ angular.module('copayApp.filters', []) .filter('removeEmpty', function() { return function(elements) { elements = elements || []; - // Hide empty addresses from other copayers + // Hide empty change addresses from other copayers return elements.filter(function(e) { - return e.owned || e.balance > 0; + return !e.isChange || e.balance > 0; }); } }) .filter('limitAddress', function() { return function(elements, showAll) { + var elements = elements.sort(function(a, b) { + return (+b.owned) - (+a.owned); + }); + if (elements.length <= 1 || showAll) { return elements; } diff --git a/js/models/core/BuilderMockV0.js b/js/models/core/BuilderMockV0.js new file mode 100644 index 000000000..be3926d10 --- /dev/null +++ b/js/models/core/BuilderMockV0.js @@ -0,0 +1,26 @@ +'use strict'; + + +var bitcore = require('bitcore'); +var Transaction = bitcore.Transaction; + +function BuilderMockV0 (data) { + this.vanilla = data; + this.tx = new Transaction(); + this.tx.parse(new Buffer(data.tx, 'hex')); +}; + +BuilderMockV0.prototype.build = function() { + return this.tx; +}; + + +BuilderMockV0.prototype.getSelectedUnspent = function() { + return []; +}; + +BuilderMockV0.prototype.toObj = function() { + return this.vanilla; +}; + +module.exports = BuilderMockV0; diff --git a/js/models/core/Structure.js b/js/models/core/Structure.js index ff47c43f0..54b51e4da 100644 --- a/js/models/core/Structure.js +++ b/js/models/core/Structure.js @@ -59,13 +59,16 @@ Structure.parseBitcoinURI = function(uri) { data = splitDots[1]; var splitQuestion = data.split('?'); ret.address = splitQuestion[0]; - var search = splitQuestion[1]; - data = JSON.parse('{"' + search.replace(/&/g, '","').replace(/=/g, '":"') + '"}', - function(key, value) { - return key === "" ? value : decodeURIComponent(value); - }); - ret.amount = parseFloat(data.amount); - ret.message = data.message; + + if (splitQuestion.length > 1) { + var search = splitQuestion[1]; + data = JSON.parse('{"' + search.replace(/&/g, '","').replace(/=/g, '":"') + '"}', + function(key, value) { + return key === "" ? value : decodeURIComponent(value); + }); + ret.amount = parseFloat(data.amount); + ret.message = data.message; + } return ret; }; diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index cd7ac2a46..77462cca8 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -5,9 +5,11 @@ var imports = require('soop').imports(); var bitcore = require('bitcore'); var util = bitcore.util; var Transaction = bitcore.Transaction; -var Builder = bitcore.TransactionBuilder; +var BuilderMockV0 = require('./BuilderMockV0');; +var TransactionBuilder = bitcore.TransactionBuilder; var Script = bitcore.Script; var buffertools = bitcore.buffertools; +var preconditions = require('preconditions').instance(); function TxProposal(opts) { this.creator = opts.creator; @@ -23,8 +25,7 @@ function TxProposal(opts) { } TxProposal.prototype.getID = function() { - var ntxid = this.builder.build().getNormalizedHash().toString('hex'); - return ntxid; + return this.builder.build().getNormalizedHash().toString('hex'); }; TxProposal.prototype.toObj = function() { @@ -40,13 +41,41 @@ TxProposal.prototype.setSent = function(sentTxid) { this.sentTs = Date.now(); }; -TxProposal.fromObj = function(o) { +TxProposal.fromObj = function(o, forceOpts) { var t = new TxProposal(o); - var b = new Builder.fromObj(o.builderObj); - t.builder = b; + + try { + // force opts is requested. + for (var k in forceOpts) { + o.builderObj.opts[k] = forceOpts[k]; + } + t.builder = TransactionBuilder.fromObj(o.builderObj); + + } catch (e) { + if (!o.version) { + t.builder = new BuilderMockV0(o.builderObj); + t.readonly = 1; + }; + } + return t; }; + +TxProposal.prototype.isValid = function() { + if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { + return false; + } + var tx = this.builder.build(); + for (var i = 0; i < tx.ins.length; i++) { + var hashType = tx.getHashType(i); + if (hashType && hashType !== Transaction.SIGHASH_ALL) { + return false; + } + } + return true; +}; + TxProposal.getSentTs = function() { return this.sentTs; }; @@ -127,6 +156,17 @@ TxProposal.prototype.mergeMetadata = function(v1, author) { }; +//This should be on bitcore / Transaction +TxProposal.prototype.countSignatures = function() { + var tx = this.builder.build(); + + var ret = 0; + for (var i in tx.ins) { + ret += tx.countInputSignatures(i); + } + return ret; +}; + module.exports = require('soop')(TxProposal); @@ -138,15 +178,18 @@ function TxProposals(opts) { this.txps = {}; } -TxProposals.fromObj = function(o) { +TxProposals.fromObj = function(o, forceOpts) { var ret = new TxProposals({ networkName: o.networkName, walletId: o.walletId, }); + o.txps.forEach(function(o2) { - var t = TxProposal.fromObj(o2); - var id = t.builder.build().getNormalizedHash().toString('hex'); - ret.txps[id] = t; + var t = TxProposal.fromObj(o2, forceOpts); + if (t.builder) { + var id = t.getID(); + ret.txps[id] = t; + } }); return ret; }; @@ -198,7 +241,6 @@ TxProposals.prototype.merge = function(inTxp, author) { return ret; }; -var preconditions = require('preconditions').instance(); TxProposals.prototype.add = function(data) { preconditions.checkArgument(data.inputChainPaths); preconditions.checkArgument(data.signedBy); diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 51924554d..cf5e13839 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -35,8 +35,8 @@ function Wallet(opts) { self[k] = opts[k]; }); if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName) - throw new Error('Network forced to '+copayConfig.networkName+ - ' and tried to create a Wallet with network '+ this.getNetworkName()); + throw new Error('Network forced to ' + copayConfig.networkName + + ' and tried to create a Wallet with network ' + this.getNetworkName()); this.log('creating ' + opts.requiredCopayers + ' of ' + opts.totalCopayers + ' wallet'); @@ -57,6 +57,14 @@ function Wallet(opts) { this.network.setHexNonces(opts.networkNonces); } + +Wallet.builderOpts = { + lockTime: null, + signhash: bitcore.Transaction.SIGNHASH_ALL, + fee: null, + feeSat: null, +}; + Wallet.parent = EventEmitter; Wallet.prototype.log = function() { if (!this.verbose) return; @@ -121,11 +129,20 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) { }; -Wallet.prototype._handleTxProposal = function(senderId, data) { - preconditions.checkArgument(senderId); - this.log('RECV TXPROPOSAL:', data); - var inTxp = TxProposals.TxProposal.fromObj(data.txProposal); +Wallet.prototype._handleTxProposal = function(senderId, data) { + this.log('RECV TXPROPOSAL: ', data); + + var inTxp = TxProposals.TxProposal.fromObj(data.txProposal, Wallet.builderOpts); + var valid = inTxp.isValid(); + if (!valid) { + var corruptEvent = { + type: 'corrupt', + cId: inTxp.creator + }; + this.emit('txProposalEvent', corruptEvent); + return; + } var mergeInfo = this.txProposals.merge(inTxp, senderId); var added = this.addSeenToTxProposals(); @@ -370,7 +387,7 @@ Wallet.fromObj = function(o, storage, network, blockchain) { opts.addressBook = o.addressBook; opts.publicKeyRing = PublicKeyRing.fromObj(o.publicKeyRing); - opts.txProposals = TxProposals.fromObj(o.txProposals); + opts.txProposals = TxProposals.fromObj(o.txProposals, Wallet.builderOpts); opts.privateKey = PrivateKey.fromObj(o.privateKey); opts.storage = storage; @@ -489,7 +506,9 @@ Wallet.prototype.getTxProposals = function() { txp.finallyRejected = true; } - ret.push(txp); + if (txp.readonly && !txp.finallyRejected && !txp.sentTs) {} else { + ret.push(txp); + } } return ret; }; @@ -509,6 +528,7 @@ Wallet.prototype.reject = function(ntxid) { }; + Wallet.prototype.sign = function(ntxid, cb) { preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined'); var self = this; @@ -522,11 +542,11 @@ Wallet.prototype.sign = function(ntxid, cb) { var keys = self.privateKey.getForPaths(txp.inputChainPaths); var b = txp.builder; - var before = b.signaturesAdded; + var before = txp.countSignatures(); b.sign(keys); var ret = false; - if (b.signaturesAdded > before) { + if (txp.countSignatures() > before) { txp.signedBy[myId] = Date.now(); self.sendTxProposal(ntxid); self.store(); @@ -697,15 +717,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var priv = this.privateKey; opts = opts || {}; - var amountSat = bignum(amountSatStr); preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName()); - if (!pkr.isComplete()) { - throw new Error('publicKeyRing is not complete'); - } - - if (comment && comment.length > 100) { - throw new Error("comment can't be longer that 100 characters"); - } + preconditions.checkState(pkr.isComplete()); + if (comment) preconditions.checkArgument(comment.length <= 100); if (!opts.remainderOut) { opts.remainderOut = { @@ -713,11 +727,15 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos }; } + for (var k in Wallet.builderOpts){ + opts[k] = Wallet.builderOpts[k]; + } + var b = new Builder(opts) .setUnspent(utxos) .setOutputs([{ address: toAddress, - amountSat: amountSat + amountSatStr: amountSatStr, }]); var selectedUtxos = b.getSelectedUnspent(); @@ -735,7 +753,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var now = Date.now(); var me = {}; - if (priv && b.signaturesAdded) me[myId] = now; + + var tx = b.build(); + if (priv && tx.countInputSignatures(0)) me[myId] = now; var meSeen = {}; if (priv) meSeen[myId] = now; diff --git a/js/services/backupService.js b/js/services/backupService.js index 0b7375c0e..dc915d632 100644 --- a/js/services/backupService.js +++ b/js/services/backupService.js @@ -28,6 +28,18 @@ BackupService.prototype.download = function(wallet) { wallet: ew }); } + + // throw an email intent if we are in the mobile version + if (window.cordova) { + var name = wallet.name ? wallet.name + ' ' : ''; + var partial = partial ? 'Partial ' : ''; + return window.plugin.email.open({ + subject: 'Copay - ' + name + 'Wallet ' + partial + 'Backup', + body: 'Here is the encrypted backup of the wallet ' + wallet.id, + attachments: ['base64:' + filename + '//' + btoa(ew)] + }); + } + // otherwise lean on the browser implementation saveAs(blob, filename); }; diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 9f6f4f3d2..7bae6bdce 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -149,15 +149,17 @@ angular.module('copayApp.services') }, 3000); }); w.on('txProposalEvent', function(e) { + var user = w.publicKeyRing.nicknameForCopayer(e.cId); switch (e.type) { case 'signed': - var user = w.publicKeyRing.nicknameForCopayer(e.cId); notification.info('Transaction Update', 'A transaction was signed by ' + user); break; case 'rejected': - var user = w.publicKeyRing.nicknameForCopayer(e.cId); notification.info('Transaction Update', 'A transaction was rejected by ' + user); break; + case 'corrupt': + notification.error('Transaction Error', 'Received corrupt transaction from '+user); + break; } }); w.on('addressBookUpdated', function(dontDigest) { diff --git a/js/services/notifications.js b/js/services/notifications.js index 0a6bdd42c..2c70ce52f 100644 --- a/js/services/notifications.js +++ b/js/services/notifications.js @@ -198,7 +198,12 @@ factory('notification', ['$timeout', $timeout(function removeFromQueueTimeout() { queue.splice(queue.indexOf(notification), 1); }, settings[type].duration); + } + // Movile notification + window.navigator.vibrate([200,100,200]); + if (document.hidden && (type == 'info' || type == 'funds')) { + new window.Notification(title, {body: content, icon:'img/notification.png'}); } this.save(); diff --git a/mobile/AndroidManifest.xml b/mobile/AndroidManifest.xml new file mode 100644 index 000000000..07a55c1bb --- /dev/null +++ b/mobile/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/build.sh b/mobile/build.sh new file mode 100644 index 000000000..50ece436a --- /dev/null +++ b/mobile/build.sh @@ -0,0 +1,76 @@ +#! /bin/bash + +# Description: This script compiles and copy the needed files to later package the application for Android + +OpenColor="\033[" +Red="1;31m" +Yellow="1;33m" +Green="1;32m" +CloseColor="\033[0m" + +# Check function OK +checkOK() { + if [ $? != 0 ]; then + echo "${OpenColor}${Red}* ERROR. Exiting...${CloseColor}" + exit 1 + fi +} + +# Configs +BUILDDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +APPDIR="$BUILDDIR/assets/www" +VERSION=`cut -d '"' -f2 $BUILDDIR/../version.js` +RELEASE=false +RUN=false + +if [[ $1 = "--release" ]] +then + RELEASE=true +fi + +if [[ $1 = "-r" ]] +then + RUN=true +fi + +# Move to the build directory +cd $BUILDDIR + +[ -z "$CROSSWALK" ] && { echo "${OpenColor}${Red}* Need to set CROSSWALK environment variable${CloseColor}"; exit 1; } + +# Create/Clean temp dir +echo "${OpenColor}${Green}* Checking temp dir...${CloseColor}" +if [ -d $APPDIR ]; then + rm -rf $APPDIR +fi + +mkdir -p $APPDIR + +# Re-compile copayBundle.js +echo "${OpenColor}${Green}* Generating copay bundle...${CloseColor}" +grunt --target=dev shell +checkOK + +# Copy all app files +echo "${OpenColor}${Green}* Copying all app files...${CloseColor}" +cd $BUILDDIR/.. +cp -af {css,font,img,js,lib,sound,config.js,version.js,$BUILDDIR/cordova.js,$BUILDDIR/cordova_plugins.js,$BUILDDIR/plugins} $APPDIR +checkOK +sed "s/<\!-- PLACEHOLDER: CORDOVA SRIPT -->/