gnunet-svn
[Top][All Lists]
Advanced

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

[GNUnet-SVN] [taler-wallet-webex] 02/02: implement new mobile-compatible


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] 02/02: implement new mobile-compatible payment logic
Date: Wed, 17 Jan 2018 03:50:05 +0100

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

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

commit c62ba4986fbfcb8637a3befadf3d3eddbd5348ca
Author: Florian Dold <address@hidden>
AuthorDate: Wed Jan 17 03:49:54 2018 +0100

    implement new mobile-compatible payment logic
---
 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                  | 282 +++--------------------------------
 src/webex/pages/confirm-contract.tsx |  83 +++++++----
 src/webex/wxApi.ts                   |  63 ++------
 src/webex/wxBackend.ts               | 183 +++++++++++++++--------
 15 files changed, 501 insertions(+), 546 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 a7d393a6..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,234 +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.contract_terms) {
-    console.error("field proposal.contract_terms field missing");
-    return;
-  }
-
-  const contractHash = await wxApi.hashContract(proposal.contract_terms);
-
-  const proposalId = await wxApi.saveProposal({
-    contractTerms: proposal.contract_terms,
-    contractTermsHash: contractHash,
-    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.contract_terms.nonce !== nonce) {
-        console.error("stale contract");
-        return;
-      }
-      await processProposal(proposal);
-      return;
-    }
-
-    if (msg.offer_url) {
-      document.location.href = msg.offer_url;
-      return;
+    for (const handler of handlers) {
+      document.removeEventListener(handler.type, handler.listener);
     }
-
-    console.log("can't proceed with payment, no way to get contract 
specified");
   });
 }
 
+type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
+
 
 function registerHandlers() {
   /**
@@ -457,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]