diff --git a/src/js/controllers/tab-send-v2.js b/src/js/controllers/tab-send-v2.js new file mode 100644 index 000000000..624acb5bb --- /dev/null +++ b/src/js/controllers/tab-send-v2.js @@ -0,0 +1,301 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('tabSendV2Controller', function($scope, $rootScope, $log, $timeout, $ionicScrollDelegate, addressbookService, profileService, lodash, $state, walletService, incomingData, popupService, platformInfo, bwcError, gettextCatalog, scannerService, configService, bitcoinCashJsService, $ionicNavBarDelegate, clipboardService) { + var clipboardHasAddress = false; + + $scope.addContact = function() { + $state.go('tabs.settings').then(function() { + $state.go('tabs.addressbook').then(function() { + $state.go('tabs.addressbook.add'); + }); + }); + }; + + $scope.pasteClipboard = function() { + clipboardService.readFromClipboard(function(text) { + $scope.formData.search = text; + $scope.findContact($scope.formData.search); + }); + } + + $scope.$on("$ionicView.enter", function(event, data) { + clipboardService.readFromClipboard(function(text) { + $scope.clipboardHasAddress = false; + if ((text.indexOf('bitcoincash:') === 0 || text[0] === 'C' || text[0] === 'H' || text[0] === 'p' || text[0] === 'q') && text.replace('bitcoincash:', '').length === 42) { // CashAddr + $scope.clipboardHasAddress = true; + } else if ((text[0] === "1" || text[0] === "3" || text.substring(0, 3) === "bc1") && text.length >= 26 && text.length <= 35) { // Legacy Addresses + $scope.clipboardHasAddress = true; + } + }); + + $ionicNavBarDelegate.showBar(true); + if (!$scope.hasWallets) { + $scope.checkingBalance = false; + return; + } + updateHasFunds(); + updateWalletsList(); + updateContactsList(function() { + updateList(); + }); + }); + + + + + + + + + + + + + + + + + var originalList; + var CONTACTS_SHOW_LIMIT; + var currentContactsPage; + + $scope.sectionDisplay = { + transferToWallet: false + }; + + var hasWallets = function() { + $scope.wallets = profileService.getWallets({ + onlyComplete: true + }); + $scope.hasWallets = lodash.isEmpty($scope.wallets) ? false : true; + }; + + // THIS is ONLY to show the 'buy bitcoins' message + // does not has any other function. + + var updateHasFunds = function() { + + if ($rootScope.everHasFunds) { + $scope.hasFunds = true; + return; + } + + $scope.hasFunds = false; + var index = 0; + lodash.each($scope.wallets, function(w) { + walletService.getStatus(w, {}, function(err, status) { + + ++index; + if (err && !status) { + $log.error(err); + // error updating the wallet. Probably a network error, do not show + // the 'buy bitcoins' message. + + $scope.hasFunds = true; + } else if (status.availableBalanceSat > 0) { + $scope.hasFunds = true; + $rootScope.everHasFunds = true; + } + + if (index == $scope.wallets.length) { + $scope.checkingBalance = false; + $timeout(function() { + $scope.$apply(); + }); + } + }); + }); + }; + + var updateWalletsList = function() { + var config = configService.getSync(); + var networkResult = lodash.countBy($scope.wallets, 'network'); + + $scope.showTransferCard = $scope.hasWallets && (networkResult.livenet > 1 || networkResult.testnet > 1); + + if ($scope.showTransferCard) { + var walletsToTransfer = $scope.wallets; + if (!(networkResult.livenet > 1)) { + walletsToTransfer = lodash.filter(walletsToTransfer, function(item) { + return item.network == 'testnet'; + }); + } + if (!(networkResult.testnet > 1)) { + walletsToTransfer = lodash.filter(walletsToTransfer, function(item) { + return item.network == 'livenet'; + }); + } + + var walletList = []; + lodash.each(walletsToTransfer, function(v) { + var displayBalanceAsFiat = + // BD got v.status as undefined here once during development, just + // after creating a new wallet. + v.status && + v.status.alternativeBalanceAvailable && + config.wallet.settings.priceDisplay === 'fiat'; + + walletList.push({ + color: v.color, + name: v.name, + recipientType: 'wallet', + coin: v.coin, + network: v.network, + balanceString: displayBalanceAsFiat ? + v.status.totalBalanceAlternative + ' ' + v.status.alternativeIsoCode : + v.cachedBalance, + getAddress: function(cb) { + walletService.getAddress(v, false, cb); + }, + }); + }); + originalList = originalList.concat(walletList); + } + } + + var updateContactsList = function(cb) { + var config = configService.getSync(); + var defaults = configService.getDefaults(); + addressbookService.list(function(err, ab) { + if (err) $log.error(err); + + $scope.hasContacts = lodash.isEmpty(ab) ? false : true; + if (!$scope.hasContacts) return cb(); + + var completeContacts = []; + lodash.each(ab, function(v, k) { + completeContacts.push({ + name: lodash.isObject(v) ? v.name : v, + address: k, + email: lodash.isObject(v) ? v.email : null, + recipientType: 'contact', + coin: v.coin, + displayCoin: (v.coin == 'bch' + ? (config.bitcoinCashAlias || defaults.bitcoinCashAlias) + : (config.bitcoinAlias || defaults.bitcoinAlias)).toUpperCase(), + getAddress: function(cb) { + return cb(null, k); + }, + }); + }); + var contacts = completeContacts.slice(0, (currentContactsPage + 1) * CONTACTS_SHOW_LIMIT); + $scope.contactsShowMore = completeContacts.length > contacts.length; + originalList = originalList.concat(contacts); + return cb(); + }); + }; + + var updateList = function() { + $scope.list = lodash.clone(originalList); + $timeout(function() { + $ionicScrollDelegate.resize(); + $scope.$apply(); + }, 10); + }; + + $scope.openScanner = function() { + var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP; + + if (!isWindowsPhoneApp) { + $state.go('tabs.scan'); + return; + } + + scannerService.useOldScanner(function(err, contents) { + if (err) { + popupService.showAlert(gettextCatalog.getString('Error'), err); + return; + } + incomingData.redir(contents); + }); + }; + + $scope.showMore = function() { + currentContactsPage++; + updateWalletsList(); + }; + + $scope.searchInFocus = function() { + $scope.searchFocus = true; + }; + + $scope.searchBlurred = function() { + if ($scope.formData.search == null || $scope.formData.search.length == 0) { + $scope.searchFocus = false; + } + }; + + $scope.findContact = function(search) { + + if (incomingData.redir(search)) { + return; + } + + if (!search || search.length < 2) { + $scope.list = originalList; + $timeout(function() { + $scope.$apply(); + }); + return; + } + + var result = lodash.filter(originalList, function(item) { + var val = item.name; + return lodash.includes(val.toLowerCase(), search.toLowerCase()); + }); + + $scope.list = result; + }; + + $scope.goToAmount = function(item) { + $timeout(function() { + item.getAddress(function(err, addr) { + if (err || !addr) { + //Error is already formated + return popupService.showAlert(err); + } + + if (item.recipientType && item.recipientType == 'contact') { + if (addr.indexOf('bch') == 0 || addr.indexOf('btc') == 0) { + addr = addr.substring(3); + } + } + + $log.debug('Got toAddress:' + addr + ' | ' + item.name); + return $state.transitionTo('tabs.send.amount', { + recipientType: item.recipientType, + displayAddress: item.coin == 'bch' ? bitcoinCashJsService.translateAddresses(addr).cashaddr : addr, + toAddress: addr, + toName: item.name, + toEmail: item.email, + toColor: item.color, + coin: item.coin + }); + }); + }); + }; + + // This could probably be enhanced refactoring the routes abstract states + $scope.createWallet = function() { + $state.go('tabs.home').then(function() { + $state.go('tabs.add.create-personal'); + }); + }; + + $scope.buyBitcoin = function() { + $state.go('tabs.home').then(function() { + $state.go('tabs.buyandsell'); + }); + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + $scope.checkingBalance = true; + $scope.formData = { + search: null + }; + originalList = []; + CONTACTS_SHOW_LIMIT = 10; + currentContactsPage = 0; + hasWallets(); + }); +}); diff --git a/src/js/directives/gravatar.js b/src/js/directives/gravatar.js index 5f7931798..c96a63cfe 100644 --- a/src/js/directives/gravatar.js +++ b/src/js/directives/gravatar.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.directives') - .directive('gravatar', function(md5) { + .directive('gravatar', function(md5, $http) { return { restrict: 'AE', replace: true, @@ -9,13 +9,24 @@ angular.module('copayApp.directives') name: '@', height: '@', width: '@', - email: '@' + email: '@', + url: '@' }, link: function(scope, el, attr) { if (typeof scope.email === "string") { scope.emailHash = md5.createHash(scope.email.toLowerCase() || ''); + var req = { + method: 'GET', + url: 'https://secure.gravatar.com/'+scope.emailHash+'.json', + }; + scope.url = 'img/contact-placeholder.svg'; + $http(req).then(function (response) { + scope.url = 'https://secure.gravatar.com/avatar/'+scope.emailHash+'.jpg?s='+scope.width+'&d=mm'; + }, function (error) { + scope.url = 'img/contact-placeholder.svg'; + }); } }, - template: '' + template: '' }; }); diff --git a/src/js/routes.js b/src/js/routes.js index 4e72246ba..2399a2db9 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -270,6 +270,15 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } } }) + .state('tabs.send2', { + url: '/send2', + views: { + 'tab-send': { + controller: 'tabSendV2Controller', + templateUrl: 'views/tab-send-v2.html', + } + } + }) .state('tabs.settings', { url: '/settings', views: { diff --git a/src/js/services/clipboardService.js b/src/js/services/clipboardService.js index e2e0e5fb3..ff28cd2e7 100644 --- a/src/js/services/clipboardService.js +++ b/src/js/services/clipboardService.js @@ -17,7 +17,27 @@ angular.module('copayApp.services').factory('clipboardService', function ($http, // No supported return; } + }; + root.readFromClipboard = function (cb) { + $log.debug("Read from clipboard"); + if (platformInfo.isCordova) { + cordova.plugins.clipboard.paste(function(text) { + cb(text); + }) + } else if (platformInfo.isNW) { + cb(nodeWebkitService.readFromClipboard()); + } else { + navigator.clipboard.readText() + .then(text => { + cb(text); + }) + .catch(err => { + $log.debug("Clipboard reading is not supported in browser.."); + }); + + return; + } }; return root; diff --git a/src/sass/buttons.scss b/src/sass/buttons.scss index 348d6d378..a8512ae64 100644 --- a/src/sass/buttons.scss +++ b/src/sass/buttons.scss @@ -16,6 +16,8 @@ &.button-primary, &.button-secondary, &.button-light, + &.button-white, + &.button-green, &.button-assertive { &.button-standard { @extend %button-standard; @@ -33,6 +35,10 @@ } } +@mixin button-shadow() { + box-shadow: 0 2px 11px 0 #C1C1C1;; +} + .button { &.button-secondary { @include button-style($v-button-secondary-bg, $v-button-secondary-border, $v-button-secondary-active-bg, $v-button-secondary-active-border, $v-button-secondary-color); @@ -47,7 +53,24 @@ } .button { + border-radius: 6px; &.button-full { display: block; } + &-green { + @include button-style(#719561, #FFF, #606060, #FFF, #FFF); + @include button-clear(#FFF); + @include button-outline(#C1C1C1); + border: 0px; + @include button-shadow(); + } + &-white { + @include button-style(#FFF, #C1C1C1, #C1C1C1, #FFF, #606060); + @include button-clear(#FFF); + @include button-outline(#C1C1C1); + @include button-shadow(); + &.activated { + color: #FFF; + } + } } \ No newline at end of file diff --git a/src/sass/views/address-book.scss b/src/sass/views/address-book.scss index c0d0f99a8..8bbdbc6be 100644 --- a/src/sass/views/address-book.scss +++ b/src/sass/views/address-book.scss @@ -124,7 +124,6 @@ position: relative; height: 70px; border-color: $royal; - background-color: $royal; padding-top: 20px; margin-bottom: 50px; text-align: center; diff --git a/src/sass/views/tab-send-v2.scss b/src/sass/views/tab-send-v2.scss new file mode 100644 index 000000000..e9905fc17 --- /dev/null +++ b/src/sass/views/tab-send-v2.scss @@ -0,0 +1,228 @@ +#tab-send2 { + @extend .deflash-blue; + .input { + width: 100%; + input { + width: 100%; + height: 57px; + background: #FFF; + border: 1px #D9D9D9 solid; + &::placeholder { + color: #DCDCDC; + } + } + i { + &.left { + padding-left: 15px; + } + &.qr { + cursor: pointer; + cursor: hand; + padding-right: 5px; + } + } + } + + .send-wrapper { + &:after { + display: block; + position: relative; + height: 1px; + background: #DEDEDE; + bottom: 0; + content: ''; + margin: 20px 6px 0px; + } + margin: 18px 0 0; + padding: 9px; + background-color: #f2f2f2; + border-radius: 3px; + border: none; + &.focus { + .search-input { + padding-left: 30px; + &:focus::-webkit-input-placeholder { + opacity: 0; + } + } + } + .buttons { + margin: auto; + margin-top: 18px; + .button { + &-clipboard-paste { + margin-left: 0; + .address { + display: none; + } + .icon { + background: url(../img/icon-clipboard-paste.svg); + width: 15px; + height: 19px; + display: inline-block; + margin-bottom: 4px; + } + &.contains-address { + background: #FAB915; + color: #FFF !important; + border: 0; + @include button-shadow(); + .icon { + background: url(../img/icon-clipboard-paste-white.svg); + } + .address { + display: inline; + } + .non-address { + display: none; + } + } + } + span { + font-size: 14px; + } + img { + height: 16px; + width: auto; + margin: 2px 0 4px; + } + height: 60px; + line-height: 16px; + margin-right: 0px; + width: 95%; + max-width: none; + padding: 2px; + &-qr { + font-weight: bold; + max-width: none; + width: 100%; + height: 95px; + margin-top: 20px; + img { + vertical-align: middle; + margin-right: 12px; + width: 43px; + height: 43px; + } + span { + font-size: 19px; + } + } + } + } + } + .search-input { + background-color: transparent; + padding-left: 30px; + } + .sendTip { + padding-top: 5vh; + text-align: center; + .item { + border-style: none; + } + & > .title { + font-size: 20px; + color: $v-dark-gray; + margin: 20px 10px; + } + & > .subtitle { + font-size: 1rem; + line-height: 1.5em; + font-weight: 300; + color: #6F6F70; + margin: 20px 1em 2.5em; + } + .big-icon-svg { + .bg.green { + padding: 0 10px; + box-shadow: none; + } + } + .buttons { + margin-top: 18px; + .button { + font-weight: bold; + font-size: 19px; + } + } + .button-first-contact img { + height: 19px; + width: 19px; + margin-right: 6px; + vertical-align: sub; + } + } + .item-heading { + line-height: 16px; + font-size: 14px; + font-weight: bold; + .subtitle { + color: #B5B2B2; + font-size: 12px; + font-weight: 300; + } + } + .list { + .item { + font-weight: 600; + padding-top: 12px; + padding-bottom: 12px; + p { + font-weight: normal; + } + &.item-icon-left { + padding-left: 64px; + } + color: #444; + //border-top: none; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + .big-icon-svg { + left: 5px; + & > .bg { + width: 30px; + height: 30px; + box-shadow: none; + } + } + &:before { + display: block; + position: absolute; + width: 100%; + height: 1px; + background: rgba(221, 221, 221, 0.3); + top: 0; + right: 0; + content: ''; + } + &.item-divider { + color: rgba(74, 74, 74, .8); + } + &.item-heading { + &:before { + top: 99%; + width: 100%; + } + } + &:nth-child(2) { + &:before { + width: 0; + } + } + .item-note { + color: rgb(58, 58, 58); + } + } + } + .scroll { + height: 100%; + } + + .card.contacts { + margin: 4px; + border-radius: 6px; + box-shadow: 0px 2px 1px 0 #C1C1C1; + } + +} diff --git a/src/sass/views/views.scss b/src/sass/views/views.scss index 37754970f..5367ac5aa 100644 --- a/src/sass/views/views.scss +++ b/src/sass/views/views.scss @@ -8,6 +8,7 @@ @import "tab-receive"; @import "tab-scan"; @import "tab-send"; +@import "tab-send-v2"; @import "tab-settings"; @import "wallet-colors"; @import "walletBalance"; diff --git a/www/img/contact-placeholder.svg b/www/img/contact-placeholder.svg index f730da86b..54b0fcec2 100644 --- a/www/img/contact-placeholder.svg +++ b/www/img/contact-placeholder.svg @@ -1,12 +1,18 @@ - - - - - - + + + + Artboard + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/img/icon-clipboard-paste-white.svg b/www/img/icon-clipboard-paste-white.svg new file mode 100644 index 000000000..be0df78bc --- /dev/null +++ b/www/img/icon-clipboard-paste-white.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/www/img/icon-clipboard-paste.svg b/www/img/icon-clipboard-paste.svg new file mode 100644 index 000000000..a82edc11b --- /dev/null +++ b/www/img/icon-clipboard-paste.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/www/img/icon-contact-add.svg b/www/img/icon-contact-add.svg new file mode 100644 index 000000000..36d1f95bc --- /dev/null +++ b/www/img/icon-contact-add.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/www/img/icon-scan-qr.svg b/www/img/icon-scan-qr.svg new file mode 100644 index 000000000..bc4a2bc56 --- /dev/null +++ b/www/img/icon-scan-qr.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/img/icon-w2w.svg b/www/img/icon-w2w.svg new file mode 100644 index 000000000..082a0d8cc --- /dev/null +++ b/www/img/icon-w2w.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/www/views/tab-send-v2.html b/www/views/tab-send-v2.html new file mode 100644 index 000000000..b42784957 --- /dev/null +++ b/www/views/tab-send-v2.html @@ -0,0 +1,143 @@ + + + {{'Send' | translate}} + + + + + + + + + + + + + + + Paste Clipboard + Paste Address + + + + + + Wallet to Wallet Transfer + + + + + + + + Scan QR Code + + + + + + + + + + + Send Bitcoin faster! + + + + Save frequently used addresses and send them Bitcoin in just one tap + + + + + Add your first contact + + + + + + + + + + Your Bitcoin wallet is empty + + + + To get started, buy Bitcoin Cash (BCH) or Bitcoin Core (BTC), or share your address. + You can receive bitcoin from any wallet or service. + + To get started, you'll need to create a bitcoin wallet and get some + bitcoin. + + + + Buy Bitcoin now + + Create bitcoin wallet + + + Show my address + + + + + + + + + + + Contacts + Saved frequently used addresses + + + + + + + + + + {{item.name}} + + {{item.displayCoin}} + + + + + Show more + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/views/tabs.html b/www/views/tabs.html index 69cf93309..19c33aad9 100644 --- a/www/views/tabs.html +++ b/www/views/tabs.html @@ -12,7 +12,7 @@ - +
Save frequently used addresses and send them Bitcoin in just one tap
To get started, buy Bitcoin Cash (BCH) or Bitcoin Core (BTC), or share your address.
You can receive bitcoin from any wallet or service.
+ {{item.displayCoin}} +