diff --git a/i18n/po/template.pot b/i18n/po/template.pot index e8821ae79..2e7cedcbe 100644 --- a/i18n/po/template.pot +++ b/i18n/po/template.pot @@ -3714,4 +3714,33 @@ msgstr "" #: src/js/controllers/amount.js:49 msgid "Address doesn\'t contain currency information, please make sure you are sending the correct currency." +msgstr "" + +#: www/views/review.html:4 +msgid "Review Transaction" +msgstr "" + +#: www/views/review.html:14 +msgid "You are sending" +msgstr "" + +#: www/views/review.html:22 +msgid "From:" +msgstr "" + +#: www/views/review.html:36 +msgid "To:" +msgstr "" + +#: www/views/review.html:53 +msgid "Add personal note" +msgstr "" + + +#: www/views/review.html:57 +msgid "Personal note:" +msgstr "" + +#: www/views/review.html:69 +msgid "Less than 1 cent" msgstr "" \ No newline at end of file diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 269ff1634..b623aa592 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -35,9 +35,9 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.save = save; vm.sendMax = sendMax; vm.errorMessage = ''; - + $scope.$on('$ionicView.beforeEnter', onBeforeEnter); - $scope.$on('$ionicView.leave', onLeave); + $scope.$on('$ionicView.leave', onLeave); var LENGTH_EXPRESSION_LIMIT = 19; var LENGTH_BEFORE_COMMA_EXPRESSION_LIMIT = 8; @@ -66,7 +66,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function onBeforeEnter(event, data) { - + initCurrencies(); passthroughParams = data.stateParams; @@ -81,7 +81,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, setAvailableUnits(); updateUnitUI(); - + var reNr = /^[1234567890\.]$/; var reOp = /^[\*\+\-\/]$/; @@ -153,7 +153,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, unitIndex = 0; - + // currency have preference var fiatName; if (data.stateParams.currency) { @@ -260,11 +260,11 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, if (vm.amount == '0' && digit == '0') return; if (availableUnits[unitIndex].isFiat && vm.amount.indexOf('.') > -1 && vm.amount[vm.amount.indexOf('.') + 2]) return; - if (vm.amount == '0' && digit != '.') { + if (vm.amount == '0' && digit != '.') { vm.amount = ''; } - if (vm.amount == '' && digit == '.') { + if (vm.amount == '' && digit == '.') { vm.amount = '0'; } @@ -335,16 +335,16 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, if (a) { amountInCrypto = a; var amountInSatoshis = a * unitToSatoshi; - vm.fundsAreInsufficient = !!passthroughParams.fromWalletId - && availableSatoshis !== null + vm.fundsAreInsufficient = !!passthroughParams.fromWalletId + && availableSatoshis !== null && availableSatoshis < amountInSatoshis; vm.alternativeAmount = txFormatService.formatAmount(amountInSatoshis, true); - vm.allowSend = lodash.isNumber(a) + vm.allowSend = lodash.isNumber(a) && a > 0 && (!vm.shapeshiftOrderId || (a >= vm.minAmount && a <= vm.maxAmount)) - && !vm.fundsAreInsufficient; + && !vm.fundsAreInsufficient; } else { if (result) { vm.alternativeAmount = 'N/A'; @@ -356,16 +356,16 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } } else { amountInCrypto = result; - vm.fundsAreInsufficient = passthroughParams.fromWalletId - && availableSatoshis !== null + vm.fundsAreInsufficient = passthroughParams.fromWalletId + && availableSatoshis !== null && availableSatoshis < result * unitToSatoshi; vm.alternativeAmount = $filter('formatFiatAmount')(toFiat(result)); - vm.allowSend = lodash.isNumber(result) + vm.allowSend = lodash.isNumber(result) && result > 0 && (!vm.shapeshiftOrderId || (result >= vm.minAmount && result <= vm.maxAmount)) - && !vm.fundsAreInsufficient; + && !vm.fundsAreInsufficient; } } else { @@ -381,7 +381,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } else if (amountInCrypto > vm.maxAmount) { vm.errorMessage = gettextCatalog.getString('Amount is above maximum'); - + } else { vm.errorMessage = ''; } @@ -474,7 +474,9 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } } - $state.transitionTo('tabs.send.confirm', confirmData); + $state.transitionTo('tabs.send.review', confirmData); + } + $scope.useSendMax = null; } }; @@ -585,7 +587,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, close(); }); }; - + function updateAvailableFundsStringIfNeeded() { if (passthroughParams.fromWalletId && availableSatoshis !== null) { availableFundsInFiat = ''; diff --git a/src/js/controllers/review.controller.js b/src/js/controllers/review.controller.js new file mode 100644 index 000000000..b81645488 --- /dev/null +++ b/src/js/controllers/review.controller.js @@ -0,0 +1,214 @@ +'use strict'; + +angular + .module('copayApp.controllers') + .controller('reviewController', reviewController); + +function reviewController(addressbookService, configService, profileService, $log, $scope, txFormatService) { + var vm = this; + + vm.destination = { + address: '', + balanceAmount: '', + balanceCurrency: '', + coin: '', + color: '', + currency: '', + currencyColor: '', + kind: '', // 'address', 'contact', 'wallet' + name: '' + }; + vm.feeCrypto = ''; + vm.feeFiat = ''; + vm.origin = { + balanceAmount: '', + balanceCurrency: '', + color: '', + currency: '', + currencyColor: '', + name: '', + }; + vm.primaryAmount = ''; + vm.primaryCurrency = ''; + vm.secondaryAmount = ''; + vm.secondaryCurrency = ''; + + var config = null; + var coin = ''; + var originWalletId = ''; + var priceDisplayIsFiat = true; + var satoshis = null; + var toAddress = ''; + var destinationWalletId = ''; + + + $scope.$on("$ionicView.beforeEnter", onBeforeEnter); + + + function onBeforeEnter(event, data) { + + originWalletId = data.stateParams.fromWalletId; + satoshis = parseInt(data.stateParams.amount, 10); + toAddress = data.stateParams.toAddr; + + var originWallet = profileService.getWallet(originWalletId); + vm.origin.currency = originWallet.coin.toUpperCase(); + vm.origin.color = originWallet.color; + vm.origin.name = originWallet.name; + coin = originWallet.coin; + + configService.get(function onConfig(err, configCache) { + if (err) { + $log.err('Error getting config.', err); + } else { + config = configCache; + priceDisplayIsFiat = config.wallet.settings.priceDisplay === 'fiat'; + vm.origin.currencyColor = originWallet.coin === 'btc' ? config.bitcoinWalletColor : config.bitcoinCashWalletColor; + } + updateSendAmounts(); + getOriginWalletBalance(originWallet); + handleDestinationAsAddress(toAddress, coin); + handleDestinationAsWallet(data.stateParams.toWalletId); + }); + } + + function getOriginWalletBalance(originWallet) { + var balanceText = getWalletBalanceDisplayText(originWallet); + vm.origin.balanceAmount = balanceText.amount; + vm.origin.balanceCurrency = balanceText.currency; + } + + function getWalletBalanceDisplayText(wallet) { + var balanceCryptoAmount = ''; + var balanceCryptoCurrencyCode = ''; + var balanceFiatAmount = ''; + var balanceFiatCurrency = '' + var displayAmount = ''; + var displayCurrency = ''; + + var walletStatus = null; + if (wallet.status.isValid) { + walletStatus = wallet.status; + } else if (wallet.cachedStatus.isValid) { + walletStatus = wallet.cachedStatus; + } + + if (walletStatus) { + var cryptoBalanceParts = walletStatus.spendableBalanceStr.split(' '); + balanceCryptoAmount = cryptoBalanceParts[0]; + balanceCryptoCurrencyCode = cryptoBalanceParts.length > 1 ? cryptoBalanceParts[1] : ''; + + if (walletStatus.alternativeBalanceAvailable) { + balanceFiatAmount = walletStatus.spendableBalanceAlternative; + balanceFiatCurrency = walletStatus.alternativeIsoCode; + } + } + + if (priceDisplayIsFiat) { + displayAmount = balanceFiatAmount ? balanceFiatAmount : balanceCryptoAmount; + displayCurrency = balanceFiatAmount ? balanceFiatCurrency : balanceCryptoCurrencyCode; + } else { + displayAmount = balanceCryptoAmount; + displayCurrency = balanceCryptoCurrencyCode; + } + + return { + amount: displayAmount, + currency: displayCurrency + }; + } + + function handleDestinationAsAddress(address, originCoin) { + if (!address) { + return; + } + + // Check if the recipient is a contact + addressbookService.get(originCoin + address, function(err, contact) { + if (!err && contact) { + console.log('destination is contact'); + handleDestinationAsContact(contact); + } else { + console.log('destination is address'); + vm.destination.address = address; + vm.destination.kind = 'address'; + } + }); + + } + + function handleDestinationAsContact(contact) { + vm.destination.kind = 'contact'; + vm.destination.name = contact.name; + vm.destination.color = contact.coin === 'btc' ? config.bitcoinWalletColor : config.bitcoinCashWalletColor; + vm.destination.currency = contact.coin.toUpperCase(); + vm.destination.currencyColor = vm.destination.color; + } + + function handleDestinationAsWallet(walletId) { + destinationWalletId = walletId; + if (!destinationWalletId) { + return; + } + + console.log('destination is wallet'); + var destinationWallet = profileService.getWallet(destinationWalletId); + vm.destination.coin = destinationWallet.coin; + vm.destination.color = destinationWallet.color; + vm.destination.currency = destinationWallet.coin.toUpperCase(); + vm.destination.kind = 'wallet'; + vm.destination.name = destinationWallet.name; + + if (config) { + vm.destination.currencyColor = vm.destination.coin === 'btc' ? config.bitcoinWalletColor : config.bitcoinCashWalletColor; + } + + var balanceText = getWalletBalanceDisplayText(destinationWallet); + vm.destination.balanceAmount = balanceText.amount; + vm.destination.balanceCurrency = balanceText.currency; + } + + function updateSendAmounts() { + if (typeof satoshis !== 'number') { + return; + } + + var cryptoAmount = ''; + var cryptoCurrencyCode = ''; + var amountStr = txFormatService.formatAmountStr(coin, satoshis); + if (amountStr) { + var amountParts = amountStr.split(' '); + cryptoAmount = amountParts[0]; + cryptoCurrencyCode = amountParts.length > 1 ? amountParts[1] : ''; + } + // Want to avoid flashing of amount strings so do all formatting after this has returned. + txFormatService.formatAlternativeStr(coin, satoshis, function(v) { + if (!v) { + vm.primaryAmount = cryptoAmount; + vm.primaryCurrency = cryptoCurrencyCode; + vm.secondaryAmount = ''; + vm.secondaryCurrency = ''; + return; + } + vm.secondaryAmount = vm.primaryAmount; + vm.secondaryCurrency = vm.primaryCurrency; + + var fiatParts = v.split(' '); + var fiatAmount = fiatParts[0]; + var fiatCurrency = fiatParts.length > 1 ? fiatParts[1] : ''; + + if (priceDisplayIsFiat) { + vm.primaryAmount = fiatAmount; + vm.primaryCurrency = fiatCurrency; + vm.secondaryAmount = cryptoAmount; + vm.secondaryCurrency = cryptoCurrencyCode; + } else { + vm.primaryAmount = cryptoAmount; + vm.primaryCurrency = cryptoCurrencyCode; + vm.secondaryAmount = fiatAmount; + vm.secondaryCurrency = fiatCurrency; + } + }); + } + +} diff --git a/src/js/directives/amount.js b/src/js/directives/amount.js index f991f0a28..9622ca09d 100644 --- a/src/js/directives/amount.js +++ b/src/js/directives/amount.js @@ -48,37 +48,44 @@ angular.module('bitcoincom.directives') return '2'; }; - switch (getDecimalPlaces($scope.currency)) { - case '0': - var valueFormatted = numberWithCommas(Math.round(parseFloat($scope.value))); - buildAmount(valueFormatted, '', ''); - break; - - case '2': - var valueProcessing = parseFloat(parseFloat($scope.value).toFixed(2)); - var valueFormatted = numberWithCommas(valueProcessing); - buildAmount(valueFormatted, '', ''); - break; - - case '3': - var valueProcessing = parseFloat(parseFloat($scope.value).toFixed(3)); - var valueFormatted = numberWithCommas(valueProcessing); - buildAmount(valueFormatted, '', ''); - break; - - case '8': - var valueFormatted = parseFloat($scope.value).toFixed(8); - if (parseFloat($scope.value) == 0) { - buildAmount('0', '', ''); - } else { + var formatNumbers = function(currency, value) { + switch (getDecimalPlaces(currency)) { + case '0': + var valueFormatted = numberWithCommas(Math.round(parseFloat(value))); buildAmount(valueFormatted, '', ''); - var start = numberWithCommas(valueFormatted.slice(0, -5)); - var middle = valueFormatted.slice(-5, -2); - var end = valueFormatted.substr(valueFormatted.length - 2); - buildAmount(start, middle, end); - } - break; + break; + + case '2': + var valueProcessing = parseFloat(parseFloat(value).toFixed(2)); + var valueFormatted = numberWithCommas(valueProcessing); + buildAmount(valueFormatted, '', ''); + break; + + case '3': + var valueProcessing = parseFloat(parseFloat(value).toFixed(3)); + var valueFormatted = numberWithCommas(valueProcessing); + buildAmount(valueFormatted, '', ''); + break; + + case '8': + var valueFormatted = parseFloat(value).toFixed(8); + if (parseFloat(value) == 0) { + buildAmount('0', '', ''); + } else { + buildAmount(valueFormatted, '', ''); + var start = numberWithCommas(valueFormatted.slice(0, -5)); + var middle = valueFormatted.slice(-5, -2); + var end = valueFormatted.substr(valueFormatted.length - 2); + buildAmount(start, middle, end); + } + break; + } } + + formatNumbers($scope.currency, $scope.value); + $scope.$watchGroup(['currency', 'value'], function() { + formatNumbers($scope.currency, $scope.value); + }); }] }; } diff --git a/src/js/routes.js b/src/js/routes.js index 7bb35a89f..286042266 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -344,6 +344,19 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } } }) + .state('tabs.send.review', { + url: '/review/:amount/:fromWalletId/:sendMax/:toAddr/:toWalletId', + views: { + 'tab-send@tabs': { + controller: 'reviewController', + controllerAs: 'vm', + templateUrl: 'views/review.html' + } + }, + params: { + paypro: null + } + }) /* * diff --git a/src/sass/components/action-minor.scss b/src/sass/components/action-minor.scss new file mode 100644 index 000000000..f158fe845 --- /dev/null +++ b/src/sass/components/action-minor.scss @@ -0,0 +1,24 @@ +.action-minor { + margin: 20px 14px; + font-size: 14px; + + &.mt-negative { + margin-top: 0; + } + + &.text-right { + text-align: right; + } + + > .action-icon { + width: 15px; + height: 15px; + vertical-align: middle; + margin-right: 3px; + } + + > .action-text { + vertical-align: middle; + color: #444444; + } +} \ No newline at end of file diff --git a/src/sass/components/address.scss b/src/sass/components/address.scss new file mode 100644 index 000000000..2848deb82 --- /dev/null +++ b/src/sass/components/address.scss @@ -0,0 +1,27 @@ +.address { + background-color: #F8F8F8; + border: 0.5px solid #EDEBEB; + border-radius: 3px; + padding: 9px; + text-align: center; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + + &.expanded { + white-space: pre-wrap; + word-break: break-all; + } + + .prefix { + color: #000000; + } + + .mid { + color: #919191; + } + + .suffix { + color: #000000; + } +} \ No newline at end of file diff --git a/src/sass/components/card.scss b/src/sass/components/card.scss new file mode 100644 index 000000000..6df235ab8 --- /dev/null +++ b/src/sass/components/card.scss @@ -0,0 +1,5 @@ +.card { + &.card-gutter-compact { + margin: 10px 12px; + } +} \ No newline at end of file diff --git a/src/sass/components/components.scss b/src/sass/components/components.scss index def6289fa..eae56e786 100644 --- a/src/sass/components/components.scss +++ b/src/sass/components/components.scss @@ -1 +1,11 @@ +@import "item"; +@import "ion-content"; +@import "card"; + +@import "header"; +@import "content-frame"; +@import "address"; +@import "action-minor"; +@import "expand-content"; +@import "fee-summary"; @import "amount.scss"; diff --git a/src/sass/components/content-frame.scss b/src/sass/components/content-frame.scss new file mode 100644 index 000000000..5766b246b --- /dev/null +++ b/src/sass/components/content-frame.scss @@ -0,0 +1,11 @@ +.content-frame { + &.negative-top { + margin-top: -40px; + + .card { + &:first-child { + margin-top: 0; + } + } + } +} \ No newline at end of file diff --git a/src/sass/components/expand-content.scss b/src/sass/components/expand-content.scss new file mode 100644 index 000000000..934a2beec --- /dev/null +++ b/src/sass/components/expand-content.scss @@ -0,0 +1,26 @@ +.expand-content-frame { + position: relative; + + .expand-content-trigger { + position: absolute; + top: 0; + transition: opacity 0.3s ease; + right: 0; + + &.expand-content-revealed { + opacity: 0; + } + } + + .expand-content { + opacity: 0; + transform-origin: 100% 0%; + transform: scale(0,0); + transition: opacity 0.3s ease, transform 0.3s ease; + + &.expand-content-revealed { + opacity: 1; + transform: scale(1,1); + } + } +} \ No newline at end of file diff --git a/src/sass/components/fee-summary.scss b/src/sass/components/fee-summary.scss new file mode 100644 index 000000000..404643a82 --- /dev/null +++ b/src/sass/components/fee-summary.scss @@ -0,0 +1,33 @@ +.fee-summary { + position: relative; + display: flex; + justify-content: space-between; + width: 100%; + padding: 5px 12px 15px; + box-sizing: border-box; + background-color: #F2F2F2; + + &:before { + content: ''; + position: absolute; + left: 0; + top: -15px; + width: 100%; + height: 15px; + background: linear-gradient(to bottom, rgba(242,242,242,0) 0%,rgba(242,242,242,1) 100%); + } + + .fee-fiat { + &.positive { + color: #70955F; + } + + &.negative { + color: #C24633; + } + } + + .fee-crypto { + color: #A7A7A7; + } +} \ No newline at end of file diff --git a/src/sass/components/header.scss b/src/sass/components/header.scss new file mode 100644 index 000000000..fad1f1812 --- /dev/null +++ b/src/sass/components/header.scss @@ -0,0 +1,36 @@ +.header { + padding: 29px 12px 61px; + background-color: #FAB915; + color: #FFFFFF; + + .title { + font-size: 18px; + font-weight: 400; + line-height: 1em; + color: #FFFFFF; + text-align: center; + + + .content { + margin-top: 23px; + } + } + + .content { + text-align: center; + + p { + margin: 0; + line-height: 1em; + font-size: 18px; + + &.large { + font-size: 29px; + font-weight: 600; + } + + + p { + margin-top: 8px; + } + } + } +} \ No newline at end of file diff --git a/src/sass/components/ion-content.scss b/src/sass/components/ion-content.scss new file mode 100644 index 000000000..56f3960a0 --- /dev/null +++ b/src/sass/components/ion-content.scss @@ -0,0 +1,17 @@ +/* +* Extends Ionic v1 ion-content +*/ + +ion-content { + &.bg-neutral { + background-color: #F2F2F2; + } + + &.padded-bottom-cta { + bottom: 92px; + } + + &.padded-bottom-cta-with-summary { + bottom: 134px; + } +} \ No newline at end of file diff --git a/src/sass/components/item.scss b/src/sass/components/item.scss new file mode 100644 index 000000000..bb75ae8e0 --- /dev/null +++ b/src/sass/components/item.scss @@ -0,0 +1,48 @@ +/* +* Extends Ionic v1 item +*/ + +.item { + &.item-compact { + padding: 11px 13px; + } + &.item-gutterless { + padding: 0; + } + + .item-content { + &.item-content-avatar { + min-height: 69px; + padding: 13px 11px 13px 68px; + + > img, + > i { + &:first-child { + position: absolute; + max-width: 40px; + max-height: 40px; + width: 100%; + height: 100%; + border-radius: 50%; + left: 13px; + top: 50%; + padding: 0; + transform: translate(0,-50%); + } + } + } + + &.item-content-compact { + min-height: 0; + padding: 13px 11px; + } + + .highlight { + color: #FAB915 + } + + + .item-content { + padding-top: 0; + } + } +} \ No newline at end of file diff --git a/src/sass/directives/directives.scss b/src/sass/directives/directives.scss index 9159d3f23..954b86c3a 100644 --- a/src/sass/directives/directives.scss +++ b/src/sass/directives/directives.scss @@ -1 +1,2 @@ @import "gravatar"; +@import "elastic"; \ No newline at end of file diff --git a/src/sass/directives/elastic.scss b/src/sass/directives/elastic.scss new file mode 100644 index 000000000..8e8aba4fa --- /dev/null +++ b/src/sass/directives/elastic.scss @@ -0,0 +1,4 @@ +.elastic { + width: 100%; + font-size: 14px; +} \ No newline at end of file diff --git a/src/sass/icons.scss b/src/sass/icons.scss index 7d14f8886..ee270408f 100644 --- a/src/sass/icons.scss +++ b/src/sass/icons.scss @@ -88,6 +88,13 @@ background-image: url('../img/icon-faucet.svg'); background-size: 70%; } + + &.icon-wallet { + background-color: #FAB915; + background-image: url('../img/icon-wallet.svg'); + border: none; + box-shadow: 0 0 0 1px rgba(0,0,0,0.3) inset; + } } } diff --git a/src/sass/views/review.scss b/src/sass/views/review.scss new file mode 100644 index 000000000..67733fe22 --- /dev/null +++ b/src/sass/views/review.scss @@ -0,0 +1,13 @@ +#view-review { + background-color: #494949; + + slide-to-accept, slide-to-accept-success { + margin-bottom: constant(safe-area-inset-bottom); /* iOS 11.0 */ + margin-bottom: env(safe-area-inset-bottom); /* iOS 11.2 */ + } + + .fee-summary { + position: absolute; + bottom: 92px; + } +} \ No newline at end of file diff --git a/src/sass/views/views.scss b/src/sass/views/views.scss index 700a45b62..1f25b564a 100644 --- a/src/sass/views/views.scss +++ b/src/sass/views/views.scss @@ -50,3 +50,4 @@ @import "includes/logOptions"; @import "includes/checkBar"; @import "cashScan"; +@import "review"; diff --git a/www/img/icon-bookmark.svg b/www/img/icon-bookmark.svg new file mode 100644 index 000000000..5db1f9047 --- /dev/null +++ b/www/img/icon-bookmark.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/www/views/review.html b/www/views/review.html new file mode 100644 index 000000000..36bb67410 --- /dev/null +++ b/www/views/review.html @@ -0,0 +1,116 @@ + + + + {{'Review Transaction' | translate}} + + + + + + +
+
+

You are sending

+

{{vm.primaryAmount}} {{vm.primaryCurrency}}

+

{{vm.secondaryAmount}} {{vm.secondaryCurrency}}

+
+
+ +
+
+
From:
+
+
+ +
+
+

{{vm.origin.name}} ({{vm.origin.currency}})

+

{{vm.origin.balanceAmount}} {{vm.origin.balanceCurrency}}

+
+
+
+
+
To:
+
+
+ + +
+
+

{{vm.destination.name}} ({{vm.destination.currency}})

+

{{vm.destination.balanceAmount}} {{vm.destination.balanceCurrency}}

+
+
+
qz9cqq5pryv9hnqwa8q8mccmynk9uf4vlu5nxerpzmc
+
+
+
+
+
+ + Add personal note +
+
+
Personal Note:
+
+
+ +
+
+
+
+
+
+ +
+
Fee: Less than 1 cent
+
+ +
+
+ + + {{buttonText}} + + + {{buttonText}} + + + Payment Sent + Proposal Created + Transaction Created + + + + + +