diff --git a/www/i18n/locale-en-GB.json b/www/i18n/locale-en-GB.json index cbb19401f213ef2c5dd92d50257487f9eb7c3606..163e74215fda97b0238527ada7a506fab4409f7f 100644 --- a/www/i18n/locale-en-GB.json +++ b/www/i18n/locale-en-GB.json @@ -415,7 +415,8 @@ "SCRYPT_DEFAULT": "Standard salt (default)", "SCRYPT_ADVANCED": "Advanced salt", "FILE": "Keychain file", - "PUBKEY": "Public key only" + "PUBKEY": "Public key only", + "SCAN": "Scan a QR code" }, "SCRYPT": { "SIMPLE": "Light salt", diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json index 9ac501dd1a71177a1265d7b8238e0615e509af20..8d455d5fc9bed0e8c5a0eacaa7cc89e5c4d098ec 100644 --- a/www/i18n/locale-en.json +++ b/www/i18n/locale-en.json @@ -415,7 +415,8 @@ "SCRYPT_DEFAULT": "Standard salt (default)", "SCRYPT_ADVANCED": "Advanced salt", "FILE": "Keychain file", - "PUBKEY": "Public key only" + "PUBKEY": "Public key only", + "SCAN": "Scan a QR code" }, "SCRYPT": { "SIMPLE": "Light salt", diff --git a/www/i18n/locale-es-ES.json b/www/i18n/locale-es-ES.json index 3196779daa38ea3290d2c76c5313afdd1365ad15..eb09ca109370465c815298c650ea83690b581718 100644 --- a/www/i18n/locale-es-ES.json +++ b/www/i18n/locale-es-ES.json @@ -89,6 +89,7 @@ "WOT": "Directorio", "CURRENCY": "Moneda", "ACCOUNT": "Mi cuenta", + "WALLETS": "Mis carteras", "TRANSFER": "Transferencia", "SCAN": "Escáner", "SETTINGS": "Configuraciones", @@ -129,6 +130,8 @@ "ENABLE_UI_EFFECTS": "Activar los efectos visuales", "HISTORY_SETTINGS": "Operaciones de cuentas", "DISPLAY_UD_HISTORY": "¿Publicar los dividendos producidos?", + "TX_HISTORY_AUTO_REFRESH": "Activar actualización automática?", + "TX_HISTORY_AUTO_REFRESH_HELP": "Actualice el saldo y las operaciones automáticamente, en cada nuevo bloque.", "AUTHENTICATION_SETTINGS": "Autenticación", "KEEP_AUTH": "Caducidad de la autenticación", "KEEP_AUTH_HELP": "Definir cuándo limpiar la memoria de autenticación", @@ -263,6 +266,7 @@ "WARN_PRE_RELEASE": "prelanzamiento (última versión estable: <b>{{version}}</b>)", "WARN_NEW_RELEASE": "Versión <b>{{version}}</b> disponible", "WS2PID": "ID:", + "PRIVATE_ACCESS": "Acceso privado", "POW_PREFIX": "Prefijo de la prueba de trabajo:", "ENDPOINTS": { "BMAS": "Interfaz segura (SSL)", @@ -390,12 +394,39 @@ "MESSAGE": "<i class=\"ion-android-time\"></i> Se ha <b>desconectado</b> de forma automática, después de un periodo de inactividad prolongada.", "BTN_RELOGIN": "Volver a conectarme", "IDLE_WARNING": "Se desconectará en... {{countdown}}" + }, + "METHOD": { + "SCRYPT_DEFAULT": "Identificación predeterminada", + "SCRYPT_ADVANCED": "Sallaje avanzado", + "FILE": "Archivo llavero", + "PUBKEY": "Solo clave pública", + "SCAN": "Escanear un código QR" + }, + "SCRYPT": { + "SIMPLE": "Ligero", + "DEFAULT": "Predeterminado", + "SECURE": "Seguro", + "HARDEST": "El más seguro", + "EXTREME": "Extremo", + "USER": "Personalizado", + "N": "N (Loop):", + "r": "r (RAM):", + "p": "p (CPU):" + }, + "FILE": { + "DATE" : "Fecha:", + "TYPE" : "Tipo:", + "SIZE": "Tamaño:", + "VALIDATING": "Validación en progreso...", + "HELP": "Formato de archivo esperado: <b>.yml</b> o <b>.dunikey</b> (tipo PubSec, WIF o EWIF)." } }, "AUTH": { "TITLE": "<i class=\"icon ion-locked\"></i> AAutenticación", "BTN_AUTH": "Autenticar", - "GENERAL_HELP": "Por favor, autentíquese:" + "GENERAL_HELP": "Por favor, autentíquese:", + "EXPECTED_UID_HELP": "Por favor inicie sesión en la cuenta de <i class=\"ion-person\"></i> {{uid}}:", + "EXPECTED_PUBKEY_HELP": "Por favor, autentifíquese en la monedero <i class=\"ion-key\"></i> {{pubkey|formatPubkey}} :" }, "ACCOUNT": { "TITLE": "Mi cuenta", @@ -407,6 +438,7 @@ "SHOW_ALL_TX": "Mostrar todo", "TX_FROM_DATE": "(límite actual a {{fromTime|formatFromNowShort}})", "PENDING_TX": "Transacciones en proceso de se procesadas", + "VALIDATING_TX": "Transacciones validadas", "ERROR_TX": "Transacciones no ejecutadas", "ERROR_TX_SENT": "Transacciones ejecutadas fallidas", "PENDING_TX_RECEIVED": "Tansacciones en espera de recepción", @@ -422,11 +454,9 @@ "SIG_STOCK": "Certificaciones emitidas", "BTN_RECEIVE_MONEY": "Recibir", "BTN_SELECT_ALTERNATIVES_IDENTITIES": "Cambiar a otra identidad...", - "BTN_MEMBERSHIP_IN_DOTS": "Volverse miembro...", "BTN_MEMBERSHIP_RENEW": "Renovar la adhesión", "BTN_MEMBERSHIP_RENEW_DOTS": "Renovar la adhesión...", "BTN_MEMBERSHIP_OUT_DOTS": "Cancelar la adhesión...", - "BTN_SEND_IDENTITY_DOTS": "Publicar su identidad...", "BTN_SECURITY_DOTS": "Cuenta y securidad...", "BTN_SHOW_DETAILS": "Publicar la información técnica", "LOCKED_OUTPUTS_POPOVER": { @@ -535,12 +565,16 @@ "COMMENT_HELP": "Comentario", "BTN_SEND": "Enviar", "BTN_ADD_COMMENT": "Añadir un comentario", + "REST": "Resto de cuenta", + "REST_TO": "a", "WARN_COMMENT_IS_PUBLIC": "Tenga en cuenta que los <b>comentarios son públicos</b> (sin encriptar).", "MODAL": { "TITLE": "Transferencia" } }, "ERROR": { + "UNKNOWN_URI_FORMAT": "Formato URI desconocido", + "PUBKEY_INVALID_CHECKSUM": "Clave pública no válida (suma de comprobación incorrecta).", "POPUP_TITLE": "Error", "UNKNOWN_ERROR": "Error desconocido", "CRYPTO_UNKNOWN_ERROR": "Su navegador parece incompatible con las funcionalidades de cryptografía.", @@ -592,7 +626,8 @@ "INVALID_NODE_SUMMARY": "Nodo ilocalizable o dirección inválida.", "INVALID_USER_ID": "El seudónimo no debe contener ni espacios ni caracteres especiales o acentuado.", "INVALID_COMMENT": "El campo 'referencia’ no debe contener carácteres acentuados.", - "INVALID_PUBKEY": "La llave pública no tiene el formato esperado.", + "INVALID_PUBKEY": "La clave pública no tiene el formato esperado.", + "INVALID_PUBKEY_CHECKSUM": "Suma de comprobación inválida.", "IDENTITY_REVOKED": "Esta identidad <b>fue revocada {{revocationTime|formatFromNow}}</b> ({{revocationTime|formatDate}}). No puede estar miembro.", "IDENTITY_PENDING_REVOCATION": "La <b>revocación de esta identidad</b> fue solicitado y esta en espera de tratamiento. Por lo que, la certificación es desactivada.", "IDENTITY_INVALID_BLOCK_HASH": "Esta solicitud de adhesión no es valida (porque denomina un bloque los nodos de la red han anulado): esta persona debe renovelar su solicitud de adhesión <b>antes que</b> estar certificada.", diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json index 971f91659efc5f1f4d6c0e747d0b3983a3ee0f88..7ba68b4e1d30c421b0366bb7643d58d54a42be41 100644 --- a/www/i18n/locale-fr-FR.json +++ b/www/i18n/locale-fr-FR.json @@ -415,7 +415,8 @@ "SCRYPT_DEFAULT": "Sallage standard (par défaut)", "SCRYPT_ADVANCED": "Sallage avancé", "FILE": "Fichier de trousseau", - "PUBKEY": "Clé publique seule" + "PUBKEY": "Clé publique seule", + "SCAN": "Scanner un QR code" }, "SCRYPT": { "SIMPLE": "Sallage léger", @@ -674,7 +675,7 @@ "LOAD_WALLET_DATA_ERROR": "Echec du chargement des données du portefeuille.", "COPY_CLIPBOARD_FAILED": "Copie de la valeur impossible.", "TAKE_PICTURE_FAILED": "Echec de la récupération de la photo.", - "SCAN_FAILED": "Echec du scan de QR Code", + "SCAN_FAILED": "Echec du scan de QR code", "SCAN_UNKNOWN_FORMAT": "Code non reconnu.", "WOT_LOOKUP_FAILED": "Echec de la recherche", "LOAD_PEER_DATA_FAILED": "Lecture du nœud Duniter impossible. Veuillez réessayer ultérieurement.", diff --git a/www/js/config.js b/www/js/config.js index 79a743c695ec21539ae6ca89ff9aa02f96101444..2cc6d8a426db84fda0bd81a37d2da8a80949de5e 100644 --- a/www/js/config.js +++ b/www/js/config.js @@ -12,64 +12,38 @@ angular.module("cesium.config", []) "cacheTimeMs": 300000, "fallbackLanguage": "en", "rememberMe": true, - "showUDHistory": true, - "timeout": 30000, + "timeout": 300000, "timeWarningExpireMembership": 5184000, "timeWarningExpire": 7776000, - "keepAuthIlde": 600, "useLocalStorage": true, "useRelative": false, - "expertMode": false, + "expertMode": true, "decimalCount": 2, - "httpsMode": false, "shareBaseUrl": "https://g1.duniter.fr", "helptip": { - "enable": true, + "enable": false, "installDocUrl": { "fr-FR": "https://duniter.org/fr/wiki/duniter/installer/", - "en": "https://duniter.org/en/wiki/duniter/install/" + "en": "https://github.com/duniter/duniter/blob/master/doc/install-a-node.md" } }, - "license": { - "fr-FR": "license/license_g1-fr-FR", - "en": "license/license_g1-en" - }, "node": { - "host": "g1.duniter.org", - "port": "443" + "host": "g1-test.duniter.org", + "port": 443 }, "fallbackNodes": [ { - "host": "g1.duniter.fr", - "port": "443" - }, - { - "host": "g1.duniter.org", - "port": "443" - } - ], - "developers": [ - { - "name": "Benoit Lavenier", - "pubkey": "38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE" + "host": "g1-test.cgeek.fr", + "port": 443 } ], "plugins": { "es": { "enable": true, "askEnable": true, - "host": "g1.data.duniter.fr", - "port": "443", - "fallbackNodes": [ - { - "host": "g1.data.le-sou.org", - "port": "443" - }, - { - "host": "g1.data.duniter.fr", - "port": "443" - } - ], + "useRemoteStorage": true, + "host": "g1-test.data.duniter.fr", + "port": 443, "notifications": { "txSent": true, "txReceived": true, @@ -80,8 +54,8 @@ angular.module("cesium.config", []) } }, "version": "1.0.6", - "build": "2018-05-09T15:28:09.984Z", - "newIssueUrl": "https://git.duniter.org/clients/cesium/cesium/issues/new" + "build": "2018-08-27T12:18:36.072Z", + "newIssueUrl": "https://git.duniter.org/clients/cesium-grp/cesium/issues/new" }) ; \ No newline at end of file diff --git a/www/js/controllers/app-controllers.js b/www/js/controllers/app-controllers.js index b1f95798cd2448d0543f0e7964505583248f2906..81c866e7fae056a21309f16c31264d2b32db5a67 100644 --- a/www/js/controllers/app-controllers.js +++ b/www/js/controllers/app-controllers.js @@ -89,7 +89,7 @@ function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $ // (code removed when NO device) //////////////////////////////////////// - function parseWIF_or_EWIF(data, options) { + $scope.parseWIF = function(data, options) { options = options || {}; options.withSecret = angular.isDefined(options.withSecret) && options.withSecret || true; options.password = function() { @@ -116,7 +116,7 @@ function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $ if (err && err == 'CANCELLED') return; if (err && err.ucode == csCrypto.errorCodes.BAD_PASSWORD) { // recursive call - return parseWIF_or_EWIF(data, {withSecret: options.withSecret, error: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_PASSWORD'}); + return $scope.parseWIF(data, {withSecret: options.withSecret, error: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_PASSWORD'}); } console.error("[app] Unable to parse as WIF or EWIF format: " + (err && err.message || err)); throw err; // rethrow @@ -136,7 +136,7 @@ function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $ // Try to parse as an URI return BMA.uri.parse(data) .then(function(res){ - if (!res || res.pubkey) throw {message: 'ERROR.SCAN_UNKNOWN_FORMAT'}; + if (!res || !res.pubkey) throw {message: 'ERROR.SCAN_UNKNOWN_FORMAT'}; // If pubkey: open the identity return $state.go('app.wot_identity', { pubkey: res.pubkey, @@ -149,48 +149,32 @@ function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $ console.debug("[app] Scan data is not an URI (get error: " + (err && err.message || err) + "). Trying to decode as a WIF or EWIF format..."); // Try to read as WIF format - return parseWIF_or_EWIF(data) + return $scope.parseWIF(data) .then(function(keypair) { if (!keypair || !keypair.signPk || !keypair.signSk) throw err; // rethrow the first error (e.g. Bad URI) - // User already logged - if (csWallet.isLogin()) { - // Open a temporary wallet - var wallet = csWallet.instance('WIF'); - csWallet.children.add(wallet); - return wallet.login({ + var pubkey = CryptoUtils.base58.encode(keypair.signPk); + console.debug("[app] Detected WIF/EWIF format. Will login to wallet {" + pubkey.substring(0, 8) + "}"); + + // Create the wallet (if need) or use default + var wallet = !csWallet.isLogin() ? csWallet : csWallet.children.create({store: false}); + + // Login using keypair + return wallet.login({ forceAuth: true, minData: false, authData: { - pubkey: CryptoUtils.base58.encode(keypair.signPk), + pubkey: pubkey, keypair: keypair } }) .then(function () { - // Transfer all wallet + // Open transfer all wallet return $state.go('app.new_transfer', { - all: true, - wallet: wallet.id + all: true, // transfer all sources + wallet: !wallet.isDefault() ? wallet.id : undefined }); }); - } - // TODO: Use a temporary wallet ? - // var wallet = csWallet.instance('WIF'); - // return wallet.login(...) - - return csWallet.login({ - forceAuth: true, - minData: false, - authData: { - pubkey: CryptoUtils.base58.encode(keypair.signPk), - keypair: keypair - } - }) - .then(function () { - // Transfer all wallet - return $state.go('app.new_transfer', {all: true}); - }); - }) // Unknown format (nor URI, nor WIF/EWIF) .catch(UIUtils.onError('ERROR.SCAN_UNKNOWN_FORMAT')); diff --git a/www/js/controllers/login-controllers.js b/www/js/controllers/login-controllers.js index 9558054dbdba4d7f635f6450bcce9b766845d142..8889928e8819af1ec85ab702c6d010ff0b656fbb 100644 --- a/www/js/controllers/login-controllers.js +++ b/www/js/controllers/login-controllers.js @@ -190,6 +190,52 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils, } } + // Scan QR code + else if (method === 'SCAN' && Device.barcode.enable) { + + // Run scan cordova plugin, on device + promise = Device.barcode.scan() + .then(function(data) { + if (!data) return; + + // Try to parse as an URI + return BMA.uri.parse(data) + .then(function(res){ + if (!res || !res.pubkey) throw {message: 'ERROR.SCAN_UNKNOWN_FORMAT'}; + // If simple pubkey + promise = UIUtils.loading.show() + .then(function() { + return { + pubkey: pubkey + }; + }); + }) + + // Not an URI: try WIF or EWIF format + .catch(function(err) { + console.debug("[login] Scan data is not an URI (get error: " + (err && err.message || err) + "). Trying to decode as a WIF or EWIF format..."); + + // Try to read as WIF format + return $scope.parseWIF(data) + .then(function(keypair) { + if (!keypair || !keypair.signPk || !keypair.signSk) throw err; // rethrow the first error (e.g. Bad URI) + + var pubkey = CryptoUtils.base58.encode(keypair.signPk); + console.debug("[login] Detected WIF/EWIF format. Will login to wallet {" + pubkey.substring(0, 8) + "}"); + + // Login using keypair + return { + pubkey: pubkey, + keypair: keypair + }; + }) + // Unknown format (nor URI, nor WIF/EWIF) + .catch(UIUtils.onError('ERROR.SCAN_UNKNOWN_FORMAT')); + }); + }) + .catch(UIUtils.onError('ERROR.SCAN_FAILED')); + } + if (!promise) { console.warn('[login] unknown method: ', method); return; diff --git a/www/js/controllers/transfer-controllers.js b/www/js/controllers/transfer-controllers.js index bb3c8a6d2ccfcebb46345e7bb7818e74e5959754..ef8cdb9570c4a7a0dcce051c0c9f7411ad12d954 100644 --- a/www/js/controllers/transfer-controllers.js +++ b/www/js/controllers/transfer-controllers.js @@ -6,7 +6,7 @@ angular.module('cesium.transfer.controllers', ['cesium.services', 'cesium.curren .state('app.new_transfer', { cache: false, - url: "/transfer?amount&udAmount&comment&restPub&all", + url: "/transfer?amount&udAmount&comment&restPub&all&wallet", views: { 'menuContent': { templateUrl: "templates/wallet/new_transfer.html", @@ -17,7 +17,7 @@ angular.module('cesium.transfer.controllers', ['cesium.services', 'cesium.curren .state('app.new_transfer_pubkey_uid', { cache: false, - url: "/transfer/:pubkey/:uid?amount&udAmount&comment&restPub&all", + url: "/transfer/:pubkey/:uid?amount&udAmount&comment&restPub&all&wallet", views: { 'menuContent': { templateUrl: "templates/wallet/new_transfer.html", @@ -28,7 +28,7 @@ angular.module('cesium.transfer.controllers', ['cesium.services', 'cesium.curren .state('app.new_transfer_pubkey', { cache: false, - url: "/transfer/:pubkey?amount&udAmount&comment&restPub&all", + url: "/transfer/:pubkey?amount&udAmount&comment&restPub&all&wallet", views: { 'menuContent': { templateUrl: "templates/wallet/new_transfer.html", @@ -145,8 +145,6 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc $scope.restPub = parameters.restPub; $scope.formData.restPub = parameters.restPub; $scope.formData.all = true; - $scope.$watch('walletData.balance', $scope.onAmountChanged, true); - $scope.$watch('formData.amount', $scope.onAmountChanged, true); } else { $scope.formData.all = false; @@ -175,6 +173,13 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc $scope.formData.walletId = wallet.id; $scope.onUseRelativeChanged(); $scope.onAmountChanged(); + + $scope.$watch('walletData.balance', $scope.onAmountChanged, true); + $scope.$watch('formData.amount', $scope.onAmountChanged, true); + + $scope.$watch('formData.useRelative', $scope.onUseRelativeChanged, true); + $scope.$watch('walletData.balance', $scope.onUseRelativeChanged, true); + UIUtils.ink({selector: '.modal-transfer .ink'}); if (!$scope.destPub || $scope.destUid) { @@ -217,25 +222,52 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc $scope.form.$valid = undefined; } }; - $scope.$watch('formData.useRelative', $scope.onUseRelativeChanged, true); - $scope.$watch('walletData.balance', $scope.onUseRelativeChanged, true); $scope.onAmountChanged = function() { - if (!$scope.formData.all || !$scope.formData.amount) { - $scope.formData.restAmount = undefined; - return; - } + var amount = $scope.formData.amount; - if (typeof amount === "string") { + if (amount && typeof amount === "string") { amount = parseFloat(amount.replace(new RegExp('[.,]'), '.')); } - if ($scope.formData.useRelative) { - $scope.formData.restAmount = $scope.walletData.balance - amount * csCurrency.data.currentUD; - if ($scope.formData.restAmount < minQuantitativeAmount) { - $scope.formData.restAmount = 0; + + var valid = true; + + // Avoid amount less than the minimal - fix #373 + if (amount && amount < $scope.minAmount) { + valid = false; + $scope.form.amount.$error = $scope.form.amount.$error || {}; + $scope.form.amount.$error.min = true; + } + else if ($scope.form.amount.$error && $scope.form.amount.$error.min){ + delete $scope.form.amount.$error.min; + } + + // Avoid amount greater than the balance + if (amount && amount > $scope.convertedBalance){ + $scope.form.$valid = false; + $scope.form.amount.$invalid = true; + $scope.form.amount.$error = $scope.form.amount.$error || {}; + $scope.form.amount.$error = {max: true}; + } + else if ($scope.form.amount.$error && $scope.form.amount.$error.max){ + delete $scope.form.amount.$error.max; + } + + $scope.form.$valid = valid; + $scope.form.amount.$invalid = !valid; + + if (!valid || !$scope.formData.all || !amount) { + $scope.formData.restAmount = undefined; + } + else { + if ($scope.formData.useRelative) { + $scope.formData.restAmount = $scope.walletData.balance - amount * csCurrency.data.currentUD; + if ($scope.formData.restAmount < minQuantitativeAmount) { + $scope.formData.restAmount = 0; + } + } else { + $scope.formData.restAmount = $scope.walletData.balance - amount * 100; } - } else { - $scope.formData.restAmount = $scope.walletData.balance - amount * 100; } }; @@ -250,19 +282,6 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc amount = parseFloat(amount.replace(new RegExp('[.,]'), '.')); } - // Avoid amount less than the minimal - fix #373 - if (amount < $scope.minAmount) { - $scope.form.$valid = false; - $scope.form.amount.$invalid = true; - $scope.form.amount.$error = $scope.form.amount.$error || {}; - $scope.form.amount.$error.min = true; - return; - } - else if ($scope.form.amount.$error && $scope.form.amount.$error.min){ - $scope.form.amount.$invalid = false; - delete $scope.form.amount.$error.min; - } - // Avoid multiple call if ($scope.sending) return; $scope.sending = true; diff --git a/www/js/controllers/wallet-controllers.js b/www/js/controllers/wallet-controllers.js index c65809ff1d3f4cdd4f9129548ba4b80f50d34f19..dba0f095db6e5da39e8a86c1faa7ecc7194fee76 100644 --- a/www/js/controllers/wallet-controllers.js +++ b/www/js/controllers/wallet-controllers.js @@ -883,6 +883,13 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location, // TODO }; + $scope.showTxErrors = function(event) { + if (wallet.isDefault()) { + return $scope.goState('app.view_wallet_tx_errors'); + } + return $scope.goState('app.view_wallet_tx_errors_by_id', {id: wallet.id}); + }; + $scope.showMoreTx = function(fromTime) { fromTime = fromTime || @@ -1030,7 +1037,7 @@ function WalletTxErrorController($scope, UIUtils, csSettings, csWallet) { $scope.loading = true; $scope.formData = {}; - $scope.$on('$ionicView.enter', function(e) { + $scope.$on('$ionicView.enter', function(e, state) { wallet = (state.stateParams && state.stateParams.id) ? csWallet.children.get(state.stateParams.id) : csWallet; if (!wallet) { @@ -1049,7 +1056,7 @@ function WalletTxErrorController($scope, UIUtils, csSettings, csWallet) { $scope.formData = walletData; $scope.loading = false; $scope.doMotion(); - $scope.showFab('fab-redo-transfer'); + //$scope.showFab('fab-redo-transfer'); UIUtils.loading.hide(); }); }; @@ -1059,7 +1066,7 @@ function WalletTxErrorController($scope, UIUtils, csSettings, csWallet) { $scope.loading = true; return (silent ? - csWallet.refreshData() : + wallet.refreshData() : UIUtils.loading.show() .then(csWallet.refreshData) .then(UIUtils.loading.hide) @@ -1111,11 +1118,12 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa $scope.isValidFile = false; - $scope.isLogin = wallet.isLogin(); + $scope.login = wallet.isLogin(); $scope.hasSelf = wallet.hasSelf(); - $scope.needSelf = $scope.isLogin && wallet.data.requirements.needSelf; - $scope.needMembership = $scope.isLogin && wallet.data.requirements.needMembership; - $scope.option = $scope.isLogin ? 'saveID' : 'recoverID'; + $scope.needSelf = $scope.login && wallet.data.requirements.needSelf; + $scope.canRevoke = $scope.login && $scope.hasSelf && !wallet.data.requirements.revoked; + $scope.needMembership = $scope.login && wallet.data.requirements.needMembership; + $scope.option = $scope.login ? 'saveID' : 'recoverID'; $scope.formData = { addQuestion: '', @@ -1532,6 +1540,7 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl return $scope.load() .then(function() { + UIUtils.loading.hide(); if (!$scope.wallets) return; // user cancel $scope.addListeners(); $scope.showFab('fab-add-wallet'); @@ -1570,8 +1579,8 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl // Save changes return csWallet.auth({minData: true}) .then(function() { - wallet.data.name = newName; - csWallet.store(); + wallet.data.localName = newName; + csWallet.storeData(); UIUtils.loading.hide(); $scope.updateView(); }) diff --git a/www/js/services/bma-services.js b/www/js/services/bma-services.js index c411664b3eff49502cd94403c55ef19b9c11bff7..ad0381788737eea3b2606d24ef70df48e5d06270 100644 --- a/www/js/services/bma-services.js +++ b/www/js/services/bma-services.js @@ -729,11 +729,17 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium. var matches = exports.regexp.PUBKEY_WITH_CHECKSUM.exec(uri); pubkey = matches[1]; var checksum = matches[2]; + console.debug("[BMA.parse] Detecting a pubkey {"+pubkey+"} with checksum {" + checksum + "}"); var expectedChecksum = csCrypto.util.pkChecksum(pubkey); - if (checksum != expectedChecksum) throw {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'}; - resolve({ - pubkey: pubkey - }); + console.debug("[BMA.parse] Expecting checksum for pubkey is {" + expectedChecksum + "}"); + if (checksum != expectedChecksum) { + reject( {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'}); + } + else { + resolve({ + pubkey: pubkey + }); + } } else if(uri.startsWith('duniter://')) { var parser = csHttp.uri.parse(uri), diff --git a/www/js/services/wallet-services.js b/www/js/services/wallet-services.js index 8a147530f07541d35bc7d4ea26c6914045360bc2..96eecea20852461fd2ef498f04dbd5e7fa0e2569 100644 --- a/www/js/services/wallet-services.js +++ b/www/js/services/wallet-services.js @@ -45,6 +45,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se data.pubkey= null; data.uid = null; + data.localName = null; data.isNew = null; data.sourcesIndexByKey = null; data.medianTime = null; @@ -208,8 +209,8 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se .then(function() { if (needLogin) { - // store wallet (pubkey+uid) - store({skipData: true}); + // store wallet + store(); } // Send auth event (if need) @@ -352,6 +353,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se // Check registration data.isMember || data.requirements.pendingMembership || + data.requirements.revoked || !data.requirements.needSelf || data.requirements.wasMember || @@ -360,6 +362,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se data.tx.pendings.length || // Check extended data (name+avatar) + data.localName || data.name || data.avatar ); @@ -489,7 +492,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se return { pubkey: wallet.data.pubkey, uid: wallet.data.uid, - name: wallet.data.name + localName: wallet.data.localName }; }); } @@ -606,7 +609,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se }, applyRestoredData = function(content) { - if (!content) return; // skip + if (!content) return $q.when(); // skip // Apply children if (content.children) { @@ -616,7 +619,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se var walletId = index+1; var wallet = service.instance(walletId, BMA); wallet.data.pubkey = child.pubkey; - wallet.data.name = child.name; + wallet.data.localName = child.localName; wallet.data.uid = child.uid; addChildWallet(wallet, {store: false/*skip store*/}); }); @@ -634,13 +637,13 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se // If auth: open encrypted data if (hasEncryptedData() && isAuth()) { - return openEncryptedData() + return openEncryptedData({store: false}) .then(function(){ return data; // Important: return the data }); } - return data; // Important: return the data + return $q.when(data); // Important: return the data }, getData = function() { @@ -1664,7 +1667,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se return getSaveIDDocument(record) .then(function(saveId) { var saveIdFile = new Blob([saveId], {type: 'text/plain; charset=utf-8'}); - FileSaver.saveAs(saveIdFile, 'saveID-{0}.txt'.format(data.pubkey.substring(0,6))); + FileSaver.saveAs(saveIdFile, '{0}-recover_ID.txt'.format(data.pubkey.substring(0,8))); }); }, @@ -1876,6 +1879,13 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se };*/ }, + createNewChildWallet = function(options) { + var walletId = getChildrenWalletCount()+1; + var child = service.instance(walletId); + addChildWallet(wallet, options); + return wallet; + }, + addChildWallet = function(wallet, options) { // Link to parent wallet.children.setParent(exports); // = link to self wallet @@ -1947,7 +1957,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se }); }, - openEncryptedData = function() { + openEncryptedData = function(options) { if (!hasEncryptedData()) return $q.when(); if (!isAuth()) return auth().then(openEncryptedData); // Force auth if need @@ -1960,8 +1970,18 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se // Then apply .then(function(content) { data.encryptedData = null; // reset encrypted data - return applyRestoredData(JSON.parse(content)); - }); + var promise = applyRestoredData(JSON.parse(content)); + + // Store (store data into session storage) + if (!options || angular.isUndefined(options.store) || options.store) { + promise.then(function() { + return storeData(); + }); + } + + return promise; + }) + ; }, /** @@ -2248,6 +2268,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se cleanByContext: cleanEventsByContext }, children: { + create: createNewChildWallet, add: addChildWallet, remove: removeChildWalletById, get: getChildWalletById, diff --git a/www/js/services/wot-services.js b/www/js/services/wot-services.js index 2c97becf76a547ccf8db17168465245f317d44ca..c363877244fbbc208a826a09ec34a401d6ad357f 100644 --- a/www/js/services/wot-services.js +++ b/www/js/services/wot-services.js @@ -80,15 +80,15 @@ angular.module('cesium.wot.services', ['ngApi', 'cesium.bma.services', 'cesium.c requirements.hasSelf = true; requirements.needSelf = false; requirements.wasMember = angular.isDefined(requirements.wasMember) ? requirements.wasMember : false; // Compat with Duniter 0.9 - requirements.needMembership = (requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn <= 0 && !requirements.wasMember); - requirements.needRenew = (!requirements.needMembership && + requirements.needMembership = (!requirements.revoked && requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn <= 0 && !requirements.wasMember); + requirements.needRenew = (!requirements.needMembership && !requirements.revoked && requirements.membershipExpiresIn <= csSettings.data.timeWarningExpireMembership && requirements.membershipPendingExpiresIn <= 0) || (requirements.wasMember && requirements.membershipExpiresIn === 0 && requirements.membershipPendingExpiresIn === 0); - requirements.canMembershipOut = (requirements.membershipExpiresIn > 0); - requirements.pendingMembership = (requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn > 0); - requirements.isMember = (requirements.membershipExpiresIn > 0); + requirements.canMembershipOut = (!requirements.revoked && requirements.membershipExpiresIn > 0); + requirements.pendingMembership = (!requirements.revoked && requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn > 0); + requirements.isMember = (!requirements.revoked && requirements.membershipExpiresIn > 0); requirements.blockUid = requirements.meta.timestamp; // Force certification count to 0, is not a member yet - fix #269 requirements.certificationCount = (requirements.isMember && requirements.certifications) ? requirements.certifications.length : 0; diff --git a/www/plugins/es/js/controllers/invitation-controllers.js b/www/plugins/es/js/controllers/invitation-controllers.js index 41e5c95a5ac7d79fa8fee5bf02feab0ecb28f16a..84002729baa24e2a51380b0dc4627652a3036a65 100644 --- a/www/plugins/es/js/controllers/invitation-controllers.js +++ b/www/plugins/es/js/controllers/invitation-controllers.js @@ -19,7 +19,7 @@ angular.module('cesium.es.invitation.controllers', ['cesium.es.services']) }) .state('app.view_invitations_by_id', { - url: "/wallet/list/:id/invitations", + url: "/wallets/:id/invitations", views: { 'menuContent': { templateUrl: "plugins/es/templates/invitation/view_invitations.html", diff --git a/www/plugins/es/js/controllers/notification-controllers.js b/www/plugins/es/js/controllers/notification-controllers.js index edbb1d82f441eb28584370a279234fa22edab173..a225e44bccdb03a40333efc1ec357b4bb6b63a5f 100644 --- a/www/plugins/es/js/controllers/notification-controllers.js +++ b/www/plugins/es/js/controllers/notification-controllers.js @@ -175,7 +175,6 @@ function NotificationsController($scope, $ionicPopover, $state, $timeout, UIUtil if ($scope.search.loading || event.preventDefault() || $scope.search.preventSelect) return; - console.log("select", item); if (item.markAsRead && typeof item.markAsRead == 'function') { $timeout(item.markAsRead); } diff --git a/www/plugins/es/js/controllers/registry-controllers.js b/www/plugins/es/js/controllers/registry-controllers.js index 11d9676b22eab26a7f344a20404486a040cf6a43..1c2f782ea3688ecdacfb2bcf2b79818b6d29d2ef 100644 --- a/www/plugins/es/js/controllers/registry-controllers.js +++ b/www/plugins/es/js/controllers/registry-controllers.js @@ -904,10 +904,10 @@ function ESRegistryRecordViewController($scope, $rootScope, $state, $q, $timeout // Edit click $scope.edit = function() { - UIUtils.loading.show(); var wallet = csWallet.isUserPubkey($scope.formData.issuer) ? csWallet : csWallet.children.getByPubkey($scope.formData.issuer); if (!wallet) return; - $state.go('app.registry_edit_record', {id: $scope.id, wallet: wallet.id}); + UIUtils.loading.show(); + return $state.go('app.registry_edit_record', {id: $scope.id, wallet: wallet.id}); }; $scope.delete = function() { diff --git a/www/plugins/es/js/controllers/subscription-controllers.js b/www/plugins/es/js/controllers/subscription-controllers.js index 43b843da2ce36104dbc67a2da504d9cb5bedabb6..b1c15c5e11a2360d9761b561caea6ba1cdea6e1f 100644 --- a/www/plugins/es/js/controllers/subscription-controllers.js +++ b/www/plugins/es/js/controllers/subscription-controllers.js @@ -20,7 +20,7 @@ angular.module('cesium.es.subscription.controllers', ['cesium.es.services']) .state('app.edit_subscriptions_by_id', { cache: false, - url: "/wallet/list/:id/subscriptions", + url: "/wallets/:id/subscriptions", views: { 'menuContent': { templateUrl: "plugins/es/templates/subscription/edit_subscriptions.html", diff --git a/www/templates/login/popover_methods.html b/www/templates/login/popover_methods.html index 076deff1061671dbb957a061019a418d7ae8a86d..bc7edabdc567e0a96a331816450536aed1e90da2 100644 --- a/www/templates/login/popover_methods.html +++ b/www/templates/login/popover_methods.html @@ -31,6 +31,13 @@ <div class="item-divider"></div> + <a class="item item-icon-left ink" + ng-if="$root.device.barcode.enable" + ng-click="changeMethod('SCAN')"> + <i class="icon ion-qr-scanner"></i> + {{'LOGIN.METHOD.SCAN' | translate}} + </a> + <a class="item item-icon-left ink" ng-click="changeMethod('PUBKEY')"> <i class="icon ion-key"></i> diff --git a/www/templates/wallet/list/item_wallet.html b/www/templates/wallet/list/item_wallet.html index 6ef3458a6d0a92c5071c18696dea833e7c67492c..df687f0b27f6bed48ad4b823659f518f33de14f8 100644 --- a/www/templates/wallet/list/item_wallet.html +++ b/www/templates/wallet/list/item_wallet.html @@ -8,8 +8,8 @@ <!--<i ng-hide=":rebind:wallet.isAuth()" class="icon-secondary assertive ion-locked" style="top: -16px; left: 66px; font-size: 20px;"></i>--> <h2> <span class="text-editable" ng-click="editWallet($event, wallet)" title="{{'ACCOUNT.WALLET_LIST.BTN_RENAME'|translate}}"> - <ng-if ng-if=":rebind:walletData.name||walletData.uid" ng-bind-html="::walletData.name||walletData.uid"></ng-if> - <ng-if ng-if=":rebind:!walletData.name && !walletData.uid">{{::walletData.pubkey|formatPubkey}}</ng-if> + <ng-if ng-if=":rebind:walletData.localName||walletData.name||walletData.uid" ng-bind-html="::walletData.localName||walletData.name||walletData.uid"></ng-if> + <ng-if ng-if=":rebind:!walletData.localName && !walletData.name && !walletData.uid">{{::walletData.pubkey|formatPubkey}}</ng-if> </span> </h2> <h4 class="gray"> diff --git a/www/templates/wallet/list/item_wallet_light.html b/www/templates/wallet/list/item_wallet_light.html index a42032208a6e0cf681c4c27fa84982204aef239e..e70d1630c694571b5af4cda115c3431fa88ef272 100644 --- a/www/templates/wallet/list/item_wallet_light.html +++ b/www/templates/wallet/list/item_wallet_light.html @@ -8,8 +8,8 @@ <i ng-if="::walletData.avatar" class="item-image avatar" style="background-image: url({{::walletData.avatar.src}})"></i> <h2> - <ng-if ng-if="::walletData.name||walletData.uid" ng-bind-html="::walletData.name||walletData.uid"></ng-if> - <ng-if ng-if="::!walletData.name && !walletData.uid">{{::walletData.pubkey|formatPubkey}}</ng-if> + <ng-if ng-if="::walletData.localName||walletData.name||walletData.uid">{{::walletData.localName||walletData.name||walletData.uid}}</ng-if> + <ng-if ng-if="::!walletData.localName && !walletData.name && !walletData.uid">{{::walletData.pubkey|formatPubkey}}</ng-if> </h2> <h4 class="gray"> @@ -24,7 +24,7 @@ <div class="badge" ng-if="formData.showBalance" ng-class="{'badge-assertive': (walletData.balance <= 0), 'badge-balanced': (walletData.balance > 0) }"> - <span ng-bind-html="walletData.balance|formatAmount:{useRelative: true, currency: currency}"></span> + <span ng-bind-html="walletData.balance|formatAmount:{useRelative: formData.useRelative, currency: currency}"></span> </div> <i class="icon ion-ios-arrow-right "></i> diff --git a/www/templates/wallet/modal_security.html b/www/templates/wallet/modal_security.html index 3157d901dd9fcd7b01205076082b8271d165b4ae..b889d001af261b24405aaecf99bf5b4455f145ae 100644 --- a/www/templates/wallet/modal_security.html +++ b/www/templates/wallet/modal_security.html @@ -35,7 +35,7 @@ <div class="list"> <div class="item item-complex card stable-bg item-icon-left item-icon-right ink" - ng-click="selectOption('recoverID')" ng-if="!isLogin"> + ng-click="selectOption('recoverID')" ng-if="!login"> <div class="item-content item-text-wrap"> <i class="item-image dark icon ion-person"></i> <b class="ion-ios-undo icon-secondary dark" style="top: -8px; left: 39px; font-size: 12px;"></b> @@ -46,7 +46,7 @@ </div> <div class="item item-complex card stable-bg item-icon-left item-icon-right ink" - ng-click="selectOption('revocation')" ng-if="!isLogin"> + ng-click="selectOption('revocation')" ng-if="!login"> <div class="item-content item-text-wrap"> <i class="item-image dark icon ion-person"></i> <b class="ion-close icon-secondary dark" style="top: -8px; left: 39px; font-size: 12px;"></b> @@ -58,7 +58,7 @@ <div class="item item-complex card stable-bg item-icon-left item-icon-right ink" - ng-click="selectOption('saveID')" ng-if="isLogin"> + ng-click="selectOption('saveID')" ng-if="login"> <div class="item-content item-text-wrap"> <i class="item-image dark icon ion-person"></i> <b class="ion-ios-redo icon-secondary dark" style="top: -8px; left: 39px; font-size: 12px;"></b> @@ -70,7 +70,7 @@ </div> <div class="item item-complex card stable-bg item-icon-left item-icon-right ink" - ng-click="selectOption('generateKeyfile')" ng-if="isLogin"> + ng-click="selectOption('generateKeyfile')" ng-if="login"> <div class="item-content item-text-wrap"> <i class="item-image dark icon ion-document-text"></i> <b class="ion-key icon-secondary dark" style="top: -8px; left: 42px; font-size: 12px;"></b> @@ -81,7 +81,7 @@ </div> <div class="item item-complex card stable-bg item-icon-left item-icon-right ink hidden-xs hidden-sm" - ng-click="downloadRevokeFile()" ng-if="isLogin && hasSelf"> + ng-click="downloadRevokeFile()" ng-if="canRevoke"> <div class="item-content item-text-wrap"> <i class="item-image dark icon ion-person"></i> <b class="ion-ios-redo icon-secondary dark" style="top: -8px; left: 39px; font-size: 12px;"></b> @@ -113,7 +113,7 @@ </div> <div class="item item-complex card stable-bg item-icon-left item-icon-right ink" - ng-click="revokeWalletIdentity()" ng-if="isLogin && hasSelf"> + ng-click="revokeWalletIdentity()" ng-if="canRevoke"> <div class="item-content item-text-wrap"> <i class="item-image icon ion-person assertive-900"></i> <b class="ion-close icon-secondary assertive-900" style="top: -8px; left: 39px; font-size: 12px;"></b> @@ -135,10 +135,10 @@ <ng-include src="'templates/wallet/slides/slides_revocation_file.html'"></ng-include> </ion-slide-page> - <ion-slide-page ng-if="isLogin && option == 'saveID'"> + <ion-slide-page ng-if="login && option == 'saveID'"> <ng-include src="'templates/wallet/slides/slides_saveID_1.html'"></ng-include> </ion-slide-page> - <ion-slide-page ng-if="isLogin && option == 'saveID'"> + <ion-slide-page ng-if="login && option == 'saveID'"> <ng-include src="'templates/wallet/slides/slides_saveID_2.html'"></ng-include> </ion-slide-page> @@ -152,7 +152,7 @@ <ng-include src="'templates/wallet/slides/slides_recoverID_3.html'"></ng-include> </ion-slide-page> - <ion-slide-page ng-if="isLogin && option == 'generateKeyfile'"> + <ion-slide-page ng-if="login && option == 'generateKeyfile'"> <ng-include src="'templates/wallet/slides/slides_generate_keyfile.html'"></ng-include> </ion-slide-page> diff --git a/www/templates/wallet/popover_actions.html b/www/templates/wallet/popover_actions.html index 244eb66e5a08468ef7b8800f08366875e37f6a6f..00f24afea27f312e3a3ad844488328e837799971 100644 --- a/www/templates/wallet/popover_actions.html +++ b/www/templates/wallet/popover_actions.html @@ -29,7 +29,7 @@ {{'ACCOUNT.BTN_MEMBERSHIP_RENEW_DOTS' | translate}} </a> <a class="item item-icon-left ink hidden-xs hidden-sm" - ng-if="!walletData.requirements.needSelf" + ng-if="!walletData.requirements.needSelf && !walletData.requirements.revoked" ng-class="{'gray':!walletData.requirements.needRenew}" ng-click="renewMembership()"> <i class="icon ion-loop"></i> diff --git a/www/templates/wallet/transfer_form.html b/www/templates/wallet/transfer_form.html index a32257921bec6af1295776e386e0f31d3c0d03c7..076932d83eb283d3ad06b668a54420363533b621 100644 --- a/www/templates/wallet/transfer_form.html +++ b/www/templates/wallet/transfer_form.html @@ -29,7 +29,7 @@ ng-click="showSelectWalletModal()"> <span class="gray" translate>TRANSFER.FROM</span> <span class="badge animate-fade-in animate-show-hide ng-hide" ng-show="!loading" - ng-class="{'badge-assertive': (convertedBalance <= 0 || (formData.amount && convertedBalance < formData.amount)), 'badge-balanced': (convertedBalance > 0 && (!formData.amount || convertedBalance >= formData.amount)) }"> + ng-class="{'badge-assertive': (convertedBalance <= 0 || form.amount.$error.max), 'badge-balanced': (convertedBalance > 0 && (!form.amount.$error.max)) }"> <ion-spinner icon="android" ng-show="!walletData.pubkey"></ion-spinner> <span ng-if="walletData.pubkey && !walletData.isMember"> <i class="ion-key"></i> {{walletData.pubkey| formatPubkey}} @@ -93,6 +93,9 @@ <div class="form-error" ng-message="min"> <span translate="ERROR.FIELD_MIN" translate-values="{min: minAmount}"></span> </div> + <div class="form-error" ng-message="max"> + <span translate="ERROR.NOT_ENOUGH_CREDIT"></span> + </div> </div> diff --git a/www/templates/wallet/view_wallet_tx.html b/www/templates/wallet/view_wallet_tx.html index 92c55bcdeddb4b44797358fb5880e5257c1b4b79..38411fee592be5fc37425ea919ce39ca4e9efca6 100644 --- a/www/templates/wallet/view_wallet_tx.html +++ b/www/templates/wallet/view_wallet_tx.html @@ -89,7 +89,8 @@ <div class="list" ng-class="::motion.ionListClass"> <!-- Errors transactions--> - <a class="item item-icon-left item-icon-right ink" ng-if="formData.tx.errors && formData.tx.errors.length" ui-sref="app.view_wallet_tx_errors"> + <a class="item item-icon-left item-icon-right ink" ng-if="formData.tx.errors && formData.tx.errors.length" + ng-click="showTxErrors()"> <i class="icon ion-alert-circled"></i> {{:locale:'ACCOUNT.ERROR_TX'|translate}} <div class="badge badge-assertive">