2014-03-14 17:38:27 -03:00
|
|
|
'use strict';
|
2014-04-22 16:07:14 -03:00
|
|
|
|
2014-11-20 01:06:30 -03:00
|
|
|
function selectText(element) {
|
2015-03-06 12:00:10 -03:00
|
|
|
var doc = document;
|
|
|
|
|
if (doc.body.createTextRange) { // ms
|
|
|
|
|
var range = doc.body.createTextRange();
|
|
|
|
|
range.moveToElementText(element);
|
|
|
|
|
range.select();
|
|
|
|
|
} else if (window.getSelection) {
|
|
|
|
|
var selection = window.getSelection();
|
|
|
|
|
var range = doc.createRange();
|
|
|
|
|
range.selectNodeContents(element);
|
|
|
|
|
selection.removeAllRanges();
|
|
|
|
|
selection.addRange(range);
|
2014-11-20 01:06:30 -03:00
|
|
|
|
2015-03-06 12:00:10 -03:00
|
|
|
}
|
2014-11-20 01:06:30 -03:00
|
|
|
}
|
2014-06-03 16:39:06 -03:00
|
|
|
angular.module('copayApp.directives')
|
2015-03-06 12:00:10 -03:00
|
|
|
.directive('validAddress', ['$rootScope', 'bitcore', 'profileService',
|
|
|
|
|
function($rootScope, bitcore, profileService) {
|
|
|
|
|
return {
|
|
|
|
|
require: 'ngModel',
|
|
|
|
|
link: function(scope, elem, attrs, ctrl) {
|
|
|
|
|
var URI = bitcore.URI;
|
|
|
|
|
var Address = bitcore.Address
|
|
|
|
|
var validator = function(value) {
|
2015-04-24 10:28:25 -03:00
|
|
|
if (!profileService.focusedClient)
|
|
|
|
|
return;
|
2015-04-24 02:42:10 -03:00
|
|
|
var networkName = profileService.focusedClient.credentials.network;
|
2015-03-06 12:00:10 -03:00
|
|
|
// Regular url
|
|
|
|
|
if (/^https?:\/\//.test(value)) {
|
|
|
|
|
ctrl.$setValidity('validAddress', true);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
2014-08-07 09:17:03 -07:00
|
|
|
|
2015-03-06 12:00:10 -03:00
|
|
|
// Bip21 uri
|
|
|
|
|
if (/^bitcoin:/.test(value)) {
|
|
|
|
|
var uri, isAddressValid;
|
|
|
|
|
var isUriValid = URI.isValid(value);
|
|
|
|
|
if (isUriValid) {
|
|
|
|
|
uri = new URI(value);
|
|
|
|
|
isAddressValid = Address.isValid(uri.address.toString(), networkName)
|
|
|
|
|
}
|
|
|
|
|
ctrl.$setValidity('validAddress', isUriValid && isAddressValid);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
2014-08-07 09:17:03 -07:00
|
|
|
|
2015-03-06 12:00:10 -03:00
|
|
|
if (typeof value == 'undefined') {
|
|
|
|
|
ctrl.$pristine = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2014-09-12 10:24:27 -03:00
|
|
|
|
2015-03-06 12:00:10 -03:00
|
|
|
// Regular Address
|
|
|
|
|
ctrl.$setValidity('validAddress', Address.isValid(value, networkName));
|
2014-08-13 18:07:57 -04:00
|
|
|
return value;
|
2015-03-06 12:00:10 -03:00
|
|
|
};
|
2014-08-13 18:07:57 -04:00
|
|
|
|
2014-09-12 10:24:27 -03:00
|
|
|
|
2015-03-06 12:00:10 -03:00
|
|
|
ctrl.$parsers.unshift(validator);
|
|
|
|
|
ctrl.$formatters.unshift(validator);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
])
|
2014-10-15 18:16:37 -03:00
|
|
|
.directive('validUrl', [
|
|
|
|
|
|
|
|
|
|
function() {
|
|
|
|
|
return {
|
|
|
|
|
require: 'ngModel',
|
|
|
|
|
link: function(scope, elem, attrs, ctrl) {
|
|
|
|
|
var validator = function(value) {
|
|
|
|
|
// Regular url
|
|
|
|
|
if (/^https?:\/\//.test(value)) {
|
|
|
|
|
ctrl.$setValidity('validUrl', true);
|
|
|
|
|
return value;
|
|
|
|
|
} else {
|
|
|
|
|
ctrl.$setValidity('validUrl', false);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.$parsers.unshift(validator);
|
|
|
|
|
ctrl.$formatters.unshift(validator);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
])
|
2015-03-06 12:00:10 -03:00
|
|
|
.directive('validAmount', ['configService', '$locale',
|
|
|
|
|
function(configService, locale) {
|
2014-09-05 15:54:44 -07:00
|
|
|
|
2014-05-07 19:04:36 -03:00
|
|
|
return {
|
|
|
|
|
require: 'ngModel',
|
|
|
|
|
link: function(scope, element, attrs, ctrl) {
|
|
|
|
|
var val = function(value) {
|
2015-08-07 11:23:32 -03:00
|
|
|
if (value) value = value.replace(/,/g, '.');
|
2015-03-06 12:00:10 -03:00
|
|
|
var settings = configService.getSync().wallet.settings;
|
|
|
|
|
var vNum = Number((value * settings.unitToSatoshi).toFixed(0));
|
2014-08-15 23:00:12 -03:00
|
|
|
|
2015-08-07 11:23:32 -03:00
|
|
|
if (typeof value == 'undefined' || value == 0) {
|
2014-10-07 12:39:16 -03:00
|
|
|
ctrl.$pristine = true;
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-07 19:04:36 -03:00
|
|
|
if (typeof vNum == "number" && vNum > 0) {
|
2015-03-06 12:00:10 -03:00
|
|
|
var decimals = Number(settings.unitDecimals);
|
2015-08-07 11:23:32 -03:00
|
|
|
var sep_index = ('' + value).indexOf('.');
|
2014-10-15 18:16:37 -03:00
|
|
|
var str_value = ('' + value).substring(sep_index + 1);
|
2014-10-07 12:39:16 -03:00
|
|
|
if (sep_index > 0 && str_value.length > decimals) {
|
|
|
|
|
ctrl.$setValidity('validAmount', false);
|
2014-05-07 19:04:36 -03:00
|
|
|
} else {
|
2014-10-07 12:39:16 -03:00
|
|
|
ctrl.$setValidity('validAmount', true);
|
2014-05-07 19:04:36 -03:00
|
|
|
}
|
|
|
|
|
} else {
|
2014-10-07 12:39:16 -03:00
|
|
|
ctrl.$setValidity('validAmount', false);
|
2014-04-23 13:16:20 -03:00
|
|
|
}
|
2014-05-07 19:04:36 -03:00
|
|
|
return value;
|
2014-04-23 13:16:20 -03:00
|
|
|
}
|
2014-05-07 19:04:36 -03:00
|
|
|
ctrl.$parsers.unshift(val);
|
|
|
|
|
ctrl.$formatters.unshift(val);
|
2014-04-23 13:16:20 -03:00
|
|
|
}
|
2014-05-07 19:04:36 -03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
])
|
2015-03-06 12:00:10 -03:00
|
|
|
.directive('walletSecret', function(bitcore) {
|
2014-11-19 17:49:45 -03:00
|
|
|
return {
|
|
|
|
|
require: 'ngModel',
|
|
|
|
|
link: function(scope, elem, attrs, ctrl) {
|
|
|
|
|
var validator = function(value) {
|
2015-03-06 12:00:10 -03:00
|
|
|
if (value.length > 0) {
|
2015-04-16 12:12:12 -03:00
|
|
|
var m = value.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/);
|
2015-03-06 12:00:10 -03:00
|
|
|
ctrl.$setValidity('walletSecret', m ? true : false);
|
|
|
|
|
}
|
2014-11-19 17:49:45 -03:00
|
|
|
return value;
|
|
|
|
|
};
|
2014-05-14 14:24:24 -07:00
|
|
|
|
2014-11-19 17:49:45 -03:00
|
|
|
ctrl.$parsers.unshift(validator);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
})
|
2014-05-07 19:04:36 -03:00
|
|
|
.directive('loading', function() {
|
2014-04-24 22:43:19 -03:00
|
|
|
return {
|
|
|
|
|
restrict: 'A',
|
2014-05-16 18:33:06 -03:00
|
|
|
link: function($scope, element, attr) {
|
2014-04-24 22:43:19 -03:00
|
|
|
var a = element.html();
|
|
|
|
|
var text = attr.loading;
|
2014-05-16 18:33:06 -03:00
|
|
|
element.on('click', function() {
|
2014-06-16 12:44:18 -03:00
|
|
|
element.html('<i class="size-21 fi-bitcoin-circle icon-rotate spinner"></i> ' + text + '...');
|
2014-05-16 18:33:06 -03:00
|
|
|
});
|
|
|
|
|
$scope.$watch('loading', function(val) {
|
|
|
|
|
if (!val) {
|
2014-04-24 22:43:19 -03:00
|
|
|
element.html(a);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2014-05-07 19:04:36 -03:00
|
|
|
.directive('ngFileSelect', function() {
|
2014-04-25 19:11:56 -03:00
|
|
|
return {
|
|
|
|
|
link: function($scope, el) {
|
|
|
|
|
el.bind('change', function(e) {
|
|
|
|
|
$scope.file = (e.srcElement || e.target).files[0];
|
|
|
|
|
$scope.getFile();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-05-26 15:03:39 -03:00
|
|
|
})
|
2014-06-23 17:34:54 -03:00
|
|
|
.directive('contact', function() {
|
|
|
|
|
return {
|
|
|
|
|
restrict: 'E',
|
2014-06-24 08:36:32 -07:00
|
|
|
link: function(scope, element, attrs) {
|
2014-06-23 17:34:54 -03:00
|
|
|
if (!scope.wallet) return;
|
|
|
|
|
|
|
|
|
|
var address = attrs.address;
|
|
|
|
|
var contact = scope.wallet.addressBook[address];
|
2014-07-07 02:21:29 -03:00
|
|
|
if (contact && !contact.hidden) {
|
2014-06-23 17:34:54 -03:00
|
|
|
element.append(contact.label);
|
2015-03-06 12:00:10 -03:00
|
|
|
element.attr('tooltip', attrs.address);
|
2014-06-23 17:34:54 -03:00
|
|
|
} else {
|
|
|
|
|
element.append(address);
|
|
|
|
|
}
|
2014-11-20 01:06:30 -03:00
|
|
|
|
|
|
|
|
element.bind('click', function() {
|
2014-11-20 03:10:43 -03:00
|
|
|
selectText(element[0]);
|
2014-11-20 01:06:30 -03:00
|
|
|
});
|
2014-06-23 17:34:54 -03:00
|
|
|
}
|
2014-06-24 08:36:32 -07:00
|
|
|
};
|
2014-06-23 17:34:54 -03:00
|
|
|
})
|
2014-06-11 17:49:14 -03:00
|
|
|
.directive('highlightOnChange', function() {
|
|
|
|
|
return {
|
|
|
|
|
restrict: 'A',
|
|
|
|
|
link: function(scope, element, attrs) {
|
2014-06-16 12:44:18 -03:00
|
|
|
scope.$watch(attrs.highlightOnChange, function(newValue, oldValue) {
|
2014-06-11 17:49:14 -03:00
|
|
|
element.addClass('highlight');
|
2014-06-16 12:44:18 -03:00
|
|
|
setTimeout(function() {
|
|
|
|
|
element.removeClass('highlight');
|
|
|
|
|
}, 500);
|
2014-06-11 17:49:14 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2014-05-26 15:03:39 -03:00
|
|
|
.directive('checkStrength', function() {
|
|
|
|
|
return {
|
|
|
|
|
replace: false,
|
|
|
|
|
restrict: 'EACM',
|
2014-06-02 14:39:12 -03:00
|
|
|
require: 'ngModel',
|
2014-05-26 15:03:39 -03:00
|
|
|
link: function(scope, element, attrs) {
|
2014-06-16 12:44:18 -03:00
|
|
|
|
2014-07-14 11:59:47 -03:00
|
|
|
var MIN_LENGTH = 8;
|
|
|
|
|
var MESSAGES = ['Very Weak', 'Very Weak', 'Weak', 'Medium', 'Strong', 'Very Strong'];
|
2014-12-18 20:05:13 -03:00
|
|
|
var COLOR = ['#dd514c', '#dd514c', '#faa732', '#faa732', '#16A085', '#16A085'];
|
2014-06-16 12:44:18 -03:00
|
|
|
|
2014-07-14 11:59:47 -03:00
|
|
|
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 {
|
|
|
|
|
text = ', add mixed case';
|
|
|
|
|
}
|
|
|
|
|
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++;
|
2014-06-16 12:44:18 -03:00
|
|
|
} else {
|
2014-07-14 11:59:47 -03:00
|
|
|
if (!text) text = ', add characters';
|
2014-06-16 12:44:18 -03:00
|
|
|
}
|
2014-07-14 11:59:47 -03:00
|
|
|
} else {
|
|
|
|
|
text = ', that\'s short';
|
|
|
|
|
}
|
|
|
|
|
if (!text) text = '';
|
2014-06-16 12:44:18 -03:00
|
|
|
|
2014-07-14 11:59:47 -03:00
|
|
|
return {
|
|
|
|
|
strength: passwordStrength,
|
|
|
|
|
message: MESSAGES[passwordStrength] + text,
|
|
|
|
|
color: COLOR[passwordStrength]
|
2014-05-26 15:03:39 -03:00
|
|
|
}
|
2014-07-14 11:59:47 -03:00
|
|
|
}
|
2014-06-02 14:39:12 -03:00
|
|
|
|
2014-06-16 12:44:18 -03:00
|
|
|
scope.$watch(attrs.ngModel, function(newValue, oldValue) {
|
2014-05-26 15:03:39 -03:00
|
|
|
if (newValue && newValue !== '') {
|
2014-07-14 11:59:47 -03:00
|
|
|
var info = evaluateMeter(newValue);
|
2014-12-17 10:37:11 -03:00
|
|
|
scope[attrs.checkStrength] = info;
|
2014-05-26 15:03:39 -03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2014-06-16 15:51:19 -03:00
|
|
|
})
|
2014-12-03 03:03:16 -03:00
|
|
|
.directive('showFocus', function($timeout) {
|
|
|
|
|
return function(scope, element, attrs) {
|
2015-03-06 12:00:10 -03:00
|
|
|
scope.$watch(attrs.showFocus,
|
|
|
|
|
function(newValue) {
|
2014-12-03 03:03:16 -03:00
|
|
|
$timeout(function() {
|
2015-03-06 12:00:10 -03:00
|
|
|
newValue && element[0].focus();
|
2014-12-03 03:03:16 -03:00
|
|
|
});
|
2015-03-06 12:00:10 -03:00
|
|
|
}, true);
|
|
|
|
|
};
|
2014-12-03 03:03:16 -03:00
|
|
|
})
|
2014-08-13 18:07:57 -04:00
|
|
|
.directive('match', function() {
|
2014-07-18 17:56:50 -03:00
|
|
|
return {
|
|
|
|
|
require: 'ngModel',
|
|
|
|
|
restrict: 'A',
|
|
|
|
|
scope: {
|
2014-08-13 18:07:57 -04:00
|
|
|
match: '='
|
2014-07-18 17:56:50 -03:00
|
|
|
},
|
|
|
|
|
link: function(scope, elem, attrs, ctrl) {
|
2014-07-18 18:17:45 -03:00
|
|
|
scope.$watch(function() {
|
|
|
|
|
return (ctrl.$pristine && angular.isUndefined(ctrl.$modelValue)) || scope.match === ctrl.$modelValue;
|
|
|
|
|
}, function(currentValue) {
|
|
|
|
|
ctrl.$setValidity('match', currentValue);
|
|
|
|
|
});
|
2014-07-18 17:56:50 -03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
})
|
2014-07-31 16:34:21 -03:00
|
|
|
.directive('clipCopy', function() {
|
|
|
|
|
return {
|
2014-12-16 10:47:31 +01:00
|
|
|
restrict: 'A',
|
2014-08-13 18:07:57 -04:00
|
|
|
scope: {
|
|
|
|
|
clipCopy: '=clipCopy'
|
|
|
|
|
},
|
2014-07-31 16:34:21 -03:00
|
|
|
link: function(scope, elm) {
|
2014-11-20 01:06:30 -03:00
|
|
|
// TODO this does not work (FIXME)
|
2015-03-06 12:00:10 -03:00
|
|
|
elm.attr('tooltip', 'Press Ctrl+C to Copy');
|
|
|
|
|
elm.attr('tooltip-placement', 'top');
|
2014-08-28 14:59:19 -03:00
|
|
|
|
2014-11-20 01:06:30 -03:00
|
|
|
elm.bind('click', function() {
|
|
|
|
|
selectText(elm[0]);
|
2014-07-31 16:34:21 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
Addon support
Addons are simple Angular modules with views, controllers, services etc. Addons can register
themselves in Copay using pluginManagerProvider. It allows them to add extra items to the bottom
menu and as well as extra tab-views:
````
addonManagerProvider.registerAddon({
menuItem: {
'title': 'Assets',
'icon': 'icon-pricetag',
'link': 'assets'
},
view: {
id: 'assets',
'class': 'assets',
template: 'colored-coins/views/assets.html'
}
});
````
Addons can consume core Copay services and listen for events to react on changes. For this very
first addon system inplementation Copay emits additional BalanceUpdated event so that interested
addons can react on new transactions (see plugin reference implementation below).
As bottom menu can accomodate only 6 items without sacrificing usability, so it was reworked to
have second layer of items. Now If menu has more than 6 items, toggle button will be added to
the menu allowing to reveal extra items in a sliding panel. Bottom menu in this case will show
only 5 items, the rest will be rendered on sliding panel.
This changes addresses issue #2949 and reference implementation of addon could be found here:
https://github.com/troggy/copay-colored-coins-plugin
2015-07-04 13:02:46 +03:00
|
|
|
})
|
|
|
|
|
.directive('menuToggle', function() {
|
|
|
|
|
return {
|
|
|
|
|
restrict: 'E',
|
|
|
|
|
replace: true,
|
|
|
|
|
templateUrl: 'views/includes/menu-toggle.html'
|
|
|
|
|
}
|
2014-07-31 14:06:55 -03:00
|
|
|
});
|