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 (b517163e -> 1a6


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated (b517163e -> 1a66e232)
Date: Mon, 29 Jan 2018 16:41:28 +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 b517163e compat module
     new aec2c130 refactor submitPay
     new c8c03e38 better types
     new 1a66e232 implement aborting and getting refunds from failed payments

The 3 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/dbTypes.ts                       |  46 ++++++-
 src/i18n/de.po                       |  29 +++--
 src/i18n/en-US.po                    |  25 ++--
 src/i18n/fr.po                       |  25 ++--
 src/i18n/it.po                       |  25 ++--
 src/i18n/strings.ts                  |  30 +++--
 src/i18n/taler-wallet-webex.pot      |  25 ++--
 src/talerTypes.ts                    |  89 ++++++++++++--
 src/wallet.ts                        | 232 ++++++++++++++++++++++-------------
 src/webex/messages.ts                |  32 +++--
 src/webex/pages/confirm-contract.tsx | 147 +++++++++++++++++-----
 src/webex/wxApi.ts                   |  33 +++--
 src/webex/wxBackend.ts               |   6 +
 13 files changed, 537 insertions(+), 207 deletions(-)

diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 035c100a..6c467ce7 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -31,8 +31,8 @@ import {
   CoinPaySig,
   ContractTerms,
   Denomination,
+  MerchantRefundPermission,
   PayReq,
-  RefundPermission,
   TipResponse,
   WireDetail,
 } from "./talerTypes";
@@ -762,9 +762,25 @@ export interface WireFee {
  * the customer accepts a proposal.  Includes refund status if applicable.
  */
 export interface PurchaseRecord {
+  /**
+   * Hash of the contract terms.
+   */
   contractTermsHash: string;
+
+  /**
+   * Contract terms we got from the merchant.
+   */
   contractTerms: ContractTerms;
+
+  /**
+   * The payment request, ready to be send to the merchant's
+   * /pay URL.
+   */
   payReq: PayReq;
+
+  /**
+   * Signature from the merchant over the contract terms.
+   */
   merchantSig: string;
 
   /**
@@ -773,8 +789,15 @@ export interface PurchaseRecord {
    */
   finished: boolean;
 
-  refundsPending: { [refundSig: string]: RefundPermission };
-  refundsDone: { [refundSig: string]: RefundPermission };
+  /**
+   * Pending refunds for the purchase.
+   */
+  refundsPending: { [refundSig: string]: MerchantRefundPermission };
+
+  /**
+   * Submitted refunds for the purchase.
+   */
+  refundsDone: { [refundSig: string]: MerchantRefundPermission };
 
   /**
    * When was the purchase made?
@@ -788,8 +811,25 @@ export interface PurchaseRecord {
    */
   timestamp_refund: number;
 
+  /**
+   * Last session id that we submitted to /pay (if any).
+   */
   lastSessionSig: string | undefined;
+
+  /**
+   * Last session signature that we submitted to /pay (if any).
+   */
   lastSessionId: string | undefined;
+
+  /**
+   * An abort (with refund) was requested for this (incomplete!) purchase.
+   */
+  abortRequested: boolean;
+
+  /**
+   * The abort (with refund) was completed for this (incomplete!) purchase.
+   */
+  abortDone: boolean;
 }
 
 
diff --git a/src/i18n/de.po b/src/i18n/de.po
index 1a003c17..398fdfab 100644
--- a/src/i18n/de.po
+++ b/src/i18n/de.po
@@ -27,28 +27,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/webex/pages/confirm-contract.tsx:73
+#: src/webex/pages/confirm-contract.tsx:74
 #, c-format
 msgid "show more details\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:87
+#: src/webex/pages/confirm-contract.tsx:88
 #, c-format
 msgid "Accepted exchanges:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:92
+#: src/webex/pages/confirm-contract.tsx:93
 #, c-format
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:200
+#: src/webex/pages/confirm-contract.tsx:211
 #, 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:202
+#: src/webex/pages/confirm-contract.tsx:213
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,16 +56,21 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:280
-#, c-format
-msgid "The merchant%1$s offers you to purchase:\n"
-msgstr ""
-
-#: src/webex/pages/confirm-contract.tsx:301
+#: src/webex/pages/confirm-contract.tsx:305
 #, fuzzy, c-format
 msgid "Confirm payment"
 msgstr "Bezahlung bestätigen"
 
+#: src/webex/pages/confirm-contract.tsx:314
+#, c-format
+msgid "Submitting payment"
+msgstr ""
+
+#: src/webex/pages/confirm-contract.tsx:349
+#, c-format
+msgid "The merchant%1$s offers you to purchase:\n"
+msgstr ""
+
 #: src/webex/pages/confirm-create-reserve.tsx:126
 #, c-format
 msgid "Select"
@@ -154,7 +159,7 @@ msgstr ""
 
 #. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:155
+#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:153
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po
index 3d3fd433..68faa6ba 100644
--- a/src/i18n/en-US.po
+++ b/src/i18n/en-US.po
@@ -27,28 +27,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/webex/pages/confirm-contract.tsx:73
+#: src/webex/pages/confirm-contract.tsx:74
 #, c-format
 msgid "show more details\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:87
+#: src/webex/pages/confirm-contract.tsx:88
 #, c-format
 msgid "Accepted exchanges:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:92
+#: src/webex/pages/confirm-contract.tsx:93
 #, c-format
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:200
+#: src/webex/pages/confirm-contract.tsx:211
 #, 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:202
+#: src/webex/pages/confirm-contract.tsx:213
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,14 +56,19 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:280
+#: src/webex/pages/confirm-contract.tsx:305
 #, c-format
-msgid "The merchant%1$s offers you to purchase:\n"
+msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:301
+#: src/webex/pages/confirm-contract.tsx:314
 #, c-format
-msgid "Confirm payment"
+msgid "Submitting payment"
+msgstr ""
+
+#: src/webex/pages/confirm-contract.tsx:349
+#, c-format
+msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
 #: src/webex/pages/confirm-create-reserve.tsx:126
@@ -154,7 +159,7 @@ msgstr ""
 
 #. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:155
+#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:153
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
diff --git a/src/i18n/fr.po b/src/i18n/fr.po
index 08f4a9d0..93077fb3 100644
--- a/src/i18n/fr.po
+++ b/src/i18n/fr.po
@@ -27,28 +27,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/webex/pages/confirm-contract.tsx:73
+#: src/webex/pages/confirm-contract.tsx:74
 #, c-format
 msgid "show more details\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:87
+#: src/webex/pages/confirm-contract.tsx:88
 #, c-format
 msgid "Accepted exchanges:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:92
+#: src/webex/pages/confirm-contract.tsx:93
 #, c-format
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:200
+#: src/webex/pages/confirm-contract.tsx:211
 #, 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:202
+#: src/webex/pages/confirm-contract.tsx:213
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,14 +56,19 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:280
+#: src/webex/pages/confirm-contract.tsx:305
 #, c-format
-msgid "The merchant%1$s offers you to purchase:\n"
+msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:301
+#: src/webex/pages/confirm-contract.tsx:314
 #, c-format
-msgid "Confirm payment"
+msgid "Submitting payment"
+msgstr ""
+
+#: src/webex/pages/confirm-contract.tsx:349
+#, c-format
+msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
 #: src/webex/pages/confirm-create-reserve.tsx:126
@@ -154,7 +159,7 @@ msgstr ""
 
 #. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:155
+#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:153
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
diff --git a/src/i18n/it.po b/src/i18n/it.po
index 08f4a9d0..93077fb3 100644
--- a/src/i18n/it.po
+++ b/src/i18n/it.po
@@ -27,28 +27,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/webex/pages/confirm-contract.tsx:73
+#: src/webex/pages/confirm-contract.tsx:74
 #, c-format
 msgid "show more details\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:87
+#: src/webex/pages/confirm-contract.tsx:88
 #, c-format
 msgid "Accepted exchanges:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:92
+#: src/webex/pages/confirm-contract.tsx:93
 #, c-format
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:200
+#: src/webex/pages/confirm-contract.tsx:211
 #, 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:202
+#: src/webex/pages/confirm-contract.tsx:213
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,14 +56,19 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:280
+#: src/webex/pages/confirm-contract.tsx:305
 #, c-format
-msgid "The merchant%1$s offers you to purchase:\n"
+msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:301
+#: src/webex/pages/confirm-contract.tsx:314
 #, c-format
-msgid "Confirm payment"
+msgid "Submitting payment"
+msgstr ""
+
+#: src/webex/pages/confirm-contract.tsx:349
+#, c-format
+msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
 #: src/webex/pages/confirm-create-reserve.tsx:126
@@ -154,7 +159,7 @@ msgstr ""
 
 #. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:155
+#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:153
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
diff --git a/src/i18n/strings.ts b/src/i18n/strings.ts
index 9e78abc3..072bd953 100644
--- a/src/i18n/strings.ts
+++ b/src/i18n/strings.ts
@@ -39,12 +39,15 @@ strings['de'] = {
       "You do not have any funds from an exchange that is accepted by this 
merchant. None of the exchanges accepted by the merchant is known to your 
wallet.": [
         ""
       ],
-      "The merchant%1$s offers you to purchase:\n": [
-        ""
-      ],
       "Confirm payment": [
         "Bezahlung bestätigen"
       ],
+      "Submitting payment": [
+        ""
+      ],
+      "The merchant%1$s offers you to purchase:\n": [
+        ""
+      ],
       "Select": [
         ""
       ],
@@ -228,10 +231,13 @@ strings['en-US'] = {
       "You do not have any funds from an exchange that is accepted by this 
merchant. None of the exchanges accepted by the merchant is known to your 
wallet.": [
         ""
       ],
-      "The merchant%1$s offers you to purchase:\n": [
+      "Confirm payment": [
         ""
       ],
-      "Confirm payment": [
+      "Submitting payment": [
+        ""
+      ],
+      "The merchant%1$s offers you to purchase:\n": [
         ""
       ],
       "Select": [
@@ -417,10 +423,13 @@ strings['fr'] = {
       "You do not have any funds from an exchange that is accepted by this 
merchant. None of the exchanges accepted by the merchant is known to your 
wallet.": [
         ""
       ],
-      "The merchant%1$s offers you to purchase:\n": [
+      "Confirm payment": [
         ""
       ],
-      "Confirm payment": [
+      "Submitting payment": [
+        ""
+      ],
+      "The merchant%1$s offers you to purchase:\n": [
         ""
       ],
       "Select": [
@@ -606,10 +615,13 @@ strings['it'] = {
       "You do not have any funds from an exchange that is accepted by this 
merchant. None of the exchanges accepted by the merchant is known to your 
wallet.": [
         ""
       ],
-      "The merchant%1$s offers you to purchase:\n": [
+      "Confirm payment": [
         ""
       ],
-      "Confirm payment": [
+      "Submitting payment": [
+        ""
+      ],
+      "The merchant%1$s offers you to purchase:\n": [
         ""
       ],
       "Select": [
diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot
index 08f4a9d0..93077fb3 100644
--- a/src/i18n/taler-wallet-webex.pot
+++ b/src/i18n/taler-wallet-webex.pot
@@ -27,28 +27,28 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/webex/pages/confirm-contract.tsx:73
+#: src/webex/pages/confirm-contract.tsx:74
 #, c-format
 msgid "show more details\n"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:87
+#: src/webex/pages/confirm-contract.tsx:88
 #, c-format
 msgid "Accepted exchanges:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:92
+#: src/webex/pages/confirm-contract.tsx:93
 #, c-format
 msgid "Exchanges in the wallet:"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:200
+#: src/webex/pages/confirm-contract.tsx:211
 #, 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:202
+#: src/webex/pages/confirm-contract.tsx:213
 #, c-format
 msgid ""
 "You do not have any funds from an exchange that is accepted by this "
@@ -56,14 +56,19 @@ msgid ""
 "wallet."
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:280
+#: src/webex/pages/confirm-contract.tsx:305
 #, c-format
-msgid "The merchant%1$s offers you to purchase:\n"
+msgid "Confirm payment"
 msgstr ""
 
-#: src/webex/pages/confirm-contract.tsx:301
+#: src/webex/pages/confirm-contract.tsx:314
 #, c-format
-msgid "Confirm payment"
+msgid "Submitting payment"
+msgstr ""
+
+#: src/webex/pages/confirm-contract.tsx:349
+#, c-format
+msgid "The merchant%1$s offers you to purchase:\n"
 msgstr ""
 
 #: src/webex/pages/confirm-create-reserve.tsx:126
@@ -154,7 +159,7 @@ msgstr ""
 
 #. #-#-#-#-#  - (PACKAGE VERSION)  #-#-#-#-#
 #. TODO:generic error reporting function or component.
-#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:155
+#: src/webex/pages/confirm-create-reserve.tsx:505 src/webex/pages/tip.tsx:153
 #, c-format
 msgid "Fatal error: \"%1$s\"."
 msgstr ""
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index d593c3d3..611d667c 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -475,46 +475,121 @@ export interface PayReq {
 /**
  * Refund permission in the format that the merchant gives it to us.
  */
-export interface RefundPermission {
address@hidden()
+export class MerchantRefundPermission {
   /**
    * Amount to be refunded.
    */
+  @Checkable.Value(() => AmountJson)
   refund_amount: AmountJson;
 
   /**
    * Fee for the refund.
    */
+  @Checkable.Value(() => AmountJson)
+  refund_fee: AmountJson;
+
+  /**
+   * Public key of the coin being refunded.
+   */
+  @Checkable.String
+  coin_pub: string;
+
+  /**
+   * Refund transaction ID between merchant and exchange.
+   */
+  @Checkable.Number
+  rtransaction_id: number;
+
+  /**
+   * Signature made by the merchant over the refund permission.
+   */
+  @Checkable.String
+  merchant_sig: string;
+
+  /**
+   * Create a MerchantRefundPermission from untyped JSON.
+   */
+  static checked: (obj: any) => MerchantRefundPermission;
+}
+
+
+/**
+ * Refund request sent to the exchange.
+ */
+export interface RefundRequest {
+  /**
+   * Amount to be refunded, can be a fraction of the
+   * coin's total deposit value (including deposit fee);
+   * must be larger than the refund fee.
+   */
+  refund_amount: AmountJson;
+
+  /**
+   * Refund fee associated with the given coin.
+   * must be smaller than the refund amount.
+   */
   refund_fee: AmountJson;
 
   /**
-   * Contract terms hash to identify the contract that this
-   * refund is for.
+   * SHA-512 hash of the contact of the merchant with the customer.
    */
   h_contract_terms: string;
 
   /**
-   * Public key of the coin being refunded.
+   * coin's public key, both ECDHE and EdDSA.
    */
   coin_pub: string;
 
   /**
-   * Refund transaction ID between merchant and exchange.
+   * 64-bit transaction id of the refund transaction between merchant and 
customer
    */
   rtransaction_id: number;
 
   /**
-   * Public key of the merchant.
+   * EdDSA public key of the merchant.
    */
   merchant_pub: string;
 
   /**
-   * Signature made by the merchant over the refund permission.
+   * EdDSA signature of the merchant affirming the refund.
    */
   merchant_sig: string;
 }
 
 
 /**
+ * Response for a refund pickup or a /pay in abort mode.
+ */
address@hidden()
+export class MerchantRefundResponse {
+  /**
+   * Public key of the merchant
+   */
+  @Checkable.String
+  merchant_pub: string;
+
+  /**
+   * Contract terms hash of the contract that
+   * is being refunded.
+   */
+  @Checkable.String
+  h_contract_terms: string;
+
+  /**
+   * The signed refund permissions, to be sent to the exchange.
+   */
+  @Checkable.List(Checkable.Value(() => MerchantRefundPermission))
+  refund_permissions: MerchantRefundPermission[];
+
+  /**
+   * Create a MerchantRefundReponse from untyped JSON.
+   */
+  static checked: (obj: any) => MerchantRefundResponse;
+}
+
+
+/**
  * Planchet detail sent to the merchant.
  */
 export interface TipPlanchetDetail {
diff --git a/src/wallet.ts b/src/wallet.ts
index 01db8c61..34b2388e 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -76,10 +76,12 @@ import {
   Denomination,
   ExchangeHandle,
   KeysJson,
+  MerchantRefundPermission,
+  MerchantRefundResponse,
   PayReq,
   PaybackConfirmation,
   Proposal,
-  RefundPermission,
+  RefundRequest,
   TipPlanchetDetail,
   TipResponse,
   TipToken,
@@ -648,6 +650,8 @@ export class Wallet {
       order_id: proposal.contractTerms.order_id,
     };
     const t: PurchaseRecord = {
+      abortDone: false,
+      abortRequested: false,
       contractTerms: proposal.contractTerms,
       contractTermsHash: proposal.contractTermsHash,
       finished: false,
@@ -676,7 +680,6 @@ export class Wallet {
    * Returns an id for it to retrieve it later.
    */
   async downloadProposal(url: string): Promise<number> {
-
     const oldProposal = await this.q().getIndexed(Stores.proposals.urlIndex, 
url);
     if (oldProposal) {
       return oldProposal.id!;
@@ -716,13 +719,37 @@ export class Wallet {
     return id;
   }
 
+
+  async refundFailedPay(proposalId: number) {
+    console.log(`refunding failed payment with proposal id ${proposalId}`);
+    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);
+    if (!purchase) {
+      throw Error("purchase not found for proposal");
+    }
+
+    if (purchase.finished) {
+      throw Error("can't auto-refund finished purchase");
+    }
+  }
+
+
   async submitPay(contractTermsHash: string, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
     const purchase = await this.q().get(Stores.purchases, contractTermsHash);
     if (!purchase) {
       throw Error("Purchase not found: " + contractTermsHash);
     }
+    if (purchase.abortRequested) {
+      throw Error("not submitting payment for aborted purchase");
+    }
     let resp;
     const payReq = { ...purchase.payReq, session_id: sessionId };
+
     try {
       const config = {
         headers: { "Content-Type": "application/json;charset=UTF-8" },
@@ -737,15 +764,44 @@ export class Wallet {
     }
     const merchantResp = resp.data;
     console.log("got success from pay_url");
+
+    const merchantPub = purchase.contractTerms.merchant_pub;
+    const valid: boolean = await (
+      this.cryptoApi.isValidPaymentSignature(merchantResp.sig, 
contractTermsHash, merchantPub)
+    );
+    if (!valid) {
+      console.error("merchant payment signature invalid");
+      // FIXME: properly display error
+      throw Error("merchant payment signature invalid");
+    }
+    purchase.finished = true;
+    const modifiedCoins: CoinRecord[] = [];
+    for (const pc of purchase.payReq.coins) {
+      const c = await this.q().get<CoinRecord>(Stores.coins, pc.coin_pub);
+      if (!c) {
+        console.error("coin not found");
+        throw Error("coin used in payment not found");
+      }
+      c.status = CoinStatus.Dirty;
+      modifiedCoins.push(c);
+    }
+
     const fu = new URI(purchase.contractTerms.fulfillment_url);
     fu.addSearch("order_id", purchase.contractTerms.order_id);
     if (merchantResp.session_sig) {
       purchase.lastSessionSig = merchantResp.session_sig;
       purchase.lastSessionId = sessionId;
       fu.addSearch("session_sig", merchantResp.session_sig);
-      await this.q().put(Stores.purchases, purchase).finish();
     }
-    await this.paymentSucceeded(purchase.contractTermsHash, merchantResp.sig);
+
+    await this.q()
+              .putAll(Stores.coins, modifiedCoins)
+              .put(Stores.purchases, purchase)
+              .finish();
+    for (const c of purchase.payReq.coins) {
+      this.refresh(c.coin_pub);
+    }
+
     const nextUrl = fu.href();
     this.cachedNextUrl[purchase.contractTerms.fulfillment_url] = { nextUrl, 
lastSessionId: sessionId };
     return { nextUrl };
@@ -753,8 +809,7 @@ export class Wallet {
 
 
   /**
-   * Add a contract to the wallet and sign coins,
-   * but do not send them yet.
+   * Add a contract to the wallet and sign coins, and send them.
    */
   async confirmPay(proposalId: number, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
     console.log(`executing confirmPay with proposalId ${proposalId} and 
sessionId ${sessionId}`);
@@ -831,6 +886,7 @@ export class Wallet {
     return sp;
   }
 
+
   /**
    * Check if payment for an offer is possible, or if the offer has already
    * been payed for.
@@ -1266,6 +1322,7 @@ export class Wallet {
     return wiJson;
   }
 
+
   async getPossibleDenoms(exchangeBaseUrl: string) {
     return (
       this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex,
@@ -2245,45 +2302,6 @@ export class Wallet {
   }
 
 
-  private async paymentSucceeded(contractTermsHash: string, merchantSig: 
string): Promise<any> {
-    const doPaymentSucceeded = async() => {
-      const t = await this.q().get<PurchaseRecord>(Stores.purchases,
-                                                   contractTermsHash);
-      if (!t) {
-        console.error("contract not found");
-        return;
-      }
-      const merchantPub = t.contractTerms.merchant_pub;
-      const valid = this.cryptoApi.isValidPaymentSignature(merchantSig, 
contractTermsHash, merchantPub);
-      if (!valid) {
-        console.error("merchant payment signature invalid");
-        // FIXME: properly display error
-        return;
-      }
-      t.finished = true;
-      const modifiedCoins: CoinRecord[] = [];
-      for (const pc of t.payReq.coins) {
-        const c = await this.q().get<CoinRecord>(Stores.coins, pc.coin_pub);
-        if (!c) {
-          console.error("coin not found");
-          return;
-        }
-        c.status = CoinStatus.Dirty;
-        modifiedCoins.push(c);
-      }
-
-      await this.q()
-                .putAll(Stores.coins, modifiedCoins)
-                .put(Stores.purchases, t)
-                .finish();
-      for (const c of t.payReq.coins) {
-        this.refresh(c.coin_pub);
-      }
-    };
-    doPaymentSucceeded();
-    return;
-  }
-
   async payback(coinPub: string): Promise<void> {
     let coin = await this.q().get(Stores.coins, coinPub);
     if (!coin) {
@@ -2532,46 +2550,13 @@ export class Wallet {
     }
   }
 
-  /**
-   * Accept a refund, return the contract hash for the contract
-   * that was involved in the refund.
-   */
-  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.refund_permissions;
+  async acceptRefundResponse(refundResponse: MerchantRefundResponse): 
Promise<string> {
+    const refundPermissions = refundResponse.refund_permissions;
 
     if (!refundPermissions.length) {
       console.warn("got empty refund list");
       throw Error("empty refund");
     }
-    const hc = refundPermissions[0].h_contract_terms;
-    if (!hc) {
-      throw Error("h_contract_terms missing in refund permission");
-    }
-    const m = refundPermissions[0].merchant_pub;
-    if (!hc) {
-      throw Error("merchant_pub missing in refund permission");
-    }
-    for (const perm of refundPermissions) {
-      if (perm.h_contract_terms !== hc) {
-        throw Error("h_contract_terms different in refund permission");
-      }
-      if (perm.merchant_pub !== m) {
-        throw Error("merchant_pub different in refund permission");
-      }
-    }
 
     /**
      * Add refund to purchase if not already added.
@@ -2592,6 +2577,8 @@ export class Wallet {
       return t;
     }
 
+    const hc = refundResponse.h_contract_terms;
+
     // Add the refund permissions to the purchase within a DB transaction
     await this.q().mutate(Stores.purchases, hc, f).finish();
     this.notifier.notify();
@@ -2599,7 +2586,29 @@ export class Wallet {
     // Start submitting it but don't wait for it here.
     this.submitRefunds(hc);
 
-    return refundPermissions[0].h_contract_terms;
+    return hc;
+  }
+
+
+  /**
+   * Accept a refund, return the contract hash for the contract
+   * that was involved in the refund.
+   */
+  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;
+    }
+
+    const refundResponse = MerchantRefundResponse.checked(resp.data);
+    return this.acceptRefundResponse(refundResponse);
   }
 
 
@@ -2615,11 +2624,20 @@ export class Wallet {
     }
     for (const pk of pendingKeys) {
       const perm = purchase.refundsPending[pk];
+      const req: RefundRequest = {
+        coin_pub: perm.coin_pub,
+        h_contract_terms: purchase.contractTermsHash,
+        merchant_pub: purchase.contractTerms.merchant_pub,
+        merchant_sig: perm.merchant_sig,
+        refund_amount: perm.refund_amount,
+        refund_fee: perm.refund_fee,
+        rtransaction_id: perm.rtransaction_id,
+      };
       console.log("sending refund permission", perm);
       // FIXME: not correct once we support multiple exchanges per payment
       const exchangeUrl = purchase.payReq.coins[0].exchange_url;
       const reqUrl = (new URI("refund")).absoluteTo(exchangeUrl);
-      const resp = await this.http.postJson(reqUrl.href(), perm);
+      const resp = await this.http.postJson(reqUrl.href(), req);
       if (resp.status !== 200) {
         console.error("refund failed", resp);
         continue;
@@ -2664,7 +2682,7 @@ export class Wallet {
     return this.q().get(Stores.purchases, contractTermsHash);
   }
 
-  async getFullRefundFees(refundPermissions: RefundPermission[]): 
Promise<AmountJson> {
+  async getFullRefundFees(refundPermissions: MerchantRefundPermission[]): 
Promise<AmountJson> {
     if (refundPermissions.length === 0) {
       throw Error("no refunds given");
     }
@@ -2839,6 +2857,54 @@ export class Wallet {
   }
 
 
+  async abortFailedPayment(contractTermsHash: string): Promise<void> {
+    const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+    if (!purchase) {
+      throw Error("Purchase not found, unable to abort with refund");
+    }
+    if (purchase.finished) {
+      throw Error("Purchase already finished, not aborting");
+    }
+    if (purchase.abortDone) {
+      console.warn("abort requested on already aborted purchase");
+      return;
+    }
+
+    purchase.abortRequested = true;
+
+    // From now on, we can't retry payment anymore,
+    // so mark this in the DB in case the /pay abort
+    // does not complete on the first try.
+    await this.q().put(Stores.purchases, purchase);
+
+    let resp;
+
+    const abortReq = { ...purchase.payReq, mode: "abort-refund" };
+
+    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, abortReq, 
config);
+    } catch (e) {
+      // Gives the user the option to retry / abort and refresh
+      console.log("aborting payment failed", e);
+      throw e;
+    }
+
+    const refundResponse = MerchantRefundResponse.checked(resp.data);
+    await this.acceptRefundResponse(refundResponse);
+
+    const markAbortDone = (p: PurchaseRecord) => {
+      p.abortDone = true;
+      return p;
+    };
+    await this.q().mutate(Stores.purchases, purchase.contractTermsHash, 
markAbortDone);
+  }
+
+
   /**
    * Synchronously get the paid URL for a resource from the plain fulfillment
    * URL.  Returns undefined if the fulfillment URL is not a resource that was
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 2219cdf1..45cac6a9 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -26,6 +26,8 @@ import * as dbTypes from "../dbTypes";
 import * as talerTypes from "../talerTypes";
 import * as walletTypes from "../walletTypes";
 
+import { UpgradeResponse } from "./wxApi";
+
 /**
  * Message type information.
  */
@@ -73,7 +75,7 @@ export interface MessageMap {
   };
   "query-payment": {
     request: { };
-    response: void;
+    response: dbTypes.PurchaseRecord;
   };
   "exchange-info": {
     request: { baseUrl: string };
@@ -129,7 +131,7 @@ export interface MessageMap {
   };
   "withdraw-payback-reserve": {
     request: { reservePub: string };
-    response: void;
+    response: dbTypes.ReserveRecord[];
   };
   "get-precoins": {
     request: { exchangeBaseUrl: string };
@@ -145,11 +147,11 @@ export interface MessageMap {
   };
   "check-upgrade": {
     request: { };
-    response: void;
+    response: UpgradeResponse;
   };
   "get-sender-wire-infos": {
     request: { };
-    response: void;
+    response: walletTypes.SenderWireInfos;
   };
   "return-coins": {
     request: { };
@@ -164,20 +166,20 @@ export interface MessageMap {
     response: void;
   };
   "get-purchase": {
-    request: any;
-    response: void;
+    request: { contractTermsHash: string };
+    response: dbTypes.PurchaseRecord;
   };
   "get-full-refund-fees": {
-    request: { refundPermissions: talerTypes.RefundPermission[] };
-    response: void;
+    request: { refundPermissions: talerTypes.MerchantRefundPermission[] };
+    response: AmountJson;
   };
   "accept-tip": {
     request: { tipToken: talerTypes.TipToken };
-    response: void;
+    response: walletTypes.TipStatus;
   };
   "get-tip-status": {
     request: { tipToken: talerTypes.TipToken };
-    response: void;
+    response: walletTypes.TipStatus;
   };
   "clear-notification": {
     request: { };
@@ -188,17 +190,21 @@ export interface MessageMap {
     response: void;
   };
   "download-proposal": {
-    request: any;
-    response: void;
+    request: { url: string };
+    response: number;
   };
   "submit-pay": {
     request: { contractTermsHash: string, sessionId: string | undefined };
-    response: void;
+    response: walletTypes.ConfirmPayResult;
   };
   "accept-refund": {
     request: { refundUrl: string }
     response: string;
   };
+  "abort-failed-payment": {
+    request: { contractTermsHash: string }
+    response: void;
+  };
 }
 
 /**
diff --git a/src/webex/pages/confirm-contract.tsx 
b/src/webex/pages/confirm-contract.tsx
index 2ec13105..f41dba06 100644
--- a/src/webex/pages/confirm-contract.tsx
+++ b/src/webex/pages/confirm-contract.tsx
@@ -40,6 +40,7 @@ import * as wxApi from "../wxApi";
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 import URI = require("urijs");
+import { WalletApiError } from "../wxApi";
 
 
 interface DetailState {
@@ -49,7 +50,7 @@ interface DetailState {
 interface DetailProps {
   contractTerms: ContractTerms;
   collapsed: boolean;
-  exchanges: null|ExchangeRecord[];
+  exchanges: ExchangeRecord[] | undefined;
 }
 
 
@@ -110,11 +111,12 @@ interface ContractPromptProps {
 
 interface ContractPromptState {
   proposalId: number | undefined;
-  proposal: ProposalDownloadRecord | null;
-  error: string |  null;
+  proposal: ProposalDownloadRecord | undefined;
+  checkPayError: string | undefined;
+  confirmPayError: object | undefined;
   payDisabled: boolean;
   alreadyPaid: boolean;
-  exchanges: null|ExchangeRecord[];
+  exchanges: ExchangeRecord[] | undefined;
   /**
    * Don't request updates to proposal state while
    * this is set to true, to avoid UI flickering
@@ -123,20 +125,31 @@ interface ContractPromptState {
   holdCheck: boolean;
   payStatus?: CheckPayResult;
   replaying: boolean;
+  payInProgress: boolean;
+  payAttempt: number;
+  working: boolean;
+  abortDone: boolean;
+  abortStarted: boolean;
 }
 
 class ContractPrompt extends React.Component<ContractPromptProps, 
ContractPromptState> {
   constructor(props: ContractPromptProps) {
     super(props);
     this.state = {
+      abortDone: false,
+      abortStarted: false,
       alreadyPaid: false,
-      error: null,
-      exchanges: null,
+      checkPayError: undefined,
+      confirmPayError: undefined,
+      exchanges: undefined,
       holdCheck: false,
+      payAttempt: 0,
       payDisabled: true,
-      proposal: null,
+      payInProgress: false,
+      proposal: undefined,
       proposalId: props.proposalId,
       replaying: false,
+      working: false,
     };
   }
 
@@ -152,7 +165,7 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
     if (this.props.resourceUrl) {
       const p = await 
wxApi.queryPaymentByFulfillmentUrl(this.props.resourceUrl);
       console.log("query for resource url", this.props.resourceUrl, "result", 
p);
-      if (p) {
+      if (p && p.finished) {
         if (p.lastSessionSig === undefined || p.lastSessionSig === 
this.props.sessionId) {
           const nextUrl = new URI(p.contractTerms.fulfillment_url);
           nextUrl.addSearch("order_id", p.contractTerms.order_id);
@@ -164,6 +177,8 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
         } else {
           // We're in a new session
           this.setState({ replaying: true });
+          // FIXME:  This could also go wrong.  However the payment
+          // was already successful once, so we can just retry and not refund 
it.
           const payResult = await wxApi.submitPay(p.contractTermsHash, 
this.props.sessionId);
           console.log("payResult", payResult);
           location.replace(payResult.nextUrl);
@@ -204,24 +219,24 @@ 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({ checkPayError: msgInsufficient });
         } else {
-          this.setState({ error: msgNoMatch });
+          this.setState({ checkPayError: msgNoMatch });
         }
       } else {
-        this.setState({ error: msgInsufficient });
+        this.setState({ checkPayError: msgInsufficient });
       }
       this.setState({ payDisabled: true });
     } else if (payStatus.status === "paid") {
-      this.setState({ alreadyPaid: true, payDisabled: false, error: null, 
payStatus });
+      this.setState({ alreadyPaid: true, payDisabled: false, checkPayError: 
undefined, payStatus });
     } else {
-      this.setState({ payDisabled: false, error: null, payStatus });
+      this.setState({ payDisabled: false, checkPayError: undefined, payStatus 
});
     }
   }
 
   async doPayment() {
     const proposal = this.state.proposal;
-    this.setState({holdCheck: true});
+    this.setState({ holdCheck: true, payAttempt: this.state.payAttempt + 1});
     if (!proposal) {
       return;
     }
@@ -231,13 +246,36 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
       return;
     }
     console.log("confirmPay with", proposalId, "and", this.props.sessionId);
-    const payResult = await wxApi.confirmPay(proposalId, this.props.sessionId);
+    let payResult;
+    this.setState({ working: true });
+    try {
+      payResult = await wxApi.confirmPay(proposalId, this.props.sessionId);
+    } catch (e) {
+      if (!(e instanceof WalletApiError)) {
+        throw e;
+      }
+      this.setState({ confirmPayError: e.detail });
+      return;
+    } finally {
+      this.setState({ working: false });
+    }
     console.log("payResult", payResult);
     document.location.href = payResult.nextUrl;
     this.setState({ holdCheck: true });
   }
 
 
+  async abortPayment() {
+    const proposal = this.state.proposal;
+    this.setState({ holdCheck: true, abortStarted: true });
+    if (!proposal) {
+      return;
+    }
+    wxApi.abortFailedPayment(proposal.contractTermsHash);
+    this.setState({ abortDone: true });
+  }
+
+
   render() {
     if (this.props.contractUrl === undefined && this.props.proposalId === 
undefined) {
       return <span>Error: either contractUrl or proposalId must be 
given</span>;
@@ -264,18 +302,72 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
     let products = null;
     if (c.products.length) {
       products = (
-        <>
+        <div>
           <span>The following items are included:</span>
           <ul>
             {c.products.map(
               (p: any, i: number) => (<li key={i}>{p.description}: 
{renderAmount(p.price)}</li>))
             }
           </ul>
-      </>
+      </div>
       );
     }
+
+    const ConfirmButton = () => (
+      <button className="pure-button button-success"
+              disabled={this.state.payDisabled}
+              onClick={() => this.doPayment()}>
+        {i18n.str`Confirm payment`}
+      </button>
+    );
+
+    const WorkingButton = () => (
+      <div>
+      <button className="pure-button button-success"
+              disabled={this.state.payDisabled}
+              onClick={() => this.doPayment()}>
+        <span><object className="svg-icon svg-baseline" 
data="/img/spinner-bars.svg" /> </span>
+        {i18n.str`Submitting payment`}
+      </button>
+      </div>
+    );
+
+    const ConfirmPayDialog = () => (
+      <div>
+        {this.state.working ? WorkingButton() : ConfirmButton()}
+        <div>
+          {(this.state.alreadyPaid
+            ? <p className="okaybox">
+                You already paid for this, clicking "Confirm payment" will not 
cost money again.
+              </p>
+            : <p />)}
+          {(this.state.checkPayError ? <p 
className="errorbox">{this.state.checkPayError}</p> : <p />)}
+        </div>
+        <Details exchanges={this.state.exchanges} contractTerms={c} 
collapsed={!this.state.checkPayError}/>
+      </div>
+    );
+
+    const PayErrorDialog = () => (
+      <div>
+        <p>There was an error paying (attempt #{this.state.payAttempt}):</p>
+        <pre>{JSON.stringify(this.state.confirmPayError)}</pre>
+        { this.state.abortStarted
+        ? <span>Aborting payment ...</span>
+        : this.state.abortDone
+        ? <span>Payment aborted!</span>
+        : <>
+            <button className="pure-button" onClick={() => this.doPayment()}>
+              Retry Payment
+            </button>
+            <button className="pure-button" onClick={() => 
this.abortPayment()}>
+              Abort Payment
+            </button>
+          </>
+        }
+      </div>
+    );
+
     return (
-      <>
         <div>
           <i18n.Translate wrap="p">
             The merchant <span>{merchantName}</span> {" "}
@@ -294,22 +386,11 @@ class ContractPrompt extends 
React.Component<ContractPromptProps, ContractPrompt
             :
             <p>The total price is <span>{amount}</span>.</p>
           }
+          { this.state.confirmPayError
+            ? PayErrorDialog()
+            : ConfirmPayDialog()
+          }
         </div>
-        <button className="pure-button button-success"
-                disabled={this.state.payDisabled}
-                onClick={() => this.doPayment()}>
-          {i18n.str`Confirm payment`}
-        </button>
-        <div>
-          {(this.state.alreadyPaid
-            ? <p className="okaybox">
-                You already paid for this, clicking "Confirm payment" will not 
cost money again.
-              </p>
-            : <p />)}
-          {(this.state.error ? <p className="errorbox">{this.state.error}</p> 
: <p />)}
-        </div>
-        <Details exchanges={this.state.exchanges} contractTerms={c} 
collapsed={!this.state.error}/>
-      </>
     );
   }
 }
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 8a7bf825..ee1ca23b 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -29,6 +29,7 @@ import {
   DenominationRecord,
   ExchangeRecord,
   PreCoinRecord,
+  ProposalDownloadRecord,
   PurchaseRecord,
   ReserveRecord,
 } from "../dbTypes";
@@ -42,7 +43,7 @@ import {
 } from "../walletTypes";
 
 import {
-  RefundPermission,
+  MerchantRefundPermission,
   TipToken,
 } from "../talerTypes";
 
@@ -71,11 +72,22 @@ export interface UpgradeResponse {
 }
 
 
-async function callBackend<T extends MessageType>(type: T, detail: 
MessageMap[T]["request"]): Promise<any> {
-  return new Promise<any>((resolve, reject) => {
+export class WalletApiError extends Error {
+  constructor(message: string, public detail: any) {
+    super(message);
+  }
+}
+
+
+async function callBackend<T extends MessageType>(
+  type: T,
+  detail: MessageMap[T]["request"],
+): Promise<MessageMap[T]["response"]> {
+  return new Promise<MessageMap[T]["response"]>((resolve, reject) => {
     chrome.runtime.sendMessage({ type, detail }, (resp) => {
-      if (resp && resp.error) {
-        reject(resp);
+      if (typeof resp === "object" && resp && resp.error) {
+        const e = new WalletApiError(resp.error.message, resp);
+        reject(e);
       } else {
         resolve(resp);
       }
@@ -201,7 +213,7 @@ export function payback(coinPub: string): Promise<void> {
 /**
  * Get a proposal stored in the wallet by its proposal id.
  */
-export function getProposal(proposalId: number) {
+export function getProposal(proposalId: number): 
Promise<ProposalDownloadRecord | undefined> {
   return callBackend("get-proposal", { proposalId });
 }
 
@@ -323,7 +335,7 @@ export function getPurchase(contractTermsHash: string): 
Promise<PurchaseRecord>
  * Get the refund fees for a refund permission, including
  * subsequent refresh and unrefreshable coins.
  */
-export function getFullRefundFees(args: { refundPermissions: 
RefundPermission[] }): Promise<AmountJson> {
+export function getFullRefundFees(args: { refundPermissions: 
MerchantRefundPermission[] }): Promise<AmountJson> {
   return callBackend("get-full-refund-fees", { refundPermissions: 
args.refundPermissions });
 }
 
@@ -370,3 +382,10 @@ export function downloadProposal(url: string): 
Promise<number> {
 export function acceptRefund(refundUrl: string): Promise<string> {
   return callBackend("accept-refund", { refundUrl });
 }
+
+/**
+ * Abort a failed payment and try to get a refund.
+ */
+export function abortFailedPayment(contractTermsHash: string) {
+  return callBackend("abort-failed-payment", { contractTermsHash });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 30842398..a778cc98 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -308,6 +308,12 @@ function handleMessage(sender: MessageSender,
     case "download-proposal": {
       return needsWallet().downloadProposal(detail.url);
     }
+    case "abort-failed-payment": {
+      if (!detail.contractTermsHash) {
+        throw Error("contracTermsHash not given");
+      }
+      return needsWallet().abortFailedPayment(detail.contractTermsHash);
+    }
     case "taler-pay": {
       const senderUrl = sender.url;
       if (!senderUrl) {

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



reply via email to

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