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 (bf70e752 -> 869


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated (bf70e752 -> 8697efd2)
Date: Sun, 27 Aug 2017 03:56:30 +0200

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

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

    from bf70e752 remove file
     new 21c176a6 add rudimentary error reporting in a new tab
     new 8697efd2 implement refunds

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 node_modules/.bin/tsc                              |   2 +-
 node_modules/.bin/tsserver                         |   2 +-
 node_modules/.bin/uglifyjs                         |   2 +-
 .../handlebars/node_modules/uglify-js/bin/uglifyjs |   0
 node_modules/nyc/node_modules/md5-hex/package.json |  97 ++---------
 .../nyc/node_modules/resolve-from/package.json     |  89 ++--------
 package.json                                       |   3 +-
 src/crypto/cryptoWorker.ts                         |   2 +-
 src/logging.ts                                     |  45 ++++-
 src/query.ts                                       |   9 +-
 src/types.ts                                       |  32 +++-
 src/wallet.ts                                      | 155 +++++++++++++----
 src/webex/messages.ts                              |  16 ++
 src/webex/notify.ts                                | 186 ++++++++-------------
 src/webex/pages/error.tsx                          |  63 +++++--
 src/webex/pages/{error.html => refund.html}        |   4 +-
 src/webex/pages/refund.tsx                         | 138 +++++++++++++++
 src/webex/renderHtml.tsx                           |   2 +
 src/webex/wxApi.ts                                 |  24 +++
 src/webex/wxBackend.ts                             |  25 +++
 webpack.config.js                                  |   1 +
 yarn.lock                                          |  15 +-
 22 files changed, 575 insertions(+), 337 deletions(-)
 mode change 100755 => 100644 
node_modules/handlebars/node_modules/uglify-js/bin/uglifyjs
 copy src/webex/pages/{error.html => refund.html} (75%)
 create mode 100644 src/webex/pages/refund.tsx

diff --git a/node_modules/.bin/tsc b/node_modules/.bin/tsc
index abecd812..0863208a 120000
--- a/node_modules/.bin/tsc
+++ b/node_modules/.bin/tsc
@@ -1 +1 @@
-../typedoc/node_modules/typescript/bin/tsc
\ No newline at end of file
+../typescript/bin/tsc
\ No newline at end of file
diff --git a/node_modules/.bin/tsserver b/node_modules/.bin/tsserver
index 1d314276..f8f8f1a0 120000
--- a/node_modules/.bin/tsserver
+++ b/node_modules/.bin/tsserver
@@ -1 +1 @@
-../typedoc/node_modules/typescript/bin/tsserver
\ No newline at end of file
+../typescript/bin/tsserver
\ No newline at end of file
diff --git a/node_modules/.bin/uglifyjs b/node_modules/.bin/uglifyjs
index 6b320d5a..fef3468b 120000
--- a/node_modules/.bin/uglifyjs
+++ b/node_modules/.bin/uglifyjs
@@ -1 +1 @@
-../handlebars/node_modules/uglify-js/bin/uglifyjs
\ No newline at end of file
+../uglify-js/bin/uglifyjs
\ No newline at end of file
diff --git a/node_modules/handlebars/node_modules/uglify-js/bin/uglifyjs 
b/node_modules/handlebars/node_modules/uglify-js/bin/uglifyjs
old mode 100755
new mode 100644
diff --git a/node_modules/nyc/node_modules/md5-hex/package.json 
b/node_modules/nyc/node_modules/md5-hex/package.json
index 02d54328..9dc26627 100644
--- a/node_modules/nyc/node_modules/md5-hex/package.json
+++ b/node_modules/nyc/node_modules/md5-hex/package.json
@@ -1,82 +1,25 @@
 {
-  "_args": [
-    [
-      {
-        "raw": "address@hidden",
-        "scope": null,
-        "escapedName": "md5-hex",
-        "name": "md5-hex",
-        "rawSpec": "^1.2.0",
-        "spec": ">=1.2.0 <2.0.0",
-        "type": "range"
-      },
-      "/Users/benjamincoe/bcoe/nyc"
-    ]
-  ],
-  "_from": "md5-hex@>=1.2.0 <2.0.0",
-  "_id": "address@hidden",
-  "_inCache": true,
-  "_location": "/md5-hex",
-  "_nodeVersion": "4.4.2",
-  "_npmOperationalInternal": {
-    "host": "packages-12-west.internal.npmjs.com",
-    "tmp": "tmp/md5-hex-1.3.0.tgz_1460471196734_0.9732175024691969"
-  },
-  "_npmUser": {
-    "name": "sindresorhus",
-    "email": "address@hidden"
-  },
-  "_npmVersion": "2.15.0",
-  "_phantomChildren": {},
-  "_requested": {
-    "raw": "address@hidden",
-    "scope": null,
-    "escapedName": "md5-hex",
-    "name": "md5-hex",
-    "rawSpec": "^1.2.0",
-    "spec": ">=1.2.0 <2.0.0",
-    "type": "range"
-  },
-  "_requiredBy": [
-    "/",
-    "/caching-transform"
-  ],
-  "_resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-1.3.0.tgz";,
-  "_shasum": "d2c4afe983c4370662179b8cad145219135046c4",
-  "_shrinkwrap": null,
-  "_spec": "address@hidden",
-  "_where": "/Users/benjamincoe/bcoe/nyc",
+  "name": "md5-hex",
+  "version": "1.3.0",
+  "description": "Create a MD5 hash with hex encoding",
+  "license": "MIT",
+  "repository": "sindresorhus/md5-hex",
   "author": {
     "name": "Sindre Sorhus",
     "email": "address@hidden",
     "url": "sindresorhus.com"
   },
   "browser": "browser.js",
-  "bugs": {
-    "url": "https://github.com/sindresorhus/md5-hex/issues";
-  },
-  "dependencies": {
-    "md5-o-matic": "^0.1.1"
-  },
-  "description": "Create a MD5 hash with hex encoding",
-  "devDependencies": {
-    "ava": "*",
-    "xo": "*"
-  },
-  "directories": {},
-  "dist": {
-    "shasum": "d2c4afe983c4370662179b8cad145219135046c4",
-    "tarball": "https://registry.npmjs.org/md5-hex/-/md5-hex-1.3.0.tgz";
-  },
   "engines": {
     "node": ">=0.10.0"
   },
+  "scripts": {
+    "test": "xo && ava"
+  },
   "files": [
     "index.js",
     "browser.js"
   ],
-  "gitHead": "273d9c659a29e4cd53512f526282afd5ac1c1413",
-  "homepage": "https://github.com/sindresorhus/md5-hex#readme";,
   "keywords": [
     "hash",
     "crypto",
@@ -86,23 +29,11 @@
     "browser",
     "browserify"
   ],
-  "license": "MIT",
-  "maintainers": [
-    {
-      "name": "sindresorhus",
-      "email": "address@hidden"
-    }
-  ],
-  "name": "md5-hex",
-  "optionalDependencies": {},
-  "readme": "# md5-hex [![Build 
Status](https://travis-ci.org/sindresorhus/md5-hex.svg?branch=master)](https://travis-ci.org/sindresorhus/md5-hex)\n\n>
 Create a MD5 hash with hex encoding\n\n*Please don't use MD5 hashes for 
anything sensitive!*\n\nCheckout 
[`hasha`](https://github.com/sindresorhus/hasha) if you need something more 
flexible.\n\n\n## Install\n\n```\n$ npm install --save md5-hex\n```\n\n\n## 
Usage\n\n```js\nconst fs = require('fs');\nconst md5Hex = 
require('md5-hex');\ncons [...]
-  "readmeFilename": "readme.md",
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/sindresorhus/md5-hex.git";
-  },
-  "scripts": {
-    "test": "xo && ava"
+  "dependencies": {
+    "md5-o-matic": "^0.1.1"
   },
-  "version": "1.3.0"
+  "devDependencies": {
+    "ava": "*",
+    "xo": "*"
+  }
 }
diff --git a/node_modules/nyc/node_modules/resolve-from/package.json 
b/node_modules/nyc/node_modules/resolve-from/package.json
index edf933ac..ee47da7c 100644
--- a/node_modules/nyc/node_modules/resolve-from/package.json
+++ b/node_modules/nyc/node_modules/resolve-from/package.json
@@ -1,73 +1,23 @@
 {
-  "_args": [
-    [
-      {
-        "raw": "address@hidden",
-        "scope": null,
-        "escapedName": "resolve-from",
-        "name": "resolve-from",
-        "rawSpec": "^2.0.0",
-        "spec": ">=2.0.0 <3.0.0",
-        "type": "range"
-      },
-      "/Users/benjamincoe/bcoe/nyc"
-    ]
-  ],
-  "_from": "resolve-from@>=2.0.0 <3.0.0",
-  "_id": "address@hidden",
-  "_inCache": true,
-  "_location": "/resolve-from",
-  "_nodeVersion": "4.2.1",
-  "_npmUser": {
-    "name": "sindresorhus",
-    "email": "address@hidden"
-  },
-  "_npmVersion": "2.14.7",
-  "_phantomChildren": {},
-  "_requested": {
-    "raw": "address@hidden",
-    "scope": null,
-    "escapedName": "resolve-from",
-    "name": "resolve-from",
-    "rawSpec": "^2.0.0",
-    "spec": ">=2.0.0 <3.0.0",
-    "type": "range"
-  },
-  "_requiredBy": [
-    "/"
-  ],
-  "_resolved": 
"https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz";,
-  "_shasum": "9480ab20e94ffa1d9e80a804c7ea147611966b57",
-  "_shrinkwrap": null,
-  "_spec": "address@hidden",
-  "_where": "/Users/benjamincoe/bcoe/nyc",
+  "name": "resolve-from",
+  "version": "2.0.0",
+  "description": "Resolve the path of a module like require.resolve() but from 
a given path",
+  "license": "MIT",
+  "repository": "sindresorhus/resolve-from",
   "author": {
     "name": "Sindre Sorhus",
     "email": "address@hidden",
     "url": "sindresorhus.com"
   },
-  "bugs": {
-    "url": "https://github.com/sindresorhus/resolve-from/issues";
-  },
-  "dependencies": {},
-  "description": "Resolve the path of a module like require.resolve() but from 
a given path",
-  "devDependencies": {
-    "ava": "*",
-    "xo": "*"
-  },
-  "directories": {},
-  "dist": {
-    "shasum": "9480ab20e94ffa1d9e80a804c7ea147611966b57",
-    "tarball": 
"https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz";
-  },
   "engines": {
     "node": ">=0.10.0"
   },
+  "scripts": {
+    "test": "xo && ava"
+  },
   "files": [
     "index.js"
   ],
-  "gitHead": "583e0f8df06e1bc4d1c96d8d4f2484c745f522c3",
-  "homepage": "https://github.com/sindresorhus/resolve-from#readme";,
   "keywords": [
     "require",
     "resolve",
@@ -77,23 +27,8 @@
     "like",
     "path"
   ],
-  "license": "MIT",
-  "maintainers": [
-    {
-      "name": "sindresorhus",
-      "email": "address@hidden"
-    }
-  ],
-  "name": "resolve-from",
-  "optionalDependencies": {},
-  "readme": "# resolve-from [![Build 
Status](https://travis-ci.org/sindresorhus/resolve-from.svg?branch=master)](https://travis-ci.org/sindresorhus/resolve-from)\n\n>
 Resolve the path of a module like 
[`require.resolve()`](http://nodejs.org/api/globals.html#globals_require_resolve)
 but from a given path\n\nUnlike `require.resolve()` it returns `null` instead 
of throwing when the module can't be found.\n\n\n## Install\n\n```\n$ npm 
install --save resolve-from\n```\n\n\n## Usage\n\n```js\n [...]
-  "readmeFilename": "readme.md",
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/sindresorhus/resolve-from.git";
-  },
-  "scripts": {
-    "test": "xo && ava"
-  },
-  "version": "2.0.0"
+  "devDependencies": {
+    "ava": "*",
+    "xo": "*"
+  }
 }
diff --git a/package.json b/package.json
index b3ef137f..03108f76 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
   },
   "dependencies": {
     "@types/react": "^16.0.2",
-    "@types/react-dom": "^15.5.2"
+    "@types/react-dom": "^15.5.2",
+    "axios": "^0.16.2"
   }
 }
diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts
index b05d7d18..1db6e62d 100644
--- a/src/crypto/cryptoWorker.ts
+++ b/src/crypto/cryptoWorker.ts
@@ -271,7 +271,7 @@ namespace RpcFunctions {
       const newAmount = new native.Amount(cd.coin.currentAmount);
       newAmount.sub(coinSpend);
       cd.coin.currentAmount = newAmount.toJson();
-      cd.coin.status = CoinStatus.TransactionPending;
+      cd.coin.status = CoinStatus.PurchasePending;
 
       const d = new native.DepositRequestPS({
         amount_with_fee: coinSpend.toNbo(),
diff --git a/src/logging.ts b/src/logging.ts
index a589c809..2c559e8d 100644
--- a/src/logging.ts
+++ b/src/logging.ts
@@ -208,6 +208,44 @@ export async function recordException(msg: string, e: 
any): Promise<void> {
   return record("error", e.toString(), stack, frame.file, frame.line, 
frame.column);
 }
 
+
+/**
+ * Cache for reports.  Also used when something is so broken that we can't even
+ * access the database.
+ */
+const reportCache: { [reportId: string]: any } = {};
+
+
+/**
+ * Get a UUID that does not use cryptographically secure randomness.
+ * Formatted as RFC4122 version 4 UUID.
+ */
+function getInsecureUuid() {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+    return v.toString(16);
+  });
+}
+
+
+/**
+ * Store a report and return a unique identifier to retrieve it later.
+ */
+export async function storeReport(report: any): Promise<string> {
+  const uid = getInsecureUuid();
+  reportCache[uid] = report;
+  return uid;
+}
+
+
+/**
+ * Retrieve a report by its unique identifier.
+ */
+export async function getReport(reportUid: string): Promise<any> {
+  return reportCache[reportUid];
+}
+
+
 /**
  * Record a log entry in the database.
  */
@@ -218,6 +256,8 @@ export async function record(level: Level,
                              line?: number,
                              col?: number): Promise<void> {
   if (typeof indexedDB === "undefined") {
+    console.log("can't access DB for logging in this context");
+    console.log("log was", { level, msg, detail, source, line, col });
     return;
   }
 
@@ -257,7 +297,7 @@ export async function record(level: Level,
   }
 }
 
-const loggingDbVersion = 1;
+const loggingDbVersion = 2;
 
 const logsStore: Store<LogEntry> = new Store<LogEntry>("logs");
 
@@ -283,7 +323,8 @@ export function openLoggingDb(): Promise<IDBDatabase> {
           console.error(e);
         }
       }
-      resDb.createObjectStore("logs", {keyPath: "id", autoIncrement: true});
+      resDb.createObjectStore("logs", { keyPath: "id", autoIncrement: true });
+      resDb.createObjectStore("reports", { keyPath: "uid", autoIncrement: 
false });
     };
   });
 }
diff --git a/src/query.ts b/src/query.ts
index dffff86e..ee1ac260 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -658,13 +658,13 @@ export class QueryRoot {
   /**
    * Get, modify and store an element inside a transaction.
    */
-  mutate<T>(store: Store<T>, key: any, f: (v: T) => T): QueryRoot {
+  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;
+        let m: T|undefined;
         try {
           m = f(r);
         } catch (e) {
@@ -674,8 +674,9 @@ export class QueryRoot {
           }
           throw e;
         }
-
-        tx.objectStore(store.name).put(m);
+        if (m !== undefined && m !== null) {
+          tx.objectStore(store.name).put(m);
+        }
       };
     };
     this.scheduleFinish();
diff --git a/src/types.ts b/src/types.ts
index 9031b19b..aabf4ffc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -798,9 +798,9 @@ export enum CoinStatus {
    */
   Fresh,
   /**
-   * Currently planned to be sent to a merchant for a transaction.
+   * Currently planned to be sent to a merchant for a purchase.
    */
-  TransactionPending,
+  PurchasePending,
   /**
    * Used for a completed transaction and now dirty.
    */
@@ -1662,3 +1662,31 @@ export class ReturnCoinsRequest {
    */
   static checked: (obj: any) => ReturnCoinsRequest;
 }
+
+
+export interface RefundPermission {
+  refund_amount: AmountJson;
+  refund_fee: AmountJson;
+  h_contract_terms: string;
+  coin_pub: string;
+  rtransaction_id: number;
+  merchant_pub: string;
+  merchant_sig: string;
+}
+
+
+export interface PurchaseRecord {
+  contractTermsHash: string;
+  contractTerms: ContractTerms;
+  payReq: PayReq;
+  merchantSig: string;
+
+  /**
+   * The purchase isn't active anymore, it's either successfully paid or
+   * refunded/aborted.
+   */
+  finished: boolean;
+
+  refundsPending: { [refundSig: string]: RefundPermission };
+  refundsDone: { [refundSig: string]: RefundPermission };
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 68d70b0b..b892e2e4 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -82,6 +82,8 @@ import {
   WalletBalanceEntry,
   WireFee,
   WireInfo,
+  RefundPermission,
+  PurchaseRecord,
 } from "./types";
 import URI = require("urijs");
 
@@ -241,19 +243,6 @@ class WireDetailJson {
 }
 
 
-interface TransactionRecord {
-  contractTermsHash: string;
-  contractTerms: ContractTerms;
-  payReq: PayReq;
-  merchantSig: string;
-
-  /**
-   * The transaction isn't active anymore, it's either successfully paid
-   * or refunded/aborted.
-   */
-  finished: boolean;
-}
-
 
 /**
  * Badge that shows activity for the wallet.
@@ -516,13 +505,13 @@ export namespace Stores {
     }
   }
 
-  class TransactionsStore extends Store<TransactionRecord> {
+  class PurchasesStore extends Store<PurchaseRecord> {
     constructor() {
-      super("transactions", {keyPath: "contractTermsHash"});
+      super("purchases", {keyPath: "contractTermsHash"});
     }
 
-    fulfillmentUrlIndex = new Index<string, TransactionRecord>(this, 
"fulfillment_url", "contractTerms.fulfillment_url");
-    orderIdIndex = new Index<string, TransactionRecord>(this, "order_id", 
"contractTerms.order_id");
+    fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, 
"fulfillment_url", "contractTerms.fulfillment_url");
+    orderIdIndex = new Index<string, PurchaseRecord>(this, "order_id", 
"contractTerms.order_id");
   }
 
   class DenominationsStore extends Store<DenominationRecord> {
@@ -568,7 +557,7 @@ export namespace Stores {
   export const proposals = new ProposalsStore();
   export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: 
"meltCoinPub"});
   export const reserves = new Store<ReserveRecord>("reserves", {keyPath: 
"reserve_pub"});
-  export const transactions = new TransactionsStore();
+  export const purchases = new PurchasesStore();
 }
 
 /* tslint:enable:completed-docs */
@@ -909,12 +898,14 @@ export class Wallet {
       merchant_pub: proposal.contractTerms.merchant_pub,
       order_id: proposal.contractTerms.order_id,
     };
-    const t: TransactionRecord = {
+    const t: PurchaseRecord = {
       contractTerms: proposal.contractTerms,
       contractTermsHash: proposal.contractTermsHash,
       finished: false,
       merchantSig: proposal.merchantSig,
       payReq,
+      refundsDone: {},
+      refundsPending: {},
     };
 
     const historyEntry: HistoryRecord = {
@@ -931,7 +922,7 @@ export class Wallet {
     };
 
     await this.q()
-              .put(Stores.transactions, t)
+              .put(Stores.purchases, t)
               .put(Stores.history, historyEntry)
               .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin))
               .finish();
@@ -972,9 +963,9 @@ export class Wallet {
       throw Error(`proposal with id ${proposalId} not found`);
     }
 
-    const transaction = await this.q().get(Stores.transactions, 
proposal.contractTermsHash);
+    const purchase = await this.q().get(Stores.purchases, 
proposal.contractTermsHash);
 
-    if (transaction) {
+    if (purchase) {
       // Already payed ...
       return "paid";
     }
@@ -1017,8 +1008,8 @@ export class Wallet {
     }
 
     // First check if we already payed for it.
-    const transaction = await this.q().get(Stores.transactions, 
proposal.contractTermsHash);
-    if (transaction) {
+    const purchase = await this.q().get(Stores.purchases, 
proposal.contractTermsHash);
+    if (purchase) {
       return "paid";
     }
 
@@ -1049,7 +1040,7 @@ export class Wallet {
   async queryPayment(url: string): Promise<QueryPaymentResult> {
     console.log("query for payment", url);
 
-    const t = await 
this.q().getIndexed(Stores.transactions.fulfillmentUrlIndex, url);
+    const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, 
url);
 
     if (!t) {
       console.log("query for payment failed");
@@ -1890,7 +1881,7 @@ export class Wallet {
       return balance;
     }
 
-    function collectPayments(t: TransactionRecord, balance: WalletBalance) {
+    function collectPayments(t: PurchaseRecord, balance: WalletBalance) {
       if (t.finished) {
         return balance;
       }
@@ -1934,7 +1925,7 @@ export class Wallet {
       .reduce(collectPendingWithdraw, balance);
     tx.iter(Stores.reserves)
       .reduce(collectPaybacks, balance);
-    tx.iter(Stores.transactions)
+    tx.iter(Stores.purchases)
       .reduce(collectPayments, balance);
     await tx.finish();
     return balance;
@@ -2282,7 +2273,7 @@ export class Wallet {
 
   async paymentSucceeded(contractTermsHash: string, merchantSig: string): 
Promise<any> {
     const doPaymentSucceeded = async() => {
-      const t = await this.q().get<TransactionRecord>(Stores.transactions,
+      const t = await this.q().get<PurchaseRecord>(Stores.purchases,
                                                     contractTermsHash);
       if (!t) {
         console.error("contract not found");
@@ -2309,7 +2300,7 @@ export class Wallet {
 
       await this.q()
                 .putAll(Stores.coins, modifiedCoins)
-                .put(Stores.transactions, t)
+                .put(Stores.purchases, t)
                 .finish();
       for (const c of t.payReq.coins) {
         this.refresh(c.coin_pub);
@@ -2560,4 +2551,110 @@ export class Wallet {
       await this.q().put(Stores.coinsReturns, currentCrr);
     }
   }
+
+  async acceptRefund(refundPermissions: RefundPermission[]): Promise<void> {
+    if (!refundPermissions.length) {
+      console.warn("got empty refund list");
+      return;
+    }
+    const hc = refundPermissions[0].h_contract_terms;
+    if (!hc) {
+      throw Error("h_contract_terms missing in refund permission");
+    }
+    const m = refundPermissions[0].merchant_pub;
+    if (!hc) {
+      throw Error("merchant_pub missing in refund permission");
+    }
+    for (const perm of refundPermissions) {
+      if (perm.h_contract_terms !== hc) {
+        throw Error("h_contract_terms different in refund permission");
+      }
+      if (perm.merchant_pub !== m) {
+        throw Error("merchant_pub different in refund permission");
+      }
+    }
+
+    /**
+     * Add refund to purchase if not already added.
+     */
+    function f(t: PurchaseRecord|undefined): PurchaseRecord|undefined {
+      if (!t) {
+        console.error("purchase not found, not adding refunds");
+        return;
+      }
+
+      for (const perm of refundPermissions) {
+        if (!t.refundsPending[perm.merchant_sig] && 
!t.refundsDone[perm.merchant_sig]) {
+          t.refundsPending[perm.merchant_sig] = perm;
+        }
+      }
+      return t;
+    }
+
+    // Add the refund permissions to the purchase within a DB transaction
+    await this.q().mutate(Stores.purchases, hc, f).finish();
+    this.notifier.notify();
+
+    // Start submitting it but don't wait for it here.
+    this.submitRefunds(hc);
+  }
+
+  async submitRefunds(contractTermsHash: string): Promise<void> {
+    const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+    if (!purchase) {
+      console.error("not submitting refunds, contract terms not found:", 
contractTermsHash);
+      return;
+    }
+    const pendingKeys = Object.keys(purchase.refundsPending);
+    if (pendingKeys.length === 0) {
+      return;
+    }
+    for (const pk of pendingKeys) {
+      const perm = purchase.refundsPending[pk];
+      console.log("sending refund permission", perm);
+      const reqUrl = (new URI("refund")).absoluteTo(purchase.payReq.exchange);
+      const resp = await this.http.postJson(reqUrl.href(), perm);
+      if (resp.status !== 200) {
+        console.error("refund failed", resp);
+        continue;
+      }
+
+      // Transactionally mark successful refunds as done
+      const transformPurchase = (t: PurchaseRecord|undefined): 
PurchaseRecord|undefined => {
+        if (!t) {
+          console.warn("purchase not found, not updating refund");
+          return;
+        }
+        if (t.refundsPending[pk]) {
+          t.refundsDone[pk] = t.refundsPending[pk];
+          delete t.refundsPending[pk];
+        }
+        return t;
+      };
+      const transformCoin = (c: CoinRecord|undefined): CoinRecord|undefined => 
{
+        if (!c) {
+          console.warn("coin not found, can't apply refund");
+          return;
+        }
+        c.status = CoinStatus.Dirty;
+        c.currentAmount = Amounts.add(c.currentAmount, 
perm.refund_amount).amount;
+        c.currentAmount = Amounts.sub(c.currentAmount, perm.refund_fee).amount;
+
+        return c;
+      };
+
+
+      await this.q()
+                .mutate(Stores.purchases, contractTermsHash, transformPurchase)
+                .mutate(Stores.coins, perm.coin_pub, transformCoin)
+                .finish();
+      this.refresh(perm.coin_pub);
+    }
+
+    this.notifier.notify();
+  }
+
+  async getPurchase(contractTermsHash: string): 
Promise<PurchaseRecord|undefined> {
+    return this.q().get(Stores.purchases, contractTermsHash);
+  }
 }
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index d7ecd06a..7de28b9e 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -176,6 +176,22 @@ export interface MessageMap {
     request: { };
     response: void;
   };
+  "log-and-display-error": {
+    request: any;
+    response: void;
+  };
+  "get-report": {
+    request: { reportUid: string };
+    response: void;
+  };
+  "accept-refund": {
+    request: any;
+    response: void;
+  };
+  "get-purchase": {
+    request: any;
+    response: void;
+  }
 }
 
 /**
diff --git a/src/webex/notify.ts b/src/webex/notify.ts
index 51abdb0e..da4657a9 100644
--- a/src/webex/notify.ts
+++ b/src/webex/notify.ts
@@ -30,6 +30,8 @@ import wxApi = require("./wxApi");
 
 import { QueryPaymentResult } from "../types";
 
+import axios from 'axios';
+
 declare var cloneInto: any;
 
 let logVerbose: boolean = false;
@@ -98,85 +100,38 @@ function setStyles(installed: boolean) {
 }
 
 
-function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
+async function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
   if (!maybeFoundResponse.found) {
     console.log("pay-failed", {hint: "payment not found in the wallet"});
     return;
   }
   const walletResp = maybeFoundResponse;
-  /**
-   * Handle a failed payment.
-   *
-   * Try to notify the wallet first, before we show a potentially
-   * synchronous error message (such as an alert) or leave the page.
-   */
-  async function handleFailedPayment(r: XMLHttpRequest) {
-    let timeoutHandle: number|null = null;
-    function err() {
-      // FIXME: proper error reporting!
-      console.log("pay-failed", {status: r.status, response: r.responseText});
-    }
-    function onTimeout() {
-      timeoutHandle = null;
-      err();
-    }
-    timeoutHandle = window.setTimeout(onTimeout, 200);
-
-    await wxApi.paymentFailed(walletResp.contractTermsHash);
-    if (timeoutHandle !== null) {
-      clearTimeout(timeoutHandle);
-      timeoutHandle = null;
-    }
-    err();
-  }
 
   logVerbose && console.log("handling taler-notify-payment: ", walletResp);
-  // Payment timeout in ms.
-  let timeout_ms = 1000;
-  // Current request.
-  let r: XMLHttpRequest|null;
-  let timeoutHandle: number|null = null;
-  function sendPay() {
-    r = new XMLHttpRequest();
-    r.open("post", walletResp.contractTerms.pay_url);
-    r.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
-    r.send(JSON.stringify(walletResp.payReq));
-    r.onload = async () => {
-      if (!r) {
-        return;
-      }
-      switch (r.status) {
-        case 200:
-          const merchantResp = JSON.parse(r.responseText);
-          logVerbose && console.log("got success from pay_url");
-          await wxApi.paymentSucceeded(walletResp.contractTermsHash, 
merchantResp.sig);
-          const nextUrl = walletResp.contractTerms.fulfillment_url;
-          logVerbose && console.log("taler-payment-succeeded done, going to", 
nextUrl);
-          window.location.href = nextUrl;
-          window.location.reload(true);
-          break;
-        default:
-          handleFailedPayment(r);
-          break;
-      }
-      r = null;
-      if (timeoutHandle !== null) {
-        clearTimeout(timeoutHandle!);
-        timeoutHandle = null;
-      }
-    };
-    function retry() {
-      if (r) {
-        r.abort();
-        r = null;
-      }
-      timeout_ms = Math.min(timeout_ms * 2, 10 * 1000);
-      logVerbose && console.log("sendPay timed out, retrying in ", timeout_ms, 
"ms");
-      sendPay();
+  let resp;
+  try {
+    const config = {
+      timeout: 5000, /* 5 seconds */
+      headers: { "Content-Type": "application/json;charset=UTF-8" },
+      validateStatus: (s: number) => s == 200,
     }
-    timeoutHandle = window.setTimeout(retry, timeout_ms);
+    resp = await axios.post(walletResp.contractTerms.pay_url, 
walletResp.payReq, config);
+  } catch (e) {
+    // Gives the user the option to retry / abort and refresh
+    wxApi.logAndDisplayError({
+      name: "pay-post-failed",
+      message: e.message,
+      response: e.response,
+    });
+    throw e;
   }
-  sendPay();
+  const merchantResp = resp.data;
+  logVerbose && console.log("got success from pay_url");
+  await wxApi.paymentSucceeded(walletResp.contractTermsHash, merchantResp.sig);
+  const nextUrl = walletResp.contractTerms.fulfillment_url;
+  logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl);
+  window.location.href = nextUrl;
+  window.location.reload(true);
 }
 
 
@@ -233,53 +188,24 @@ function init() {
 
 type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
 
-function downloadContract(url: string, nonce: string): Promise<any> {
+async function downloadContract(url: string, nonce: string): Promise<any> {
   const parsed_url = new URI(url);
   url = parsed_url.setQuery({nonce}).href();
-  // FIXME: include and check nonce!
-  return new Promise((resolve, reject) => {
-    const contract_request = new XMLHttpRequest();
-    console.log("downloading contract from '" + url + "'");
-    contract_request.open("GET", url, true);
-    contract_request.onload = (e) => {
-      if (contract_request.readyState === 4) {
-        if (contract_request.status === 200) {
-          console.log("response text:",
-                      contract_request.responseText);
-          const contract_wrapper = JSON.parse(contract_request.responseText);
-          if (!contract_wrapper) {
-            console.error("response text was invalid json");
-            const detail = {
-              body: contract_request.responseText,
-              hint: "invalid json",
-              status: contract_request.status,
-            };
-            reject(detail);
-            return;
-          }
-          resolve(contract_wrapper);
-        } else {
-          const detail = {
-            body: contract_request.responseText,
-            hint: "contract download failed",
-            status: contract_request.status,
-          };
-          reject(detail);
-          return;
-        }
-      }
-    };
-    contract_request.onerror = (e) => {
-      const detail = {
-        body: contract_request.responseText,
-        hint: "contract download failed",
-        status: contract_request.status,
-      };
-      reject(detail);
-      return;
-    };
-    contract_request.send();
-  });
+  console.log("downloading contract from '" + url + "'");
+  let resp;
+  try {
+    resp = await axios.get(url, { validateStatus: (s) => s == 200 });
+  } catch (e) {
+    wxApi.logAndDisplayError({
+      name: "contract-download-failed",
+      message: e.message,
+      response: e.response,
+      sameTab: true,
+    });
+    throw e;
+  }
+  console.log("got response", resp);
+  return resp.data;
 }
 
 async function processProposal(proposal: any) {
@@ -328,8 +254,38 @@ async function processProposal(proposal: any) {
   document.location.replace(target);
 }
 
+
+/**
+ * Handle a payment request (coming either from an HTTP 402 or
+ * the JS wallet API).
+ */
 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.refund_url) {
+      console.log("processing refund");
+      let resp;
+      try {
+        const config = {
+          validateStatus: (s: number) => s == 200,
+        }
+        resp = await axios.get(msg.refund_url, config);
+      } catch (e) {
+        wxApi.logAndDisplayError({
+          name: "refund-download-failed",
+          message: e.message,
+          response: e.response,
+          sameTab: true,
+        });
+        throw e;
+      }
+      await wxApi.acceptRefund(resp.data);
+      const hc = resp.data.refund_permissions[0].h_contract_terms;
+      document.location.href = 
chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);
+      return;
+    }
+
     // current URL without fragment
     const url = new URI(document.location.href).fragment("").href();
     const res = await wxApi.queryPayment(url);
diff --git a/src/webex/pages/error.tsx b/src/webex/pages/error.tsx
index e86b6cf4..3f3940d7 100644
--- a/src/webex/pages/error.tsx
+++ b/src/webex/pages/error.tsx
@@ -22,40 +22,69 @@
  * @author Florian Dold
  */
 
+
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 import URI = require("urijs");
 
+import * as wxApi from "../wxApi";
+
 interface ErrorProps {
-  message: string;
+  report: any;
 }
 
 class ErrorView extends React.Component<ErrorProps, { }> {
   render(): JSX.Element {
-    return (
-      <div>
-        An error occurred: {this.props.message}
-      </div>
-    );
+    const report = this.props.report;
+    if (!report) {
+      return (
+        <div>
+          <h1>Error Report Not Found</h1>
+          <p>This page is supposed to display an error reported by the GNU 
Taler wallet,
+              but the corresponding error report can't be found.</p>
+          <p>Maybe the error occured before the browser was restarted or the 
wallet was reloaded.</p>
+        </div>
+      );
+    }
+    switch (report.name) {
+      default:
+        return (
+          <div>
+            <h1>Unknown Error</h1>
+            The GNU Taler wallet reported an unknown error.  Here are the 
details:
+            <pre>
+              {JSON.stringify(report, null, " ")}
+            </pre>
+          </div>
+        );
+    }
   }
 }
 
 async function main() {
-  try {
-    const url = new URI(document.location.href);
-    const query: any = URI.parseQuery(url.query());
+  const url = new URI(document.location.href);
+  const query: any = URI.parseQuery(url.query());
 
-    const message: string = query.message || "unknown error";
+  const container = document.getElementById("container");
+  if (!container) {
+    console.error("fatal: can't mount component, countainer missing");
+    return;
+  }
 
-    ReactDOM.render(<ErrorView message={message} />, document.getElementById(
-      "container")!);
+  // report that we'll render, either looked up from the
+  // logging module or synthesized here for fixed/fatal errors
+  let report;
 
-  } catch (e) {
-    // TODO: provide more context information, maybe factor it out into a
-    // TODO:generic error reporting function or component.
-    document.body.innerText = `Fatal error: "${e.message}".`;
-    console.error(`got error "${e.message}"`, e);
+  const reportUid: string = query.reportUid;
+  if (!reportUid) {
+    report = {
+      name: "missing-error",
+    };
+  } else {
+    report = await wxApi.getReport(reportUid);
   }
+
+  ReactDOM.render(<ErrorView report={report} />, container);
 }
 
 document.addEventListener("DOMContentLoaded", () => main());
diff --git a/src/webex/pages/error.html b/src/webex/pages/refund.html
similarity index 75%
copy from src/webex/pages/error.html
copy to src/webex/pages/refund.html
index 51a8fd73..f97dc9d6 100644
--- a/src/webex/pages/error.html
+++ b/src/webex/pages/refund.html
@@ -3,14 +3,14 @@
 
 <head>
   <meta charset="UTF-8">
-  <title>Taler Wallet: Error Occured</title>
+  <title>Taler Wallet: Refund Status</title>
 
   <link rel="stylesheet" type="text/css" href="../style/wallet.css">
 
   <link rel="icon" href="/img/icon.png">
 
   <script src="/dist/page-common-bundle.js"></script>
-  <script src="/dist/error-bundle.js"></script>
+  <script src="/dist/refund-bundle.js"></script>
 
   <body>
     <div id="container"></div>
diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx
new file mode 100644
index 00000000..b9506bf2
--- /dev/null
+++ b/src/webex/pages/refund.tsx
@@ -0,0 +1,138 @@
+/*
+ This file is part of TALER
+ (C) 2015-2016 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 that shows refund status for purchases.
+ *
+ * @author Florian Dold
+ */
+
+
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import URI = require("urijs");
+
+import * as wxApi from "../wxApi";
+import * as types from "../../types";
+
+import { AmountDisplay } from "../renderHtml";
+
+interface RefundStatusViewProps {
+  contractTermsHash: string;
+}
+
+interface RefundStatusViewState {
+  purchase?: types.PurchaseRecord;
+  gotResult: boolean;
+}
+
+
+const RefundDetail = ({purchase}: {purchase: types.PurchaseRecord}) => {
+  const pendingKeys = Object.keys(purchase.refundsPending);
+  const doneKeys = Object.keys(purchase.refundsDone);
+  if (pendingKeys.length == 0 && doneKeys.length == 0) {
+    return <p>No refunds</p>;
+  }
+
+  const currency = { ...purchase.refundsDone, ...purchase.refundsPending 
}[([...pendingKeys, ...doneKeys][0])].refund_amount.currency;
+  if (!currency) {
+    throw Error("invariant");
+  }
+
+  let amountPending = types.Amounts.getZero(currency);
+  let feesPending = types.Amounts.getZero(currency)
+  for (let k of pendingKeys) {
+    amountPending = types.Amounts.add(amountPending, 
purchase.refundsPending[k].refund_amount).amount;
+    feesPending = types.Amounts.add(feesPending, 
purchase.refundsPending[k].refund_fee).amount;
+  }
+  let amountDone = types.Amounts.getZero(currency);
+  let feesDone = types.Amounts.getZero(currency);
+  for (let k of doneKeys) {
+    amountDone = types.Amounts.add(amountDone, 
purchase.refundsDone[k].refund_amount).amount;
+    feesDone = types.Amounts.add(feesDone, 
purchase.refundsDone[k].refund_fee).amount;
+  }
+
+  return (
+    <div>
+      <p>Refund fully received: <AmountDisplay amount={amountDone} /> (refund 
fees: <AmountDisplay amount={feesDone} />)</p>
+      <p>Refund incoming: <AmountDisplay amount={amountPending} /> (refund 
fees: <AmountDisplay amount={feesPending} />)</p>
+    </div>
+  );
+};
+
+class RefundStatusView extends React.Component<RefundStatusViewProps, 
RefundStatusViewState> {
+
+  constructor(props: RefundStatusViewProps) {
+    super(props);
+    this.state = { gotResult: false };
+  }
+
+  componentDidMount() {
+    this.update();
+    const port = chrome.runtime.connect();
+    port.onMessage.addListener((msg: any) => {
+      if (msg.notify) {
+        console.log("got notified");
+        this.update();
+      }
+    });
+  }
+
+  render(): JSX.Element {
+    const purchase = this.state.purchase;
+    if (!purchase) {
+      if (this.state.gotResult) {
+        return <span>No purchase with contract terms hash 
{this.props.contractTermsHash} found</span>;
+      } else {
+        return <span>...</span>;
+      }
+    }
+    const merchantName = purchase.contractTerms.merchant.name || "(unknown)";
+    const summary = purchase.contractTerms.summary || 
purchase.contractTerms.order_id;
+    return (
+      <div id="main">
+        <h1>Refund Status</h1>
+        <p>Status of purchase <strong>{summary}</strong> from merchant 
<strong>{merchantName}</strong> (order id 
{purchase.contractTerms.order_id}).</p>
+        <p>Total amount: <AmountDisplay amount={purchase.contractTerms.amount} 
/></p>
+        {purchase.finished ? <RefundDetail purchase={purchase} /> : 
<p>Purchase not completed.</p>}
+      </div>
+    );
+  }
+
+  async update() {
+    const purchase = await wxApi.getPurchase(this.props.contractTermsHash);
+    console.log("got purchase", purchase);
+    this.setState({ purchase, gotResult: true });
+  }
+}
+
+
+async function main() {
+  const url = new URI(document.location.href);
+  const query: any = URI.parseQuery(url.query());
+
+  const container = document.getElementById("container");
+  if (!container) {
+    console.error("fatal: can't mount component, countainer missing");
+    return;
+  }
+
+  const contractTermsHash = query.contractTermsHash || "(none)";
+  ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} />, 
container);
+}
+
+document.addEventListener("DOMContentLoaded", () => main());
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index 51f9019e..fe964e68 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -73,6 +73,8 @@ export function renderAmount(amount: AmountJson) {
   return <span>{x}&nbsp;{amount.currency}</span>;
 }
 
+export const AmountDisplay = ({amount}: {amount: AmountJson}) => 
renderAmount(amount);
+
 
 /**
  * Abbreviate a string to a given length, and show the full
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 1371e27e..1423da53 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -31,6 +31,7 @@ import {
   DenominationRecord,
   ExchangeRecord,
   PreCoinRecord,
+  PurchaseRecord,
   QueryPaymentResult,
   ReserveCreationInfo,
   ReserveRecord,
@@ -321,3 +322,26 @@ export function getSenderWireInfos(): 
Promise<SenderWireInfos> {
 export function returnCoins(args: { amount: AmountJson, exchange: string, 
senderWire: object }): Promise<void> {
   return callBackend("return-coins", args);
 }
+
+
+/**
+ * Record an error report and display it in a tabl.
+ *
+ * If sameTab is set, the error report will be opened in the current tab,
+ * otherwise in a new tab.
+ */
+export function logAndDisplayError(args: any): Promise<void> {
+  return callBackend("log-and-display-error", args);
+}
+
+export function getReport(reportUid: string): Promise<void> {
+  return callBackend("get-report", { reportUid });
+}
+
+export function acceptRefund(refundData: any): Promise<number> {
+  return callBackend("accept-refund", refundData);
+}
+
+export function getPurchase(contractTermsHash: string): 
Promise<PurchaseRecord> {
+  return callBackend("get-purchase", { contractTermsHash });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 974bcb3c..0d1c2d8c 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -303,6 +303,26 @@ function handleMessage(sender: MessageSender,
       }
       return resp;
     }
+    case "log-and-display-error":
+      logging.storeReport(detail).then((reportUid) => {
+        const url = 
chrome.extension.getURL(`/src/webex/pages/error.html?reportUid=${reportUid}`);
+        if (detail.sameTab && sender && sender.tab && sender.tab.id) {
+          chrome.tabs.update(detail.tabId, { url });
+        } else {
+          chrome.tabs.create({ url });
+        }
+      });
+      return;
+    case "get-report":
+      return logging.getReport(detail.reportUid);
+    case "accept-refund":
+      return needsWallet().acceptRefund(detail.refund_permissions);
+    case "get-purchase":
+      const contractTermsHash = detail.contractTermsHash;
+      if (!contractTermsHash) {
+        throw Error("contractTermsHash missing");
+      }
+      return needsWallet().getPurchase(contractTermsHash);
     default:
       // Exhaustiveness check.
       // See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@@ -371,6 +391,9 @@ class ChromeNotifier implements Notifier {
 
 /**
  * Mapping from tab ID to payment information (if any).
+ *
+ * Used to pass information from an intercepted HTTP header to the content
+ * script on the page.
  */
 const paymentRequestCookies: { [n: number]: any } = {};
 
@@ -392,6 +415,7 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
   const fields = {
     contract_url: headers["x-taler-contract-url"],
     offer_url: headers["x-taler-offer-url"],
+    refund_url: headers["x-taler-refund-url"],
   };
 
   const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as 
any)[x]).length !== 0;
@@ -406,6 +430,7 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
   const payDetail = {
     contract_url: fields.contract_url,
     offer_url: fields.offer_url,
+    refund_url: fields.refund_url,
   };
 
   console.log("got pay detail", payDetail);
diff --git a/webpack.config.js b/webpack.config.js
index 89a4a5ae..af586dc5 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -76,6 +76,7 @@ module.exports = function (env) {
       "popup": "./src/webex/pages/popup.tsx",
       "reset-required": "./src/webex/pages/reset-required.tsx",
       "return-coins": "./src/webex/pages/return-coins.tsx",
+      "refund": "./src/webex/pages/refund.tsx",
       "show-db": "./src/webex/pages/show-db.ts",
       "tree": "./src/webex/pages/tree.tsx",
     },
diff --git a/yarn.lock b/yarn.lock
index 1c767f3f..3b845856 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -490,6 +490,13 @@ address@hidden:
   version "1.6.0"
   resolved 
"https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e";
 
address@hidden:
+  version "0.16.2"
+  resolved 
"https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d";
+  dependencies:
+    follow-redirects "^1.2.3"
+    is-buffer "^1.1.5"
+
 address@hidden:
   version "6.22.0"
   resolved 
"https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4";
@@ -1523,7 +1530,7 @@ address@hidden:
   version "1.0.1"
   resolved 
"https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f";
 
address@hidden, address@hidden, address@hidden, address@hidden:
address@hidden, address@hidden, address@hidden, address@hidden, address@hidden:
   version "2.6.8"
   resolved 
"https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc";
   dependencies:
@@ -2091,6 +2098,12 @@ address@hidden:
   version "2.0.1"
   resolved 
"https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7";
 
address@hidden:
+  version "1.2.4"
+  resolved 
"https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea";
+  dependencies:
+    debug "^2.4.5"
+
 address@hidden, address@hidden:
   version "1.0.2"
   resolved 
"https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80";

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



reply via email to

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