gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (9996c274 -> 93972900)


From: gnunet
Subject: [taler-wallet-core] branch master updated (9996c274 -> 93972900)
Date: Tue, 03 May 2022 00:21:44 +0200

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

sebasjm pushed a change to branch master
in repository wallet-core.

    from 9996c274 wallet-core: make coin selection aware of age restriction
     new e5c9f588 add prepareRefund operation to gather information about the 
refund before confirm
     new 93972900 tip and refund stories and test

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


Summary of changes:
 packages/taler-util/src/walletTypes.ts             |  23 ++
 .../taler-wallet-core/src/operations/refund.ts     |  68 ++++
 packages/taler-wallet-core/src/wallet.ts           |  76 +++--
 .../taler-wallet-webextension/src/cta/Deposit.tsx  |  28 +-
 .../taler-wallet-webextension/src/cta/Pay.test.ts  |   2 +-
 packages/taler-wallet-webextension/src/cta/Pay.tsx |   2 +-
 .../src/cta/Refund.stories.tsx                     |  96 +++---
 .../src/cta/Refund.test.ts                         | 243 ++++++++++++++
 .../taler-wallet-webextension/src/cta/Refund.tsx   | 350 ++++++++++++++++-----
 .../src/cta/Tip.stories.tsx                        |  28 +-
 .../taler-wallet-webextension/src/cta/Tip.test.ts  | 192 +++++++++++
 packages/taler-wallet-webextension/src/cta/Tip.tsx | 280 +++++++++++------
 .../src/wallet/Transaction.tsx                     |   4 +-
 packages/taler-wallet-webextension/src/wxApi.ts    |   7 +
 14 files changed, 1091 insertions(+), 308 deletions(-)
 create mode 100644 packages/taler-wallet-webextension/src/cta/Refund.test.ts
 create mode 100644 packages/taler-wallet-webextension/src/cta/Tip.test.ts

diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index e094bc38..c6367f8e 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -276,6 +276,18 @@ export class ReturnCoinsRequest {
   static checked: (obj: any) => ReturnCoinsRequest;
 }
 
+export interface PrepareRefundResult {
+  proposalId: string;
+
+  applied: number;
+  failed: number;
+  total: number;
+
+  amountEffectivePaid: AmountString;
+
+  info: OrderShortInfo;
+}
+
 export interface PrepareTipResult {
   /**
    * Unique ID for the tip assigned by the wallet.
@@ -1003,6 +1015,17 @@ export const codecForForceRefreshRequest = (): 
Codec<ForceRefreshRequest> =>
     .property("coinPubList", codecForList(codecForString()))
     .build("ForceRefreshRequest");
 
+
+
+export interface PrepareRefundRequest {
+  talerRefundUri: string;
+}
+
+export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
+  buildCodecForObject<PrepareRefundRequest>()
+    .property("talerRefundUri", codecForString())
+    .build("PrepareRefundRequest");
+
 export interface PrepareTipRequest {
   talerTipUri: string;
 }
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
index 7ef8076f..dad8c600 100644
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -46,6 +46,8 @@ import {
   AbsoluteTime,
   TalerProtocolTimestamp,
   Duration,
+  PrepareRefundRequest,
+  PrepareRefundResult,
 } from "@gnu-taler/taler-util";
 import {
   AbortStatus,
@@ -69,6 +71,72 @@ import { guardOperationException } from "./common.js";
 
 const logger = new Logger("refund.ts");
 
+
+export async function prepareRefund(
+  ws: InternalWalletState,
+  talerRefundUri: string,
+): Promise<PrepareRefundResult> {
+  const parseResult = parseRefundUri(talerRefundUri);
+
+  logger.trace("preparing refund offer", parseResult);
+
+  if (!parseResult) {
+    throw Error("invalid refund URI");
+  }
+
+  const purchase = await ws.db
+    .mktx((x) => ({
+      purchases: x.purchases,
+    }))
+    .runReadOnly(async (tx) => {
+      return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
+        parseResult.merchantBaseUrl,
+        parseResult.orderId,
+      ]);
+    });
+
+  if (!purchase) {
+    throw Error(
+      `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
+    );
+  }
+
+  const proposalId = purchase.proposalId;
+  const rfs = Object.values(purchase.refunds)
+
+  let applied = 0;
+  let failed = 0;
+  const total = rfs.length;
+  rfs.forEach((refund) => {
+    if (refund.type === RefundState.Failed) {
+      failed = failed + 1;
+    }
+    if (refund.type === RefundState.Applied) {
+      applied = applied + 1;
+    }
+  });
+
+  const { contractData: c } = purchase.download
+
+  return {
+    proposalId,
+    amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
+    applied,
+    failed,
+    total,
+    info: {
+      contractTermsHash: c.contractTermsHash,
+      merchant: c.merchant,
+      orderId: c.orderId,
+      products: c.products,
+      summary: c.summary,
+      fulfillmentMessage: c.fulfillmentMessage,
+      summary_i18n: c.summaryI18n,
+      fulfillmentMessage_i18n:
+        c.fulfillmentMessageI18n,
+    },
+  }
+}
 /**
  * Retry querying and applying refunds for an order later.
  */
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 96722aef..7760c0be 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -23,9 +23,7 @@
  * Imports.
  */
 import {
-  AcceptManualWithdrawalResult,
-  AcceptWithdrawalResponse,
-  AmountJson,
+  AbsoluteTime, AcceptManualWithdrawalResult, AmountJson,
   Amounts,
   BalancesResponse,
   codecForAbortPayWithRefundRequest,
@@ -48,8 +46,7 @@ import {
   codecForImportDbRequest,
   codecForIntegrationTestArgs,
   codecForListKnownBankAccounts,
-  codecForPreparePayRequest,
-  codecForPrepareTipRequest,
+  codecForPreparePayRequest, codecForPrepareRefundRequest, 
codecForPrepareTipRequest,
   codecForRetryTransactionRequest,
   codecForSetCoinSuspendedRequest,
   codecForSetWalletDeviceIdRequest,
@@ -59,8 +56,7 @@ import {
   codecForWithdrawFakebankRequest,
   codecForWithdrawTestBalance,
   CoinDumpJson,
-  CoreApiResponse,
-  durationFromSpec,
+  CoreApiResponse, Duration, durationFromSpec,
   durationMin,
   ExchangeListItem,
   ExchangesListRespose,
@@ -73,27 +69,13 @@ import {
   parsePaytoUri,
   PaytoUri,
   RefreshReason,
-  TalerErrorCode,
-  AbsoluteTime,
-  URL,
-  WalletNotification,
-  Duration,
-  CancellationToken,
+  TalerErrorCode, URL,
+  WalletNotification
 } from "@gnu-taler/taler-util";
-import { timeStamp } from "console";
-import {
-  DenomInfo,
-  ExchangeOperations,
-  InternalWalletState,
-  MerchantInfo,
-  MerchantOperations,
-  NotificationListener,
-  RecoupOperations,
-  ReserveOperations,
-} from "./internal-wallet-state.js";
+import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 import {
   CryptoDispatcher,
-  CryptoWorkerFactory,
+  CryptoWorkerFactory
 } from "./crypto/workers/cryptoDispatcher.js";
 import {
   AuditorTrustRecord,
@@ -101,9 +83,19 @@ import {
   exportDb,
   importDb,
   ReserveRecordStatus,
-  WalletStoresV1,
+  WalletStoresV1
 } from "./db.js";
 import { getErrorDetailFromException, TalerError } from "./errors.js";
+import {
+  DenomInfo,
+  ExchangeOperations,
+  InternalWalletState,
+  MerchantInfo,
+  MerchantOperations,
+  NotificationListener,
+  RecoupOperations,
+  ReserveOperations
+} from "./internal-wallet-state.js";
 import { exportBackup } from "./operations/backup/export.js";
 import {
   addBackupProvider,
@@ -115,7 +107,7 @@ import {
   loadBackupRecovery,
   processBackupForProvider,
   removeBackupProvider,
-  runBackupCycle,
+  runBackupCycle
 } from "./operations/backup/index.js";
 import { setWalletDeviceId } from "./operations/backup/state.js";
 import { getBalances } from "./operations/balance.js";
@@ -123,7 +115,7 @@ import {
   createDepositGroup,
   getFeeForDeposit,
   processDepositGroup,
-  trackDepositGroup,
+  trackDepositGroup
 } from "./operations/deposits.js";
 import {
   acceptExchangeTermsOfService,
@@ -132,69 +124,69 @@ import {
   getExchangeRequestTimeout,
   getExchangeTrust,
   updateExchangeFromUrl,
-  updateExchangeTermsOfService,
+  updateExchangeTermsOfService
 } from "./operations/exchanges.js";
 import { getMerchantInfo } from "./operations/merchants.js";
 import {
   confirmPay,
   preparePayForUri,
   processDownloadProposal,
-  processPurchasePay,
+  processPurchasePay
 } from "./operations/pay.js";
 import { getPendingOperations } from "./operations/pending.js";
 import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
 import {
   autoRefresh,
   createRefreshGroup,
-  processRefreshGroup,
+  processRefreshGroup
 } from "./operations/refresh.js";
 import {
   abortFailedPayWithRefund,
   applyRefund,
-  processPurchaseQueryRefund,
+  prepareRefund,
+  processPurchaseQueryRefund
 } from "./operations/refund.js";
 import {
   createReserve,
   createTalerWithdrawReserve,
   getFundingPaytoUris,
-  processReserve,
+  processReserve
 } from "./operations/reserves.js";
 import {
   runIntegrationTest,
   testPay,
-  withdrawTestBalance,
+  withdrawTestBalance
 } from "./operations/testing.js";
 import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
 import {
   deleteTransaction,
   getTransactions,
-  retryTransaction,
+  retryTransaction
 } from "./operations/transactions.js";
 import {
   getExchangeWithdrawalInfo,
   getWithdrawalDetailsForUri,
-  processWithdrawGroup,
+  processWithdrawGroup
 } from "./operations/withdraw.js";
 import {
   PendingOperationsResponse,
   PendingTaskInfo,
-  PendingTaskType,
+  PendingTaskType
 } from "./pending-types.js";
 import { assertUnreachable } from "./util/assertUnreachable.js";
 import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
 import {
   HttpRequestLibrary,
-  readSuccessResponseJsonOrThrow,
+  readSuccessResponseJsonOrThrow
 } from "./util/http.js";
 import {
   AsyncCondition,
   OpenedPromise,
-  openPromise,
+  openPromise
 } from "./util/promiseUtils.js";
 import { DbAccess, GetReadWriteAccess } from "./util/query.js";
 import { TimerAPI, TimerGroup } from "./util/timer.js";
 import { WalletCoreApiClient } from "./wallet-api-types.js";
-import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 
 const builtinAuditors: AuditorTrustRecord[] = [
   {
@@ -908,6 +900,10 @@ async function dispatchRequestInternal(
       const req = codecForPrepareTipRequest().decode(payload);
       return await prepareTip(ws, req.talerTipUri);
     }
+    case "prepareRefund": {
+      const req = codecForPrepareRefundRequest().decode(payload);
+      return await prepareRefund(ws, req.talerRefundUri);
+    }
     case "acceptTip": {
       const req = codecForAcceptTipRequest().decode(payload);
       await acceptTip(ws, req.walletTipId);
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.tsx 
b/packages/taler-wallet-webextension/src/cta/Deposit.tsx
index 23c557b0..529da11b 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit.tsx
@@ -24,35 +24,13 @@
  * Imports.
  */
 
-import {
-  AmountJson,
-  Amounts,
-  amountToPretty,
-  ConfirmPayResult,
-  ConfirmPayResultType,
-  ContractTerms,
-  NotificationType,
-  PreparePayResult,
-  PreparePayResultType,
-} from "@gnu-taler/taler-util";
-import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
 import { Loading } from "../components/Loading.js";
 import { LoadingError } from "../components/LoadingError.js";
 import { LogoHeader } from "../components/LogoHeader.js";
-import { Part } from "../components/Part.js";
-import {
-  ErrorBox,
-  SubTitle,
-  SuccessBox,
-  WalletAction,
-  WarningBox,
-} from "../components/styled/index.js";
+import { SubTitle, WalletAction } from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
-import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
-import * as wxApi from "../wxApi.js";
+import { HookError } from "../hooks/useAsyncAsHook.js";
 
 interface Props {
   talerDepositUri?: string;
@@ -102,7 +80,7 @@ export function View({ state }: ViewProps): VNode {
       <LogoHeader />
 
       <SubTitle>
-        <i18n.Translate>Digital cash deposit</i18n.Translate>
+        <i18n.Translate>Digital cash refund</i18n.Translate>
       </SubTitle>
     </WalletAction>
   );
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.test.ts 
b/packages/taler-wallet-webextension/src/cta/Pay.test.ts
index 4c0fe45c..7e9d5338 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Pay.test.ts
@@ -32,7 +32,7 @@ type Subs = {
   [key in NotificationType]?: VoidFunction
 }
 
-class SubsHandler {
+export class SubsHandler {
   private subs: Subs = {};
 
   constructor() {
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx 
b/packages/taler-wallet-webextension/src/cta/Pay.tsx
index 3e9e34fe..0e253014 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx
@@ -353,7 +353,7 @@ export function View({
   );
 }
 
-function ProductList({ products }: { products: Product[] }): VNode {
+export function ProductList({ products }: { products: Product[] }): VNode {
   const { i18n } = useTranslationContext();
   return (
     <Fragment>
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
index c4884171..6b7cf462 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
@@ -19,7 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { OrderShortInfo } from "@gnu-taler/taler-util";
+import { Amounts } from "@gnu-taler/taler-util";
 import { createExample } from "../test-utils.js";
 import { View as TestedComponent } from "./Refund.js";
 
@@ -30,46 +30,70 @@ export default {
 };
 
 export const Complete = createExample(TestedComponent, {
-  applyResult: {
-    amountEffectivePaid: "USD:10",
-    amountRefundGone: "USD:0",
-    amountRefundGranted: "USD:2",
-    contractTermsHash: "QWEASDZXC",
-    info: {
-      summary: "tasty cold beer",
-      contractTermsHash: "QWEASDZXC",
-    } as Partial<OrderShortInfo> as any,
-    pendingAtExchange: false,
-    proposalId: "proposal123",
+  state: {
+    status: "completed",
+    amount: Amounts.parseOrThrow("USD:1"),
+    hook: undefined,
+    merchantName: "the merchant",
+    products: undefined,
   },
 });
 
-export const Partial = createExample(TestedComponent, {
-  applyResult: {
-    amountEffectivePaid: "USD:10",
-    amountRefundGone: "USD:1",
-    amountRefundGranted: "USD:2",
-    contractTermsHash: "QWEASDZXC",
-    info: {
-      summary: "tasty cold beer",
-      contractTermsHash: "QWEASDZXC",
-    } as Partial<OrderShortInfo> as any,
-    pendingAtExchange: false,
-    proposalId: "proposal123",
+export const InProgress = createExample(TestedComponent, {
+  state: {
+    status: "in-progress",
+    hook: undefined,
+    amount: Amounts.parseOrThrow("USD:1"),
+    merchantName: "the merchant",
+    products: undefined,
+    progress: 0.5,
   },
 });
 
-export const InProgress = createExample(TestedComponent, {
-  applyResult: {
-    amountEffectivePaid: "USD:10",
-    amountRefundGone: "USD:1",
-    amountRefundGranted: "USD:2",
-    contractTermsHash: "QWEASDZXC",
-    info: {
-      summary: "tasty cold beer",
-      contractTermsHash: "QWEASDZXC",
-    } as Partial<OrderShortInfo> as any,
-    pendingAtExchange: true,
-    proposalId: "proposal123",
+export const Ready = createExample(TestedComponent, {
+  state: {
+    status: "ready",
+    hook: undefined,
+    accept: {},
+    ignore: {},
+
+    amount: Amounts.parseOrThrow("USD:1"),
+    merchantName: "the merchant",
+    products: [],
+    orderId: "abcdef",
+  },
+});
+
+import beer from "../../static-dev/beer.png";
+
+export const WithAProductList = createExample(TestedComponent, {
+  state: {
+    status: "ready",
+    hook: undefined,
+    accept: {},
+    ignore: {},
+    amount: Amounts.parseOrThrow("USD:1"),
+    merchantName: "the merchant",
+    products: [
+      {
+        description: "beer",
+        image: beer,
+        quantity: 2,
+      },
+      {
+        description: "t-shirt",
+        price: "EUR:1",
+        quantity: 5,
+      },
+    ],
+    orderId: "abcdef",
+  },
+});
+
+export const Ignored = createExample(TestedComponent, {
+  state: {
+    status: "ignored",
+    hook: undefined,
+    merchantName: "the merchant",
   },
 });
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.test.ts 
b/packages/taler-wallet-webextension/src/cta/Refund.test.ts
new file mode 100644
index 00000000..e77f8e68
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Refund.test.ts
@@ -0,0 +1,243 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts, NotificationType, PrepareRefundResult } from 
"@gnu-taler/taler-util";
+import { expect } from "chai";
+import { mountHook } from "../test-utils.js";
+import { SubsHandler } from "./Pay.test.js";
+import { useComponentState } from "./Refund.jsx";
+
+// onUpdateNotification: subscriptions.saveSubscription,
+
+describe("Refund CTA states", () => {
+  it("should tell the user that the URI is missing", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(undefined, {
+        prepareRefund: async () => ({}),
+        applyRefund: async () => ({}),
+        onUpdateNotification: async () => ({})
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading')
+      if (!hook) expect.fail();
+      if (!hook.hasError) expect.fail();
+      if (hook.operational) expect.fail();
+      expect(hook.message).eq("ERROR_NO-URI-FOR-REFUND");
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should be ready after loading", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState("taler://refund/asdasdas", {
+        prepareRefund: async () => ({
+          total: 0,
+          applied: 0,
+          failed: 0,
+          amountEffectivePaid: 'EUR:2',
+          info: {
+            contractTermsHash: '123',
+            merchant: {
+              name: 'the merchant name'
+            },
+            orderId: 'orderId1',
+            summary: 'the sumary'
+          }
+        } as PrepareRefundResult as any),
+        applyRefund: async () => ({}),
+        onUpdateNotification: async () => ({})
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== 'ready') expect.fail();
+      if (state.hook) expect.fail();
+      expect(state.accept.onClick).not.undefined;
+      expect(state.ignore.onClick).not.undefined;
+      expect(state.merchantName).eq('the merchant name');
+      expect(state.orderId).eq('orderId1');
+      expect(state.products).undefined;
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should be ignored after clicking the ignore button", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState("taler://refund/asdasdas", {
+        prepareRefund: async () => ({
+          total: 0,
+          applied: 0,
+          failed: 0,
+          amountEffectivePaid: 'EUR:2',
+          info: {
+            contractTermsHash: '123',
+            merchant: {
+              name: 'the merchant name'
+            },
+            orderId: 'orderId1',
+            summary: 'the sumary'
+          }
+        } as PrepareRefundResult as any),
+        applyRefund: async () => ({}),
+        onUpdateNotification: async () => ({})
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== 'ready') expect.fail();
+      if (state.hook) expect.fail();
+      expect(state.accept.onClick).not.undefined;
+      expect(state.merchantName).eq('the merchant name');
+      expect(state.orderId).eq('orderId1');
+      expect(state.products).undefined;
+
+      if (state.ignore.onClick === undefined) expect.fail();
+      state.ignore.onClick()
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== 'ignored') expect.fail();
+      if (state.hook) expect.fail();
+      expect(state.merchantName).eq('the merchant name');
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should be in progress when doing refresh", async () => {
+    let numApplied = 1;
+    const subscriptions = new SubsHandler();
+
+    function notifyMelt(): void {
+      numApplied++;
+      subscriptions.notifyEvent(NotificationType.RefreshMelted)
+    }
+
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState("taler://refund/asdasdas", {
+        prepareRefund: async () => ({
+          total: 3,
+          applied: numApplied,
+          failed: 0,
+          amountEffectivePaid: 'EUR:2',
+          info: {
+            contractTermsHash: '123',
+            merchant: {
+              name: 'the merchant name'
+            },
+            orderId: 'orderId1',
+            summary: 'the sumary'
+          }
+        } as PrepareRefundResult as any),
+        applyRefund: async () => ({}),
+        onUpdateNotification: subscriptions.saveSubscription,
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== 'in-progress') expect.fail();
+      if (state.hook) expect.fail();
+      expect(state.merchantName).eq('the merchant name');
+      expect(state.products).undefined;
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
+      expect(state.progress).closeTo(1 / 3, 0.01)
+
+      notifyMelt()
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== 'in-progress') expect.fail();
+      if (state.hook) expect.fail();
+      expect(state.merchantName).eq('the merchant name');
+      expect(state.products).undefined;
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
+      expect(state.progress).closeTo(2 / 3, 0.01)
+
+      notifyMelt()
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== 'completed') expect.fail();
+      if (state.hook) expect.fail();
+      expect(state.merchantName).eq('the merchant name');
+      expect(state.products).undefined;
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
+    }
+
+    await assertNoPendingUpdate()
+  });
+});
\ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.tsx 
b/packages/taler-wallet-webextension/src/cta/Refund.tsx
index 23231328..f69fc431 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund.tsx
@@ -21,129 +21,311 @@
  */
 
 import {
-  amountFractionalBase,
   AmountJson,
   Amounts,
-  ApplyRefundResponse,
+  NotificationType,
+  Product,
 } from "@gnu-taler/taler-util";
 import { h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
-import { SubTitle, Title } from "../components/styled/index.js";
+import { Amount } from "../components/Amount.js";
+import { Loading } from "../components/Loading.js";
+import { LoadingError } from "../components/LoadingError.js";
+import { LogoHeader } from "../components/LogoHeader.js";
+import { Part } from "../components/Part.js";
+import {
+  Button,
+  ButtonSuccess,
+  SubTitle,
+  WalletAction,
+} from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { ButtonHandler } from "../mui/handlers.js";
 import * as wxApi from "../wxApi.js";
+import { ProductList } from "./Pay.js";
 
 interface Props {
   talerRefundUri?: string;
 }
 export interface ViewProps {
-  applyResult: ApplyRefundResponse;
+  state: State;
 }
-export function View({ applyResult }: ViewProps): VNode {
+export function View({ state }: ViewProps): VNode {
   const { i18n } = useTranslationContext();
-  return (
-    <section class="main">
-      <Title>GNU Taler Wallet</Title>
-      <article class="fade">
+  if (state.status === "loading") {
+    if (!state.hook) {
+      return <Loading />;
+    }
+    return (
+      <LoadingError
+        title={<i18n.Translate>Could not load refund status</i18n.Translate>}
+        error={state.hook}
+      />
+    );
+  }
+
+  if (state.status === "ignored") {
+    return (
+      <WalletAction>
+        <LogoHeader />
+
         <SubTitle>
-          <i18n.Translate>Refund Status</i18n.Translate>
+          <i18n.Translate>Digital cash refund</i18n.Translate>
         </SubTitle>
-        <p>
-          <i18n.Translate>
-            The product <em>{applyResult.info.summary}</em> has received a 
total
-            effective refund of{" "}
-          </i18n.Translate>
-          <AmountView amount={applyResult.amountRefundGranted} />.
-        </p>
-        {applyResult.pendingAtExchange ? (
+        <section>
+          <p>
+            <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
+          </p>
+        </section>
+      </WalletAction>
+    );
+  }
+
+  if (state.status === "in-progress") {
+    return (
+      <WalletAction>
+        <LogoHeader />
+
+        <SubTitle>
+          <i18n.Translate>Digital cash refund</i18n.Translate>
+        </SubTitle>
+        <section>
           <p>
-            <i18n.Translate>
-              Refund processing is still in progress.
-            </i18n.Translate>
+            <i18n.Translate>The refund is in progress.</i18n.Translate>
           </p>
-        ) : null}
-        {!Amounts.isZero(applyResult.amountRefundGone) ? (
+        </section>
+        <section>
+          <Part
+            big
+            title={<i18n.Translate>Total to refund</i18n.Translate>}
+            text={<Amount value={state.amount} />}
+            kind="negative"
+          />
+        </section>
+        {state.products && state.products.length ? (
+          <section>
+            <ProductList products={state.products} />
+          </section>
+        ) : undefined}
+        <section>
+          <ProgressBar value={state.progress} />
+        </section>
+      </WalletAction>
+    );
+  }
+
+  if (state.status === "completed") {
+    return (
+      <WalletAction>
+        <LogoHeader />
+
+        <SubTitle>
+          <i18n.Translate>Digital cash refund</i18n.Translate>
+        </SubTitle>
+        <section>
           <p>
-            <i18n.Translate>
-              The refund amount of{" "}
-              <AmountView amount={applyResult.amountRefundGone} /> could not be
-              applied.
-            </i18n.Translate>
+            <i18n.Translate>this refund is already accepted.</i18n.Translate>
           </p>
-        ) : null}
-      </article>
-    </section>
+        </section>
+      </WalletAction>
+    );
+  }
+
+  return (
+    <WalletAction>
+      <LogoHeader />
+
+      <SubTitle>
+        <i18n.Translate>Digital cash refund</i18n.Translate>
+      </SubTitle>
+      <section>
+        <p>
+          <i18n.Translate>
+            The merchant &quot;<b>{state.merchantName}</b>&quot; is offering 
you
+            a refund.
+          </i18n.Translate>
+        </p>
+      </section>
+      <section>
+        <Part
+          big
+          title={<i18n.Translate>Total to refund</i18n.Translate>}
+          text={<Amount value={state.amount} />}
+          kind="negative"
+        />
+      </section>
+      {state.products && state.products.length ? (
+        <section>
+          <ProductList products={state.products} />
+        </section>
+      ) : undefined}
+      <section>
+        <ButtonSuccess onClick={state.accept.onClick}>
+          <i18n.Translate>Confirm refund</i18n.Translate>
+        </ButtonSuccess>
+        <Button onClick={state.ignore.onClick}>
+          <i18n.Translate>Ignore</i18n.Translate>
+        </Button>
+      </section>
+    </WalletAction>
   );
 }
-export function RefundPage({ talerRefundUri }: Props): VNode {
-  const [applyResult, setApplyResult] = useState<
-    ApplyRefundResponse | undefined
-  >(undefined);
-  const { i18n } = useTranslationContext();
-  const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
+
+type State = Loading | Ready | Ignored | InProgress | Completed;
+
+interface Loading {
+  status: "loading";
+  hook: HookError | undefined;
+}
+interface Ready {
+  status: "ready";
+  hook: undefined;
+  merchantName: string;
+  products: Product[] | undefined;
+  amount: AmountJson;
+  accept: ButtonHandler;
+  ignore: ButtonHandler;
+  orderId: string;
+}
+interface Ignored {
+  status: "ignored";
+  hook: undefined;
+  merchantName: string;
+}
+interface InProgress {
+  status: "in-progress";
+  hook: undefined;
+  merchantName: string;
+  products: Product[] | undefined;
+  amount: AmountJson;
+  progress: number;
+}
+interface Completed {
+  status: "completed";
+  hook: undefined;
+  merchantName: string;
+  products: Product[] | undefined;
+  amount: AmountJson;
+}
+
+export function useComponentState(
+  talerRefundUri: string | undefined,
+  api: typeof wxApi,
+): State {
+  const [ignored, setIgnored] = useState(false);
+
+  const info = useAsyncAsHook(async () => {
+    if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
+    const refund = await api.prepareRefund({ talerRefundUri });
+    return { refund, uri: talerRefundUri };
+  });
 
   useEffect(() => {
-    if (!talerRefundUri) return;
-    const doFetch = async (): Promise<void> => {
-      try {
-        const result = await wxApi.applyRefund(talerRefundUri);
-        setApplyResult(result);
-      } catch (e) {
-        if (e instanceof Error) {
-          setErrMsg(e.message);
-          console.log("err message", e.message);
-        }
-      }
+    api.onUpdateNotification([NotificationType.RefreshMelted], () => {
+      info?.retry();
+    });
+  });
+
+  if (!info || info.hasError) {
+    return {
+      status: "loading",
+      hook: info,
     };
-    doFetch();
-  }, [talerRefundUri]);
+  }
 
-  console.log("rendering");
+  const { refund, uri } = info.response;
 
-  if (!talerRefundUri) {
-    return (
-      <span>
-        <i18n.Translate>missing taler refund uri</i18n.Translate>
-      </span>
-    );
+  const doAccept = async (): Promise<void> => {
+    await api.applyRefund(uri);
+    info.retry();
+  };
+
+  const doIgnore = async (): Promise<void> => {
+    setIgnored(true);
+  };
+
+  if (ignored) {
+    return {
+      status: "ignored",
+      hook: undefined,
+      merchantName: info.response.refund.info.merchant.name,
+    };
   }
 
-  if (errMsg) {
-    return (
-      <span>
-        <i18n.Translate>Error: {errMsg}</i18n.Translate>
-      </span>
-    );
+  const pending = refund.total > refund.applied + refund.failed;
+  const completed = refund.total > 0 && refund.applied === refund.total;
+
+  if (pending) {
+    return {
+      status: "in-progress",
+      hook: undefined,
+      amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
+      merchantName: info.response.refund.info.merchant.name,
+      products: info.response.refund.info.products,
+      progress: (refund.applied + refund.failed) / refund.total,
+    };
   }
 
-  if (!applyResult) {
+  if (completed) {
+    return {
+      status: "completed",
+      hook: undefined,
+      amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
+      merchantName: info.response.refund.info.merchant.name,
+      products: info.response.refund.info.products,
+    };
+  }
+
+  return {
+    status: "ready",
+    hook: undefined,
+    amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
+    merchantName: info.response.refund.info.merchant.name,
+    products: info.response.refund.info.products,
+    orderId: info.response.refund.info.orderId,
+    accept: {
+      onClick: doAccept,
+    },
+    ignore: {
+      onClick: doIgnore,
+    },
+  };
+}
+
+export function RefundPage({ talerRefundUri }: Props): VNode {
+  const { i18n } = useTranslationContext();
+
+  const state = useComponentState(talerRefundUri, wxApi);
+
+  if (!talerRefundUri) {
     return (
       <span>
-        <i18n.Translate>Updating refund status</i18n.Translate>
+        <i18n.Translate>missing taler refund uri</i18n.Translate>
       </span>
     );
   }
 
-  return <View applyResult={applyResult} />;
+  return <View state={state} />;
 }
 
-export function renderAmount(amount: AmountJson | string): VNode {
-  let a;
-  if (typeof amount === "string") {
-    a = Amounts.parse(amount);
-  } else {
-    a = amount;
-  }
-  if (!a) {
-    return <span>(invalid amount)</span>;
-  }
-  const x = a.value + a.fraction / amountFractionalBase;
+function ProgressBar({ value }: { value: number }): VNode {
   return (
-    <span>
-      {x}&nbsp;{a.currency}
-    </span>
+    <div
+      style={{
+        width: 400,
+        height: 20,
+        backgroundColor: "white",
+        border: "solid black 1px",
+      }}
+    >
+      <div
+        style={{
+          width: `${value * 100}%`,
+          height: "100%",
+          backgroundColor: "lightgreen",
+        }}
+      ></div>
+    </div>
   );
 }
-
-function AmountView({ amount }: { amount: AmountJson | string }): VNode {
-  return renderAmount(amount);
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
index debf64aa..0d6102d8 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
@@ -19,7 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { Amounts } from "@gnu-taler/taler-util";
 import { createExample } from "../test-utils.js";
 import { View as TestedComponent } from "./Tip.js";
 
@@ -30,25 +30,23 @@ export default {
 };
 
 export const Accepted = createExample(TestedComponent, {
-  prepareTipResult: {
-    accepted: true,
-    merchantBaseUrl: "",
+  state: {
+    status: "accepted",
+    hook: undefined,
+    amount: Amounts.parseOrThrow("EUR:1"),
     exchangeBaseUrl: "",
-    expirationTimestamp: TalerProtocolTimestamp.fromSeconds(1),
-    tipAmountEffective: "USD:10",
-    tipAmountRaw: "USD:5",
-    walletTipId: "id",
+    merchantBaseUrl: "",
   },
 });
 
-export const NotYetAccepted = createExample(TestedComponent, {
-  prepareTipResult: {
-    accepted: false,
+export const Ready = createExample(TestedComponent, {
+  state: {
+    status: "ready",
+    hook: undefined,
+    amount: Amounts.parseOrThrow("EUR:1"),
     merchantBaseUrl: "http://merchant.url/";,
     exchangeBaseUrl: "http://exchange.url/";,
-    expirationTimestamp: TalerProtocolTimestamp.fromSeconds(1),
-    tipAmountEffective: "USD:10",
-    tipAmountRaw: "USD:5",
-    walletTipId: "id",
+    accept: {},
+    ignore: {},
   },
 });
diff --git a/packages/taler-wallet-webextension/src/cta/Tip.test.ts 
b/packages/taler-wallet-webextension/src/cta/Tip.test.ts
new file mode 100644
index 00000000..0eda9b5b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Tip.test.ts
@@ -0,0 +1,192 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts, PrepareTipResult } from "@gnu-taler/taler-util";
+import { expect } from "chai";
+import { mountHook } from "../test-utils.js";
+import { useComponentState } from "./Tip.jsx";
+
+describe("Tip CTA states", () => {
+  it("should tell the user that the URI is missing", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(undefined, {
+        prepareTip: async () => ({}),
+        acceptTip: async () => ({})
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading')
+      if (!hook) expect.fail();
+      if (!hook.hasError) expect.fail();
+      if (hook.operational) expect.fail();
+      expect(hook.message).eq("ERROR_NO-URI-FOR-TIP");
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should be ready for accepting the tip", async () => {
+    let tipAccepted = false;
+
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState("taler://tip/asd", {
+        prepareTip: async () => ({
+          accepted: tipAccepted,
+          exchangeBaseUrl: "exchange url",
+          merchantBaseUrl: "merchant url",
+          tipAmountEffective: "EUR:1",
+          walletTipId: "tip_id",
+        } as PrepareTipResult as any),
+        acceptTip: async () => {
+          tipAccepted = true
+        }
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== "ready") expect.fail()
+      if (state.hook) expect.fail();
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
+      expect(state.merchantBaseUrl).eq("merchant url");
+      expect(state.exchangeBaseUrl).eq("exchange url");
+      if (state.accept.onClick === undefined) expect.fail();
+
+      state.accept.onClick();
+    }
+
+    await waitNextUpdate()
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== "accepted") expect.fail()
+      if (state.hook) expect.fail();
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
+      expect(state.merchantBaseUrl).eq("merchant url");
+      expect(state.exchangeBaseUrl).eq("exchange url");
+
+    }
+    await assertNoPendingUpdate()
+  });
+
+  it("should be ignored after clicking the ignore button", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState("taler://tip/asd", {
+        prepareTip: async () => ({
+          exchangeBaseUrl: "exchange url",
+          merchantBaseUrl: "merchant url",
+          tipAmountEffective: "EUR:1",
+          walletTipId: "tip_id",
+        } as PrepareTipResult as any),
+        acceptTip: async () => ({})
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== "ready") expect.fail()
+      if (state.hook) expect.fail();
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
+      expect(state.merchantBaseUrl).eq("merchant url");
+      expect(state.exchangeBaseUrl).eq("exchange url");
+      if (state.ignore.onClick === undefined) expect.fail();
+
+      state.ignore.onClick();
+    }
+
+    await waitNextUpdate()
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== "ignored") expect.fail()
+      if (state.hook) expect.fail();
+
+    }
+    await assertNoPendingUpdate()
+  });
+
+  it("should render accepted if the tip has been used previously", async () => 
{
+
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState("taler://tip/asd", {
+        prepareTip: async () => ({
+          accepted: true,
+          exchangeBaseUrl: "exchange url",
+          merchantBaseUrl: "merchant url",
+          tipAmountEffective: "EUR:1",
+          walletTipId: "tip_id",
+        } as PrepareTipResult as any),
+        acceptTip: async () => ({})
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const state = getLastResultOrThrow()
+
+      if (state.status !== "accepted") expect.fail()
+      if (state.hook) expect.fail();
+      expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
+      expect(state.merchantBaseUrl).eq("merchant url");
+      expect(state.exchangeBaseUrl).eq("exchange url");
+
+    }
+    await assertNoPendingUpdate()
+  });
+
+
+});
\ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/cta/Tip.tsx 
b/packages/taler-wallet-webextension/src/cta/Tip.tsx
index 071243f3..dc4757b3 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Tip.tsx
@@ -20,146 +20,218 @@
  * @author sebasjm
  */
 
-import {
-  amountFractionalBase,
-  AmountJson,
-  Amounts,
-  PrepareTipResult,
-} from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, PrepareTipResult } from "@gnu-taler/taler-util";
 import { h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
+import { Amount } from "../components/Amount.js";
 import { Loading } from "../components/Loading.js";
-import { Title } from "../components/styled/index.js";
+import { LoadingError } from "../components/LoadingError.js";
+import { LogoHeader } from "../components/LogoHeader.js";
+import { Part } from "../components/Part.js";
+import {
+  Button,
+  ButtonSuccess,
+  SubTitle,
+  WalletAction,
+} from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { ButtonHandler } from "../mui/handlers.js";
 import * as wxApi from "../wxApi.js";
 
 interface Props {
   talerTipUri?: string;
 }
-export interface ViewProps {
-  prepareTipResult: PrepareTipResult;
-  onAccept: () => void;
-  onIgnore: () => void;
-}
-export function View({
-  prepareTipResult,
-  onAccept,
-  onIgnore,
-}: ViewProps): VNode {
-  const { i18n } = useTranslationContext();
-  return (
-    <section class="main">
-      <Title>GNU Taler Wallet</Title>
-      <article class="fade">
-        {prepareTipResult.accepted ? (
-          <span>
-            <i18n.Translate>
-              Tip from <code>{prepareTipResult.merchantBaseUrl}</code> 
accepted.
-              Check your transactions list for more details.
-            </i18n.Translate>
-          </span>
-        ) : (
-          <div>
-            <p>
-              <i18n.Translate>
-                The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is
-                offering you a tip of{" "}
-                <strong>
-                  <AmountView amount={prepareTipResult.tipAmountEffective} />
-                </strong>{" "}
-                via the exchange 
<code>{prepareTipResult.exchangeBaseUrl}</code>
-              </i18n.Translate>
-            </p>
-            <button onClick={onAccept}>
-              <i18n.Translate>Accept tip</i18n.Translate>
-            </button>
-            <button onClick={onIgnore}>
-              <i18n.Translate>Ignore</i18n.Translate>
-            </button>
-          </div>
-        )}
-      </article>
-    </section>
-  );
+
+type State = Loading | Ready | Accepted | Ignored;
+
+interface Loading {
+  status: "loading";
+  hook: HookError | undefined;
 }
 
-export function TipPage({ talerTipUri }: Props): VNode {
-  const { i18n } = useTranslationContext();
-  const [updateCounter, setUpdateCounter] = useState<number>(0);
-  const [prepareTipResult, setPrepareTipResult] = useState<
-    PrepareTipResult | undefined
-  >(undefined);
+interface Ignored {
+  status: "ignored";
+  hook: undefined;
+}
+interface Accepted {
+  status: "accepted";
+  hook: undefined;
+  merchantBaseUrl: string;
+  amount: AmountJson;
+  exchangeBaseUrl: string;
+}
+interface Ready {
+  status: "ready";
+  hook: undefined;
+  merchantBaseUrl: string;
+  amount: AmountJson;
+  exchangeBaseUrl: string;
+  accept: ButtonHandler;
+  ignore: ButtonHandler;
+}
 
+export function useComponentState(
+  talerTipUri: string | undefined,
+  api: typeof wxApi,
+): State {
   const [tipIgnored, setTipIgnored] = useState(false);
 
-  useEffect(() => {
-    if (!talerTipUri) return;
-    const doFetch = async (): Promise<void> => {
-      const p = await wxApi.prepareTip({ talerTipUri });
-      setPrepareTipResult(p);
+  const tipInfo = useAsyncAsHook(async () => {
+    if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP");
+    const tip = await api.prepareTip({ talerTipUri });
+    return { tip };
+  });
+
+  if (!tipInfo || tipInfo.hasError) {
+    return {
+      status: "loading",
+      hook: tipInfo,
     };
-    doFetch();
-  }, [talerTipUri, updateCounter]);
+  }
+
+  const { tip } = tipInfo.response;
 
   const doAccept = async (): Promise<void> => {
-    if (!prepareTipResult) {
-      return;
-    }
-    await wxApi.acceptTip({ walletTipId: prepareTipResult?.walletTipId });
-    setUpdateCounter(updateCounter + 1);
+    await api.acceptTip({ walletTipId: tip.walletTipId });
+    tipInfo.retry();
   };
 
-  const doIgnore = (): void => {
+  const doIgnore = async (): Promise<void> => {
     setTipIgnored(true);
   };
 
-  if (!talerTipUri) {
+  if (tipIgnored) {
+    return {
+      status: "ignored",
+      hook: undefined,
+    };
+  }
+
+  if (tip.accepted) {
+    return {
+      status: "accepted",
+      hook: undefined,
+      merchantBaseUrl: tip.merchantBaseUrl,
+      exchangeBaseUrl: tip.exchangeBaseUrl,
+      amount: Amounts.parseOrThrow(tip.tipAmountEffective),
+    };
+  }
+
+  return {
+    status: "ready",
+    hook: undefined,
+    merchantBaseUrl: tip.merchantBaseUrl,
+    exchangeBaseUrl: tip.exchangeBaseUrl,
+    accept: {
+      onClick: doAccept,
+    },
+    ignore: {
+      onClick: doIgnore,
+    },
+    amount: Amounts.parseOrThrow(tip.tipAmountEffective),
+  };
+}
+
+export function View({ state }: { state: State }): VNode {
+  const { i18n } = useTranslationContext();
+  if (state.status === "loading") {
+    if (!state.hook) {
+      return <Loading />;
+    }
     return (
-      <span>
-        <i18n.Translate>missing tip uri</i18n.Translate>
-      </span>
+      <LoadingError
+        title={<i18n.Translate>Could not load tip status</i18n.Translate>}
+        error={state.hook}
+      />
     );
   }
 
-  if (tipIgnored) {
+  if (state.status === "ignored") {
     return (
-      <span>
-        <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
-      </span>
+      <WalletAction>
+        <LogoHeader />
+
+        <SubTitle>
+          <i18n.Translate>Digital cash tip</i18n.Translate>
+        </SubTitle>
+        <span>
+          <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
+        </span>
+      </WalletAction>
     );
   }
 
-  if (!prepareTipResult) {
-    return <Loading />;
+  if (state.status === "accepted") {
+    return (
+      <WalletAction>
+        <LogoHeader />
+
+        <SubTitle>
+          <i18n.Translate>Digital cash tip</i18n.Translate>
+        </SubTitle>
+        <section>
+          <i18n.Translate>
+            Tip from <code>{state.merchantBaseUrl}</code> accepted. Check your
+            transactions list for more details.
+          </i18n.Translate>
+        </section>
+      </WalletAction>
+    );
   }
 
   return (
-    <View
-      prepareTipResult={prepareTipResult}
-      onAccept={doAccept}
-      onIgnore={doIgnore}
-    />
+    <WalletAction>
+      <LogoHeader />
+
+      <SubTitle>
+        <i18n.Translate>Digital cash tip</i18n.Translate>
+      </SubTitle>
+
+      <section>
+        <p>
+          <i18n.Translate>The merchant is offering you a tip</i18n.Translate>
+        </p>
+        <Part
+          title={<i18n.Translate>Amount</i18n.Translate>}
+          text={<Amount value={state.amount} />}
+          kind="positive"
+          big
+        />
+        <Part
+          title={<i18n.Translate>Merchant URL</i18n.Translate>}
+          text={state.merchantBaseUrl}
+          kind="neutral"
+        />
+        <Part
+          title={<i18n.Translate>Exchange</i18n.Translate>}
+          text={state.exchangeBaseUrl}
+          kind="neutral"
+        />
+      </section>
+      <section>
+        <ButtonSuccess onClick={state.accept.onClick}>
+          <i18n.Translate>Accept tip</i18n.Translate>
+        </ButtonSuccess>
+        <Button onClick={state.ignore.onClick}>
+          <i18n.Translate>Ignore</i18n.Translate>
+        </Button>
+      </section>
+    </WalletAction>
   );
 }
 
-function renderAmount(amount: AmountJson | string): VNode {
-  let a;
-  if (typeof amount === "string") {
-    a = Amounts.parse(amount);
-  } else {
-    a = amount;
-  }
-  if (!a) {
-    return <span>(invalid amount)</span>;
+export function TipPage({ talerTipUri }: Props): VNode {
+  const { i18n } = useTranslationContext();
+  const state = useComponentState(talerTipUri, wxApi);
+
+  if (!talerTipUri) {
+    return (
+      <span>
+        <i18n.Translate>missing tip uri</i18n.Translate>
+      </span>
+    );
   }
-  const x = a.value + a.fraction / amountFractionalBase;
-  return (
-    <span>
-      {x}&nbsp;{a.currency}
-    </span>
-  );
-}
 
-function AmountView({ amount }: { amount: AmountJson | string }): VNode {
-  return renderAmount(amount);
+  return <View state={state} />;
 }
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx 
b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 584fe427..6f7c208d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -515,13 +515,13 @@ export function TransactionView({
         <Part
           big
           title={<i18n.Translate>Total tip</i18n.Translate>}
-          text={<Amount value={transaction.amountEffective} />}
+          text={<Amount value={transaction.amountRaw} />}
           kind="positive"
         />
         <Part
           big
           title={<i18n.Translate>Received amount</i18n.Translate>}
-          text={<Amount value={transaction.amountRaw} />}
+          text={<Amount value={transaction.amountEffective} />}
           kind="neutral"
         />
         <Part
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts 
b/packages/taler-wallet-webextension/src/wxApi.ts
index 3079392b..d2e90305 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -44,6 +44,8 @@ import {
   KnownBankAccounts,
   NotificationType,
   PreparePayResult,
+  PrepareRefundRequest,
+  PrepareRefundResult,
   PrepareTipRequest,
   PrepareTipResult,
   RetryTransactionRequest,
@@ -405,6 +407,11 @@ export function addExchange(req: AddExchangeRequest): 
Promise<void> {
   return callBackend("addExchange", req);
 }
 
+export function prepareRefund(req: PrepareRefundRequest): 
Promise<PrepareRefundResult> {
+  return callBackend("prepareRefund", req);
+}
+
+
 export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
   return callBackend("prepareTip", req);
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

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