From 29d5d708701a9934c98a26a8aa86cb4b8416d9ac Mon Sep 17 00:00:00 2001
From: blavenie <benoit.lavenier@e-is.pro>
Date: Tue, 22 Aug 2017 11:34:48 +0200
Subject: [PATCH] [enh] Finish payment API (doc + transfer form) - fix #527

---
 www/i18n/locale-en-GB.json         |  59 +++++++++++
 www/i18n/locale-en.json            |  59 +++++++++++
 www/i18n/locale-fr-FR.json         |  23 +++-
 www/js/api/app.js                  | 164 ++++++++++++++++++++++++-----
 www/js/config.js                   |   2 +-
 www/js/services/utils-services.js  |   2 +-
 www/js/services/wallet-services.js |   6 ++
 www/templates/api/doc.html         |   6 +-
 www/templates/api/transfer.html    |  15 ++-
 9 files changed, 297 insertions(+), 39 deletions(-)

diff --git a/www/i18n/locale-en-GB.json b/www/i18n/locale-en-GB.json
index c19c8e568..0de122a39 100644
--- a/www/i18n/locale-en-GB.json
+++ b/www/i18n/locale-en-GB.json
@@ -763,5 +763,64 @@
       "END_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>Welcome to the <b>free economy</b>!",
       "END_NOT_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>If you wish to join the currency {{currency|capitalize}}, simply click <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> below."
     }
+  },
+  "API" :{
+    "COMMON": {
+      "LINK_DOC": "API documentation",
+      "LINK_DOC_HELP": "API documentation for developers"
+    },
+    "HOME": {
+      "TITLE": "{{'COMMON.APP_NAME'|translate}} API Documentation",
+      "MESSAGE": "Welcome to the {{'COMMON.APP_NAME'|translate}} <b>API documentation </b>.<br/>Connect your web site to <a href=\"http://duniter.org\" target=\"_system\">Duniter</a> very easily!",
+      "MESSAGE_SHORT": "Connect your websites to <a href=\"http://duniter.org\" target=\"_system\">Duniter</a> very easily!",
+      "DOC_HEADER": "Available services:"
+    },
+    "TRANSFER": {
+      "TITLE": "{{'COMMON.APP_NAME'|translate}} - Online payment",
+      "TITLE_SHORT": "Online payment",
+      "SUMMARY": "Order summary:",
+      "AMOUNT": "Amount:",
+      "NAME": "Recipient:",
+      "PUBKEY": "Public key of the recipient:",
+      "COMMENT": "Order reference:",
+
+      "DEMO": {
+        "SALT": "demo",
+        "PASSWORD": "demo",
+        "PUBKEY": "3G28bL6deXQBYpPBpLFuECo46d3kfYMJwst7uhdVBnD1",
+        "HELP": "<b>Demonstration mode</b>: No payment will actually be sent during this simulation.<br/>Please use credentials: <b>{{'API.TRANSFER.DEMO.SALT'|translate}} / {{'API.TRANSFER.DEMO.PASSWORD'|translate}}</b>",
+        "BAD_CREDENTIALS": "Invalid credentials.<br/>In demonstration mode, credentials should be: {{'API.TRANSFER.DEMO.SALT'|translate}} / {{'API.TRANSFER.DEMO.PASSWORD'|translate}}"
+      },
+      "INFO": {
+        "SUCCESS_REDIRECTING_WITH_NAME": "Payment sent.<br/>Redirect to <b>{{name}}</b>...",
+        "SUCCESS_REDIRECTING": "Payment sent.<br/>Redirect to the seller's website...",
+        "CANCEL_REDIRECTING_WITH_NAME": "Payment cancelled.<br/>Redirect to <b>{{name}}</b>...",
+        "CANCEL_REDIRECTING": "Payment cancelled.<br/>Redirect to the seller's website..."
+      },
+      "ERROR": {
+        "TRANSFER_FAILED": "Payment failed"
+      }
+    },
+    "DOC": {
+      "BTN_DEMO": "Try",
+      "DESCRIPTION_DIVIDER": "Description",
+      "URL_DIVIDER": "Access URL",
+      "PARAMETERS_DIVIDER": "Parameters",
+      "AVAILABLE_PARAMETERS": "Here is the list of parameters and options:",
+      "SUCCEED": "<i class=\"icon ion-checkmark\"></i> Success!",
+      "CANCELLED": "<i class=\"icon ion-close\"></i> Canceled by user",
+      "RESULT": "Result returned by call:",
+      "TRANSFER": {
+        "TITLE": "Payments",
+        "DESCRIPTION": "From a site (eg online marketplace) you can delegate payment in free currency to Cesium API. To do this, simply open a page at the following address:",
+        "PARAM_PUBKEY_HELP": "Recipient's public key (required)",
+        "PARAM_AMOUNT": "Amount",
+        "PARAM_AMOUNT_HELP": "Transaction amount (required)",
+        "PARAM_COMMENT_HELP": "Reference or comment. You will allow for example to identify the payment in the BlockChain.",
+        "PARAM_NAME_HELP": "The name of your website. This can be a readable name (eg \"My online site\"), or a web address (eg \"www.MySite.com\").",
+        "PARAM_REDIRECT_URL_HELP": "Redirect URL. Can contain the following strings, which will be replaced by the values of the transaction: \"{tx}\", \"{hash}\", \"{comment}\" and \"{amount}\".",
+        "PARAM_CANCEL_URL_HELP": "URL in case of cancellation"
+      }
+    }
   }
 }
diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json
index 6c77fc94e..aadb06afa 100644
--- a/www/i18n/locale-en.json
+++ b/www/i18n/locale-en.json
@@ -763,5 +763,64 @@
       "END_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>Welcome to the <b>free economy</b>!",
       "END_NOT_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>If you wish to join the currency {{currency|capitalize}}, simply click <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> below."
     }
+  },
+  "API" :{
+    "COMMON": {
+      "LINK_DOC": "API documentation",
+      "LINK_DOC_HELP": "API documentation for developers"
+    },
+    "HOME": {
+      "TITLE": "{{'COMMON.APP_NAME'|translate}} API Documentation",
+      "MESSAGE": "Welcome to the {{'COMMON.APP_NAME'|translate}} <b>API documentation </b>.<br/>Connect your web site to <a href=\"http://duniter.org\" target=\"_system\">Duniter</a> very easily!",
+      "MESSAGE_SHORT": "Connect your websites to <a href=\"http://duniter.org\" target=\"_system\">Duniter</a> very easily!",
+      "DOC_HEADER": "Available services:"
+    },
+    "TRANSFER": {
+      "TITLE": "{{'COMMON.APP_NAME'|translate}} - Online payment",
+      "TITLE_SHORT": "Online payment",
+      "SUMMARY": "Order summary:",
+      "AMOUNT": "Amount:",
+      "NAME": "Recipient:",
+      "PUBKEY": "Public key of the recipient:",
+      "COMMENT": "Order reference:",
+
+      "DEMO": {
+        "SALT": "demo",
+        "PASSWORD": "demo",
+        "PUBKEY": "3G28bL6deXQBYpPBpLFuECo46d3kfYMJwst7uhdVBnD1",
+        "HELP": "<b>Demonstration mode</b>: No payment will actually be sent during this simulation.<br/>Please use credentials: <b>{{'API.TRANSFER.DEMO.SALT'|translate}} / {{'API.TRANSFER.DEMO.PASSWORD'|translate}}</b>",
+        "BAD_CREDENTIALS": "Invalid credentials.<br/>In demonstration mode, credentials should be: {{'API.TRANSFER.DEMO.SALT'|translate}} / {{'API.TRANSFER.DEMO.PASSWORD'|translate}}"
+      },
+      "INFO": {
+        "SUCCESS_REDIRECTING_WITH_NAME": "Payment sent.<br/>Redirect to <b>{{name}}</b>...",
+        "SUCCESS_REDIRECTING": "Payment sent.<br/>Redirect to the seller's website...",
+        "CANCEL_REDIRECTING_WITH_NAME": "Payment cancelled.<br/>Redirect to <b>{{name}}</b>...",
+        "CANCEL_REDIRECTING": "Payment cancelled.<br/>Redirect to the seller's website..."
+      },
+      "ERROR": {
+        "TRANSFER_FAILED": "Payment failed"
+      }
+    },
+    "DOC": {
+      "BTN_DEMO": "Try",
+      "DESCRIPTION_DIVIDER": "Description",
+      "URL_DIVIDER": "Access URL",
+      "PARAMETERS_DIVIDER": "Parameters",
+      "AVAILABLE_PARAMETERS": "Here is the list of parameters and options:",
+      "SUCCEED": "<i class=\"icon ion-checkmark\"></i> Success!",
+      "CANCELLED": "<i class=\"icon ion-close\"></i> Canceled by user",
+      "RESULT": "Result returned by call:",
+      "TRANSFER": {
+        "TITLE": "Payments",
+        "DESCRIPTION": "From a site (eg online marketplace) you can delegate payment in free currency to Cesium API. To do this, simply open a page at the following address:",
+        "PARAM_PUBKEY_HELP": "Recipient's public key (required)",
+        "PARAM_AMOUNT": "Amount",
+        "PARAM_AMOUNT_HELP": "Transaction amount (required)",
+        "PARAM_COMMENT_HELP": "Reference or comment. You will allow for example to identify the payment in the BlockChain.",
+        "PARAM_NAME_HELP": "The name of your website. This can be a readable name (eg \"My online site\"), or a web address (eg \"www.MySite.com\").",
+        "PARAM_REDIRECT_URL_HELP": "Redirect URL. Can contain the following strings, which will be replaced by the values of the transaction: \"{tx}\", \"{hash}\", \"{comment}\" and \"{amount}\".",
+        "PARAM_CANCEL_URL_HELP": "URL in case of cancellation"
+      }
+    }
   }
 }
diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json
index ecf9836d3..0ffe7469c 100644
--- a/www/i18n/locale-fr-FR.json
+++ b/www/i18n/locale-fr-FR.json
@@ -783,6 +783,20 @@
       "NAME": "Destinataire :",
       "PUBKEY": "Clé publique du destinaire :",
       "COMMENT": "Référence de la commande :",
+
+      "DEMO": {
+        "SALT": "demo",
+        "PASSWORD": "demo",
+        "PUBKEY": "3G28bL6deXQBYpPBpLFuECo46d3kfYMJwst7uhdVBnD1",
+        "HELP": "<b>Mode démonstration</b> : Aucun paiement ne sera réellement envoyé pendant cette simulation.<br/>Veuillez utiliser les identifiants : <b>{{'API.TRANSFER.DEMO.SALT'|translate}} / {{'API.TRANSFER.DEMO.PASSWORD'|translate}}</b>",
+        "BAD_CREDENTIALS": "Vérifiez votre saisie.<br/>En mode démonstration, les identifiants sont : {{'API.TRANSFER.DEMO.SALT'|translate}} / {{'API.TRANSFER.DEMO.PASSWORD'|translate}}"
+      },
+      "INFO": {
+        "SUCCESS_REDIRECTING_WITH_NAME": "Paiement envoyé.<br/>Redirection vers <b>{{name}}</b>...",
+        "SUCCESS_REDIRECTING": "Paiement envoyé.<br/>Redirection vers le site du vendeur...",
+        "CANCEL_REDIRECTING_WITH_NAME": "Paiement annulé.<br/>Redirection vers <b>{{name}}</b>...",
+        "CANCEL_REDIRECTING": "Paiement annulé.<br/>Redirection vers le site du vendeur..."
+      },
       "ERROR": {
         "TRANSFER_FAILED": "Echec du paiement"
       }
@@ -791,19 +805,20 @@
       "BTN_DEMO": "Démo",
       "DESCRIPTION_DIVIDER": "Description",
       "URL_DIVIDER": "URL d'accès",
-      "PARAMETERS_DIVIDER": "Options",
-      "AVAILABLE_PARAMETERS": "Voici la liste des options possibles :",
+      "PARAMETERS_DIVIDER": "Paramètres",
+      "AVAILABLE_PARAMETERS": "Voici la liste des paramètres et options :",
       "SUCCEED": "<i class=\"icon ion-checkmark\"></i> Succès !",
       "CANCELLED": "<i class=\"icon ion-close\"></i> Annulé par l'utilisateur",
       "RESULT": "Résultat retourné par l'appel :",
       "TRANSFER": {
         "TITLE": "Paiements",
         "DESCRIPTION": "Depuis un site (ex: vente en ligne) vous pouvez déléguer le paiement en monnaie libre à Cesium API. Pour cela, il vous suffit de déclencher l'ouveture d'un page sur l'adresse suivante :",
+        "PARAM_PUBKEY_HELP": "Clé publique du destinataire (obligatoire)",
         "PARAM_AMOUNT": "Montant",
         "PARAM_AMOUNT_HELP": "Montant de la transaction (obligatoire)",
         "PARAM_COMMENT_HELP": "Référence ou commentaire. Vous permettra par exemple d'identifier le paiement dans la BlockChain.",
-        "PARAM_NAME_HELP": "Le nom de votre site. Cela peut-etre un nom lisible (\"Mon site en ligne\"), ou encore une pseudo-adresse web (\"MonSite.com\").",
-        "PARAM_REDIRECT_URL_HELP": "URL de redirection",
+        "PARAM_NAME_HELP": "Le nom de votre site web. Cela peut-etre un nom lisible (\"Mon site en ligne\"), ou encore une pseudo-adresse web (\"MonSite.com\").",
+        "PARAM_REDIRECT_URL_HELP": "URL de redirection. Peut contenir les chaines suivantes, qui seront remplacée par les valeurs de la transaction : \"{tx}\", \"{hash}\", \"{comment}\" et \"{amount}\".",
         "PARAM_CANCEL_URL_HELP": "URL en cas d'annulation"
       }
     }
diff --git a/www/js/api/app.js b/www/js/api/app.js
index 255c37840..4331715a2 100644
--- a/www/js/api/app.js
+++ b/www/js/api/app.js
@@ -42,6 +42,7 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
       })
 
       .state('api.transfer', {
+        cache: false,
         url: "/payment/:pubkey?name&amount&udAmount&comment&redirect_url&cancel_url&demo",
         views: {
           'menuContent': {
@@ -78,8 +79,8 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
       comment: 'REF-0111',
       name: 'mon-site.com',
       pubkey: 'G2CBgZBPLe6FSFUgpx2Jf1Aqsgta6iib3vmDRA1yLiqU',
-      redirect_url: $rootScope.rootPath + '#/app/home?result={comment}&service=payment',
-      cancel_url: $rootScope.rootPath + '#/app/home?cancel&service=payment'
+      redirect_url: $rootScope.rootPath + '#/app/home?service=payment&result={comment}%0A{hash}%0A{tx}',
+      cancel_url: $rootScope.rootPath + '#/app/home?service=payment&cancel'
     };
     $scope.result = {};
 
@@ -100,7 +101,7 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
   })
 
   .controller('ApiTransferCtrl', function ($scope, $rootScope, $timeout, $controller, $state, $q, $translate, $filter,
-                                           BMA, UIUtils, csCurrency, csTx, csWallet){
+                                           BMA, CryptoUtils, UIUtils, csCurrency, csTx, csWallet){
     'ngInject';
 
     // Initialize the super class and extend it.
@@ -141,27 +142,74 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
     };
     $scope.$on('$ionicView.enter', $scope.enter);
 
-    function createDemoWallet() {
+    function createDemoWallet(authData) {
+      var demoPubkey = '3G28bL6deXQBYpPBpLFuECo46d3kfYMJwst7uhdVBnD1';
+
       return {
         start: function() {
           return $q.when();
         },
         login: function() {
-          return $q.when({
-            uid: 'Demo',
-            pubkey: 'fakeDemoPublicKey'
-          });
+          var self = this;
+          return $translate('API.TRANSFER.DEMO.PUBKEY')
+            .then(function(pubkey) {
+              if (!authData || authData.pubkey != '3G28bL6deXQBYpPBpLFuECo46d3kfYMJwst7uhdVBnD1') {
+                throw {message: 'API.TRANSFER.DEMO.BAD_CREDENTIALS'};
+              }
+              self.data = {
+                keypair: authData.keypair
+              };
+              return {
+                uid: 'Demo',
+                pubkey: demoPubkey
+              };
+            });
         },
         transfer: function() {
-          return $q.when();
+          var self = this;
+          return BMA.blockchain.current()
+            .then(function(block) {
+              var tx = 'Version: '+ BMA.constants.PROTOCOL_VERSION +'\n' +
+                'Type: Transaction\n' +
+                'Currency: ' + block.currency + '\n' +
+                'Blockstamp: ' + block.number + '-' + block.hash + '\n' +
+                'Locktime: 0\n' + // no lock
+                'Issuers:\n' +
+                demoPubkey + '\n' +
+                'Inputs:\n' +
+                [$scope.transferData.amount, block.unitbase, 'T', 'FakeId27jQMAf3jqL2fr75ckZ6Jgi9TZL9fMf9TR9vBvG', 0].join(':')+ '\n' +
+                'Unlocks:\n' +
+                '0:SIG(0)\n' +
+                'Outputs:\n' +
+                [$scope.transferData.amount, block.unitbase, 'SIG(' + $scope.transferData.pubkey + ')'].join(':')+'\n' +
+                'Comment: '+ ($scope.transferData.comment||'') + '\n';
+
+              return CryptoUtils.sign(tx, self.data.keypair)
+                .then(function(signature) {
+                  var signedTx = tx + signature + "\n";
+                  return CryptoUtils.util.hash(signedTx)
+                    .then(function(txHash) {
+                      return $q.when({
+                        tx: signedTx,
+                        hash: txHash
+                      });
+                    });
+                })
+            });
         }
       };
     }
 
     function onLogin(authData) {
-      if (!authData) return;
 
-      var wallet = $scope.demo ? createDemoWallet() : csWallet.instance('api', BMA);
+      // User cancelled
+      if (!authData) return $scope.onCancel();
+
+      // Avoid multiple click
+      if ($scope.sending) return;
+      $scope.sending = true;
+
+      var wallet = $scope.demo ? createDemoWallet(authData) : csWallet.instance('api', BMA);
 
       UIUtils.loading.show();
 
@@ -177,33 +225,37 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
         })
         .then(function(confirm) {
           if (!confirm) return;
+
           // sent transfer
           return UIUtils.loading.show()
             .then(function(){
               var amount = parseInt($scope.transferData.amount); // remove 2 decimals on quantitative mode
               return wallet.transfer($scope.transferData.pubkey, amount, $scope.transferData.comment, false /*always quantitative mode*/);
             })
-            .then(function() {
+            .then(function(txRes) {
+
               UIUtils.loading.hide();
-              return true; // sent
+              return txRes;
             })
-            .catch(UIUtils.onError('API.TRANSFER.ERROR.TRANSFER_FAILED'));
+            .catch(function(err) {
+              UIUtils.onError()(err);
+              return false;
+            });
         })
-        .then(function(sent) {
-          if (sent && $scope.transferData.redirect_url) {
-            return $timeout(function () {
-              // if iframe: send to parent
-              if (window.top && window.top.location) {
-                window.top.location.href = $scope.transferData.redirect_url;
-              }
-              else if (parent && parent.document && parent.document.location) {
-                parent.document.location.href = $scope.transferData.redirect_url;
-              }
-              else {
-                window.location.assign($scope.transferData.redirect_url);
-              }
-            }, 3000);
+        .then(function(txRes) {
+          if (txRes) {
+            return $scope.onSuccess(txRes);
+          }
+          else {
+            $scope.sending = false;
+          }
+        })
+        .catch(function(err){
+          if (err && err === 'CANCELLED') {
+            return $scope.onCancel();
           }
+          $scope.sending = false;
+          UIUtils.onError()(err);
         });
     }
 
@@ -222,6 +274,62 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
         .then(UIUtils.alert.confirm);
     };
 
+    $scope.onSuccess = function(txRes) {
+      if (!$scope.transferData.redirect_url) {
+        return UIUtils.toast.show('INFO.TRANSFER_SENT');
+      }
+
+      return ($scope.transferData.name ? $translate('API.TRANSFER.INFO.SUCCESS_REDIRECTING_WITH_NAME', $scope.transferData) : $translate('API.TRANSFER.INFO.SUCCESS_REDIRECTING'))
+        .then(function(message){
+          return UIUtils.loading.show({template: message});
+        })
+        .then(function() {
+          var url = $scope.transferData.redirect_url;
+          // Make replacements
+          url = url.replace(/\{hash\}/g, txRes.hash||'');
+          url = url.replace(/\{comment\}/g, $scope.transferData.comment||'');
+          url = url.replace(/\{amount\}/g, $scope.transferData.amount.toString());
+          url = url.replace(/\{tx\}/g, encodeURI(txRes.tx));
+
+          return $scope.redirectToUrl(url, 2500);
+        });
+    };
+
+    $scope.onCancel = function() {
+      if (!$scope.transferData.cancel_url) {
+        // TODO: clean form ?
+        $scope.formData.salt = undefined;
+        $scope.formData.password = undefined;
+        return; // nothing to do
+      }
+
+      return ($scope.transferData.name ? $translate('API.TRANSFER.INFO.CANCEL_REDIRECTING_WITH_NAME', $scope.transferData) : $translate('API.TRANSFER.INFO.CANCEL_REDIRECTING'))
+        .then(function(message){
+          return UIUtils.loading.show({template: message});
+        })
+        .then(function() {
+          return $scope.redirectToUrl($scope.transferData.cancel_url, 1500);
+        });
+    };
+
+    $scope.redirectToUrl = function(url, timeout) {
+      if (!url) return;
+
+      return $timeout(function() {
+        // if iframe: send to parent
+        if (window.top && window.top.location) {
+          window.top.location.href = url;
+        }
+        else if (parent && parent.document && parent.document.location) {
+          parent.document.location.href = url;
+        }
+        else {
+          window.location.assign(url);
+        }
+        return UIUtils.loading.hide();
+      }, timeout||0);
+    };
+
     /* -- methods need by Login controller -- */
 
     $scope.setForm = function(form) {
diff --git a/www/js/config.js b/www/js/config.js
index 60dfb25af..25377cb55 100644
--- a/www/js/config.js
+++ b/www/js/config.js
@@ -67,7 +67,7 @@ angular.module("cesium.config", [])
 		}
 	},
 	"version": "0.16.1",
-	"build": "2017-08-21T21:55:39.967Z",
+	"build": "2017-08-22T06:59:53.332Z",
 	"newIssueUrl": "https://github.com/duniter/cesium/issues/new?labels=bug"
 })
 
diff --git a/www/js/services/utils-services.js b/www/js/services/utils-services.js
index f99cba37f..588bf2db5 100644
--- a/www/js/services/utils-services.js
+++ b/www/js/services/utils-services.js
@@ -129,7 +129,7 @@ angular.module('cesium.utils.services', [])
       return $translate('COMMON.LOADING')
         .then(function(translation){
           loadingTextCache = translation;
-          return showLoading();
+          return showLoading(options);
         });
     }
     options = options || {};
diff --git a/www/js/services/wallet-services.js b/www/js/services/wallet-services.js
index b0d835508..891cf66a1 100644
--- a/www/js/services/wallet-services.js
+++ b/www/js/services/wallet-services.js
@@ -936,6 +936,11 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se
                   // API extension
                   api.data.raise.balanceChanged(data);
                   api.data.raise.newTx(data);
+
+                  // Return TX hash (if chained TXs, return the last tx hash) - required by Cesium-API
+                  return {
+                    hash: res.hash
+                  };
                 });
             })
             .catch(function(err) {
@@ -1133,6 +1138,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se
                 output.pending = true;
               });
               return {
+                tx: signedTx,
                 hash: txHash,
                 sources: newSources
               };
diff --git a/www/templates/api/doc.html b/www/templates/api/doc.html
index 373bb0cf6..9bb07912c 100644
--- a/www/templates/api/doc.html
+++ b/www/templates/api/doc.html
@@ -30,6 +30,10 @@
 
     <div class="item item-text-wrap">
       <p translate>API.DOC.AVAILABLE_PARAMETERS</p>
+      <div class="row">
+        <div class="col col-20 text-italic">pubkey</div>
+        <div class="col" translate>API.DOC.TRANSFER.PARAM_PUBKEY_HELP</div>
+      </div>
       <div class="row">
         <div class="col col-20 text-italic">amount</div>
         <div class="col" translate>API.DOC.TRANSFER.PARAM_AMOUNT_HELP</div>
@@ -61,7 +65,7 @@
     <div class="item item-text-wrap" ng-if="result.type === 'payment' && !result.cancelled">
       <h2 class="text-right balanced" translate>API.DOC.SUCCEED</h2>
       <h4 class="gray" translate>API.DOC.RESULT</h4>
-      <p class="balanced-100-bg padding dark"> {{result.content}}</p>
+      <p class="balanced-100-bg padding dark text-keep-lines">{{result.content}}</p>
     </div>
     <div class="item item-text-wrap" ng-if="result.type === 'payment' && result.cancelled">
       <h2 class="text-right assertive" translate>API.DOC.CANCELLED</h2>
diff --git a/www/templates/api/transfer.html b/www/templates/api/transfer.html
index 01fc26567..ee0d5c8c8 100644
--- a/www/templates/api/transfer.html
+++ b/www/templates/api/transfer.html
@@ -16,7 +16,7 @@
     </button>
   </ion-nav-buttons>
 
-  <ion-content class=" has-header no-padding-xs positive-900-bg text-center">
+  <ion-content class=" has-header no-padding-xs positive-900-bg">
 
     <br class="hidden-xs"/>
 
@@ -28,11 +28,18 @@
 
         <div class="light-bg">
 
-          <h2 class="padding-top">
+          <h2 class="padding-top text-center">
             <span class="hidden-xs" translate>API.TRANSFER.TITLE</span>
             <span class="visible-xs" translate>API.TRANSFER.TITLE_SHORT</span>
           </h2>
 
+          <div class="no-padding energized-100-bg" ng-if="demo">
+            <div class="item item-icon-left item-text-wrap no-border">
+              <i class="icon ion-information-circled positive" ></i>
+              <p translate>API.TRANSFER.DEMO.HELP</p>
+            </div>
+          </div>
+
           <ng-include src="'templates/login/form_login.html'"></ng-include>
 
         </div>
@@ -78,12 +85,12 @@
       </div>
     </div>
 
-    <p class="visible-xs visible-sm light padding-top">
+    <p class="visible-xs visible-sm light padding-top text-center">
       {{'COMMON.APP_NAME'|translate}}
       - <a href="#" ng-click="showAboutModal($event)">v{{$root.config.version}}</a>
     </p>
 
-    <p class="hidden-xs hidden-sm gray padding-top">
+    <p class="hidden-xs hidden-sm gray padding-top text-center">
       {{'COMMON.APP_NAME'|translate}} v{{$root.config.version}}
       - <a href="#" ng-click="showAboutModal($event)" title="{{'HOME.BTN_ABOUT'|translate}}">{{'HOME.BTN_ABOUT'|translate}}</a>
       - <a ui-sref="app.home" target="_blank" title="{{'API.COMMON.LINK_DOC_HELP'|translate}}">{{'API.COMMON.LINK_DOC'|translate}}</a>
-- 
GitLab