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 (09c7be80 -> c62


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated (09c7be80 -> c62ba498)
Date: Wed, 17 Jan 2018 03:50:03 +0100

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

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

    from 09c7be80 fix /pay API
     new 894a09a5 rename data -> contract_terms
     new c62ba498 implement new mobile-compatible payment logic

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:
 src/checkable.ts                     |  25 +--
 src/dbTypes.ts                       |  51 +++---
 src/i18n/de.po                       |   8 +-
 src/i18n/en-US.po                    |   8 +-
 src/i18n/fr.po                       |   8 +-
 src/i18n/it.po                       |   8 +-
 src/i18n/taler-wallet-webex.pot      |   8 +-
 src/talerTypes.ts                    |  56 ++++---
 src/wallet.ts                        | 213 ++++++++++++++++++++-----
 src/walletTypes.ts                   |  15 +-
 src/webex/messages.ts                |  36 ++---
 src/webex/notify.ts                  | 293 ++---------------------------------
 src/webex/pages/confirm-contract.tsx |  83 ++++++----
 src/webex/wxApi.ts                   |  63 ++------
 src/webex/wxBackend.ts               | 183 ++++++++++++++--------
 15 files changed, 501 insertions(+), 557 deletions(-)

diff --git a/src/checkable.ts b/src/checkable.ts
index 124eb658..159e5a85 100644
--- a/src/checkable.ts
+++ b/src/checkable.ts
@@ -15,8 +15,6 @@
  */
 
 
-"use strict";
-
 /**
  * Decorators for validating JSON objects and converting them to a typed
  * object.
@@ -55,6 +53,7 @@ export namespace Checkable {
     propertyKey: any;
     checker: any;
     type?: any;
+    typeThunk?: () => any;
     elementChecker?: any;
     elementProp?: any;
     keyProp?: any;
@@ -167,11 +166,18 @@ export namespace Checkable {
 
 
   function checkValue(target: any, prop: Prop, path: Path): any {
-    const type = prop.type;
-    const typeName = type.name || "??";
-    if (!type) {
-      throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
+    let type;
+    if (prop.type) {
+     type = prop.type;
+    } else if (prop.typeThunk) {
+      type = prop.typeThunk();
+      if (!type) {
+        throw Error(`assertion failed: typeThunk returned null (prop is 
${JSON.stringify(prop)})`);
+      }
+    } else {
+      throw Error(`assertion failed: type/typeThunk missing (prop is 
${JSON.stringify(prop)})`);
     }
+    const typeName = type.name || "??";
     const v = target;
     if (!v || typeof v !== "object") {
       throw new SchemaError(
@@ -236,16 +242,13 @@ export namespace Checkable {
   /**
    * Target property must be a Checkable object of the given type.
    */
-  export function Value(type: any) {
-    if (!type) {
-      throw Error("Type does not exist yet (wrong order of definitions?)");
-    }
+  export function Value(typeThunk: () => any) {
     function deco(target: object, propertyKey: string | symbol): void {
       const chk = getCheckableInfo(target);
       chk.props.push({
         checker: checkValue,
         propertyKey,
-        type,
+        typeThunk,
       });
     }
 
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index b5040bee..86f3e0a1 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -49,7 +49,7 @@ import {
  * In the future we might consider adding migration functions for
  * each version increment.
  */
-export const WALLET_DB_VERSION = 24;
+export const WALLET_DB_VERSION = 25;
 
 
 /**
@@ -206,7 +206,7 @@ export class DenominationRecord {
   /**
    * Value of one coin of the denomination.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   value: AmountJson;
 
   /**
@@ -225,25 +225,25 @@ export class DenominationRecord {
   /**
    * Fee for withdrawing.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   feeWithdraw: AmountJson;
 
   /**
    * Fee for depositing.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   feeDeposit: AmountJson;
 
   /**
    * Fee for refreshing.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   feeRefresh: AmountJson;
 
   /**
    * Fee for refunding.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   feeRefund: AmountJson;
 
   /**
@@ -491,15 +491,22 @@ export interface CoinRecord {
   status: CoinStatus;
 }
 
+
 /**
  * Proposal record, stored in the wallet's database.
  */
 @Checkable.Class()
-export class ProposalRecord {
+export class ProposalDownloadRecord {
+  /**
+   * URL where the proposal was downloaded.
+   */
+  @Checkable.String
+  url: string;
+
   /**
    * The contract that was offered by the merchant.
    */
-  @Checkable.Value(ContractTerms)
+  @Checkable.Value(() => ContractTerms)
   contractTerms: ContractTerms;
 
   /**
@@ -528,10 +535,16 @@ export class ProposalRecord {
   timestamp: number;
 
   /**
+   * Private key for the nonce.
+   */
+  @Checkable.String
+  noncePriv: string;
+
+  /**
    * Verify that a value matches the schema of this class and convert it into a
    * member.
    */
-  static checked: (obj: any) => ProposalRecord;
+  static checked: (obj: any) => ProposalDownloadRecord;
 }
 
 
@@ -789,15 +802,6 @@ export interface SenderWireRecord {
 
 
 /**
- * Nonce record as stored in the wallet's database.
- */
-export interface NonceRecord {
-  priv: string;
-  pub: string;
-}
-
-
-/**
  * Configuration key/value entries to configure
  * the wallet.
  */
@@ -869,12 +873,6 @@ export namespace Stores {
     pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", 
"masterPublicKey");
   }
 
-  class NonceStore extends Store<NonceRecord> {
-    constructor() {
-      super("nonces", { keyPath: "pub" });
-    }
-  }
-
   class CoinsStore extends Store<CoinRecord> {
     constructor() {
       super("coins", { keyPath: "coinPub" });
@@ -884,14 +882,14 @@ export namespace Stores {
     denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", 
"denomPub");
   }
 
-  class ProposalsStore extends Store<ProposalRecord> {
+  class ProposalsStore extends Store<ProposalDownloadRecord> {
     constructor() {
       super("proposals", {
         autoIncrement: true,
         keyPath: "id",
       });
     }
-    timestampIndex = new Index<string, ProposalRecord>(this, "timestampIndex", 
"timestamp");
+    timestampIndex = new Index<string, ProposalDownloadRecord>(this, 
"timestampIndex", "timestamp");
   }
 
   class PurchasesStore extends Store<PurchaseRecord> {
@@ -965,7 +963,6 @@ export namespace Stores {
   export const denominations = new DenominationsStore();
   export const exchangeWireFees = new ExchangeWireFeesStore();
   export const exchanges = new ExchangeStore();
-  export const nonces = new NonceStore();
   export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: 
"coinPub"});
   export const proposals = new ProposalsStore();
   export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: 
"id", autoIncrement: true});
diff --git a/src/i18n/de.po b/src/i18n/de.po
index 21ff8dfe..5f163a0d 100644
--- a/src/i18n/de.po
+++ b/src/i18n/de.po
@@ -42,13 +42,13 @@ msgstr ""
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
 #, c-format
 msgid "You have insufficient funds of the requested currency in your wallet."
 msgstr ""
 
 #. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
 #, c-format
 msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
 #, fuzzy, c-format
 msgid "Confirm payment"
 msgstr "Bezahlung bestätigen"
diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po
index 3307229b..0dfa852c 100644
--- a/src/i18n/en-US.po
+++ b/src/i18n/en-US.po
@@ -42,13 +42,13 @@ msgstr ""
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
 #, c-format
 msgid "You have insufficient funds of the requested currency in your wallet."
 msgstr ""
 
 #. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
 #, c-format
 msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
 #, c-format
 msgid "Confirm payment"
 msgstr ""
diff --git a/src/i18n/fr.po b/src/i18n/fr.po
index b955dc6a..55677763 100644
--- a/src/i18n/fr.po
+++ b/src/i18n/fr.po
@@ -42,13 +42,13 @@ msgstr ""
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
 #, c-format
 msgid "You have insufficient funds of the requested currency in your wallet."
 msgstr ""
 
 #. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
 #, c-format
 msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
 #, c-format
 msgid "Confirm payment"
 msgstr ""
diff --git a/src/i18n/it.po b/src/i18n/it.po
index b955dc6a..55677763 100644
--- a/src/i18n/it.po
+++ b/src/i18n/it.po
@@ -42,13 +42,13 @@ msgstr ""
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
 #, c-format
 msgid "You have insufficient funds of the requested currency in your wallet."
 msgstr ""
 
 #. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
 #, c-format
 msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
 #, c-format
 msgid "Confirm payment"
 msgstr ""
diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot
index b955dc6a..55677763 100644
--- a/src/i18n/taler-wallet-webex.pot
+++ b/src/i18n/taler-wallet-webex.pot
@@ -42,13 +42,13 @@ msgstr ""
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:159
+#: src/webex/pages/confirm-contract.tsx:175
 #, c-format
 msgid "You have insufficient funds of the requested currency in your wallet."
 msgstr ""
 
 #. tslint:disable-next-line:max-line-length
-#: src/webex/pages/confirm-contract.tsx:161
+#: src/webex/pages/confirm-contract.tsx:177
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,12 +56,12 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:217
+#: src/webex/pages/confirm-contract.tsx:236
 #, c-format
 msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:238
+#: src/webex/pages/confirm-contract.tsx:257
 #, c-format
 msgid "Confirm payment"
 msgstr ""
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index 27bf7b43..d593c3d3 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -38,7 +38,7 @@ export class Denomination {
   /**
    * Value of one coin of the denomination.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   value: AmountJson;
 
   /**
@@ -50,25 +50,25 @@ export class Denomination {
   /**
    * Fee for withdrawing.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   fee_withdraw: AmountJson;
 
   /**
    * Fee for depositing.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   fee_deposit: AmountJson;
 
   /**
    * Fee for refreshing.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   fee_refresh: AmountJson;
 
   /**
    * Fee for refunding.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   fee_refund: AmountJson;
 
   /**
@@ -151,7 +151,7 @@ export class Auditor {
   /**
    * List of signatures for denominations by the auditor.
    */
-  @Checkable.List(Checkable.Value(AuditorDenomSig))
+  @Checkable.List(Checkable.Value(() => AuditorDenomSig))
   denomination_keys: AuditorDenomSig[];
 }
 
@@ -204,7 +204,7 @@ export class PaybackConfirmation {
    * How much will the exchange pay back (needed by wallet in
    * case coin was partially spent and wallet got restored from backup)
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
@@ -336,7 +336,7 @@ export class ContractTerms {
   /**
    * Total amount payable.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
@@ -360,7 +360,7 @@ export class ContractTerms {
   /**
    * Maximum deposit fee covered by the merchant.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   max_fee: AmountJson;
 
   /**
@@ -378,7 +378,7 @@ export class ContractTerms {
   /**
    * List of accepted exchanges.
    */
-  @Checkable.List(Checkable.Value(ExchangeHandle))
+  @Checkable.List(Checkable.Value(() => ExchangeHandle))
   exchanges: ExchangeHandle[];
 
   /**
@@ -428,7 +428,7 @@ export class ContractTerms {
   /**
    * Maximum wire fee that the merchant agrees to pay for.
    */
-  @Checkable.Optional(Checkable.Value(AmountJson))
+  @Checkable.Optional(Checkable.Value(() => AmountJson))
   max_wire_fee?: AmountJson;
 
   /**
@@ -578,7 +578,7 @@ export class TipResponse {
   /**
    * The order of the signatures matches the planchets list.
    */
-  @Checkable.List(Checkable.Value(ReserveSigSingleton))
+  @Checkable.List(Checkable.Value(() => ReserveSigSingleton))
   reserve_sigs: ReserveSigSingleton[];
 
   /**
@@ -620,7 +620,7 @@ export class TipToken {
   /**
    * Amount of tip.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
@@ -659,7 +659,7 @@ export class KeysJson {
   /**
    * List of offered denominations.
    */
-  @Checkable.List(Checkable.Value(Denomination))
+  @Checkable.List(Checkable.Value(() => Denomination))
   denoms: Denomination[];
 
   /**
@@ -671,7 +671,7 @@ export class KeysJson {
   /**
    * The list of auditors (partially) auditing the exchange.
    */
-  @Checkable.List(Checkable.Value(Auditor))
+  @Checkable.List(Checkable.Value(() => Auditor))
   auditors: Auditor[];
 
   /**
@@ -683,7 +683,7 @@ export class KeysJson {
   /**
    * List of paybacks for compromised denominations.
    */
-  @Checkable.Optional(Checkable.List(Checkable.Value(Payback)))
+  @Checkable.Optional(Checkable.List(Checkable.Value(() => Payback)))
   payback?: Payback[];
 
   /**
@@ -715,13 +715,13 @@ export class WireFeesJson {
   /**
    * Cost of a wire transfer.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   wire_fee: AmountJson;
 
   /**
    * Cost of clising a reserve.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   closing_fee: AmountJson;
 
   /**
@@ -765,7 +765,7 @@ export class WireDetailJson {
   /**
    * Fees associated with the wire transfer method.
    */
-  @Checkable.List(Checkable.Value(WireFeesJson))
+  @Checkable.List(Checkable.Value(() => WireFeesJson))
   fees: WireFeesJson[];
 
   /**
@@ -788,3 +788,21 @@ export type WireDetail = object & { type: string };
 export function isWireDetail(x: any): x is WireDetail {
   return x && typeof x === "object" && typeof x.type === "string";
 }
+
+/**
+ * Proposal returned from the contract URL.
+ */
address@hidden({extra: true})
+export class Proposal {
+  @Checkable.Value(() => ContractTerms)
+  contract_terms: ContractTerms;
+
+  @Checkable.String
+  sig: string;
+
+  /**
+   * Verify that a value matches the schema of this class and convert it into a
+   * member.
+   */
+  static checked: (obj: any) => Proposal;
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 8a63e45e..24fab9f8 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -49,6 +49,8 @@ import * as Amounts from "./amounts";
 
 import URI = require("urijs");
 
+import axios from "axios";
+
 import {
   CoinRecord,
   CoinStatus,
@@ -59,7 +61,7 @@ import {
   ExchangeRecord,
   ExchangeWireFeesRecord,
   PreCoinRecord,
-  ProposalRecord,
+  ProposalDownloadRecord,
   PurchaseRecord,
   RefreshPreCoinRecord,
   RefreshSessionRecord,
@@ -76,9 +78,11 @@ import {
   KeysJson,
   PayReq,
   PaybackConfirmation,
+  Proposal,
   RefundPermission,
   TipPlanchetDetail,
   TipResponse,
+  TipToken,
   WireDetailJson,
   isWireDetail,
 } from "./talerTypes";
@@ -109,7 +113,7 @@ interface SpeculativePayData {
   payCoinInfo: PayCoinInfo;
   exchangeUrl: string;
   proposalId: number;
-  proposal: ProposalRecord;
+  proposal: ProposalDownloadRecord;
 }
 
 
@@ -624,9 +628,9 @@ export class Wallet {
    * Record all information that is necessary to
    * pay for a proposal in the wallet's database.
    */
-  private async recordConfirmPay(proposal: ProposalRecord,
+  private async recordConfirmPay(proposal: ProposalDownloadRecord,
                                  payCoinInfo: PayCoinInfo,
-                                 chosenExchange: string): Promise<void> {
+                                 chosenExchange: string): 
Promise<PurchaseRecord> {
     const payReq: PayReq = {
       coins: payCoinInfo.sigs,
       merchant_pub: proposal.contractTerms.merchant_pub,
@@ -651,15 +655,42 @@ export class Wallet {
               .finish();
     this.badge.showNotification();
     this.notifier.notify();
+    return t;
   }
 
 
   /**
-   * Save a proposal in the database and return an id for it to
-   * retrieve it later.
+   * Download a proposal and store it in the database.
+   * Returns an id for it to retrieve it later.
    */
-  async saveProposal(proposal: ProposalRecord): Promise<number> {
-    const id = await this.q().putWithResult(Stores.proposals, proposal);
+  async downloadProposal(url: string): Promise<number> {
+    const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
+    const parsed_url = new URI(url);
+    url = parsed_url.setQuery({ nonce: pub }).href();
+    console.log("downloading contract from '" + url + "'");
+    let resp;
+    try {
+      resp = await axios.get(url, { validateStatus: (s) => s === 200 });
+    } catch (e) {
+      console.log("contract download failed", e);
+      throw e;
+    }
+    console.log("got response", resp);
+
+    const proposal = Proposal.checked(resp.data);
+
+    const contractTermsHash = await this.hashContract(proposal.contract_terms);
+
+    const proposalRecord: ProposalDownloadRecord = {
+      contractTerms: proposal.contract_terms,
+      contractTermsHash,
+      merchantSig: proposal.sig,
+      noncePriv: priv,
+      timestamp: (new Date()).getTime(),
+      url,
+    };
+
+    const id = await this.q().putWithResult(Stores.proposals, proposalRecord);
     this.notifier.notify();
     if (typeof id !== "number") {
       throw Error("db schema wrong");
@@ -667,24 +698,50 @@ export class Wallet {
     return id;
   }
 
+  async submitPay(purchase: PurchaseRecord, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
+    let resp;
+    const payReq = { ...purchase.payReq, session_id: sessionId };
+    try {
+      const config = {
+        headers: { "Content-Type": "application/json;charset=UTF-8" },
+        timeout: 5000, /* 5 seconds */
+        validateStatus: (s: number) => s === 200,
+      };
+      resp = await axios.post(purchase.contractTerms.pay_url, payReq, config);
+    } catch (e) {
+      // Gives the user the option to retry / abort and refresh
+      console.log("payment failed", e);
+      throw e;
+    }
+    const merchantResp = resp.data;
+    console.log("got success from pay_url");
+    await this.paymentSucceeded(purchase.contractTermsHash, merchantResp.sig);
+    const fu = new URI(purchase.contractTerms.fulfillment_url);
+    fu.addSearch("order_id", purchase.contractTerms.order_id);
+    if (merchantResp.session_sig) {
+      fu.addSearch("session_sig", merchantResp.session_sig);
+    }
+    const nextUrl = fu.href();
+    return { nextUrl };
+  }
+
 
   /**
    * Add a contract to the wallet and sign coins,
    * but do not send them yet.
    */
-  async confirmPay(proposalId: number): Promise<ConfirmPayResult> {
-    console.log("executing confirmPay");
-    const proposal: ProposalRecord|undefined = await 
this.q().get(Stores.proposals, proposalId);
+  async confirmPay(proposalId: number, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
+    console.log(`executing confirmPay with proposalId ${proposalId} and 
sessionId ${sessionId}`);
+    const proposal: ProposalDownloadRecord|undefined = await 
this.q().get(Stores.proposals, proposalId);
 
     if (!proposal) {
       throw Error(`proposal with id ${proposalId} not found`);
     }
 
-    const purchase = await this.q().get(Stores.purchases, 
proposal.contractTermsHash);
+    let purchase = await this.q().get(Stores.purchases, 
proposal.contractTermsHash);
 
     if (purchase) {
-      // Already payed ...
-      return "paid";
+      return this.submitPay(purchase, sessionId);
     }
 
     const res = await this.getCoinsForPayment({
@@ -702,22 +759,24 @@ export class Wallet {
     console.log("coin selection result", res);
 
     if (!res) {
+      // Should not happen, since checkPay should be called first
       console.log("not confirming payment, insufficient coins");
-      return "insufficient-balance";
+      throw Error("insufficient balance");
     }
 
     const sd = await this.getSpeculativePayData(proposalId);
     if (!sd) {
       const { exchangeUrl, cds } = res;
       const payCoinInfo = await 
this.cryptoApi.signDeposit(proposal.contractTerms, cds);
-      await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl);
+      purchase = await this.recordConfirmPay(proposal, payCoinInfo, 
exchangeUrl);
     } else {
-      await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl);
+      purchase = await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, 
sd.exchangeUrl);
     }
 
-    return "paid";
+    return this.submitPay(purchase, sessionId);
   }
 
+
   /**
    * Get the speculative pay data, but only if coins have not changed in 
between.
    */
@@ -803,7 +862,7 @@ export class Wallet {
    * Retrieve information required to pay for a contract, where the
    * contract is identified via the fulfillment url.
    */
-  async queryPayment(url: string): Promise<QueryPaymentResult> {
+  async queryPaymentByFulfillmentUrl(url: string): Promise<QueryPaymentResult> 
{
     console.log("query for payment", url);
 
     const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, 
url);
@@ -823,6 +882,30 @@ export class Wallet {
     };
   }
 
+  /**
+   * Retrieve information required to pay for a contract, where the
+   * contract is identified via the contract terms hash.
+   */
+  async queryPaymentByContractTermsHash(contractTermsHash: string): 
Promise<QueryPaymentResult> {
+    console.log("query for payment", contractTermsHash);
+
+    const t = await this.q().get(Stores.purchases, contractTermsHash);
+
+    if (!t) {
+      console.log("query for payment failed");
+      return {
+        found: false,
+      };
+    }
+    console.log("query for payment succeeded:", t);
+    return {
+      contractTerms: t.contractTerms,
+      contractTermsHash: t.contractTermsHash,
+      found: true,
+      payReq: t.payReq,
+    };
+  }
+
 
   /**
    * First fetch information requred to withdraw from the reserve,
@@ -2020,7 +2103,7 @@ export class Wallet {
 
     // FIXME: do pagination instead of generating the full history
 
-    const proposals = await 
this.q().iter<ProposalRecord>(Stores.proposals).toArray();
+    const proposals = await 
this.q().iter<ProposalDownloadRecord>(Stores.proposals).toArray();
     for (const p of proposals) {
       history.push({
         detail: {
@@ -2111,7 +2194,7 @@ export class Wallet {
     return denoms;
   }
 
-  async getProposal(proposalId: number): Promise<ProposalRecord|undefined> {
+  async getProposal(proposalId: number): 
Promise<ProposalDownloadRecord|undefined> {
     const proposal = await this.q().get(Stores.proposals, proposalId);
     return proposal;
   }
@@ -2162,18 +2245,6 @@ export class Wallet {
   }
 
 
-  /**
-   * Generate a nonce in form of an EdDSA public key.
-   * Store the private key in our DB, so we can prove ownership.
-   */
-  async generateNonce(): Promise<string> {
-    const {priv, pub} = await this.cryptoApi.createEddsaKeypair();
-    await this.q()
-              .put(Stores.nonces, {priv, pub})
-              .finish();
-    return pub;
-  }
-
   async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> 
{
     return this.q().get(Stores.currencies, currency);
   }
@@ -2466,10 +2537,25 @@ export class Wallet {
     }
   }
 
-  async acceptRefund(refundPermissions: RefundPermission[]): Promise<void> {
+  async acceptRefund(refundUrl: string): Promise<string> {
+    console.log("processing refund");
+    let resp;
+    try {
+      const config = {
+        validateStatus: (s: number) => s === 200,
+      };
+      resp = await axios.get(refundUrl, config);
+    } catch (e) {
+      console.log("error downloading refund permission", e);
+      throw e;
+    }
+
+    // FIXME: validate schema
+    const refundPermissions = resp.data;
+
     if (!refundPermissions.length) {
       console.warn("got empty refund list");
-      return;
+      throw Error("empty refund");
     }
     const hc = refundPermissions[0].h_contract_terms;
     if (!hc) {
@@ -2513,6 +2599,8 @@ export class Wallet {
 
     // Start submitting it but don't wait for it here.
     this.submitRefunds(hc);
+
+    return refundPermissions[0].h_contract_terms;
   }
 
   async submitRefunds(contractTermsHash: string): Promise<void> {
@@ -2646,6 +2734,54 @@ export class Wallet {
     return planchetDetail;
   }
 
+
+  async processTip(tipToken: TipToken): Promise<void> {
+    console.log("got tip token", tipToken);
+
+    const deadlineSec = getTalerStampSec(tipToken.expiration);
+    if (!deadlineSec) {
+      throw Error("tipping failed (invalid expiration)");
+    }
+
+    const merchantDomain = new URI(document.location.href).origin();
+    let walletResp;
+    walletResp = await this.getTipPlanchets(merchantDomain,
+                                              tipToken.tip_id,
+                                              tipToken.amount,
+                                              deadlineSec,
+                                              tipToken.exchange_url,
+                                              tipToken.next_url);
+
+    const planchets = walletResp;
+
+    if (!planchets) {
+      console.log("failed tip", walletResp);
+      throw Error("processing tip failed");
+    }
+
+    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) {
+      console.log("tipping failed", e);
+      throw e;
+    }
+
+    try {
+      this.processTipResponse(merchantDomain, tipToken.tip_id, 
merchantResp.data);
+    } catch (e) {
+      console.log("processTipResponse failed", e);
+      throw e;
+    }
+
+    return;
+  }
+
   /**
    * Accept a merchant's response to a tip pickup and start withdrawing the 
coins.
    * These coins will not appear in the wallet yet.
@@ -2725,6 +2861,11 @@ export class Wallet {
     return tipStatus;
   }
 
+
+  getNextUrlFromResourceUrl(resourceUrl: string): string | undefined {
+    return;
+  }
+
   /**
    * Remove unreferenced / expired data from the wallet's database
    * based on the current system time.
@@ -2745,7 +2886,7 @@ export class Wallet {
     };
     await this.q().deleteIf(Stores.reserves, gcReserve).finish();
 
-    const gcProposal = (d: ProposalRecord, n: number) => {
+    const gcProposal = (d: ProposalDownloadRecord, n: number) => {
       // Delete proposal after 60 minutes or 5 minutes before pay deadline,
       // whatever comes first.
       const deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! 
* 1000;
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index 3c7bff1e..d1a4f874 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -246,9 +246,11 @@ export interface CheckPayResult {
 
 
 /**
- * Possible results for confirmPay.
+ * Result for confirmPay
  */
-export type ConfirmPayResult = "paid" | "insufficient-balance";
+export interface ConfirmPayResult {
+  nextUrl: string;
+}
 
 
 /**
@@ -299,6 +301,7 @@ export interface QueryPaymentFound {
   found: true;
   contractTermsHash: string;
   contractTerms: ContractTerms;
+  lastSessionSig?: string;
   payReq: PayReq;
 }
 
@@ -329,7 +332,7 @@ export class CreateReserveRequest {
   /**
    * The initial amount for the reserve.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
@@ -380,7 +383,7 @@ export class ReturnCoinsRequest {
   /**
    * The amount to wire.
    */
-  @Checkable.Value(AmountJson)
+  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
@@ -511,7 +514,7 @@ export class ProcessTipResponseRequest {
   /**
    * Tip response from the merchant.
    */
-  @Checkable.Value(TipResponse)
+  @Checkable.Value(() => TipResponse)
   tipResponse: TipResponse;
 
   /**
@@ -543,7 +546,7 @@ export class GetTipPlanchetsRequest {
   /**
    * Amount of the tip.
    */
-  @Checkable.Optional(Checkable.Value(AmountJson))
+  @Checkable.Optional(Checkable.Value(() => AmountJson))
   amount: AmountJson;
 
   /**
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 0d032980..0fcd6047 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -44,10 +44,6 @@ export interface MessageMap {
     };
     response: void;
   };
-  "get-tab-cookie": {
-    request: { }
-    response: any;
-  };
   "ping": {
     request: { };
     response: void;
@@ -67,12 +63,8 @@ export interface MessageMap {
     request: { reservePub: string };
     response: void;
   };
-  "generate-nonce": {
-    request: { }
-    response: string;
-  };
   "confirm-pay": {
-    request: { proposalId: number; };
+    request: { proposalId: number; sessionId?: string };
     response: walletTypes.ConfirmPayResult;
   };
   "check-pay": {
@@ -95,10 +87,6 @@ export interface MessageMap {
     request: { contract: object };
     response: string;
   };
-  "save-proposal": {
-    request: { proposal: dbTypes.ProposalRecord };
-    response: void;
-  };
   "reserve-creation-info": {
     request: { baseUrl: string, amount: AmountJson };
     response: walletTypes.ReserveCreationInfo;
@@ -109,7 +97,7 @@ export interface MessageMap {
   };
   "get-proposal": {
     request: { proposalId: number };
-    response: dbTypes.ProposalRecord | undefined;
+    response: dbTypes.ProposalDownloadRecord | undefined;
   };
   "get-coins": {
     request: { exchangeBaseUrl: string };
@@ -155,14 +143,6 @@ export interface MessageMap {
     request: { coinPub: string };
     response: void;
   };
-  "payment-failed": {
-    request: { contractTermsHash: string };
-    response: void;
-  };
-  "payment-succeeded": {
-    request: { contractTermsHash: string; merchantSig: string };
-    response: void;
-  };
   "check-upgrade": {
     request: { };
     response: void;
@@ -183,10 +163,6 @@ export interface MessageMap {
     request: { reportUid: string };
     response: void;
   };
-  "accept-refund": {
-    request: any;
-    response: void;
-  };
   "get-purchase": {
     request: any;
     response: void;
@@ -215,6 +191,14 @@ export interface MessageMap {
     request: { };
     response: void;
   };
+  "taler-pay": {
+    request: any;
+    response: void;
+  };
+  "download-proposal": {
+    request: any;
+    response: void;
+  };
 }
 
 /**
diff --git a/src/webex/notify.ts b/src/webex/notify.ts
index 1a447c0a..e163a627 100644
--- a/src/webex/notify.ts
+++ b/src/webex/notify.ts
@@ -28,13 +28,6 @@ import URI = require("urijs");
 
 import wxApi = require("./wxApi");
 
-import { getTalerStampSec } from "../helpers";
-import { TipToken } from "../talerTypes";
-import { QueryPaymentResult } from "../walletTypes";
-
-
-import axios from "axios";
-
 declare var cloneInto: any;
 
 let logVerbose: boolean = false;
@@ -103,42 +96,6 @@ function setStyles(installed: boolean) {
 }
 
 
-async function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
-  if (!maybeFoundResponse.found) {
-    console.log("pay-failed", {hint: "payment not found in the wallet"});
-    return;
-  }
-  const walletResp = maybeFoundResponse;
-
-  logVerbose && console.log("handling taler-notify-payment: ", walletResp);
-  let resp;
-  try {
-    const config = {
-      headers: { "Content-Type": "application/json;charset=UTF-8" },
-      timeout: 5000, /* 5 seconds */
-      validateStatus: (s: number) => s === 200,
-    };
-    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({
-      contractTerms: walletResp.contractTerms,
-      message: e.message,
-      name: "pay-post-failed",
-      response: e.response,
-    });
-    throw e;
-  }
-  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);
-}
-
-
 function onceOnComplete(cb: () => void) {
   if (document.readyState === "complete") {
     cb();
@@ -153,245 +110,29 @@ function onceOnComplete(cb: () => void) {
 
 
 function init() {
-  // Only place where we don't use the nicer RPC wrapper, since the wallet
-  // backend might not be ready (during install, upgrade, etc.)
-  chrome.runtime.sendMessage({type: "get-tab-cookie"}, (resp) => {
-    logVerbose && console.log("got response for get-tab-cookie");
-    if (chrome.runtime.lastError) {
-      logVerbose && console.log("extension not yet ready");
-      window.setTimeout(init, 200);
-      return;
-    }
-    onceOnComplete(() => {
-      if (document.documentElement.getAttribute("data-taler-nojs")) {
-        initStyle();
-        setStyles(true);
-      }
-    });
-    registerHandlers();
-    // Hack to know when the extension is unloaded
-    const port = chrome.runtime.connect();
-
-    port.onDisconnect.addListener(() => {
-      logVerbose && console.log("chrome runtime disconnected, removing 
handlers");
-      if (document.documentElement.getAttribute("data-taler-nojs")) {
-        setStyles(false);
-      }
-      for (const handler of handlers) {
-        document.removeEventListener(handler.type, handler.listener);
-      }
-    });
-
-    if (resp && resp.type === "pay") {
-      logVerbose && console.log("doing taler.pay with", resp.payDetail);
-      talerPay(resp.payDetail).then(handlePaymentResponse);
+  onceOnComplete(() => {
+    if (document.documentElement.getAttribute("data-taler-nojs")) {
+      initStyle();
+      setStyles(true);
     }
   });
-}
-
-type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
-
-async function downloadContract(url: string, nonce: string): Promise<any> {
-  const parsed_url = new URI(url);
-  url = parsed_url.setQuery({nonce}).href();
-  console.log("downloading contract from '" + url + "'");
-  let resp;
-  try {
-    resp = await axios.get(url, { validateStatus: (s) => s === 200 });
-  } catch (e) {
-    wxApi.logAndDisplayError({
-      message: e.message,
-      name: "contract-download-failed",
-      response: e.response,
-      sameTab: true,
-    });
-    throw e;
-  }
-  console.log("got response", resp);
-  return resp.data;
-}
-
-async function processProposal(proposal: any) {
-
-  if (!proposal.data) {
-    console.error("field proposal.data field missing");
-    return;
-  }
-
-  if (!proposal.hash) {
-    console.error("proposal.hash field missing");
-    return;
-  }
-
-  const contractHash = await wxApi.hashContract(proposal.data);
-
-  if (contractHash !== proposal.hash) {
-    console.error(`merchant-supplied contract hash is wrong (us: 
${contractHash}, merchant: ${proposal.hash})`);
-    console.dir(proposal.data);
-    return;
-  }
-
-  const proposalId = await wxApi.saveProposal({
-    contractTerms: proposal.data,
-    contractTermsHash: proposal.hash,
-    merchantSig: proposal.sig,
-    timestamp: (new Date()).getTime(),
-  });
-
-  const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
-  const params = {
-    proposalId: proposalId.toString(),
-  };
-  const target = uri.query(params).href();
-  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.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,
-                                                 tipToken.next_url);
-      } catch (e) {
-        wxApi.logAndDisplayError({
-          message: e.message,
-          name: "tipping-failed",
-          response: e.response,
-          sameTab: true,
-        });
-        throw e;
-      }
-
-      const planchets = walletResp;
-
-      if (!planchets) {
-        wxApi.logAndDisplayError({
-          detail: walletResp,
-          message: "processing tip failed",
-          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;
-      try {
-        const config = {
-          validateStatus: (s: number) => s === 200,
-        };
-        resp = await axios.get(msg.refund_url, config);
-      } catch (e) {
-        wxApi.logAndDisplayError({
-          message: e.message,
-          name: "refund-download-failed",
-          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);
-    logVerbose && console.log("taler-pay: got response", res);
-    if (res && res.found && res.payReq) {
-      resolve(res);
-      return;
+  registerHandlers();
+  // Hack to know when the extension is unloaded
+  const port = chrome.runtime.connect();
+
+  port.onDisconnect.addListener(() => {
+    logVerbose && console.log("chrome runtime disconnected, removing 
handlers");
+    if (document.documentElement.getAttribute("data-taler-nojs")) {
+      setStyles(false);
     }
-    if (msg.contract_url) {
-      const nonce = await wxApi.generateNonce();
-      const proposal = await downloadContract(msg.contract_url, nonce);
-      if (proposal.data.nonce !== nonce) {
-        console.error("stale contract");
-        return;
-      }
-      await processProposal(proposal);
-      return;
+    for (const handler of handlers) {
+      document.removeEventListener(handler.type, handler.listener);
     }
-
-    if (msg.offer_url) {
-      document.location.href = msg.offer_url;
-      return;
-    }
-
-    console.log("can't proceed with payment, no way to get contract 
specified");
   });
 }
 
+type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
+
 
 function registerHandlers() {
   /**
@@ -468,7 +209,7 @@ function registerHandlers() {
   });
 
   addHandler("taler-pay", async(msg: any, sendResponse: any) => {
-    const resp = await talerPay(msg);
+    const resp = await wxApi.talerPay(msg);
     sendResponse(resp);
   });
 }
diff --git a/src/webex/pages/confirm-contract.tsx 
b/src/webex/pages/confirm-contract.tsx
index 83de738b..09073747 100644
--- a/src/webex/pages/confirm-contract.tsx
+++ b/src/webex/pages/confirm-contract.tsx
@@ -27,7 +27,7 @@ import * as i18n from "../../i18n";
 
 import {
   ExchangeRecord,
-  ProposalRecord,
+  ProposalDownloadRecord,
 } from "../../dbTypes";
 import { ContractTerms } from "../../talerTypes";
 import {
@@ -102,12 +102,15 @@ class Details extends React.Component<DetailProps, 
DetailState> {
 }
 
 interface ContractPromptProps {
-  proposalId: number;
+  proposalId?: number;
+  contractUrl?: string;
+  sessionId?: string;
 }
 
 interface ContractPromptState {
-  proposal: ProposalRecord|null;
-  error: string|null;
+  proposalId: number | undefined;
+  proposal: ProposalDownloadRecord | null;
+  error: string |  null;
   payDisabled: boolean;
   alreadyPaid: boolean;
   exchanges: null|ExchangeRecord[];
@@ -130,6 +133,7 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
       holdCheck: false,
       payDisabled: true,
       proposal: null,
+      proposalId: props.proposalId,
     };
   }
 
@@ -142,11 +146,19 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
   }
 
   async update() {
-    const proposal = await wxApi.getProposal(this.props.proposalId);
-    this.setState({proposal} as any);
+    let proposalId = this.props.proposalId;
+    if (proposalId === undefined) {
+      if (this.props.contractUrl === undefined) {
+        // Nothing we can do ...
+        return;
+      }
+      proposalId = await wxApi.downloadProposal(this.props.contractUrl);
+    }
+    const proposal = await wxApi.getProposal(proposalId);
+    this.setState({ proposal, proposalId });
     this.checkPayment();
     const exchanges = await wxApi.getExchanges();
-    this.setState({exchanges} as any);
+    this.setState({ exchanges });
   }
 
   async checkPayment() {
@@ -154,7 +166,11 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
     if (this.state.holdCheck) {
       return;
     }
-    const payStatus = await wxApi.checkPay(this.props.proposalId);
+    const proposalId = this.state.proposalId;
+    if (proposalId === undefined) {
+      return;
+    }
+    const payStatus = await wxApi.checkPay(proposalId);
     if (payStatus.status === "insufficient-balance") {
       const msgInsufficient = i18n.str`You have insufficient funds of the 
requested currency in your wallet.`;
       // tslint:disable-next-line:max-line-length
@@ -163,18 +179,18 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
         const acceptedExchangePubs = 
this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub);
         const ex = this.state.exchanges.find((e) => 
acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0);
         if (ex) {
-          this.setState({error: msgInsufficient});
+          this.setState({ error: msgInsufficient });
         } else {
-          this.setState({error: msgNoMatch});
+          this.setState({ error: msgNoMatch });
         }
       } else {
-        this.setState({error: msgInsufficient});
+        this.setState({ error: msgInsufficient });
       }
-      this.setState({payDisabled: true});
+      this.setState({ payDisabled: true });
     } else if (payStatus.status === "paid") {
-      this.setState({alreadyPaid: true, payDisabled: false, error: null, 
payStatus});
+      this.setState({ alreadyPaid: true, payDisabled: false, error: null, 
payStatus });
     } else {
-      this.setState({payDisabled: false, error: null, payStatus});
+      this.setState({ payDisabled: false, error: null, payStatus });
     }
   }
 
@@ -184,21 +200,24 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
     if (!proposal) {
       return;
     }
-    const payStatus = await wxApi.confirmPay(this.props.proposalId);
-    switch (payStatus) {
-      case "insufficient-balance":
-        this.checkPayment();
-        return;
-      case "paid":
-        console.log("contract", proposal.contractTerms);
-        document.location.href = proposal.contractTerms.fulfillment_url;
-        break;
+    const proposalId = proposal.id;
+    if (proposalId === undefined) {
+      console.error("proposal has no id");
+      return;
     }
-    this.setState({holdCheck: true});
+    const payResult = await wxApi.confirmPay(proposalId, this.props.sessionId);
+    document.location.href = payResult.nextUrl;
+    this.setState({ holdCheck: true });
   }
 
 
   render() {
+    if (this.props.contractUrl === undefined && this.props.proposalId === 
undefined) {
+      return <span>Error: either contractUrl or proposalId must be 
given</span>;
+    }
+    if (this.state.proposalId === undefined) {
+      return <span>Downloading contract terms</span>;
+    }
     if (!this.state.proposal) {
       return <span>...</span>;
     }
@@ -255,8 +274,18 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
 document.addEventListener("DOMContentLoaded", () => {
   const url = new URI(document.location.href);
   const query: any = URI.parseQuery(url.query());
-  const proposalId = JSON.parse(query.proposalId);
 
-  ReactDOM.render(<ContractPrompt proposalId={proposalId}/>, 
document.getElementById(
-    "contract")!);
+  let proposalId;
+  try {
+    proposalId = JSON.parse(query.proposalId);
+  } catch  {
+    // ignore error
+  }
+
+  const sessionId = query.sessionId;
+  const contractUrl = query.contractUrl;
+
+  ReactDOM.render(
+    <ContractPrompt {...{ proposalId, contractUrl, sessionId }}/>,
+    document.getElementById("contract")!);
 });
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 2f7a13c4..efebf21d 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -217,8 +217,8 @@ export function checkPay(proposalId: number): 
Promise<CheckPayResult> {
 /**
  * Pay for a proposal.
  */
-export function confirmPay(proposalId: number): Promise<ConfirmPayResult> {
-  return callBackend("confirm-pay", { proposalId });
+export function confirmPay(proposalId: number, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
+  return callBackend("confirm-pay", { proposalId, sessionId });
 }
 
 /**
@@ -228,15 +228,6 @@ export function hashContract(contract: object): 
Promise<string> {
   return callBackend("hash-contract", { contract });
 }
 
-
-/**
- * Save a proposal in the wallet.  Returns the proposal id that
- * the proposal is stored under.
- */
-export function saveProposal(proposal: any): Promise<number> {
-  return callBackend("save-proposal", { proposal });
-}
-
 /**
  * Mark a reserve as confirmed.
  */
@@ -252,36 +243,6 @@ export function queryPayment(url: string): 
Promise<QueryPaymentResult> {
 }
 
 /**
- * Mark a payment as succeeded.
- */
-export function paymentSucceeded(contractTermsHash: string, merchantSig: 
string): Promise<void> {
-  return callBackend("payment-succeeded", { contractTermsHash, merchantSig });
-}
-
-/**
- * Mark a payment as succeeded.
- */
-export function paymentFailed(contractTermsHash: string): Promise<void> {
-  return callBackend("payment-failed", { contractTermsHash });
-}
-
-/**
- * Get the payment cookie for the current tab, or undefined if no payment
- * cookie was set.
- */
-export function getTabCookie(): Promise<any> {
-  return callBackend("get-tab-cookie", { });
-}
-
-/**
- * Generate a contract nonce (EdDSA key pair), store it in the wallet's
- * database and return the public key.
- */
-export function generateNonce(): Promise<string> {
-  return callBackend("generate-nonce", { });
-}
-
-/**
  * Check upgrade information
  */
 export function checkUpgrade(): Promise<UpgradeResponse> {
@@ -344,12 +305,6 @@ export function getReport(reportUid: string): Promise<any> 
{
   return callBackend("get-report", { reportUid });
 }
 
-/**
- * Apply a refund that we got from the merchant.
- */
-export function acceptRefund(refundData: any): Promise<number> {
-  return callBackend("accept-refund", refundData);
-}
 
 /**
  * Look up a purchase in the wallet database from
@@ -407,3 +362,17 @@ export function processTipResponse(merchantDomain: string, 
tipId: string, tipRes
 export function clearNotification(): Promise<void> {
   return callBackend("clear-notification", { });
 }
+
+/**
+ * Trigger taler payment processing (for payment, tipping and refunds).
+ */
+export function talerPay(msg: any): Promise<void> {
+  return callBackend("taler-pay", msg);
+}
+
+/**
+ * Download a contract.
+ */
+export function downloadProposal(url: string): Promise<number> {
+  return callBackend("download-proposal", { url });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 02a1543e..c0b42a76 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -33,7 +33,6 @@ import {
 
 import { AmountJson } from "../amounts";
 
-import { ProposalRecord } from "../dbTypes";
 import {
   AcceptTipRequest,
   ConfirmReserveRequest,
@@ -41,6 +40,7 @@ import {
   GetTipPlanchetsRequest,
   Notifier,
   ProcessTipResponseRequest,
+  QueryPaymentFound,
   ReturnCoinsRequest,
   TipStatusRequest,
 } from "../walletTypes";
@@ -62,6 +62,7 @@ import * as wxApi from "./wxApi";
 import URI = require("urijs");
 import Port = chrome.runtime.Port;
 import MessageSender = chrome.runtime.MessageSender;
+import { TipToken } from "../talerTypes";
 
 
 const DB_NAME = "taler";
@@ -93,15 +94,6 @@ function handleMessage(sender: MessageSender,
       const db = needsWallet().db;
       return importDb(db, detail.dump);
     }
-    case "get-tab-cookie": {
-      if (!sender || !sender.tab || !sender.tab.id) {
-        return Promise.resolve();
-      }
-      const id: number = sender.tab.id;
-      const info: any = paymentRequestCookies[id] as any;
-      delete paymentRequestCookies[id];
-      return Promise.resolve(info);
-    }
     case "ping": {
       return Promise.resolve();
     }
@@ -138,14 +130,11 @@ function handleMessage(sender: MessageSender,
       const req = ConfirmReserveRequest.checked(d);
       return needsWallet().confirmReserve(req);
     }
-    case "generate-nonce": {
-      return needsWallet().generateNonce();
-    }
     case "confirm-pay": {
       if (typeof detail.proposalId !== "number") {
         throw Error("proposalId must be number");
       }
-      return needsWallet().confirmPay(detail.proposalId);
+      return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
     }
     case "check-pay": {
       if (typeof detail.proposalId !== "number") {
@@ -166,7 +155,7 @@ function handleMessage(sender: MessageSender,
           return Promise.resolve(msg);
         }
       }
-      return needsWallet().queryPayment(detail.url);
+      return needsWallet().queryPaymentByFulfillmentUrl(detail.url);
     }
     case "exchange-info": {
       if (!detail.baseUrl) {
@@ -188,11 +177,6 @@ function handleMessage(sender: MessageSender,
         return hash;
       });
     }
-    case "save-proposal": {
-      console.log("handling save-proposal", detail);
-      const checkedRecord = ProposalRecord.checked(detail.proposal);
-      return needsWallet().saveProposal(checkedRecord);
-    }
     case "reserve-creation-info": {
       if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
         return Promise.resolve({ error: "bad url" });
@@ -261,25 +245,6 @@ function handleMessage(sender: MessageSender,
       }
       return needsWallet().payback(detail.coinPub);
     }
-    case "payment-failed": {
-      // For now we just update exchanges (maybe the exchange did something
-      // wrong and the keys were messed up).
-      // FIXME: in the future we should look at what actually went wrong.
-      console.error("payment reported as failed");
-      needsWallet().updateExchanges();
-      return Promise.resolve();
-    }
-    case "payment-succeeded": {
-      const contractTermsHash = detail.contractTermsHash;
-      const merchantSig = detail.merchantSig;
-      if (!contractTermsHash) {
-        return Promise.reject(Error("contractHash missing"));
-      }
-      if (!merchantSig) {
-        return Promise.reject(Error("merchantSig missing"));
-      }
-      return needsWallet().paymentSucceeded(contractTermsHash, merchantSig);
-    }
     case "get-sender-wire-infos": {
       return needsWallet().getSenderWireInfos();
     }
@@ -316,8 +281,6 @@ function handleMessage(sender: MessageSender,
       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) {
@@ -351,6 +314,28 @@ function handleMessage(sender: MessageSender,
     case "clear-notification": {
       return needsWallet().clearNotification();
     }
+    case "download-proposal": {
+      return needsWallet().downloadProposal(detail.url);
+    }
+    case "taler-pay": {
+      const senderUrl = sender.url;
+      if (!senderUrl) {
+        console.log("can't trigger payment, no sender URL");
+        return;
+      }
+      const tab = sender.tab;
+      if (!tab) {
+        console.log("can't trigger payment, no sender tab");
+        return;
+      }
+      const tabId = tab.id;
+      if (typeof tabId !== "string") {
+        console.log("can't trigger payment, no sender tab id");
+        return;
+      }
+      talerPay(detail, senderUrl, tabId);
+      return;
+    }
     default:
       // Exhaustiveness check.
       // See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@@ -417,13 +402,67 @@ 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 } = {};
+async function talerPay(fields: any, url: string, tabId: number): 
Promise<string | undefined> {
+  if (!currentWallet) {
+    console.log("can't handle payment, no wallet");
+    return undefined;
+  }
+
+  const w = currentWallet;
+
+  const goToPayment = (p: QueryPaymentFound): string => {
+    const nextUrl = new URI(p.contractTerms.fulfillment_url);
+    nextUrl.addSearch("order_id", p.contractTerms.order_id);
+    if (p.lastSessionSig) {
+      nextUrl.addSearch("session_sig", p.lastSessionSig);
+    }
+    return url;
+  };
+
+  if (fields.resource_url) {
+    const p = await w.queryPaymentByFulfillmentUrl(fields.resource_url);
+    if (p.found) {
+      return goToPayment(p);
+    }
+  }
+  if (fields.contract_hash) {
+    const p = await w.queryPaymentByContractTermsHash(fields.contract_hash);
+    if (p.found) {
+      goToPayment(p);
+      return goToPayment(p);
+    }
+  }
+  if (fields.contract_url) {
+    const proposalId = await w.downloadProposal(fields.contract_url);
+    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
+    if (fields.session_id) {
+      uri.addSearch("sessionId", fields.session_id);
+    }
+    uri.addSearch("proposalId", proposalId);
+    const redirectUrl = uri.href();
+    return redirectUrl;
+  }
+  if (fields.offer_url) {
+    return fields.offer_url;
+  }
+  if (fields.refund_url) {
+    console.log("processing refund");
+    const hc = await w.acceptRefund(fields.refund_url);
+    return 
chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);
+  }
+  if (fields.tip) {
+    const tipToken = TipToken.checked(fields.tip);
+    w.processTip(tipToken);
+    // Go to tip dialog page, where the user can confirm the tip or
+    // decline if they are not happy with the exchange.
+    const merchantDomain = new URI(url).origin();
+    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();
+    return redirectUrl;
+  }
+  return undefined;
+}
 
 
 /**
@@ -433,6 +472,11 @@ const paymentRequestCookies: { [n: number]: any } = {};
  * in this tab.
  */
 function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: 
string, tabId: number): any {
+  if (!currentWallet) {
+    console.log("can't handle payment, no wallet");
+    return;
+  }
+
   const headers: { [s: string]: string } = {};
   for (const kv of headerList) {
     if (kv.value) {
@@ -441,9 +485,12 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
   }
 
   const fields = {
+    contract_hash: headers["x-taler-contract-hash"],
     contract_url: headers["x-taler-contract-url"],
     offer_url: headers["x-taler-offer-url"],
     refund_url: headers["x-taler-refund-url"],
+    resource_url: headers["x-taler-resource-url"],
+    session_id: headers["x-taler-session-id"],
     tip: headers["x-taler-tip"],
   };
 
@@ -456,21 +503,33 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
     return;
   }
 
-  const payDetail = {
-    contract_url: fields.contract_url,
-    offer_url: fields.offer_url,
-    refund_url: fields.refund_url,
-    tip: fields.tip,
-  };
+  console.log("got pay detail", fields);
 
-  console.log("got pay detail", payDetail);
+  // Fast path for existing payment
+  if (fields.resource_url) {
+    const nextUrl = 
currentWallet.getNextUrlFromResourceUrl(fields.resource_url);
+    if (nextUrl) {
+      return { redirectUrl: nextUrl };
+    }
+  }
+  // Fast path for new contract
+  if (!fields.contract_hash && fields.contract_url) {
+    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
+    uri.addSearch("contractUrl", fields.contract_url);
+    if (fields.session_id) {
+      uri.addSearch("sessionId", fields.session_id);
+    }
+    return { redirectUrl: uri.href() };
+  }
 
-  // This cookie will be read by the injected content script
-  // in the tab that displays the page.
-  paymentRequestCookies[tabId] = {
-    payDetail,
-    type: "pay",
-  };
+  // We need to do some asynchronous operation, we can't directly redirect
+  talerPay(fields, url, tabId).then((nextUrl) => {
+    if (nextUrl) {
+      chrome.tabs.update(tabId, { url: nextUrl });
+    }
+  });
+
+  return;
 }
 
 
@@ -541,7 +600,7 @@ function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHea
     const redirectUrl = uri.query(params).href();
     console.log("redirecting to", redirectUrl);
     // FIXME: use direct redirect when 
https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed
-    chrome.tabs.update(tabId, {url: redirectUrl});
+    chrome.tabs.update(tabId, { url: redirectUrl });
     return;
   }
 

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



reply via email to

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