gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GNUnet-SVN] [taler-wallet-webex] branch master updated: partial impleme


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated: partial implementation of tipping
Date: Fri, 01 Dec 2017 03:00:17 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-webex.

The following commit(s) were added to refs/heads/master by this push:
     new b8ccc7c9 partial implementation of tipping
b8ccc7c9 is described below

commit b8ccc7c990a1542cf80578b41972f9a5b0870af9
Author: Florian Dold <address@hidden>
AuthorDate: Thu Nov 30 04:07:36 2017 +0100

    partial implementation of tipping
---
 img/spinner-bars.svg                       |  53 +++++++
 src/crypto/cryptoApi.ts                    |   5 +
 src/crypto/cryptoWorker.ts                 |  31 ++++
 src/i18n/de.po                             | 113 +++++++--------
 src/i18n/en-US.po                          | 113 +++++++--------
 src/i18n/fr.po                             | 113 +++++++--------
 src/i18n/it.po                             | 113 +++++++--------
 src/i18n/strings.ts                        | 192 ++++++++++++-------------
 src/i18n/taler-wallet-webex.pot            | 113 +++++++--------
 src/query.ts                               |  70 ++++++---
 src/types.ts                               | 224 +++++++++++++++++++++++++++++
 src/wallet.ts                              | 183 +++++++++++++++++++----
 src/webex/messages.ts                      |  16 +++
 src/webex/notify.ts                        |  85 ++++++++++-
 src/webex/pages/confirm-create-reserve.tsx | 135 +----------------
 src/webex/pages/tip.html                   |  24 ++++
 src/webex/pages/tip.tsx                    | 155 ++++++++++++++++++++
 src/webex/renderHtml.tsx                   | 145 +++++++++++++++++++
 src/webex/style/wallet.css                 |  15 ++
 src/webex/wxApi.ts                         |  23 +++
 src/webex/wxBackend.ts                     |  25 +++-
 tsconfig.json                              |   1 +
 webpack.config.js                          |   1 +
 23 files changed, 1393 insertions(+), 555 deletions(-)

diff --git a/img/spinner-bars.svg b/img/spinner-bars.svg
new file mode 100644
index 00000000..f6f7dfcb
--- /dev/null
+++ b/img/spinner-bars.svg
@@ -0,0 +1,53 @@
+<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
+<svg width="135" height="140" viewBox="0 0 135 140" 
xmlns="http://www.w3.org/2000/svg"; fill="#fff">
+    <rect y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.5s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.5s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="30" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.25s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.25s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="60" width="15" height="140" rx="6">
+        <animate attributeName="height"
+             begin="0s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="90" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.25s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.25s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="120" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.5s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.5s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+</svg>
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts
index 00013f0d..5300c137 100644
--- a/src/crypto/cryptoApi.ts
+++ b/src/crypto/cryptoApi.ts
@@ -34,6 +34,7 @@ import {
   PreCoinRecord,
   RefreshSessionRecord,
   ReserveRecord,
+  TipPlanchet,
   WireFee,
 } from "../types";
 
@@ -253,6 +254,10 @@ export class CryptoApi {
     return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve);
   }
 
+  createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {
+    return this.doRpc<TipPlanchet>("createTipPlanchet", 1, denom);
+  }
+
   hashString(str: string): Promise<string> {
     return this.doRpc<string>("hashString", 1, str);
   }
diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts
index 0a93fcb0..5ec7c18e 100644
--- a/src/crypto/cryptoWorker.ts
+++ b/src/crypto/cryptoWorker.ts
@@ -40,6 +40,7 @@ import {
   RefreshPreCoinRecord,
   RefreshSessionRecord,
   ReserveRecord,
+  TipPlanchet,
   WireFee,
 } from "../types";
 
@@ -103,6 +104,7 @@ namespace RpcFunctions {
       coinValue: denom.value,
       denomPub: denomPub.encode().toCrock(),
       exchangeBaseUrl: reserve.exchange_base_url,
+      isFromTip: false,
       reservePub: reservePub.toCrock(),
       withdrawSig: sig.toCrock(),
     };
@@ -110,6 +112,35 @@ namespace RpcFunctions {
   }
 
 
+  export function createTipPlanchet(denom: DenominationRecord): TipPlanchet {
+    const denomPub = native.RsaPublicKey.fromCrock(denom.denomPub);
+    const coinPriv = native.EddsaPrivateKey.create();
+    const coinPub = coinPriv.getPublicKey();
+    const blindingFactor = native.RsaBlindingKeySecret.create();
+    const pubHash: native.HashCode = coinPub.hash();
+    const ev = native.rsaBlind(pubHash, blindingFactor, denomPub);
+
+    if (!ev) {
+      throw Error("couldn't blind (malicious exchange key?)");
+    }
+
+    if (!denom.feeWithdraw) {
+      throw Error("Field fee_withdraw missing");
+    }
+
+    const tipPlanchet: TipPlanchet = {
+      blindingKey: blindingFactor.toCrock(),
+      coinEv: ev.toCrock(),
+      coinPriv: coinPriv.toCrock(),
+      coinPub: coinPub.toCrock(),
+      coinValue: denom.value,
+      denomPubHash: denomPub.encode().hash().toCrock(),
+      denomPub: denomPub.encode().toCrock(),
+    };
+    return tipPlanchet;
+  }
+
+
   /**
    * Create and sign a message to request payback for a coin.
    */
diff --git a/src/i18n/de.po b/src/i18n/de.po
index 253abb9e..23b5ad53 100644
--- a/src/i18n/de.po
+++ b/src/i18n/de.po
@@ -66,67 +66,27 @@ msgstr ""
 msgid "Confirm payment"
 msgstr "Bezahlung bestätigen"
 
-#: src/webex/pages/confirm-create-reserve.tsx:178
-#, fuzzy, c-format
-msgid "Withdrawal fees:"
-msgstr "Abheben bei %1$s"
-
-#: src/webex/pages/confirm-create-reserve.tsx:179
-#, c-format
-msgid "Rounding loss:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:180
-#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:185
-#, c-format
-msgid "# Coins"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:186
-#, c-format
-msgid "Value"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:187
-#, fuzzy, c-format
-msgid "Withdraw Fee"
-msgstr "Abheben bei %1$s"
-
-#: src/webex/pages/confirm-create-reserve.tsx:188
-#, c-format
-msgid "Refresh Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:189
-#, c-format
-msgid "Deposit Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:243
+#: src/webex/pages/confirm-create-reserve.tsx:121
 #, c-format
 msgid "Select"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:259
+#: src/webex/pages/confirm-create-reserve.tsx:137
 #, c-format
 msgid "Error: URL may not be relative"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:327
+#: src/webex/pages/confirm-create-reserve.tsx:205
 #, c-format
 msgid "The exchange is trusted by the wallet.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:333
+#: src/webex/pages/confirm-create-reserve.tsx:211
 #, c-format
 msgid "The exchange is audited by a trusted auditor.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:339
+#: src/webex/pages/confirm-create-reserve.tsx:217
 #, c-format
 msgid ""
 "Warning:  The exchange is neither directly trusted nor audited by a trusted "
@@ -134,7 +94,7 @@ msgid ""
 "If you withdraw from this exchange, it will be trusted in the future.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:348
+#: src/webex/pages/confirm-create-reserve.tsx:226
 #, c-format
 msgid ""
 "Using exchange provider%1$s.\n"
@@ -142,58 +102,59 @@ msgid ""
 " %2$s in fees.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:362
+#: src/webex/pages/confirm-create-reserve.tsx:240
 #, c-format
 msgid ""
 "Waiting for a response from\n"
 " %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:379
+#: src/webex/pages/confirm-create-reserve.tsx:257
 #, c-format
 msgid ""
 "Information about fees will be available when an exchange provider is "
 "selected."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:422
+#: src/webex/pages/confirm-create-reserve.tsx:300
 #, c-format
 msgid "Accept fees and withdraw"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:427
+#: src/webex/pages/confirm-create-reserve.tsx:305
 #, c-format
 msgid "Change Exchange Provider"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:484
+#: src/webex/pages/confirm-create-reserve.tsx:357
 #, c-format
 msgid "You are about to withdraw %1$s from your bank account into your wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:569
+#: src/webex/pages/confirm-create-reserve.tsx:442
 #, c-format
 msgid ""
 "Oops, something went wrong. The wallet responded with error status (%1$s)."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:578
+#: src/webex/pages/confirm-create-reserve.tsx:451
 #, c-format
 msgid "Checking URL, please wait ..."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:592
+#: src/webex/pages/confirm-create-reserve.tsx:465
 #, c-format
 msgid "Can't parse amount: %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:599
+#: src/webex/pages/confirm-create-reserve.tsx:472
 #, c-format
 msgid "Can't parse wire_types: %1$s"
 msgstr ""
 
+#. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:625
+#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
@@ -324,6 +285,46 @@ msgstr "Bezahlung bestätigen"
 msgid "Cancel"
 msgstr "Saldo"
 
+#: src/webex/renderHtml.tsx:209
+#, fuzzy, c-format
+msgid "Withdrawal fees:"
+msgstr "Abheben bei %1$s"
+
+#: src/webex/renderHtml.tsx:210
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:211
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:216
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:217
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:218
+#, fuzzy, c-format
+msgid "Withdraw Fee"
+msgstr "Abheben bei %1$s"
+
+#: src/webex/renderHtml.tsx:219
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:220
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
 #: src/wire.ts:38
 #, c-format
 msgid "Invalid Wire"
diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po
index 85926f21..6ceaf22a 100644
--- a/src/i18n/en-US.po
+++ b/src/i18n/en-US.po
@@ -66,67 +66,27 @@ msgstr ""
 msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:178
-#, c-format
-msgid "Withdrawal fees:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:179
-#, c-format
-msgid "Rounding loss:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:180
-#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:185
-#, c-format
-msgid "# Coins"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:186
-#, c-format
-msgid "Value"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:187
-#, c-format
-msgid "Withdraw Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:188
-#, c-format
-msgid "Refresh Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:189
-#, c-format
-msgid "Deposit Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:243
+#: src/webex/pages/confirm-create-reserve.tsx:121
 #, c-format
 msgid "Select"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:259
+#: src/webex/pages/confirm-create-reserve.tsx:137
 #, c-format
 msgid "Error: URL may not be relative"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:327
+#: src/webex/pages/confirm-create-reserve.tsx:205
 #, c-format
 msgid "The exchange is trusted by the wallet.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:333
+#: src/webex/pages/confirm-create-reserve.tsx:211
 #, c-format
 msgid "The exchange is audited by a trusted auditor.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:339
+#: src/webex/pages/confirm-create-reserve.tsx:217
 #, c-format
 msgid ""
 "Warning:  The exchange is neither directly trusted nor audited by a trusted "
@@ -134,7 +94,7 @@ msgid ""
 "If you withdraw from this exchange, it will be trusted in the future.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:348
+#: src/webex/pages/confirm-create-reserve.tsx:226
 #, c-format
 msgid ""
 "Using exchange provider%1$s.\n"
@@ -142,58 +102,59 @@ msgid ""
 " %2$s in fees.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:362
+#: src/webex/pages/confirm-create-reserve.tsx:240
 #, c-format
 msgid ""
 "Waiting for a response from\n"
 " %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:379
+#: src/webex/pages/confirm-create-reserve.tsx:257
 #, c-format
 msgid ""
 "Information about fees will be available when an exchange provider is "
 "selected."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:422
+#: src/webex/pages/confirm-create-reserve.tsx:300
 #, c-format
 msgid "Accept fees and withdraw"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:427
+#: src/webex/pages/confirm-create-reserve.tsx:305
 #, c-format
 msgid "Change Exchange Provider"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:484
+#: src/webex/pages/confirm-create-reserve.tsx:357
 #, c-format
 msgid "You are about to withdraw %1$s from your bank account into your wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:569
+#: src/webex/pages/confirm-create-reserve.tsx:442
 #, c-format
 msgid ""
 "Oops, something went wrong. The wallet responded with error status (%1$s)."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:578
+#: src/webex/pages/confirm-create-reserve.tsx:451
 #, c-format
 msgid "Checking URL, please wait ..."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:592
+#: src/webex/pages/confirm-create-reserve.tsx:465
 #, c-format
 msgid "Can't parse amount: %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:599
+#: src/webex/pages/confirm-create-reserve.tsx:472
 #, c-format
 msgid "Can't parse wire_types: %1$s"
 msgstr ""
 
+#. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:625
+#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
@@ -321,6 +282,46 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
+#: src/webex/renderHtml.tsx:209
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:210
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:211
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:216
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:217
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:218
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:219
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:220
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
 #: src/wire.ts:38
 #, c-format
 msgid "Invalid Wire"
diff --git a/src/i18n/fr.po b/src/i18n/fr.po
index f8d2023b..41c6e6d6 100644
--- a/src/i18n/fr.po
+++ b/src/i18n/fr.po
@@ -66,67 +66,27 @@ msgstr ""
 msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:178
-#, c-format
-msgid "Withdrawal fees:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:179
-#, c-format
-msgid "Rounding loss:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:180
-#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:185
-#, c-format
-msgid "# Coins"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:186
-#, c-format
-msgid "Value"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:187
-#, c-format
-msgid "Withdraw Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:188
-#, c-format
-msgid "Refresh Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:189
-#, c-format
-msgid "Deposit Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:243
+#: src/webex/pages/confirm-create-reserve.tsx:121
 #, c-format
 msgid "Select"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:259
+#: src/webex/pages/confirm-create-reserve.tsx:137
 #, c-format
 msgid "Error: URL may not be relative"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:327
+#: src/webex/pages/confirm-create-reserve.tsx:205
 #, c-format
 msgid "The exchange is trusted by the wallet.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:333
+#: src/webex/pages/confirm-create-reserve.tsx:211
 #, c-format
 msgid "The exchange is audited by a trusted auditor.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:339
+#: src/webex/pages/confirm-create-reserve.tsx:217
 #, c-format
 msgid ""
 "Warning:  The exchange is neither directly trusted nor audited by a trusted "
@@ -134,7 +94,7 @@ msgid ""
 "If you withdraw from this exchange, it will be trusted in the future.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:348
+#: src/webex/pages/confirm-create-reserve.tsx:226
 #, c-format
 msgid ""
 "Using exchange provider%1$s.\n"
@@ -142,58 +102,59 @@ msgid ""
 " %2$s in fees.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:362
+#: src/webex/pages/confirm-create-reserve.tsx:240
 #, c-format
 msgid ""
 "Waiting for a response from\n"
 " %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:379
+#: src/webex/pages/confirm-create-reserve.tsx:257
 #, c-format
 msgid ""
 "Information about fees will be available when an exchange provider is "
 "selected."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:422
+#: src/webex/pages/confirm-create-reserve.tsx:300
 #, c-format
 msgid "Accept fees and withdraw"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:427
+#: src/webex/pages/confirm-create-reserve.tsx:305
 #, c-format
 msgid "Change Exchange Provider"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:484
+#: src/webex/pages/confirm-create-reserve.tsx:357
 #, c-format
 msgid "You are about to withdraw %1$s from your bank account into your wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:569
+#: src/webex/pages/confirm-create-reserve.tsx:442
 #, c-format
 msgid ""
 "Oops, something went wrong. The wallet responded with error status (%1$s)."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:578
+#: src/webex/pages/confirm-create-reserve.tsx:451
 #, c-format
 msgid "Checking URL, please wait ..."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:592
+#: src/webex/pages/confirm-create-reserve.tsx:465
 #, c-format
 msgid "Can't parse amount: %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:599
+#: src/webex/pages/confirm-create-reserve.tsx:472
 #, c-format
 msgid "Can't parse wire_types: %1$s"
 msgstr ""
 
+#. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:625
+#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
@@ -321,6 +282,46 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
+#: src/webex/renderHtml.tsx:209
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:210
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:211
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:216
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:217
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:218
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:219
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:220
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
 #: src/wire.ts:38
 #, c-format
 msgid "Invalid Wire"
diff --git a/src/i18n/it.po b/src/i18n/it.po
index f8d2023b..41c6e6d6 100644
--- a/src/i18n/it.po
+++ b/src/i18n/it.po
@@ -66,67 +66,27 @@ msgstr ""
 msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:178
-#, c-format
-msgid "Withdrawal fees:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:179
-#, c-format
-msgid "Rounding loss:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:180
-#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:185
-#, c-format
-msgid "# Coins"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:186
-#, c-format
-msgid "Value"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:187
-#, c-format
-msgid "Withdraw Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:188
-#, c-format
-msgid "Refresh Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:189
-#, c-format
-msgid "Deposit Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:243
+#: src/webex/pages/confirm-create-reserve.tsx:121
 #, c-format
 msgid "Select"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:259
+#: src/webex/pages/confirm-create-reserve.tsx:137
 #, c-format
 msgid "Error: URL may not be relative"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:327
+#: src/webex/pages/confirm-create-reserve.tsx:205
 #, c-format
 msgid "The exchange is trusted by the wallet.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:333
+#: src/webex/pages/confirm-create-reserve.tsx:211
 #, c-format
 msgid "The exchange is audited by a trusted auditor.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:339
+#: src/webex/pages/confirm-create-reserve.tsx:217
 #, c-format
 msgid ""
 "Warning:  The exchange is neither directly trusted nor audited by a trusted "
@@ -134,7 +94,7 @@ msgid ""
 "If you withdraw from this exchange, it will be trusted in the future.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:348
+#: src/webex/pages/confirm-create-reserve.tsx:226
 #, c-format
 msgid ""
 "Using exchange provider%1$s.\n"
@@ -142,58 +102,59 @@ msgid ""
 " %2$s in fees.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:362
+#: src/webex/pages/confirm-create-reserve.tsx:240
 #, c-format
 msgid ""
 "Waiting for a response from\n"
 " %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:379
+#: src/webex/pages/confirm-create-reserve.tsx:257
 #, c-format
 msgid ""
 "Information about fees will be available when an exchange provider is "
 "selected."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:422
+#: src/webex/pages/confirm-create-reserve.tsx:300
 #, c-format
 msgid "Accept fees and withdraw"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:427
+#: src/webex/pages/confirm-create-reserve.tsx:305
 #, c-format
 msgid "Change Exchange Provider"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:484
+#: src/webex/pages/confirm-create-reserve.tsx:357
 #, c-format
 msgid "You are about to withdraw %1$s from your bank account into your wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:569
+#: src/webex/pages/confirm-create-reserve.tsx:442
 #, c-format
 msgid ""
 "Oops, something went wrong. The wallet responded with error status (%1$s)."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:578
+#: src/webex/pages/confirm-create-reserve.tsx:451
 #, c-format
 msgid "Checking URL, please wait ..."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:592
+#: src/webex/pages/confirm-create-reserve.tsx:465
 #, c-format
 msgid "Can't parse amount: %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:599
+#: src/webex/pages/confirm-create-reserve.tsx:472
 #, c-format
 msgid "Can't parse wire_types: %1$s"
 msgstr ""
 
+#. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:625
+#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
@@ -321,6 +282,46 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
+#: src/webex/renderHtml.tsx:209
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:210
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:211
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:216
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:217
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:218
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:219
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:220
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
 #: src/wire.ts:38
 #, c-format
 msgid "Invalid Wire"
diff --git a/src/i18n/strings.ts b/src/i18n/strings.ts
index ddf93e68..737458c4 100644
--- a/src/i18n/strings.ts
+++ b/src/i18n/strings.ts
@@ -45,30 +45,6 @@ strings['de'] = {
       "Confirm payment": [
         "Bezahlung bestätigen"
       ],
-      "Withdrawal fees:": [
-        "Abheben bei %1$s"
-      ],
-      "Rounding loss:": [
-        ""
-      ],
-      "Earliest expiration (for deposit): %1$s": [
-        ""
-      ],
-      "# Coins": [
-        ""
-      ],
-      "Value": [
-        ""
-      ],
-      "Withdraw Fee": [
-        "Abheben bei %1$s"
-      ],
-      "Refresh Fee": [
-        ""
-      ],
-      "Deposit Fee": [
-        ""
-      ],
       "Select": [
         ""
       ],
@@ -186,6 +162,30 @@ strings['de'] = {
       "Cancel": [
         "Saldo"
       ],
+      "Withdrawal fees:": [
+        "Abheben bei %1$s"
+      ],
+      "Rounding loss:": [
+        ""
+      ],
+      "Earliest expiration (for deposit): %1$s": [
+        ""
+      ],
+      "# Coins": [
+        ""
+      ],
+      "Value": [
+        ""
+      ],
+      "Withdraw Fee": [
+        "Abheben bei %1$s"
+      ],
+      "Refresh Fee": [
+        ""
+      ],
+      "Deposit Fee": [
+        ""
+      ],
       "Invalid Wire": [
         ""
       ],
@@ -231,30 +231,6 @@ strings['en-US'] = {
       "Confirm payment": [
         ""
       ],
-      "Withdrawal fees:": [
-        ""
-      ],
-      "Rounding loss:": [
-        ""
-      ],
-      "Earliest expiration (for deposit): %1$s": [
-        ""
-      ],
-      "# Coins": [
-        ""
-      ],
-      "Value": [
-        ""
-      ],
-      "Withdraw Fee": [
-        ""
-      ],
-      "Refresh Fee": [
-        ""
-      ],
-      "Deposit Fee": [
-        ""
-      ],
       "Select": [
         ""
       ],
@@ -372,6 +348,30 @@ strings['en-US'] = {
       "Cancel": [
         ""
       ],
+      "Withdrawal fees:": [
+        ""
+      ],
+      "Rounding loss:": [
+        ""
+      ],
+      "Earliest expiration (for deposit): %1$s": [
+        ""
+      ],
+      "# Coins": [
+        ""
+      ],
+      "Value": [
+        ""
+      ],
+      "Withdraw Fee": [
+        ""
+      ],
+      "Refresh Fee": [
+        ""
+      ],
+      "Deposit Fee": [
+        ""
+      ],
       "Invalid Wire": [
         ""
       ],
@@ -417,30 +417,6 @@ strings['fr'] = {
       "Confirm payment": [
         ""
       ],
-      "Withdrawal fees:": [
-        ""
-      ],
-      "Rounding loss:": [
-        ""
-      ],
-      "Earliest expiration (for deposit): %1$s": [
-        ""
-      ],
-      "# Coins": [
-        ""
-      ],
-      "Value": [
-        ""
-      ],
-      "Withdraw Fee": [
-        ""
-      ],
-      "Refresh Fee": [
-        ""
-      ],
-      "Deposit Fee": [
-        ""
-      ],
       "Select": [
         ""
       ],
@@ -558,6 +534,30 @@ strings['fr'] = {
       "Cancel": [
         ""
       ],
+      "Withdrawal fees:": [
+        ""
+      ],
+      "Rounding loss:": [
+        ""
+      ],
+      "Earliest expiration (for deposit): %1$s": [
+        ""
+      ],
+      "# Coins": [
+        ""
+      ],
+      "Value": [
+        ""
+      ],
+      "Withdraw Fee": [
+        ""
+      ],
+      "Refresh Fee": [
+        ""
+      ],
+      "Deposit Fee": [
+        ""
+      ],
       "Invalid Wire": [
         ""
       ],
@@ -603,30 +603,6 @@ strings['it'] = {
       "Confirm payment": [
         ""
       ],
-      "Withdrawal fees:": [
-        ""
-      ],
-      "Rounding loss:": [
-        ""
-      ],
-      "Earliest expiration (for deposit): %1$s": [
-        ""
-      ],
-      "# Coins": [
-        ""
-      ],
-      "Value": [
-        ""
-      ],
-      "Withdraw Fee": [
-        ""
-      ],
-      "Refresh Fee": [
-        ""
-      ],
-      "Deposit Fee": [
-        ""
-      ],
       "Select": [
         ""
       ],
@@ -744,6 +720,30 @@ strings['it'] = {
       "Cancel": [
         ""
       ],
+      "Withdrawal fees:": [
+        ""
+      ],
+      "Rounding loss:": [
+        ""
+      ],
+      "Earliest expiration (for deposit): %1$s": [
+        ""
+      ],
+      "# Coins": [
+        ""
+      ],
+      "Value": [
+        ""
+      ],
+      "Withdraw Fee": [
+        ""
+      ],
+      "Refresh Fee": [
+        ""
+      ],
+      "Deposit Fee": [
+        ""
+      ],
       "Invalid Wire": [
         ""
       ],
diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot
index f8d2023b..41c6e6d6 100644
--- a/src/i18n/taler-wallet-webex.pot
+++ b/src/i18n/taler-wallet-webex.pot
@@ -66,67 +66,27 @@ msgstr ""
 msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:178
-#, c-format
-msgid "Withdrawal fees:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:179
-#, c-format
-msgid "Rounding loss:"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:180
-#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:185
-#, c-format
-msgid "# Coins"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:186
-#, c-format
-msgid "Value"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:187
-#, c-format
-msgid "Withdraw Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:188
-#, c-format
-msgid "Refresh Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:189
-#, c-format
-msgid "Deposit Fee"
-msgstr ""
-
-#: src/webex/pages/confirm-create-reserve.tsx:243
+#: src/webex/pages/confirm-create-reserve.tsx:121
 #, c-format
 msgid "Select"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:259
+#: src/webex/pages/confirm-create-reserve.tsx:137
 #, c-format
 msgid "Error: URL may not be relative"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:327
+#: src/webex/pages/confirm-create-reserve.tsx:205
 #, c-format
 msgid "The exchange is trusted by the wallet.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:333
+#: src/webex/pages/confirm-create-reserve.tsx:211
 #, c-format
 msgid "The exchange is audited by a trusted auditor.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:339
+#: src/webex/pages/confirm-create-reserve.tsx:217
 #, c-format
 msgid ""
 "Warning:  The exchange is neither directly trusted nor audited by a trusted "
@@ -134,7 +94,7 @@ msgid ""
 "If you withdraw from this exchange, it will be trusted in the future.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:348
+#: src/webex/pages/confirm-create-reserve.tsx:226
 #, c-format
 msgid ""
 "Using exchange provider%1$s.\n"
@@ -142,58 +102,59 @@ msgid ""
 " %2$s in fees.\n"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:362
+#: src/webex/pages/confirm-create-reserve.tsx:240
 #, c-format
 msgid ""
 "Waiting for a response from\n"
 " %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:379
+#: src/webex/pages/confirm-create-reserve.tsx:257
 #, c-format
 msgid ""
 "Information about fees will be available when an exchange provider is "
 "selected."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:422
+#: src/webex/pages/confirm-create-reserve.tsx:300
 #, c-format
 msgid "Accept fees and withdraw"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:427
+#: src/webex/pages/confirm-create-reserve.tsx:305
 #, c-format
 msgid "Change Exchange Provider"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:484
+#: src/webex/pages/confirm-create-reserve.tsx:357
 #, c-format
 msgid "You are about to withdraw %1$s from your bank account into your wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:569
+#: src/webex/pages/confirm-create-reserve.tsx:442
 #, c-format
 msgid ""
 "Oops, something went wrong. The wallet responded with error status (%1$s)."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:578
+#: src/webex/pages/confirm-create-reserve.tsx:451
 #, c-format
 msgid "Checking URL, please wait ..."
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:592
+#: src/webex/pages/confirm-create-reserve.tsx:465
 #, c-format
 msgid "Can't parse amount: %1$s"
 msgstr ""
 
-#: src/webex/pages/confirm-create-reserve.tsx:599
+#: src/webex/pages/confirm-create-reserve.tsx:472
 #, c-format
 msgid "Can't parse wire_types: %1$s"
 msgstr ""
 
+#. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:625
+#: src/webex/pages/confirm-create-reserve.tsx:498 src/webex/pages/tip.tsx:148
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
@@ -321,6 +282,46 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
+#: src/webex/renderHtml.tsx:209
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:210
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:211
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:216
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:217
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:218
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:219
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:220
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
 #: src/wire.ts:38
 #, c-format
 msgid "Invalid Wire"
diff --git a/src/query.ts b/src/query.ts
index 653e91a1..554f937a 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -51,6 +51,20 @@ export class Store<T> {
 
 
 /**
+ * Options for an index.
+ */
+export interface IndexOptions {
+  /**
+   * If true and the path resolves to an array, create an index entry for
+   * each member of the array (instead of one index entry containing the full 
array).
+   *
+   * Defaults to false.
+   */
+  multiEntry?: boolean;
+}
+
+
+/**
  * Definition of an index.
  */
 export class Index<S extends IDBValidKey, T> {
@@ -59,7 +73,16 @@ export class Index<S extends IDBValidKey, T> {
    */
   storeName: string;
 
-  constructor(s: Store<T>, public indexName: string, public keyPath: string | 
string[]) {
+  /**
+   * Options to use for the index.
+   */
+  options: IndexOptions;
+
+  constructor(s: Store<T>, public indexName: string, public keyPath: string | 
string[], options?: IndexOptions) {
+    const defaultOptions = {
+      multiEntry: false,
+    };
+    this.options = { ...defaultOptions, ...(options || {}) };
     this.storeName = s.name;
   }
 
@@ -671,26 +694,33 @@ export class QueryRoot {
 
 
   /**
-   * Get, modify and store an element inside a transaction.
+   * Update objects inside a transaction.
+   *
+   * If the mutation function throws AbortTransaction, the whole transaction 
will be aborted.
+   * If the mutation function returns undefined or null, no modification will 
be made.
    */
   mutate<T>(store: Store<T>, key: any, f: (v: T|undefined) => T|undefined): 
QueryRoot {
     this.checkFinished();
     const doPut = (tx: IDBTransaction) => {
-      const reqGet = tx.objectStore(store.name).get(key);
-      reqGet.onsuccess = () => {
-        const r = reqGet.result;
-        let m: T|undefined;
-        try {
-          m = f(r);
-        } catch (e) {
-          if (e === AbortTransaction) {
-            tx.abort();
-            return;
+      const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key));
+      req.onsuccess = () => {
+        const cursor = req.result;
+        if (cursor) {
+          const value = cursor.value;
+          let modifiedValue: T|undefined;
+          try {
+            modifiedValue = f(value);
+          } catch (e) {
+            if (e === AbortTransaction) {
+              tx.abort();
+              return;
+            }
+            throw e;
           }
-          throw e;
-        }
-        if (m !== undefined && m !== null) {
-          tx.objectStore(store.name).put(m);
+          if (modifiedValue !== undefined && modifiedValue !== null) {
+            cursor.update(modifiedValue);
+          }
+          cursor.continue();
         }
       };
     };
@@ -702,8 +732,6 @@ export class QueryRoot {
 
   /**
    * Add all object from an iterable to the given object store.
-   * Fails if the object's key is already present
-   * in the object store.
    */
   putAll<T>(store: Store<T>, iterable: T[]): QueryRoot {
     this.checkFinished();
@@ -822,13 +850,13 @@ export class QueryRoot {
   /**
    * Delete an object by from the given object store.
    */
-  delete(storeName: string, key: any): QueryRoot {
+  delete<T>(store: Store<T>, key: any): QueryRoot {
     this.checkFinished();
     const doDelete = (tx: IDBTransaction) => {
-      tx.objectStore(storeName).delete(key);
+      tx.objectStore(store.name).delete(key);
     };
     this.scheduleFinish();
-    this.addWork(doDelete, storeName, true);
+    this.addWork(doDelete, store.name, true);
     return this;
   }
 
diff --git a/src/types.ts b/src/types.ts
index 20517b7a..03ba597f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -598,6 +598,12 @@ export interface PreCoinRecord {
   coinEv: string;
   exchangeBaseUrl: string;
   coinValue: AmountJson;
+  /**
+   * Set to true if this pre-coin came from a tip.
+   * Until the tip is marked as "accepted", the resulting
+   * coin will not be used for payments.
+   */
+  isFromTip: boolean;
 }
 
 /**
@@ -836,6 +842,10 @@ export enum CoinStatus {
    * Coin was dirty but can't be refreshed.
    */
   Useless,
+  /**
+   * The coin was withdrawn for a tip that the user hasn't accepted yet.
+   */
+  TainedByTip,
 }
 
 
@@ -1782,3 +1792,217 @@ export interface CoinWithDenom {
    */
   denom: DenominationRecord;
 }
+
+
+/**
+ * Planchet detail sent to the merchant.
+ */
+export interface TipPlanchetDetail {
+  /**
+   * Hashed denomination public key.
+   */
+  denom_pub_hash: string;
+
+  /**
+   * Coin's blinded public key.
+   */
+  coin_ev: string;
+}
+
+
+export interface TipPickupRequest {
+  /**
+   * Identifier of the tip.
+   */
+  tip_id: string;
+
+  /**
+   * List of planchets the wallet wants to use for the tip.
+   */
+  planchets: TipPlanchetDetail[];
+}
+
address@hidden()
+export class ReserveSigSingleton {
+  @Checkable.String
+  reserve_sig: string;
+
+  static checked: (obj: any) => ReserveSigSingleton;
+}
+
+/**
+ * Response of the merchant
+ * to the TipPickupRequest.
+ */
address@hidden()
+export class TipResponse {
+  /**
+   * Public key of the reserve
+   */
+  @Checkable.String
+  reserve_pub: string;
+
+  /**
+   * The order of the signatures matches the planchets list.
+   */
+  @Checkable.List(Checkable.Value(ReserveSigSingleton))
+  reserve_sigs: ReserveSigSingleton[];
+
+  static checked: (obj: any) => TipResponse;
+}
+
+
+/**
+ * Tipping planchet stored in the database.
+ */
+export interface TipPlanchet {
+  blindingKey: string;
+  coinEv: string;
+  coinPriv: string;
+  coinPub: string;
+  coinValue: AmountJson;
+  denomPubHash: string;
+  denomPub: string;
+}
+
+/**
+ * Status of a tip we got from a merchant.
+ */
+export interface TipRecord {
+  /**
+   * Has the user accepted the tip?  Only after the tip has been accepted coins
+   * withdrawn from the tip may be used.
+   */
+  accepted: boolean;
+
+  /**
+   * The tipped amount.
+   */
+  amount: AmountJson;
+
+  /**
+   * Coin public keys from the planchets.
+   * This field is redundant and used for indexing the record via
+   * a multi-entry index to look up tip records by coin public key.
+   */
+  coinPubs: string[];
+
+  /**
+   * Timestamp, the tip can't be picked up anymore after this deadline.
+   */
+  deadline: number;
+
+  /**
+   * The exchange that will sign our coins, chosen by the merchant.
+   */
+  exchangeUrl: string;
+
+  /**
+   * Domain of the merchant, necessary to uniquely identify the tip since
+   * merchants can freely choose the ID and a malicious merchant might cause a
+   * collision.
+   */
+  merchantDomain: string;
+
+  /**
+   * Planchets, the members included in TipPlanchetDetail will be sent to the
+   * merchant.
+   */
+  planchets: TipPlanchet[];
+
+  /**
+   * Response if the merchant responded,
+   * undefined otherwise.
+   */
+  response?: TipResponse[];
+
+  /**
+   * Identifier for the tip, chosen by the merchant.
+   */
+  tipId: string;
+}
+
+
+export interface TipStatus {
+  tip: TipRecord;
+  rci?: ReserveCreationInfo;
+}
+
+
address@hidden()
+export class TipStatusRequest {
+  @Checkable.String
+  tipId: string;
+
+  @Checkable.String
+  merchantDomain: string;
+
+  static checked: (obj: any) => TipStatusRequest;
+}
+
+
address@hidden()
+export class AcceptTipRequest {
+  @Checkable.String
+  tipId: string;
+
+  @Checkable.String
+  merchantDomain: string;
+
+  static checked: (obj: any) => AcceptTipRequest;
+}
+
+
address@hidden()
+export class ProcessTipResponseRequest {
+  @Checkable.String
+  tipId: string;
+
+  @Checkable.String
+  merchantDomain: string;
+
+  @Checkable.Value(TipResponse)
+  tipResponse: TipResponse;
+
+  static checked: (obj: any) => ProcessTipResponseRequest;
+}
+
address@hidden()
+export class GetTipPlanchetsRequest {
+  @Checkable.String
+  tipId: string;
+
+  @Checkable.String
+  merchantDomain: string;
+
+  @Checkable.Optional(Checkable.Value(AmountJson))
+  amount: AmountJson;
+
+  @Checkable.Number
+  deadline: number;
+
+  @Checkable.String
+  exchangeUrl: string;
+
+  static checked: (obj: any) => GetTipPlanchetsRequest;
+}
+
address@hidden()
+export class TipToken {
+  @Checkable.String
+  expiration: string;
+
+  @Checkable.String
+  exchange_url: string;
+
+  @Checkable.String
+  pickup_url: string;
+
+  @Checkable.String
+  tip_id: string;
+
+  @Checkable.Value(AmountJson)
+  amount: AmountJson;
+
+  static checked: (obj: any) => TipToken;
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 703c7b4a..14c614e6 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -81,6 +81,10 @@ import {
   ReserveRecord,
   ReturnCoinsRequest,
   SenderWireInfos,
+  TipPlanchetDetail,
+  TipRecord,
+  TipResponse,
+  TipStatus,
   WalletBalance,
   WalletBalanceEntry,
   WireFee,
@@ -324,7 +328,7 @@ export const WALLET_PROTOCOL_VERSION = "0:0:0";
  * In the future we might consider adding migration functions for
  * each version increment.
  */
-export const WALLET_DB_VERSION = 20;
+export const WALLET_DB_VERSION = 21;
 
 const builtinCurrencies: CurrencyRecord[] = [
   {
@@ -506,7 +510,7 @@ export namespace Stores {
       super("exchanges", {keyPath: "baseUrl"});
     }
 
-    pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKey", 
"masterPublicKey");
+    pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", 
"masterPublicKey");
   }
 
   class NonceStore extends Store<NonceRecord> {
@@ -521,7 +525,7 @@ export namespace Stores {
     }
 
     exchangeBaseUrlIndex = new Index<string, CoinRecord>(this, 
"exchangeBaseUrl", "exchangeBaseUrl");
-    denomPubIndex = new Index<string, CoinRecord>(this, "denomPub", 
"denomPub");
+    denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", 
"denomPub");
   }
 
   class ProposalsStore extends Store<ProposalRecord> {
@@ -531,7 +535,7 @@ export namespace Stores {
         keyPath: "id",
       });
     }
-    timestampIndex = new Index<string, ProposalRecord>(this, "timestamp", 
"timestamp");
+    timestampIndex = new Index<string, ProposalRecord>(this, "timestampIndex", 
"timestamp");
   }
 
   class PurchasesStore extends Store<PurchaseRecord> {
@@ -539,9 +543,9 @@ export namespace Stores {
       super("purchases", {keyPath: "contractTermsHash"});
     }
 
-    fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, 
"fulfillment_url", "contractTerms.fulfillment_url");
-    orderIdIndex = new Index<string, PurchaseRecord>(this, "order_id", 
"contractTerms.order_id");
-    timestampIndex = new Index<string, PurchaseRecord>(this, "timestamp", 
"timestamp");
+    fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, 
"fulfillmentUrlIndex", "contractTerms.fulfillment_url");
+    orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", 
"contractTerms.order_id");
+    timestampIndex = new Index<string, PurchaseRecord>(this, "timestampIndex", 
"timestamp");
   }
 
   class DenominationsStore extends Store<DenominationRecord> {
@@ -551,9 +555,9 @@ export namespace Stores {
             {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
     }
 
-    denomPubHashIndex = new Index<string, DenominationRecord>(this, 
"denomPubHash", "denomPubHash");
-    exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, 
"exchangeBaseUrl", "exchangeBaseUrl");
-    denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", 
"denomPub");
+    denomPubHashIndex = new Index<string, DenominationRecord>(this, 
"denomPubHashIndex", "denomPubHash");
+    exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, 
"exchangeBaseUrlIndex", "exchangeBaseUrl");
+    denomPubIndex = new Index<string, DenominationRecord>(this, 
"denomPubIndex", "denomPub");
   }
 
   class CurrenciesStore extends Store<CurrencyRecord> {
@@ -578,9 +582,16 @@ export namespace Stores {
     constructor() {
       super("reserves", {keyPath: "reserve_pub"});
     }
-    timestampCreatedIndex = new Index<string, ReserveRecord>(this, 
"timestampCreated", "created");
-    timestampConfirmedIndex = new Index<string, ReserveRecord>(this, 
"timestampConfirmed", "timestamp_confirmed");
-    timestampDepletedIndex = new Index<string, ReserveRecord>(this, 
"timestampDepleted", "timestamp_depleted");
+    timestampCreatedIndex = new Index<string, ReserveRecord>(this, 
"timestampCreatedIndex", "created");
+    timestampConfirmedIndex = new Index<string, ReserveRecord>(this, 
"timestampConfirmedIndex", "timestamp_confirmed");
+    timestampDepletedIndex = new Index<string, ReserveRecord>(this, 
"timestampDepletedIndex", "timestamp_depleted");
+  }
+
+  class TipsStore extends Store<TipRecord> {
+    constructor() {
+      super("tips", {keyPath: ["tipId", "merchantDomain"] as any as 
IDBKeyPath});
+    }
+    coinPubIndex = new Index<string, TipRecord>(this, "coinPubIndex", 
"coinPubs", { multiEntry: true });
   }
 
   export const coins = new CoinsStore();
@@ -596,6 +607,7 @@ export namespace Stores {
   export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: 
"id", autoIncrement: true});
   export const reserves = new ReservesStore();
   export const purchases = new PurchasesStore();
+  export const tips = new TipsStore();
 }
 
 /* tslint:enable:completed-docs */
@@ -1126,7 +1138,7 @@ export class Wallet {
                             () => this.processPreCoin(preCoin, 
Math.min(retryDelayMs * 2, 5 * 60 * 1000)));
       return;
     }
-    console.log("executing processPreCoin");
+    console.log("executing processPreCoin", preCoin);
     this.processPreCoinConcurrent++;
     try {
       const exchange = await this.q().get(Stores.exchanges,
@@ -1143,6 +1155,7 @@ export class Wallet {
       }
 
       const coin = await this.withdrawExecute(preCoin);
+      console.log("processPreCoin: got coin", coin);
 
       const mutateReserve = (r: ReserveRecord) => {
 
@@ -1160,10 +1173,28 @@ export class Wallet {
 
       await this.q()
                 .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
-                .delete("precoins", coin.coinPub)
+                .delete(Stores.precoins, coin.coinPub)
                 .add(Stores.coins, coin)
                 .finish();
 
+      if (coin.status === CoinStatus.TainedByTip) {
+        let tip = await this.q().getIndexed(Stores.tips.coinPubIndex, 
coin.coinPub);
+        if (!tip) {
+          throw Error(`inconsistent DB: tip for coin pub ${coin.coinPub} not 
found.`);
+        }
+
+        if (tip.accepted) {
+          // Transactionall set coin to fresh.
+          const mutateCoin = (c: CoinRecord) => {
+            if (c.status === CoinStatus.TainedByTip) {
+              c.status = CoinStatus.Fresh;
+            }
+            return c;
+          }
+          await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin)
+        }
+      }
+
       this.notifier.notify();
     } catch (e) {
       console.error("Failed to withdraw coin from precoin, retrying in",
@@ -1266,19 +1297,12 @@ export class Wallet {
 
 
   private async withdrawExecute(pc: PreCoinRecord): Promise<CoinRecord> {
-    const reserve = await this.q().get<ReserveRecord>(Stores.reserves,
-                                                    pc.reservePub);
-
-    if (!reserve) {
-      throw Error("db inconsistent");
-    }
-
     const wd: any = {};
     wd.denom_pub = pc.denomPub;
     wd.reserve_pub = pc.reservePub;
     wd.reserve_sig = pc.withdrawSig;
     wd.coin_ev = pc.coinEv;
-    const reqUrl = (new 
URI("reserve/withdraw")).absoluteTo(reserve.exchange_base_url);
+    const reqUrl = (new 
URI("reserve/withdraw")).absoluteTo(pc.exchangeBaseUrl);
     const resp = await this.http.postJson(reqUrl.href(), wd);
 
     if (resp.status !== 200) {
@@ -1289,8 +1313,8 @@ export class Wallet {
     }
     const r = JSON.parse(resp.responseText);
     const denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig,
-                                                   pc.blindingKey,
-                                                   pc.denomPub);
+                                                     pc.blindingKey,
+                                                     pc.denomPub);
     const coin: CoinRecord = {
       blindingKey: pc.blindingKey,
       coinPriv: pc.coinPriv,
@@ -2809,4 +2833,113 @@ export class Wallet {
     }
     return feeAcc;
   }
+
+  /**
+   * Get planchets for a tip.  Creates new planchets if they don't exist 
already
+   * for this tip.  The tip is uniquely identified by the merchant's domain 
and the tip id.
+   */
+  async getTipPlanchets(merchantDomain: string, tipId: string, amount: 
AmountJson, deadline: number, exchangeUrl: string): 
Promise<TipPlanchetDetail[]> {
+    let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
+    if (!tipRecord) {
+      await this.updateExchangeFromUrl(exchangeUrl);
+      const denomsForWithdraw = await 
this.getVerifiedWithdrawDenomList(exchangeUrl, amount);
+      const planchets = await Promise.all(denomsForWithdraw.map(d => 
this.cryptoApi.createTipPlanchet(d)));
+      const coinPubs: string[] = planchets.map(x => x.coinPub);
+      tipRecord = {
+        accepted: false,
+        amount,
+        coinPubs,
+        deadline,
+        exchangeUrl,
+        merchantDomain,
+        planchets,
+        tipId,
+      };
+      await this.q().put(Stores.tips, tipRecord).finish();
+    }
+    // Planchets in the form that the merchant expects
+    const planchetDetail: TipPlanchetDetail[]= tipRecord.planchets.map((p) => 
({
+      denom_pub_hash: p.denomPubHash,
+      coin_ev: p.coinEv,
+    }));
+    return planchetDetail;
+  }
+
+  /**
+   * Accept a merchant's response to a tip pickup and start withdrawing the 
coins.
+   * These coins will not appear in the wallet yet.
+   */
+  async processTipResponse(merchantDomain: string, tipId: string, response: 
TipResponse): Promise<void> {
+    let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
+    if (!tipRecord) {
+      throw Error("tip not found");
+    }
+    console.log("processing tip response", response);
+    if (response.reserve_sigs.length !== tipRecord.planchets.length) {
+      throw Error("number of tip responses does not match requested 
planchets");
+    }
+
+    for (let i = 0; i < tipRecord.planchets.length; i++) {
+      let planchet = tipRecord.planchets[i];
+      let preCoin = {
+        coinPub: planchet.coinPub,
+        coinPriv: planchet.coinPriv,
+        coinEv: planchet.coinEv,
+        coinValue: planchet.coinValue,
+        reservePub: response.reserve_pub,
+        denomPub: planchet.denomPub,
+        blindingKey: planchet.blindingKey,
+        withdrawSig: response.reserve_sigs[i].reserve_sig,
+        exchangeBaseUrl: tipRecord.exchangeUrl,
+        isFromTip: true,
+      };
+      await this.q().put(Stores.precoins, preCoin);
+      this.processPreCoin(preCoin);
+    }
+  }
+
+  /**
+   * Start using the coins from a tip.
+   */
+  async acceptTip(merchantDomain: string, tipId: string): Promise<void> {
+    const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
+    if (!tipRecord) {
+      throw Error("tip not found");
+    }
+    tipRecord.accepted = true;
+
+    // Create one transactional query, within this transaction
+    // both the tip will be marked as accepted and coins
+    // already withdrawn will be untainted.
+    const q = this.q();
+
+    q.put(Stores.tips, tipRecord);
+
+    const updateCoin = (c: CoinRecord) => {
+      if (c.status === CoinStatus.TainedByTip) {
+        c.status = CoinStatus.Fresh;
+      }
+      return c;
+    };
+
+    for (const coinPub of tipRecord.coinPubs) {
+      q.mutate(Stores.coins, coinPub, updateCoin);
+    }
+
+    await q.finish();
+    this.notifier.notify();
+  }
+
+  async getTipStatus(merchantDomain: string, tipId: string): 
Promise<TipStatus> {
+    const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
+    if (!tipRecord) {
+      throw Error("tip not found");
+    }
+    const rci = await this.getReserveCreationInfo(tipRecord.exchangeUrl, 
tipRecord.amount);
+    const tipStatus: TipStatus = {
+      rci,
+      tip: tipRecord,
+    };
+    return tipStatus;
+  }
 }
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 0ca90315..7cc6c425 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -192,6 +192,22 @@ export interface MessageMap {
     request: { refundPermissions: types.RefundPermission[] };
     response: void;
   };
+  "get-tip-planchets": {
+    request: types.GetTipPlanchetsRequest;
+    response: void;
+  };
+  "process-tip-response": {
+    request: types.ProcessTipResponseRequest;
+    response: void;
+  };
+  "accept-tip": {
+    request: types.AcceptTipRequest;
+    response: void;
+  };
+  "get-tip-status": {
+    request: types.TipStatusRequest;
+    response: void;
+  };
 }
 
 /**
diff --git a/src/webex/notify.ts b/src/webex/notify.ts
index cc8086ce..ecc04e8a 100644
--- a/src/webex/notify.ts
+++ b/src/webex/notify.ts
@@ -28,7 +28,9 @@ import URI = require("urijs");
 
 import wxApi = require("./wxApi");
 
-import { QueryPaymentResult } from "../types";
+import { getTalerStampSec } from "../helpers";
+import { TipToken, QueryPaymentResult } from "../types";
+
 
 import axios from "axios";
 
@@ -260,6 +262,87 @@ function talerPay(msg: any): Promise<any> {
   // Use a promise directly instead of of an async
   // function since some paths never resolve the promise.
   return new Promise(async(resolve, reject) => {
+    if (msg.tip) {
+      const tipToken = TipToken.checked(JSON.parse(msg.tip));
+
+      console.log("got tip token", tipToken);
+
+      const deadlineSec = getTalerStampSec(tipToken.expiration);
+      if (!deadlineSec) {
+        wxApi.logAndDisplayError({
+          message: "invalid expiration",
+          name: "tipping-failed",
+          sameTab: true,
+        });
+        return;
+      }
+
+      const merchantDomain = new URI(document.location.href).origin();
+      let walletResp;
+      try {
+        walletResp = await wxApi.getTipPlanchets(merchantDomain, 
tipToken.tip_id, tipToken.amount, deadlineSec, tipToken.exchange_url);
+      } catch (e) {
+        wxApi.logAndDisplayError({
+          message: e.message,
+          name: "tipping-failed",
+          response: e.response,
+          sameTab: true,
+        });
+        throw e;
+      }
+
+      let planchets = walletResp;
+
+      if (!planchets) {
+        wxApi.logAndDisplayError({
+          message: "processing tip failed",
+          detail: walletResp,
+          name: "tipping-failed",
+          sameTab: true,
+        });
+        return;
+      }
+
+      let merchantResp;
+
+      try {
+        const config = {
+          validateStatus: (s: number) => s === 200,
+        };
+        const req = { planchets, tip_id: tipToken.tip_id };
+        merchantResp = await axios.post(tipToken.pickup_url, req, config);
+      } catch (e) {
+        wxApi.logAndDisplayError({
+          message: e.message,
+          name: "tipping-failed",
+          response: e.response,
+          sameTab: true,
+        });
+        throw e;
+      }
+
+      try {
+        wxApi.processTipResponse(merchantDomain, tipToken.tip_id, 
merchantResp.data);
+      } catch (e) {
+        wxApi.logAndDisplayError({
+          message: e.message,
+          name: "tipping-failed",
+          response: e.response,
+          sameTab: true,
+        });
+        throw e;
+      }
+
+      // Go to tip dialog page, where the user can confirm the tip or
+      // decline if they are not happy with the exchange.
+      const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/tip.html"));
+      const params = { tip_id: tipToken.tip_id, merchant_domain: 
merchantDomain };
+      const redirectUrl = uri.query(params).href();
+      window.location.href = redirectUrl;
+
+      return;
+    }
+
     if (msg.refund_url) {
       console.log("processing refund");
       let resp;
diff --git a/src/webex/pages/confirm-create-reserve.tsx 
b/src/webex/pages/confirm-create-reserve.tsx
index 0e1cb17d..53b0d635 100644
--- a/src/webex/pages/confirm-create-reserve.tsx
+++ b/src/webex/pages/confirm-create-reserve.tsx
@@ -22,18 +22,17 @@
  * @author Florian Dold
  */
 
-import {canonicalizeBaseUrl} from "../../helpers";
+import { canonicalizeBaseUrl } from "../../helpers";
 import * as i18n from "../../i18n";
 import {
   AmountJson,
   Amounts,
   CreateReserveResponse,
   CurrencyRecord,
-  DenominationRecord,
   ReserveCreationInfo,
 } from "../../types";
 
-import {ImplicitStateComponent, StateHolder} from "../components";
+import { ImplicitStateComponent, StateHolder } from "../components";
 import {
   createReserve,
   getCurrency,
@@ -41,9 +40,8 @@ import {
   getReserveCreationInfo,
 } from "../wxApi";
 
-import {Collapsible, renderAmount} from "../renderHtml";
+import { renderAmount, WithdrawDetailView } from "../renderHtml";
 
-import * as moment from "moment";
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 import URI = require("urijs");
@@ -80,126 +78,6 @@ class EventTrigger {
 }
 
 
-function renderAuditorDetails(rci: ReserveCreationInfo|null) {
-  console.log("rci", rci);
-  if (!rci) {
-    return (
-      <p>
-        Details will be displayed when a valid exchange provider URL is 
entered.
-      </p>
-    );
-  }
-  if (rci.exchangeInfo.auditors.length === 0) {
-    return (
-      <p>
-        The exchange is not audited by any auditors.
-      </p>
-    );
-  }
-  return (
-    <div>
-      {rci.exchangeInfo.auditors.map((a) => (
-        <div>
-          <h3>Auditor {a.auditor_url}</h3>
-          <p>Public key: {a.auditor_pub}</p>
-          <p>Trusted: {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? 
"yes" : "no"}</p>
-          <p>Audits {a.denomination_keys.length} of {rci.numOfferedDenoms} 
denominations</p>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
-  if (!rci) {
-    return (
-      <p>
-        Details will be displayed when a valid exchange provider URL is 
entered.
-      </p>
-    );
-  }
-
-  const denoms = rci.selectedDenoms;
-
-  const countByPub: {[s: string]: number} = {};
-  const uniq: DenominationRecord[] = [];
-
-  denoms.forEach((x: DenominationRecord) => {
-    let c = countByPub[x.denomPub] || 0;
-    if (c === 0) {
-      uniq.push(x);
-    }
-    c += 1;
-    countByPub[x.denomPub] = c;
-  });
-
-  function row(denom: DenominationRecord) {
-    return (
-      <tr>
-        <td>{countByPub[denom.denomPub] + "x"}</td>
-        <td>{renderAmount(denom.value)}</td>
-        <td>{renderAmount(denom.feeWithdraw)}</td>
-        <td>{renderAmount(denom.feeRefresh)}</td>
-        <td>{renderAmount(denom.feeDeposit)}</td>
-      </tr>
-    );
-  }
-
-  function wireFee(s: string) {
-    return [
-      <thead>
-        <tr>
-        <th colSpan={3}>Wire Method {s}</th>
-        </tr>
-        <tr>
-        <th>Applies Until</th>
-        <th>Wire Fee</th>
-        <th>Closing Fee</th>
-        </tr>
-      </thead>,
-      <tbody>
-      {rci!.wireFees.feesForType[s].map((f) => (
-        <tr>
-          <td>{moment.unix(f.endStamp).format("llll")}</td>
-          <td>{renderAmount(f.wireFee)}</td>
-          <td>{renderAmount(f.closingFee)}</td>
-        </tr>
-      ))}
-      </tbody>,
-    ];
-  }
-
-  const withdrawFee = renderAmount(rci.withdrawFee);
-  const overhead = renderAmount(rci.overhead);
-
-  return (
-    <div>
-      <h3>Overview</h3>
-      <p>{i18n.str`Withdrawal fees:`} {withdrawFee}</p>
-      <p>{i18n.str`Rounding loss:`} {overhead}</p>
-      <p>{i18n.str`Earliest expiration (for deposit): 
${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p>
-      <h3>Coin Fees</h3>
-      <table className="pure-table">
-        <thead>
-        <tr>
-          <th>{i18n.str`# Coins`}</th>
-          <th>{i18n.str`Value`}</th>
-          <th>{i18n.str`Withdraw Fee`}</th>
-          <th>{i18n.str`Refresh Fee`}</th>
-          <th>{i18n.str`Deposit Fee`}</th>
-        </tr>
-        </thead>
-        <tbody>
-        {uniq.map(row)}
-        </tbody>
-      </table>
-      <h3>Wire Fees</h3>
-      <table className="pure-table">
-      {Object.keys(rci.wireFees.feesForType).map(wireFee)}
-      </table>
-    </div>
-  );
-}
 
 
 interface ExchangeSelectionProps {
@@ -428,12 +306,7 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
         </button>
         </p>
         {this.renderUpdateStatus()}
-        <Collapsible initiallyCollapsed={true} title="Fee and Spending 
Details">
-          {renderReserveCreationDetails(this.reserveCreationInfo())}
-        </Collapsible>
-        <Collapsible initiallyCollapsed={true} title="Auditor Details">
-          {renderAuditorDetails(this.reserveCreationInfo())}
-        </Collapsible>
+        <WithdrawDetailView rci={this.reserveCreationInfo()} />
       </div>
     );
   }
diff --git a/src/webex/pages/tip.html b/src/webex/pages/tip.html
new file mode 100644
index 00000000..72d91a12
--- /dev/null
+++ b/src/webex/pages/tip.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="UTF-8">
+  <title>Taler Wallet: Received Tip</title>
+
+  <link rel="icon" href="/img/icon.png">
+  <link rel="stylesheet" type="text/css" href="../style/pure.css">
+  <link rel="stylesheet" type="text/css" href="../style/wallet.css">
+
+  <script src="/dist/page-common-bundle.js"></script>
+  <script src="/dist/tip-bundle.js"></script>
+
+</head>
+
+<body>
+  <section id="main">
+    <h1>GNU Taler Wallet</h1>
+    <div id="container"></div>
+  </section>
+</body>
+
+</html>
diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx
new file mode 100644
index 00000000..7f3a7c1f
--- /dev/null
+++ b/src/webex/pages/tip.tsx
@@ -0,0 +1,155 @@
+/*
+ This file is part of TALER
+ (C) 2017 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import URI = require("urijs");
+
+import * as i18n from "../../i18n";
+
+import {
+  acceptTip,
+  getTipStatus,
+} from "../wxApi";
+
+import { renderAmount, WithdrawDetailView } from "../renderHtml";
+
+import { Amounts, TipStatus } from "../../types";
+
+interface TipDisplayProps {
+  merchantDomain: string;
+  tipId: string;
+}
+
+interface TipDisplayState {
+  tipStatus?: TipStatus;
+  working: boolean;
+}
+
+class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
+  constructor(props: TipDisplayProps) {
+    super(props);
+    this.state = { working: false };
+  }
+
+  async update() {
+    let tipStatus = await getTipStatus(this.props.merchantDomain, 
this.props.tipId);
+    this.setState({ tipStatus });
+  }
+
+  componentDidMount() {
+    this.update();
+    const port = chrome.runtime.connect();
+    port.onMessage.addListener((msg: any) => {
+      if (msg.notify) {
+        console.log("got notified");
+        this.update();
+      }
+    });
+    this.update();
+  }
+
+  renderExchangeInfo(ts: TipStatus) {
+    const rci = ts.rci;
+    if (!rci) {
+      return <p>Waiting for info about exchange ...</p>
+    }
+    const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
+    return (
+      <div>
+        <p>
+          The tip is handled by the exchange 
<strong>{rci.exchangeInfo.baseUrl}</strong>.{" "}
+          The exchange provider will charge
+          {" "}
+          <strong>{renderAmount(totalCost)}</strong>
+          {" "}.
+        </p>
+        <WithdrawDetailView rci={rci} />
+      </div>
+    );
+  }
+
+  accept() {
+    this.setState({ working: true});
+    acceptTip(this.props.merchantDomain, this.props.tipId);
+  }
+
+  renderButtons() {
+    return (
+      <form className="pure-form">
+        <button
+            className="pure-button pure-button-primary"
+            type="button"
+            onClick={() => this.accept()}>
+          { this.state.working ? <span><object className="svg-icon 
svg-baseline" data="/img/spinner-bars.svg" /> </span> : null }
+          Accept tip
+        </button>
+        {" "}
+        <button className="pure-button" type="button" onClick={() => { 
window.close(); }}>Discard tip</button>
+      </form>
+    );
+  }
+
+  render(): JSX.Element {
+    const ts = this.state.tipStatus;
+    if (!ts) {
+      return <p>Processing ...</p>;
+    }
+    return (
+      <div>
+        <h2>Tip Received!</h2>
+        <p>You received a tip of 
<strong>{renderAmount(ts.tip.amount)}</strong> from 
<strong>{this.props.merchantDomain}</strong>.</p>
+        {ts.tip.accepted
+          ? <p>You've accepted this tip!</p>
+          : this.renderButtons()
+        }
+        {this.renderExchangeInfo(ts)}
+      </div>
+    );
+  }
+}
+
+async function main() {
+  try {
+    const url = new URI(document.location.href);
+    const query: any = URI.parseQuery(url.query());
+  
+    let merchantDomain = query.merchant_domain;
+    let tipId = query.tip_id;
+    let props: TipDisplayProps = { tipId, merchantDomain };
+
+    ReactDOM.render(<TipDisplay {...props} />,
+                    document.getElementById("container")!);
+
+  } catch (e) {
+    // TODO: provide more context information, maybe factor it out into a
+    // TODO:generic error reporting function or component.
+    document.body.innerText = i18n.str`Fatal error: "${e.message}".`;
+    console.error(`got error "${e.message}"`, e);
+  }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+  main();
+});
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index 792ba2f2..d4c536fa 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -27,8 +27,14 @@
 import {
   AmountJson,
   Amounts,
+  DenominationRecord,
+  ReserveCreationInfo,
 } from "../types";
 
+import * as moment from "moment";
+
+import * as i18n from "../i18n";
+
 import * as React from "react";
 
 
@@ -101,3 +107,142 @@ export class Collapsible extends 
React.Component<CollapsibleProps, CollapsibleSt
     );
   }
 }
+
+
+function AuditorDetailsView(props: {rci: ReserveCreationInfo|null}): 
JSX.Element {
+  const rci = props.rci;
+  console.log("rci", rci);
+  if (!rci) {
+    return (
+      <p>
+        Details will be displayed when a valid exchange provider URL is 
entered.
+      </p>
+    );
+  }
+  if (rci.exchangeInfo.auditors.length === 0) {
+    return (
+      <p>
+        The exchange is not audited by any auditors.
+      </p>
+    );
+  }
+  return (
+    <div>
+      {rci.exchangeInfo.auditors.map((a) => (
+        <div>
+          <h3>Auditor {a.auditor_url}</h3>
+          <p>Public key: {a.auditor_pub}</p>
+          <p>Trusted: {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? 
"yes" : "no"}</p>
+          <p>Audits {a.denomination_keys.length} of {rci.numOfferedDenoms} 
denominations</p>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+function FeeDetailsView(props: {rci: ReserveCreationInfo|null}): JSX.Element {
+  const rci = props.rci;
+  if (!rci) {
+    return (
+      <p>
+        Details will be displayed when a valid exchange provider URL is 
entered.
+      </p>
+    );
+  }
+
+  const denoms = rci.selectedDenoms;
+
+  const countByPub: {[s: string]: number} = {};
+  const uniq: DenominationRecord[] = [];
+
+  denoms.forEach((x: DenominationRecord) => {
+    let c = countByPub[x.denomPub] || 0;
+    if (c === 0) {
+      uniq.push(x);
+    }
+    c += 1;
+    countByPub[x.denomPub] = c;
+  });
+
+  function row(denom: DenominationRecord) {
+    return (
+      <tr>
+        <td>{countByPub[denom.denomPub] + "x"}</td>
+        <td>{renderAmount(denom.value)}</td>
+        <td>{renderAmount(denom.feeWithdraw)}</td>
+        <td>{renderAmount(denom.feeRefresh)}</td>
+        <td>{renderAmount(denom.feeDeposit)}</td>
+      </tr>
+    );
+  }
+
+  function wireFee(s: string) {
+    return [
+      <thead>
+        <tr>
+        <th colSpan={3}>Wire Method {s}</th>
+        </tr>
+        <tr>
+        <th>Applies Until</th>
+        <th>Wire Fee</th>
+        <th>Closing Fee</th>
+        </tr>
+      </thead>,
+      <tbody>
+      {rci!.wireFees.feesForType[s].map((f) => (
+        <tr>
+          <td>{moment.unix(f.endStamp).format("llll")}</td>
+          <td>{renderAmount(f.wireFee)}</td>
+          <td>{renderAmount(f.closingFee)}</td>
+        </tr>
+      ))}
+      </tbody>,
+    ];
+  }
+
+  const withdrawFee = renderAmount(rci.withdrawFee);
+  const overhead = renderAmount(rci.overhead);
+
+  return (
+    <div>
+      <h3>Overview</h3>
+      <p>{i18n.str`Withdrawal fees:`} {withdrawFee}</p>
+      <p>{i18n.str`Rounding loss:`} {overhead}</p>
+      <p>{i18n.str`Earliest expiration (for deposit): 
${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p>
+      <h3>Coin Fees</h3>
+      <table className="pure-table">
+        <thead>
+        <tr>
+          <th>{i18n.str`# Coins`}</th>
+          <th>{i18n.str`Value`}</th>
+          <th>{i18n.str`Withdraw Fee`}</th>
+          <th>{i18n.str`Refresh Fee`}</th>
+          <th>{i18n.str`Deposit Fee`}</th>
+        </tr>
+        </thead>
+        <tbody>
+        {uniq.map(row)}
+        </tbody>
+      </table>
+      <h3>Wire Fees</h3>
+      <table className="pure-table">
+      {Object.keys(rci.wireFees.feesForType).map(wireFee)}
+      </table>
+    </div>
+  );
+}
+
+
+export function WithdrawDetailView(props: {rci: ReserveCreationInfo | null}): 
JSX.Element {
+  const rci = props.rci;
+  return (
+    <div>
+      <Collapsible initiallyCollapsed={true} title="Fee and Spending Details">
+        <FeeDetailsView rci={rci} />
+      </Collapsible>
+      <Collapsible initiallyCollapsed={true} title="Auditor Details">
+        <AuditorDetailsView rci={rci} />
+      </Collapsible>
+    </div>
+  );
+}
diff --git a/src/webex/style/wallet.css b/src/webex/style/wallet.css
index 61dd611e..dde17e89 100644
--- a/src/webex/style/wallet.css
+++ b/src/webex/style/wallet.css
@@ -251,3 +251,18 @@ a.opener  {
 .opener-collapsed::before {
   content: "\25b6 "
 }
+
+.svg-icon {
+  display: inline-flex;
+  align-self: center;
+  position: relative;
+  height: 1em;
+  width: 1em;
+}
+.svg-icon svg {
+  height:1em;
+  width:1em;
+}
+object.svg-icon.svg-baseline {
+  transform: translate(0, 0.125em);
+}
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 7afc116b..e362fc34 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -37,6 +37,9 @@ import {
   ReserveCreationInfo,
   ReserveRecord,
   SenderWireInfos,
+  TipResponse,
+  TipPlanchetDetail,
+  TipStatus,
   WalletBalance,
 } from "../types";
 
@@ -358,3 +361,23 @@ export function getPurchase(contractTermsHash: string): 
Promise<PurchaseRecord>
 export function getFullRefundFees(args: { refundPermissions: 
RefundPermission[] }): Promise<AmountJson> {
   return callBackend("get-full-refund-fees", { refundPermissions: 
args.refundPermissions });
 }
+
+
+/**
+ * Get or generate planchets to give the merchant that wants to tip us.
+ */
+export function getTipPlanchets(merchantDomain: string, tipId: string, amount: 
AmountJson, deadline: number, exchangeUrl: string): 
Promise<TipPlanchetDetail[]> {
+  return callBackend("get-tip-planchets", { merchantDomain, tipId, amount, 
deadline, exchangeUrl });
+}
+
+export function getTipStatus(merchantDomain: string, tipId: string): 
Promise<TipStatus> {
+  return callBackend("get-tip-status", { merchantDomain, tipId });
+}
+
+export function acceptTip(merchantDomain: string, tipId: string): 
Promise<TipStatus> {
+  return callBackend("accept-tip", { merchantDomain, tipId });
+}
+
+export function processTipResponse(merchantDomain: string, tipId: string, 
tipResponse: TipResponse): Promise<void> {
+  return callBackend("process-tip-response", { merchantDomain, tipId, 
tipResponse });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 7393c888..a17f516a 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -31,12 +31,16 @@ import {
   Store,
 } from "../query";
 import {
+  AcceptTipRequest,
   AmountJson,
   ConfirmReserveRequest,
   CreateReserveRequest,
+  GetTipPlanchetsRequest,
   Notifier,
+  ProcessTipResponseRequest,
   ProposalRecord,
   ReturnCoinsRequest,
+  TipStatusRequest,
 } from "../types";
 import {
   Stores,
@@ -44,6 +48,7 @@ import {
   Wallet,
 } from "../wallet";
 
+
 import { ChromeBadge } from "./chromeBadge";
 import { MessageType } from "./messages";
 import * as wxApi from "./wxApi";
@@ -316,6 +321,22 @@ function handleMessage(sender: MessageSender,
     }
     case "get-full-refund-fees":
       return needsWallet().getFullRefundFees(detail.refundPermissions);
+    case "get-tip-status": {
+      const req = TipStatusRequest.checked(detail);
+      return needsWallet().getTipStatus(req.merchantDomain, req.tipId);
+    }
+    case "accept-tip": {
+      const req = AcceptTipRequest.checked(detail);
+      return needsWallet().acceptTip(req.merchantDomain, req.tipId);
+    }
+    case "process-tip-response": {
+      const req = ProcessTipResponseRequest.checked(detail);
+      return needsWallet().processTipResponse(req.merchantDomain, req.tipId, 
req.tipResponse);
+    }
+    case "get-tip-planchets": {
+      const req = GetTipPlanchetsRequest.checked(detail);
+      return needsWallet().getTipPlanchets(req.merchantDomain, req.tipId, 
req.amount, req.deadline, req.exchangeUrl);
+    }
     default:
       // Exhaustiveness check.
       // See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@@ -409,6 +430,7 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
     contract_url: headers["x-taler-contract-url"],
     offer_url: headers["x-taler-offer-url"],
     refund_url: headers["x-taler-refund-url"],
+    tip: headers["x-taler-tip"],
   };
 
   const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as 
any)[x]).length !== 0;
@@ -424,6 +446,7 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
     contract_url: fields.contract_url,
     offer_url: fields.offer_url,
     refund_url: fields.refund_url,
+    tip: fields.tip,
   };
 
   console.log("got pay detail", payDetail);
@@ -728,7 +751,7 @@ function openTalerDb(): Promise<IDBDatabase> {
               for (const indexName in (si as any)) {
                 if ((si as any)[indexName] instanceof Index) {
                   const ii: Index<any, any> = (si as any)[indexName];
-                  s.createIndex(ii.indexName, ii.keyPath);
+                  s.createIndex(ii.indexName, ii.keyPath, ii.options);
                 }
               }
             }
diff --git a/tsconfig.json b/tsconfig.json
index baceaa72..3e7c6837 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -65,6 +65,7 @@
     "src/webex/pages/reset-required.tsx",
     "src/webex/pages/return-coins.tsx",
     "src/webex/pages/show-db.ts",
+    "src/webex/pages/tip.tsx",
     "src/webex/pages/tree.tsx",
     "src/webex/renderHtml.tsx",
     "src/webex/wxApi.ts",
diff --git a/webpack.config.js b/webpack.config.js
index af586dc5..b4d1b907 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -78,6 +78,7 @@ module.exports = function (env) {
       "return-coins": "./src/webex/pages/return-coins.tsx",
       "refund": "./src/webex/pages/refund.tsx",
       "show-db": "./src/webex/pages/show-db.ts",
+      "tip": "./src/webex/pages/tip.tsx",
       "tree": "./src/webex/pages/tree.tsx",
     },
     plugins: [

-- 
To stop receiving notification emails like this one, please contact
address@hidden



reply via email to

[Prev in Thread] Current Thread [Next in Thread]