gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: fix 7465


From: gnunet
Subject: [taler-wallet-core] branch master updated: fix 7465
Date: Fri, 25 Nov 2022 03:16:10 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new e05ba843a fix 7465
e05ba843a is described below

commit e05ba843a061c8050648ce922f36ed3d8e1cf24a
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Nov 24 23:16:01 2022 -0300

    fix 7465
---
 packages/taler-util/src/taleruri.ts                |  17 ++
 packages/taler-util/src/wallet-types.ts            | 189 +++++++++++++++++-
 packages/taler-wallet-core/src/db.ts               |  27 +++
 .../taler-wallet-core/src/operations/attention.ts  | 145 ++++++++++++++
 .../src/operations/backup/index.ts                 | 140 ++++++++++---
 .../src/operations/pay-merchant.ts                 |  43 +++-
 .../src/util/assertUnreachable.ts                  |   2 +-
 packages/taler-wallet-core/src/wallet-api-types.ts |  37 ++++
 packages/taler-wallet-core/src/wallet.ts           |  19 ++
 .../src/NavigationBar.tsx                          |  45 ++++-
 .../src/components/AmountField.stories.tsx         |   1 -
 .../src/components/TransactionItem.tsx             |   4 +-
 .../src/cta/InvoicePay/state.ts                    |   1 +
 .../src/cta/Payment/index.ts                       |  17 +-
 .../src/cta/Payment/state.ts                       |   8 +
 .../src/cta/Payment/stories.tsx                    |   8 +
 .../src/cta/Payment/views.tsx                      |  25 ++-
 .../src/popup/Application.tsx                      |   4 +
 .../src/wallet/AddBackupProvider/state.ts          |   6 +-
 .../src/wallet/Application.tsx                     |  12 +-
 .../src/wallet/Backup.stories.tsx                  |   2 +
 .../src/wallet/Notifications/index.ts              |  61 ++++++
 .../src/wallet/Notifications/state.ts              |  48 +++++
 .../src/wallet/Notifications/stories.tsx           |  58 ++++++
 .../src/wallet/Notifications/test.ts}              |  17 +-
 .../src/wallet/Notifications/views.tsx             | 220 +++++++++++++++++++++
 .../src/wallet/ProviderDetail.stories.tsx          |   2 +
 .../src/wallet/ProviderDetailPage.tsx              |  81 +++++---
 .../src/wallet/index.stories.tsx                   |   2 +
 29 files changed, 1167 insertions(+), 74 deletions(-)

diff --git a/packages/taler-util/src/taleruri.ts 
b/packages/taler-util/src/taleruri.ts
index 13cdde9a9..4e47acbce 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -216,6 +216,23 @@ export function parsePayUri(s: string): PayUriResult | 
undefined {
   };
 }
 
+export function constructPayUri(
+  merchantBaseUrl: string,
+  orderId: string,
+  sessionId: string,
+  claimToken?: string,
+  noncePriv?: string,
+): string {
+  const base = canonicalizeBaseUrl(merchantBaseUrl);
+  const url = new URL(base);
+  const isHttp = base.startsWith("http://";);
+  let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
+  result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`;
+  if (claimToken) result += `c=${claimToken}`;
+  if (noncePriv) result += `n=${noncePriv}`;
+  return result;
+}
+
 export function parsePayPushUri(s: string): PayPushUriResult | undefined {
   const pi = parseProtoInfo(s, talerActionPayPush);
   if (!pi) {
diff --git a/packages/taler-util/src/wallet-types.ts 
b/packages/taler-util/src/wallet-types.ts
index 900fb7407..0c837f2d0 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -40,6 +40,7 @@ import {
   codecForAny,
   codecForBoolean,
   codecForConstString,
+  codecForEither,
   codecForList,
   codecForMap,
   codecForNumber,
@@ -384,6 +385,7 @@ export enum PreparePayResultType {
   PaymentPossible = "payment-possible",
   InsufficientBalance = "insufficient-balance",
   AlreadyConfirmed = "already-confirmed",
+  Lost = "lost",
 }
 
 export const codecForPreparePayResultPaymentPossible =
@@ -394,6 +396,7 @@ export const codecForPreparePayResultPaymentPossible =
       .property("contractTerms", codecForMerchantContractTerms())
       .property("proposalId", codecForString())
       .property("contractTermsHash", codecForString())
+      .property("talerUri", codecForString())
       .property("noncePriv", codecForString())
       .property(
         "status",
@@ -406,6 +409,7 @@ export const codecForPreparePayResultInsufficientBalance =
     buildCodecForObject<PreparePayResultInsufficientBalance>()
       .property("amountRaw", codecForAmountString())
       .property("contractTerms", codecForAny())
+      .property("talerUri", codecForString())
       .property("proposalId", codecForString())
       .property("noncePriv", codecForString())
       .property(
@@ -424,11 +428,18 @@ export const codecForPreparePayResultAlreadyConfirmed =
       .property("amountEffective", codecForAmountString())
       .property("amountRaw", codecForAmountString())
       .property("paid", codecForBoolean())
+      .property("talerUri", codecOptional(codecForString()))
       .property("contractTerms", codecForAny())
       .property("contractTermsHash", codecForString())
       .property("proposalId", codecForString())
       .build("PreparePayResultAlreadyConfirmed");
 
+export const codecForPreparePayResultPaymentLost =
+  (): Codec<PreparePayResultPaymentLost> =>
+    buildCodecForObject<PreparePayResultPaymentLost>()
+      .property("status", codecForConstString(PreparePayResultType.Lost))
+      .build("PreparePayResultLost");
+
 export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
   buildCodecForUnion<PreparePayResult>()
     .discriminateOn("status")
@@ -444,6 +455,10 @@ export const codecForPreparePayResult = (): 
Codec<PreparePayResult> =>
       PreparePayResultType.PaymentPossible,
       codecForPreparePayResultPaymentPossible(),
     )
+    .alternative(
+      PreparePayResultType.Lost,
+      codecForPreparePayResultPaymentLost(),
+    )
     .build("PreparePayResult");
 
 /**
@@ -452,7 +467,8 @@ export const codecForPreparePayResult = (): 
Codec<PreparePayResult> =>
 export type PreparePayResult =
   | PreparePayResultInsufficientBalance
   | PreparePayResultAlreadyConfirmed
-  | PreparePayResultPaymentPossible;
+  | PreparePayResultPaymentPossible
+  | PreparePayResultPaymentLost;
 
 /**
  * Payment is possible.
@@ -465,6 +481,7 @@ export interface PreparePayResultPaymentPossible {
   amountRaw: string;
   amountEffective: string;
   noncePriv: string;
+  talerUri: string;
 }
 
 export interface PreparePayResultInsufficientBalance {
@@ -473,6 +490,7 @@ export interface PreparePayResultInsufficientBalance {
   contractTerms: MerchantContractTerms;
   amountRaw: string;
   noncePriv: string;
+  talerUri: string;
 }
 
 export interface PreparePayResultAlreadyConfirmed {
@@ -483,6 +501,11 @@ export interface PreparePayResultAlreadyConfirmed {
   amountEffective: string;
   contractTermsHash: string;
   proposalId: string;
+  talerUri?: string;
+}
+
+export interface PreparePayResultPaymentLost {
+  status: PreparePayResultType.Lost;
 }
 
 export interface BankWithdrawDetails {
@@ -1677,6 +1700,170 @@ export interface WithdrawFakebankRequest {
   bank: string;
 }
 
+export enum AttentionPriority {
+  High = "high",
+  Medium = "medium",
+  Low = "low",
+}
+
+export interface UserAttentionByIdRequest {
+  entityId: string;
+  type: AttentionType;
+}
+
+export const codecForUserAttentionByIdRequest =
+  (): Codec<UserAttentionByIdRequest> =>
+    buildCodecForObject<UserAttentionByIdRequest>()
+      .property("type", codecForAny())
+      .property("entityId", codecForString())
+      .build("UserAttentionByIdRequest");
+
+export const codecForUserAttentionsRequest = (): Codec<UserAttentionsRequest> 
=>
+  buildCodecForObject<UserAttentionsRequest>()
+    .property(
+      "priority",
+      codecOptional(
+        codecForEither(
+          codecForConstString(AttentionPriority.Low),
+          codecForConstString(AttentionPriority.Medium),
+          codecForConstString(AttentionPriority.High),
+        ),
+      ),
+    )
+    .build("UserAttentionsRequest");
+
+export interface UserAttentionsRequest {
+  priority?: AttentionPriority;
+}
+
+export type AttentionInfo =
+  | AttentionKycWithdrawal
+  | AttentionBackupUnpaid
+  | AttentionBackupExpiresSoon
+  | AttentionMerchantRefund
+  | AttentionExchangeTosChanged
+  | AttentionExchangeKeyExpired
+  | AttentionExchangeDenominationExpired
+  | AttentionAuditorTosChanged
+  | AttentionAuditorKeyExpires
+  | AttentionAuditorDenominationExpires
+  | AttentionPullPaymentPaid
+  | AttentionPushPaymentReceived;
+
+export enum AttentionType {
+  KycWithdrawal = "kyc-withdrawal",
+
+  BackupUnpaid = "backup-unpaid",
+  BackupExpiresSoon = "backup-expires-soon",
+  MerchantRefund = "merchant-refund",
+
+  ExchangeTosChanged = "exchange-tos-changed",
+  ExchangeKeyExpired = "exchange-key-expired",
+  ExchangeKeyExpiresSoon = "exchange-key-expires-soon",
+  ExchangeDenominationsExpired = "exchange-denominations-expired",
+  ExchangeDenominationsExpiresSoon = "exchange-denominations-expires-soon",
+
+  AuditorTosChanged = "auditor-tos-changed",
+  AuditorKeyExpires = "auditor-key-expires",
+  AuditorDenominationsExpires = "auditor-denominations-expires",
+
+  PullPaymentPaid = "pull-payment-paid",
+  PushPaymentReceived = "push-payment-withdrawn",
+}
+
+export const UserAttentionPriority: {
+  [type in AttentionType]: AttentionPriority;
+} = {
+  "kyc-withdrawal": AttentionPriority.Medium,
+
+  "backup-unpaid": AttentionPriority.High,
+  "backup-expires-soon": AttentionPriority.Medium,
+  "merchant-refund": AttentionPriority.Medium,
+
+  "exchange-tos-changed": AttentionPriority.Medium,
+
+  "exchange-key-expired": AttentionPriority.High,
+  "exchange-key-expires-soon": AttentionPriority.Medium,
+  "exchange-denominations-expired": AttentionPriority.High,
+  "exchange-denominations-expires-soon": AttentionPriority.Medium,
+
+  "auditor-tos-changed": AttentionPriority.Medium,
+  "auditor-key-expires": AttentionPriority.Medium,
+  "auditor-denominations-expires": AttentionPriority.Medium,
+
+  "pull-payment-paid": AttentionPriority.High,
+  "push-payment-withdrawn": AttentionPriority.High,
+};
+
+interface AttentionBackupExpiresSoon {
+  type: AttentionType.BackupExpiresSoon;
+  provider_base_url: string;
+}
+interface AttentionBackupUnpaid {
+  type: AttentionType.BackupUnpaid;
+  provider_base_url: string;
+  talerUri: string;
+}
+
+interface AttentionMerchantRefund {
+  type: AttentionType.MerchantRefund;
+  transactionId: string;
+}
+
+interface AttentionKycWithdrawal {
+  type: AttentionType.KycWithdrawal;
+  transactionId: string;
+}
+
+interface AttentionExchangeTosChanged {
+  type: AttentionType.ExchangeTosChanged;
+  exchange_base_url: string;
+}
+interface AttentionExchangeKeyExpired {
+  type: AttentionType.ExchangeKeyExpired;
+  exchange_base_url: string;
+}
+interface AttentionExchangeDenominationExpired {
+  type: AttentionType.ExchangeDenominationsExpired;
+  exchange_base_url: string;
+}
+interface AttentionAuditorTosChanged {
+  type: AttentionType.AuditorTosChanged;
+  auditor_base_url: string;
+}
+
+interface AttentionAuditorKeyExpires {
+  type: AttentionType.AuditorKeyExpires;
+  auditor_base_url: string;
+}
+interface AttentionAuditorDenominationExpires {
+  type: AttentionType.AuditorDenominationsExpires;
+  auditor_base_url: string;
+}
+interface AttentionPullPaymentPaid {
+  type: AttentionType.PullPaymentPaid;
+  transactionId: string;
+}
+
+interface AttentionPushPaymentReceived {
+  type: AttentionType.PushPaymentReceived;
+  transactionId: string;
+}
+
+export type UserAttentionUnreadList = Array<{
+  info: AttentionInfo;
+  when: AbsoluteTime;
+  read: boolean;
+}>;
+
+export interface UserAttentionsResponse {
+  pending: UserAttentionUnreadList;
+}
+
+export interface UserAttentionsCountResponse {
+  total: number;
+}
+
 export const codecForWithdrawFakebankRequest =
   (): Codec<WithdrawFakebankRequest> =>
     buildCodecForObject<WithdrawFakebankRequest>()
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index bbd93f669..2bf417cac 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -48,6 +48,9 @@ import {
   WireInfo,
   HashCodeString,
   Amounts,
+  AttentionPriority,
+  AttentionInfo,
+  AbsoluteTime,
 } from "@gnu-taler/taler-util";
 import {
   describeContents,
@@ -1540,6 +1543,8 @@ export interface BackupProviderRecord {
    */
   currentPaymentProposalId?: string;
 
+  shouldRetryFreshProposal: boolean;
+
   /**
    * Proposals that were used to pay (or attempt to pay) the provider.
    *
@@ -1841,6 +1846,21 @@ export interface ContractTermsRecord {
   contractTermsRaw: any;
 }
 
+export interface UserAttentionRecord {
+  info: AttentionInfo;
+
+  entityId: string;
+  /**
+   * When the notification was created.
+   */
+  createdMs: number;
+
+  /**
+   * When the user mark this notification as read.
+   */
+  read: TalerProtocolTimestamp | undefined;
+}
+
 /**
  * Schema definition for the IndexedDB
  * wallet database.
@@ -2137,6 +2157,13 @@ export const WalletStoresV1 = {
     }),
     {},
   ),
+  userAttention: describeStore(
+    "userAttention",
+    describeContents<UserAttentionRecord>({
+      keyPath: ["entityId", "info.type"],
+    }),
+    {},
+  ),
 };
 
 /**
diff --git a/packages/taler-wallet-core/src/operations/attention.ts 
b/packages/taler-wallet-core/src/operations/attention.ts
new file mode 100644
index 000000000..95db7bde0
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/attention.ts
@@ -0,0 +1,145 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  AbsoluteTime,
+  AttentionInfo,
+  Logger,
+  TalerProtocolTimestamp,
+  UserAttentionByIdRequest,
+  UserAttentionPriority,
+  UserAttentionsCountResponse,
+  UserAttentionsRequest,
+  UserAttentionsResponse,
+  UserAttentionUnreadList,
+} from "@gnu-taler/taler-util";
+import { InternalWalletState } from "../internal-wallet-state.js";
+
+const logger = new Logger("operations/attention.ts");
+
+export async function getUserAttentionsUnreadCount(
+  ws: InternalWalletState,
+  req: UserAttentionsRequest,
+): Promise<UserAttentionsCountResponse> {
+  const total = await ws.db
+    .mktx((x) => [x.userAttention])
+    .runReadOnly(async (tx) => {
+      let count = 0;
+      await tx.userAttention.iter().forEach((x) => {
+        if (
+          req.priority !== undefined &&
+          UserAttentionPriority[x.info.type] !== req.priority
+        )
+          return;
+        if (x.read !== undefined) return;
+        count++;
+      });
+
+      return count;
+    });
+
+  return { total };
+}
+
+export async function getUserAttentions(
+  ws: InternalWalletState,
+  req: UserAttentionsRequest,
+): Promise<UserAttentionsResponse> {
+  return await ws.db
+    .mktx((x) => [x.userAttention])
+    .runReadOnly(async (tx) => {
+      const pending: UserAttentionUnreadList = [];
+      await tx.userAttention.iter().forEach((x) => {
+        if (
+          req.priority !== undefined &&
+          UserAttentionPriority[x.info.type] !== req.priority
+        )
+          return;
+        pending.push({
+          info: x.info,
+          when: {
+            t_ms: x.createdMs,
+          },
+          read: x.read !== undefined,
+        });
+      });
+
+      return { pending };
+    });
+}
+
+export async function markAttentionRequestAsRead(
+  ws: InternalWalletState,
+  req: UserAttentionByIdRequest,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.userAttention])
+    .runReadWrite(async (tx) => {
+      const ua = await tx.userAttention.get([req.entityId, req.type]);
+      if (!ua) throw Error("attention request not found");
+      tx.userAttention.put({
+        ...ua,
+        read: TalerProtocolTimestamp.now(),
+      });
+    });
+}
+
+/**
+ * the wallet need the user attention to complete a task
+ * internal API
+ *
+ * @param ws
+ * @param info
+ */
+export async function addAttentionRequest(
+  ws: InternalWalletState,
+  info: AttentionInfo,
+  entityId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.userAttention])
+    .runReadWrite(async (tx) => {
+      await tx.userAttention.put({
+        info,
+        entityId,
+        createdMs: AbsoluteTime.now().t_ms as number,
+        read: undefined,
+      });
+    });
+}
+
+/**
+ * user completed the task, attention request is not needed
+ * internal API
+ *
+ * @param ws
+ * @param created
+ */
+export async function removeAttentionRequest(
+  ws: InternalWalletState,
+  req: UserAttentionByIdRequest,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.userAttention])
+    .runReadWrite(async (tx) => {
+      const ua = await tx.userAttention.get([req.entityId, req.type]);
+      if (!ua) throw Error("attention request not found");
+      await tx.userAttention.delete([req.entityId, req.type]);
+    });
+}
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index aed37b865..eef838b0c 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -27,6 +27,7 @@
 import {
   AbsoluteTime,
   AmountString,
+  AttentionType,
   BackupRecovery,
   buildCodecForObject,
   buildCodecForUnion,
@@ -57,13 +58,17 @@ import {
   kdf,
   Logger,
   notEmpty,
+  PaymentStatus,
+  PreparePayResult,
   PreparePayResultType,
   RecoveryLoadRequest,
   RecoveryMergeStrategy,
+  ReserveTransactionType,
   rsaBlind,
   secretbox,
   secretbox_open,
   stringToBytes,
+  TalerErrorCode,
   TalerErrorDetail,
   TalerProtocolTimestamp,
   URL,
@@ -80,6 +85,7 @@ import {
   ConfigRecordKey,
   WalletBackupConfState,
 } from "../../db.js";
+import { TalerError } from "../../errors.js";
 import { InternalWalletState } from "../../internal-wallet-state.js";
 import { assertUnreachable } from "../../util/assertUnreachable.js";
 import {
@@ -96,6 +102,7 @@ import {
   RetryTags,
   scheduleRetryInTx,
 } from "../../util/retries.js";
+import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
 import {
   checkPaymentByProposalId,
   confirmPay,
@@ -198,6 +205,7 @@ async function computeBackupCryptoData(
     );
   }
   for (const purch of backupContent.purchases) {
+    if (!purch.contract_terms_raw) continue;
     const { h: contractTermsHash } = await cryptoApi.hashString({
       str: canonicalJson(purch.contract_terms_raw),
     });
@@ -251,7 +259,7 @@ function getNextBackupTimestamp(): TalerProtocolTimestamp {
 async function runBackupCycleForProvider(
   ws: InternalWalletState,
   args: BackupForProviderArgs,
-): Promise<OperationAttemptResult<unknown, { talerUri: string }>> {
+): Promise<OperationAttemptResult<unknown, { talerUri?: string }>> {
   const provider = await ws.db
     .mktx((x) => [x.backupProviders])
     .runReadOnly(async (tx) => {
@@ -292,6 +300,10 @@ async function runBackupCycleForProvider(
     provider.baseUrl,
   );
 
+  if (provider.shouldRetryFreshProposal) {
+    accountBackupUrl.searchParams.set("fresh", "yes");
+  }
+
   const resp = await ws.http.fetch(accountBackupUrl.href, {
     method: "POST",
     body: encBackup,
@@ -324,6 +336,12 @@ async function runBackupCycleForProvider(
         };
         await tx.backupProviders.put(prov);
       });
+
+    removeAttentionRequest(ws, {
+      entityId: provider.baseUrl,
+      type: AttentionType.BackupUnpaid,
+    });
+
     return {
       type: OperationAttemptResultType.Finished,
       result: undefined,
@@ -340,8 +358,51 @@ async function runBackupCycleForProvider(
 
     //We can't delay downloading the proposal since we need the id
     //FIXME: check download errors
+    let res: PreparePayResult | undefined = undefined;
+    try {
+      res = await preparePayForUri(ws, talerUri);
+    } catch (e) {
+      const error = TalerError.fromException(e);
+      if (!error.hasErrorCode(TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED)) {
+        throw error;
+      }
+    }
+
+    if (
+      res === undefined ||
+      res.status === PreparePayResultType.AlreadyConfirmed
+    ) {
+      //claimed
+
+      await ws.db
+        .mktx((x) => [x.backupProviders, x.operationRetries])
+        .runReadWrite(async (tx) => {
+          const prov = await tx.backupProviders.get(provider.baseUrl);
+          if (!prov) {
+            logger.warn("backup provider not found anymore");
+            return;
+          }
+          const opId = RetryTags.forBackup(prov);
+          await scheduleRetryInTx(ws, tx, opId);
+          prov.shouldRetryFreshProposal = true;
+          prov.state = {
+            tag: BackupProviderStateTag.Retrying,
+          };
+          await tx.backupProviders.put(prov);
+        });
 
-    const res = await preparePayForUri(ws, talerUri);
+      return {
+        type: OperationAttemptResultType.Pending,
+        result: {
+          talerUri,
+        },
+      };
+    }
+    const result = res;
+
+    if (result.status === PreparePayResultType.Lost) {
+      throw Error("invalid state, could not get proposal for backup");
+    }
 
     await ws.db
       .mktx((x) => [x.backupProviders, x.operationRetries])
@@ -353,13 +414,24 @@ async function runBackupCycleForProvider(
         }
         const opId = RetryTags.forBackup(prov);
         await scheduleRetryInTx(ws, tx, opId);
-        prov.currentPaymentProposalId = res.proposalId;
+        prov.currentPaymentProposalId = result.proposalId;
+        prov.shouldRetryFreshProposal = false;
         prov.state = {
           tag: BackupProviderStateTag.Retrying,
         };
         await tx.backupProviders.put(prov);
       });
 
+    addAttentionRequest(
+      ws,
+      {
+        type: AttentionType.BackupUnpaid,
+        provider_base_url: provider.baseUrl,
+        talerUri,
+      },
+      provider.baseUrl,
+    );
+
     return {
       type: OperationAttemptResultType.Pending,
       result: {
@@ -384,6 +456,12 @@ async function runBackupCycleForProvider(
         };
         await tx.backupProviders.put(prov);
       });
+
+    removeAttentionRequest(ws, {
+      entityId: provider.baseUrl,
+      type: AttentionType.BackupUnpaid,
+    });
+
     return {
       type: OperationAttemptResultType.Finished,
       result: undefined,
@@ -564,7 +642,7 @@ interface AddBackupProviderOk {
 }
 interface AddBackupProviderPaymentRequired {
   status: "payment-required";
-  talerUri: string;
+  talerUri?: string;
 }
 interface AddBackupProviderError {
   status: "error";
@@ -580,7 +658,7 @@ export const codecForAddBackupProviderPaymenrRequired =
   (): Codec<AddBackupProviderPaymentRequired> =>
     buildCodecForObject<AddBackupProviderPaymentRequired>()
       .property("status", codecForConstString("payment-required"))
-      .property("talerUri", codecForString())
+      .property("talerUri", codecOptional(codecForString()))
       .build("AddBackupProviderPaymentRequired");
 
 export const codecForAddBackupProviderError =
@@ -655,6 +733,7 @@ export async function addBackupProvider(
           storageLimitInMegabytes: terms.storage_limit_in_megabytes,
           supportedProtocolVersion: terms.version,
         },
+        shouldRetryFreshProposal: false,
         paymentProposalIds: [],
         baseUrl: canonUrl,
         uids: [encodeCrock(getRandomBytes(32))],
@@ -779,10 +858,12 @@ export interface ProviderPaymentUnpaid {
 
 export interface ProviderPaymentInsufficientBalance {
   type: ProviderPaymentType.InsufficientBalance;
+  amount: AmountString;
 }
 
 export interface ProviderPaymentPending {
   type: ProviderPaymentType.Pending;
+  talerUri?: string;
 }
 
 export interface ProviderPaymentPaid {
@@ -810,32 +891,40 @@ async function getProviderPaymentInfo(
     ws,
     provider.currentPaymentProposalId,
   );
-  if (status.status === PreparePayResultType.InsufficientBalance) {
-    return {
-      type: ProviderPaymentType.InsufficientBalance,
-    };
-  }
-  if (status.status === PreparePayResultType.PaymentPossible) {
-    return {
-      type: ProviderPaymentType.Pending,
-    };
-  }
-  if (status.status === PreparePayResultType.AlreadyConfirmed) {
-    if (status.paid) {
+
+  switch (status.status) {
+    case PreparePayResultType.InsufficientBalance:
       return {
-        type: ProviderPaymentType.Paid,
-        paidUntil: AbsoluteTime.addDuration(
-          AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
-          durationFromSpec({ years: 1 }),
-        ),
+        type: ProviderPaymentType.InsufficientBalance,
+        amount: status.amountRaw,
       };
-    } else {
+    case PreparePayResultType.PaymentPossible:
       return {
         type: ProviderPaymentType.Pending,
+        talerUri: status.talerUri,
       };
-    }
+    case PreparePayResultType.Lost:
+      return {
+        type: ProviderPaymentType.Unpaid,
+      };
+    case PreparePayResultType.AlreadyConfirmed:
+      if (status.paid) {
+        return {
+          type: ProviderPaymentType.Paid,
+          paidUntil: AbsoluteTime.addDuration(
+            AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
+            durationFromSpec({ years: 1 }), //FIXME: take this from the 
contract term
+          ),
+        };
+      } else {
+        return {
+          type: ProviderPaymentType.Pending,
+          talerUri: status.talerUri,
+        };
+      }
+    default:
+      assertUnreachable(status);
   }
-  throw Error("not reached");
 }
 
 /**
@@ -936,6 +1025,7 @@ async function backupRecoveryTheirs(
             baseUrl: prov.url,
             name: prov.name,
             paymentProposalIds: [],
+            shouldRetryFreshProposal: false,
             state: {
               tag: BackupProviderStateTag.Ready,
               nextBackupTimestamp: TalerProtocolTimestamp.now(),
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts 
b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index 6246951ad..d3d0a12bd 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -72,6 +72,7 @@ import {
   TalerProtocolTimestamp,
   TransactionType,
   URL,
+  constructPayUri,
 } from "@gnu-taler/taler-util";
 import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
 import {
@@ -1290,7 +1291,10 @@ export async function checkPaymentByProposalId(
       return tx.purchases.get(proposalId);
     });
   if (!proposal) {
-    throw Error(`could not get proposal ${proposalId}`);
+    // throw Error(`could not get proposal ${proposalId}`);
+    return {
+      status: PreparePayResultType.Lost,
+    };
   }
   if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) {
     const existingProposalId = proposal.repurchaseProposalId;
@@ -1316,6 +1320,14 @@ export async function checkPaymentByProposalId(
 
   proposalId = proposal.proposalId;
 
+  const talerUri = constructPayUri(
+    proposal.merchantBaseUrl,
+    proposal.orderId,
+    proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
+    proposal.claimToken,
+    proposal.noncePriv,
+  );
+
   // First check if we already paid for it.
   const purchase = await ws.db
     .mktx((x) => [x.purchases])
@@ -1345,6 +1357,7 @@ export async function checkPaymentByProposalId(
         proposalId: proposal.proposalId,
         noncePriv: proposal.noncePriv,
         amountRaw: Amounts.stringify(d.contractData.amount),
+        talerUri,
       };
     }
 
@@ -1360,6 +1373,7 @@ export async function checkPaymentByProposalId(
       amountEffective: Amounts.stringify(totalCost),
       amountRaw: Amounts.stringify(res.paymentAmount),
       contractTermsHash: d.contractData.contractTermsHash,
+      talerUri,
     };
   }
 
@@ -1396,6 +1410,7 @@ export async function checkPaymentByProposalId(
       amountRaw: Amounts.stringify(download.contractData.amount),
       amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
       proposalId,
+      talerUri,
     };
   } else if (!purchase.timestampFirstSuccessfulPay) {
     const download = await expectProposalDownload(ws, purchase);
@@ -1407,6 +1422,7 @@ export async function checkPaymentByProposalId(
       amountRaw: Amounts.stringify(download.contractData.amount),
       amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
       proposalId,
+      talerUri,
     };
   } else {
     const paid =
@@ -1423,6 +1439,7 @@ export async function checkPaymentByProposalId(
       amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
       ...(paid ? { nextUrl: download.contractData.orderId } : {}),
       proposalId,
+      talerUri,
     };
   }
 }
@@ -1468,7 +1485,7 @@ export async function preparePayForUri(
     );
   }
 
-  let proposalId = await startDownloadProposal(
+  const proposalId = await startDownloadProposal(
     ws,
     uriResult.merchantBaseUrl,
     uriResult.orderId,
@@ -1930,6 +1947,28 @@ export async function processPurchasePay(
       );
     }
 
+    if (resp.status === HttpStatusCode.Gone) {
+      const errDetails = await readUnexpectedResponseDetails(resp);
+      logger.warn("unexpected 410 response for /pay");
+      logger.warn(j2s(errDetails));
+      await ws.db
+        .mktx((x) => [x.purchases])
+        .runReadWrite(async (tx) => {
+          const purch = await tx.purchases.get(proposalId);
+          if (!purch) {
+            return;
+          }
+          // FIXME: Should be some "PayPermanentlyFailed" and error info 
should be stored
+          purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
+          await tx.purchases.put(purch);
+        });
+      throw makePendingOperationFailedError(
+        errDetails,
+        TransactionType.Payment,
+        proposalId,
+      );
+    }
+
     if (resp.status === HttpStatusCode.Conflict) {
       const err = await readTalerErrorResponse(resp);
       if (
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts 
b/packages/taler-wallet-core/src/util/assertUnreachable.ts
index ffdf88f04..1819fd09e 100644
--- a/packages/taler-wallet-core/src/util/assertUnreachable.ts
+++ b/packages/taler-wallet-core/src/util/assertUnreachable.ts
@@ -15,5 +15,5 @@
  */
 
 export function assertUnreachable(x: never): never {
-  throw new Error("Didn't expect to get here");
+  throw new Error(`Didn't expect to get here ${x}`);
 }
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts 
b/packages/taler-wallet-core/src/wallet-api-types.ts
index 04c1bb6b4..f4fb16e80 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -71,6 +71,9 @@ import {
   KnownBankAccounts,
   ListKnownBankAccountsRequest,
   ManualWithdrawalDetails,
+  UserAttentionsCountResponse,
+  UserAttentionsRequest,
+  UserAttentionsResponse,
   PrepareDepositRequest,
   PrepareDepositResponse,
   PreparePayRequest,
@@ -102,6 +105,7 @@ import {
   WithdrawFakebankRequest,
   WithdrawTestBalanceRequest,
   WithdrawUriInfoResponse,
+  UserAttentionByIdRequest,
 } from "@gnu-taler/taler-util";
 import { WalletContractData } from "./db.js";
 import {
@@ -133,6 +137,9 @@ export enum WalletApiOperation {
   GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
   AcceptManualWithdrawal = "acceptManualWithdrawal",
   GetBalances = "getBalances",
+  GetUserAttentionRequests = "getUserAttentionRequests",
+  GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
+  MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
   GetPendingOperations = "getPendingOperations",
   SetExchangeTosAccepted = "setExchangeTosAccepted",
   ApplyRefund = "applyRefund",
@@ -746,6 +753,33 @@ export type WithdrawFakebankOp = {
   response: EmptyObject;
 };
 
+/**
+ * Get wallet-internal pending tasks.
+ */
+export type GetUserAttentionRequests = {
+  op: WalletApiOperation.GetUserAttentionRequests;
+  request: UserAttentionsRequest;
+  response: UserAttentionsResponse;
+};
+
+/**
+ * Get wallet-internal pending tasks.
+ */
+export type MarkAttentionRequestAsRead = {
+  op: WalletApiOperation.MarkAttentionRequestAsRead;
+  request: UserAttentionByIdRequest;
+  response: EmptyObject;
+};
+
+/**
+ * Get wallet-internal pending tasks.
+ */
+export type GetUserAttentionsUnreadCount = {
+  op: WalletApiOperation.GetUserAttentionUnreadCount;
+  request: UserAttentionsRequest;
+  response: UserAttentionsCountResponse;
+};
+
 /**
  * Get wallet-internal pending tasks.
  */
@@ -798,6 +832,9 @@ export type WalletOperations = {
   [WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
   [WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
   [WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
+  [WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
+  [WalletApiOperation.GetUserAttentionUnreadCount]: 
GetUserAttentionsUnreadCount;
+  [WalletApiOperation.MarkAttentionRequestAsRead]: MarkAttentionRequestAsRead;
   [WalletApiOperation.DumpCoins]: DumpCoinsOp;
   [WalletApiOperation.SetCoinSuspended]: SetCoinSuspendedOp;
   [WalletApiOperation.ForceRefresh]: ForceRefreshOp;
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 9fa0e32ba..5ad86dfe8 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -55,6 +55,7 @@ import {
   codecForInitiatePeerPushPaymentRequest,
   codecForIntegrationTestArgs,
   codecForListKnownBankAccounts,
+  codecForUserAttentionsRequest,
   codecForPrepareDepositRequest,
   codecForPreparePayRequest,
   codecForPreparePeerPullPaymentRequest,
@@ -98,6 +99,7 @@ import {
   URL,
   WalletCoreVersion,
   WalletNotification,
+  codecForUserAttentionByIdRequest,
 } from "@gnu-taler/taler-util";
 import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 import {
@@ -147,6 +149,11 @@ import {
 } from "./operations/backup/index.js";
 import { setWalletDeviceId } from "./operations/backup/state.js";
 import { getBalances } from "./operations/balance.js";
+import {
+  getUserAttentions,
+  getUserAttentionsUnreadCount,
+  markAttentionRequestAsRead,
+} from "./operations/attention.js";
 import {
   getExchangeTosStatus,
   makeExchangeListItem,
@@ -1094,6 +1101,18 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
     case WalletApiOperation.GetBalances: {
       return await getBalances(ws);
     }
+    case WalletApiOperation.GetUserAttentionRequests: {
+      const req = codecForUserAttentionsRequest().decode(payload);
+      return await getUserAttentions(ws, req);
+    }
+    case WalletApiOperation.MarkAttentionRequestAsRead: {
+      const req = codecForUserAttentionByIdRequest().decode(payload);
+      return await markAttentionRequestAsRead(ws, req);
+    }
+    case WalletApiOperation.GetUserAttentionUnreadCount: {
+      const req = codecForUserAttentionsRequest().decode(payload);
+      return await getUserAttentionsUnreadCount(ws, req);
+    }
     case WalletApiOperation.GetPendingOperations: {
       return await getPendingOperations(ws);
     }
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx 
b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index ff2404800..b900fab9d 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -24,7 +24,7 @@
 /**
  * Imports.
  */
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
 import {
   NavigationHeader,
   NavigationHeaderHolder,
@@ -33,6 +33,11 @@ import {
 import { useTranslationContext } from "./context/translation.js";
 import settingsIcon from "./svg/settings_black_24dp.svg";
 import qrIcon from "./svg/qr_code_24px.svg";
+import warningIcon from "./svg/warning_24px.svg";
+import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
+import { wxApi } from "./wxApi.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { JustInDevMode } from "./components/JustInDevMode.js";
 
 /**
  * List of pages used by the wallet
@@ -102,6 +107,7 @@ export const Pages = {
   backupProviderAdd: "/backup/provider/add",
 
   qr: "/qr",
+  notifications: "/notifications",
   settings: "/settings",
   settingsExchangeAdd: pageDefinition<{ currency?: string }>(
     "/settings/exchange/add/:currency?",
@@ -127,7 +133,21 @@ export const Pages = {
   ),
 };
 
-export function PopupNavBar({ path = "" }: { path?: string }): VNode {
+export function PopupNavBar({
+  path = "",
+}: {
+  path?: string;
+}): // api: typeof wxApi,
+VNode {
+  const api = wxApi; //FIXME: as parameter
+  const hook = useAsyncAsHook(async () => {
+    return await api.wallet.call(
+      WalletApiOperation.GetUserAttentionUnreadCount,
+      {},
+    );
+  });
+  const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
+
   const { i18n } = useTranslationContext();
   return (
     <NavigationHeader>
@@ -141,6 +161,17 @@ export function PopupNavBar({ path = "" }: { path?: string 
}): VNode {
         <i18n.Translate>Backup</i18n.Translate>
       </a>
       <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
+        {attentionCount > 0 ? (
+          <a href={Pages.notifications}>
+            <SvgIcon
+              title={i18n.str`Notifications`}
+              dangerouslySetInnerHTML={{ __html: warningIcon }}
+              color="yellow"
+            />
+          </a>
+        ) : (
+          <Fragment />
+        )}
         <a href={Pages.qr}>
           <SvgIcon
             title={i18n.str`QR Reader and Taler URI`}
@@ -178,10 +209,16 @@ export function WalletNavBar({ path = "" }: { path?: 
string }): VNode {
           <i18n.Translate>Backup</i18n.Translate>
         </a>
 
-        <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
-          <i18n.Translate>Dev</i18n.Translate>
+        <a href={Pages.notifications}>
+          <i18n.Translate>Notifications</i18n.Translate>
         </a>
 
+        <JustInDevMode>
+          <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
+            <i18n.Translate>Dev</i18n.Translate>
+          </a>
+        </JustInDevMode>
+
         <div
           style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
         >
diff --git 
a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx 
b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
index 3183364a8..ff9a71992 100644
--- a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
@@ -50,7 +50,6 @@ function RenderAmount(): VNode {
       <AmountField
         required
         label={<i18n.Translate>Amount</i18n.Translate>}
-        currency="USD"
         highestDenom={2000000}
         lowestDenom={0.01}
         handler={handler}
diff --git 
a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx 
b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
index f8b23081d..c2c4b52e3 100644
--- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
@@ -27,6 +27,7 @@ import { h, VNode } from "preact";
 import { useTranslationContext } from "../context/translation.js";
 import { Avatar } from "../mui/Avatar.js";
 import { Pages } from "../NavigationBar.js";
+import { assertUnreachable } from "../utils/index.js";
 import {
   Column,
   ExtraLargeText,
@@ -175,8 +176,7 @@ export function TransactionItem(props: { tx: Transaction 
}): VNode {
         />
       );
     default: {
-      const pe: never = tx;
-      throw Error(`unsupported transaction type ${pe}`);
+      assertUnreachable(tx);
     }
   }
 }
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts 
b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
index 1846794fc..c7fb48958 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
@@ -89,6 +89,7 @@ export function useComponentState(
 
   const insufficientBalance: PreparePayResult = {
     status: PreparePayResultType.InsufficientBalance,
+    talerUri: "taler://pay",
     proposalId: "fakeID",
     contractTerms: {} as any,
     amountRaw: hook.response.p2p.amount,
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts 
b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
index f0270b96c..80822b381 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
@@ -18,6 +18,7 @@ import {
   AmountJson,
   PreparePayResult,
   PreparePayResultAlreadyConfirmed,
+  PreparePayResultInsufficientBalance,
   PreparePayResultPaymentPossible,
 } from "@gnu-taler/taler-util";
 import { Loading } from "../../components/Loading.js";
@@ -26,7 +27,7 @@ import { ButtonHandler } from "../../mui/handlers.js";
 import { compose, StateViewMap } from "../../utils/index.js";
 import { wxApi } from "../../wxApi.js";
 import { useComponentState } from "./state.js";
-import { BaseView, LoadingUriView } from "./views.js";
+import { BaseView, LoadingUriView, LostView } from "./views.js";
 
 export interface Props {
   talerPayUri?: string;
@@ -40,6 +41,7 @@ export type State =
   | State.LoadingUriError
   | State.Ready
   | State.NoEnoughBalance
+  | State.Lost
   | State.NoBalanceForCurrency
   | State.Confirmed;
 
@@ -62,12 +64,15 @@ export namespace State {
   }
   export interface NoBalanceForCurrency extends BaseInfo {
     status: "no-balance-for-currency";
-    payStatus: PreparePayResult;
+    payStatus:
+      | PreparePayResultInsufficientBalance
+      | PreparePayResultPaymentPossible
+      | PreparePayResultAlreadyConfirmed;
     balance: undefined;
   }
   export interface NoEnoughBalance extends BaseInfo {
     status: "no-enough-balance";
-    payStatus: PreparePayResult;
+    payStatus: PreparePayResultInsufficientBalance;
     balance: AmountJson;
   }
   export interface Ready extends BaseInfo {
@@ -77,6 +82,11 @@ export namespace State {
     balance: AmountJson;
   }
 
+  export interface Lost {
+    status: "lost";
+    error: undefined;
+  }
+
   export interface Confirmed extends BaseInfo {
     status: "confirmed";
     payStatus: PreparePayResultAlreadyConfirmed;
@@ -89,6 +99,7 @@ const viewMapping: StateViewMap<State> = {
   "loading-uri": LoadingUriView,
   "no-balance-for-currency": BaseView,
   "no-enough-balance": BaseView,
+  lost: LostView,
   confirmed: BaseView,
   ready: BaseView,
 };
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts 
b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
index 49d022320..b90b1e495 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -82,6 +82,14 @@ export function useComponentState(
     };
   }
   const { payStatus } = hook.response;
+
+  if (payStatus.status === PreparePayResultType.Lost) {
+    return {
+      status: "lost",
+      error: undefined,
+    };
+  }
+
   const amount = Amounts.parseOrThrow(payStatus.amountRaw);
 
   const foundBalance = hook.response.balance.balances.find(
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index 7d5a7694e..fd437d5d2 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -44,6 +44,7 @@ export const NoBalance = createExample(BaseView, {
   uri: "",
   payStatus: {
     status: PreparePayResultType.InsufficientBalance,
+    talerUri: "taler://pay/..",
     noncePriv: "",
     proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     contractTerms: {
@@ -73,6 +74,7 @@ export const NoEnoughBalance = createExample(BaseView, {
   uri: "",
   payStatus: {
     status: PreparePayResultType.InsufficientBalance,
+    talerUri: "taler://pay/..",
     noncePriv: "",
     proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     contractTerms: {
@@ -102,6 +104,7 @@ export const EnoughBalanceButRestricted = 
createExample(BaseView, {
   uri: "",
   payStatus: {
     status: PreparePayResultType.InsufficientBalance,
+    talerUri: "taler://pay/..",
     noncePriv: "",
     proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     contractTerms: {
@@ -136,6 +139,7 @@ export const PaymentPossible = createExample(BaseView, {
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
     status: PreparePayResultType.PaymentPossible,
+    talerUri: "taler://pay/..",
     amountEffective: "USD:10",
     amountRaw: "USD:10",
     noncePriv: "",
@@ -176,6 +180,7 @@ export const PaymentPossibleWithFee = 
createExample(BaseView, {
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
     status: PreparePayResultType.PaymentPossible,
+    talerUri: "taler://pay/..",
     amountEffective: "USD:10.20",
     amountRaw: "USD:10",
     noncePriv: "",
@@ -213,6 +218,7 @@ export const TicketWithAProductList = 
createExample(BaseView, {
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
     status: PreparePayResultType.PaymentPossible,
+    talerUri: "taler://pay/..",
     amountEffective: "USD:10.20",
     amountRaw: "USD:10",
     noncePriv: "",
@@ -269,6 +275,7 @@ export const TicketWithShipping = createExample(BaseView, {
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
     status: PreparePayResultType.PaymentPossible,
+    talerUri: "taler://pay/..",
     amountEffective: "USD:10.20",
     amountRaw: "USD:10",
     noncePriv: "",
@@ -315,6 +322,7 @@ export const AlreadyConfirmedByOther = 
createExample(BaseView, {
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
     status: PreparePayResultType.AlreadyConfirmed,
+    talerUri: "taler://pay/..",
     amountEffective: "USD:10",
     amountRaw: "USD:10",
     contractTerms: {
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index d9b6eaa02..6b502a87f 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -26,6 +26,7 @@ import {
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Amount } from "../../components/Amount.js";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
 import { LoadingError } from "../../components/LoadingError.js";
 import { LogoHeader } from "../../components/LogoHeader.js";
 import { Part } from "../../components/Part.js";
@@ -43,6 +44,7 @@ import { Time } from "../../components/Time.js";
 import { useTranslationContext } from "../../context/translation.js";
 import { Button } from "../../mui/Button.js";
 import { ButtonHandler } from "../../mui/handlers.js";
+import { assertUnreachable } from "../../utils/index.js";
 import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
 import { State } from "./index.js";
 
@@ -63,8 +65,24 @@ type SupportedStates =
   | State.NoBalanceForCurrency
   | State.NoEnoughBalance;
 
+export function LostView(state: State.Lost): VNode {
+  const { i18n } = useTranslationContext();
+
+  return (
+    <ErrorMessage
+      title={<i18n.Translate>Could not load pay status</i18n.Translate>}
+      description={
+        <i18n.Translate>
+          The proposal was lost, another should be downloaded
+        </i18n.Translate>
+      }
+    />
+  );
+}
+
 export function BaseView(state: SupportedStates): VNode {
   const { i18n } = useTranslationContext();
+
   const contractTerms: ContractTerms = state.payStatus.contractTerms;
 
   const price = {
@@ -399,8 +417,9 @@ export function ButtonsSection({
       </Fragment>
     );
   }
+  if (payStatus.status === PreparePayResultType.Lost) {
+    return <Fragment />;
+  }
 
-  const error: never = payStatus;
-
-  return <Fragment />;
+  assertUnreachable(payStatus);
 }
diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx 
b/packages/taler-wallet-webextension/src/popup/Application.tsx
index 457f26cfd..8186c6790 100644
--- a/packages/taler-wallet-webextension/src/popup/Application.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Application.tsx
@@ -150,6 +150,10 @@ export function Application(): VNode {
                   component={RedirectToWalletPage}
                 />
                 <Route path={Pages.dev} component={RedirectToWalletPage} />
+                <Route
+                  path={Pages.notifications}
+                  component={RedirectToWalletPage}
+                />
 
                 <Route default component={Redirect} to={Pages.balance} />
               </Router>
diff --git 
a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts 
b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
index 0b3c17902..504ee4678 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
@@ -171,7 +171,11 @@ export function useComponentState(
 
     switch (resp.status) {
       case "payment-required":
-        return onPaymentRequired(resp.talerUri);
+        if (resp.talerUri) {
+          return onPaymentRequired(resp.talerUri);
+        } else {
+          return onComplete(url);
+        }
       case "error":
         return setOperationError(resp.error);
       case "ok":
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx 
b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index 6b265c1ba..6362f1924 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -66,6 +66,7 @@ import { TransferPickupPage } from 
"../cta/TransferPickup/index.js";
 import { InvoicePayPage } from "../cta/InvoicePay/index.js";
 import { RecoveryPage } from "../cta/Recovery/index.js";
 import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
+import { NotificationsPage } from "./Notifications/index.js";
 
 export function Application(): VNode {
   const [globalNotification, setGlobalNotification] = useState<
@@ -206,6 +207,7 @@ export function Application(): VNode {
               />
 
               <Route path={Pages.settings} component={SettingsPage} />
+              <Route path={Pages.notifications} component={NotificationsPage} 
/>
 
               {/**
                * BACKUP
@@ -218,6 +220,12 @@ export function Application(): VNode {
               <Route
                 path={Pages.backupProviderDetail.pattern}
                 component={ProviderDetailPage}
+                onPayProvider={(uri: string) =>
+                  redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+                }
+                onWithdraw={(amount: string) =>
+                  redirectTo(Pages.receiveCash({ amount }))
+                }
                 onBack={() => redirectTo(Pages.backup)}
               />
               <Route
@@ -254,7 +262,7 @@ export function Application(): VNode {
                 path={Pages.ctaPay}
                 component={PaymentPage}
                 goToWalletManualWithdraw={(amount?: string) =>
-                  redirectTo(Pages.ctaWithdrawManual({ amount }))
+                  redirectTo(Pages.receiveCash({ amount }))
                 }
                 cancel={() => redirectTo(Pages.balance)}
                 onSuccess={(tid: string) =>
@@ -321,7 +329,7 @@ export function Application(): VNode {
                 path={Pages.ctaInvoicePay}
                 component={InvoicePayPage}
                 goToWalletManualWithdraw={(amount?: string) =>
-                  redirectTo(Pages.ctaWithdrawManual({ amount }))
+                  redirectTo(Pages.receiveCash({ amount }))
                 }
                 onClose={() => redirectTo(Pages.balance)}
                 onSuccess={(tid: string) =>
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index b12f5e5f6..2e19d3944 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -89,6 +89,7 @@ export const LotOfProviders = createExample(TestedComponent, {
       paymentProposalIds: [],
       paymentStatus: {
         type: ProviderPaymentType.Pending,
+        talerUri: "taler://",
       },
       terms: {
         annualFee: "KUDOS:0.1",
@@ -103,6 +104,7 @@ export const LotOfProviders = 
createExample(TestedComponent, {
       paymentProposalIds: [],
       paymentStatus: {
         type: ProviderPaymentType.InsufficientBalance,
+        amount: "KUDOS:10",
       },
       terms: {
         annualFee: "KUDOS:0.1",
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts 
b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
new file mode 100644
index 000000000..253a0e629
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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/>
+ */
+
+import { UserAttentionUnreadList } from "@gnu-taler/taler-util";
+import { Loading } from "../../components/Loading.js";
+import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { wxApi } from "../../wxApi.js";
+import { useComponentState } from "./state.js";
+import { LoadingUriView, ReadyView } from "./views.js";
+
+export interface Props {}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+  export interface Loading {
+    status: "loading";
+    error: undefined;
+  }
+
+  export interface LoadingUriError {
+    status: "loading-error";
+    error: HookError;
+  }
+
+  export interface BaseInfo {
+    error: undefined;
+  }
+
+  export interface Ready extends BaseInfo {
+    status: "ready";
+    error: undefined;
+    list: UserAttentionUnreadList;
+  }
+}
+
+const viewMapping: StateViewMap<State> = {
+  loading: Loading,
+  "loading-error": LoadingUriView,
+  ready: ReadyView,
+};
+
+export const NotificationsPage = compose(
+  "NotificationsPage",
+  (p: Props) => useComponentState(p, wxApi),
+  viewMapping,
+);
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts 
b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
new file mode 100644
index 000000000..093722cf0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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/>
+ */
+
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { wxApi } from "../../wxApi.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({}: Props, api: typeof wxApi): State {
+  const hook = useAsyncAsHook(async () => {
+    return await api.wallet.call(
+      WalletApiOperation.GetUserAttentionRequests,
+      {},
+    );
+  });
+
+  if (!hook) {
+    return {
+      status: "loading",
+      error: undefined,
+    };
+  }
+  if (hook.hasError) {
+    return {
+      status: "loading-error",
+      error: hook,
+    };
+  }
+
+  return {
+    status: "ready",
+    error: undefined,
+    list: hook.response.pending,
+  };
+}
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
new file mode 100644
index 000000000..e4c7105e9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
@@ -0,0 +1,58 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { AbsoluteTime, AttentionType } from "@gnu-taler/taler-util";
+import { createExample } from "../../test-utils.js";
+import { ReadyView } from "./views.js";
+
+export default {
+  title: "wallet/notifications",
+};
+
+export const Ready = createExample(ReadyView, {
+  list: [
+    {
+      when: AbsoluteTime.now(),
+      read: false,
+      info: {
+        type: AttentionType.KycWithdrawal,
+        transactionId: "123",
+      },
+    },
+    {
+      when: AbsoluteTime.now(),
+      read: false,
+      info: {
+        type: AttentionType.MerchantRefund,
+        transactionId: "123",
+      },
+    },
+    {
+      when: AbsoluteTime.now(),
+      read: false,
+      info: {
+        type: AttentionType.BackupUnpaid,
+        provider_base_url: "http://sync.taler.net";,
+        talerUri: "taler://payment/asdasdasd",
+      },
+    },
+  ],
+});
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts 
b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
similarity index 74%
copy from packages/taler-wallet-core/src/util/assertUnreachable.ts
copy to packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
index ffdf88f04..eae4d4ca2 100644
--- a/packages/taler-wallet-core/src/util/assertUnreachable.ts
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2022 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
@@ -14,6 +14,15 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-export function assertUnreachable(x: never): never {
-  throw new Error("Didn't expect to get here");
-}
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("test description", () => {
+  it("should assert", () => {
+    expect([]).deep.equals([]);
+  });
+});
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx 
b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
new file mode 100644
index 000000000..9146d8837
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
@@ -0,0 +1,220 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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/>
+ */
+
+import {
+  AbsoluteTime,
+  AttentionInfo,
+  AttentionType,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { LoadingError } from "../../components/LoadingError.js";
+import {
+  Column,
+  DateSeparator,
+  HistoryRow,
+  LargeText,
+  SmallLightText,
+} from "../../components/styled/index.js";
+import { Time } from "../../components/Time.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { Avatar } from "../../mui/Avatar.js";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import { Pages } from "../../NavigationBar.js";
+import { assertUnreachable } from "../../utils/index.js";
+import { State } from "./index.js";
+
+export function LoadingUriView({ error }: State.LoadingUriError): VNode {
+  const { i18n } = useTranslationContext();
+
+  return (
+    <LoadingError
+      title={<i18n.Translate>Could not load notifications</i18n.Translate>}
+      error={error}
+    />
+  );
+}
+
+const term = 1000 * 60 * 60 * 24;
+function normalizeToDay(x: number): number {
+  return Math.round(x / term) * term;
+}
+
+export function ReadyView({ list }: State.Ready): VNode {
+  const { i18n } = useTranslationContext();
+  if (list.length < 1) {
+    return (
+      <section>
+        <i18n.Translate>No notification left</i18n.Translate>
+      </section>
+    );
+  }
+
+  const byDate = list.reduce((rv, x) => {
+    const theDate = x.when.t_ms === "never" ? 0 : normalizeToDay(x.when.t_ms);
+    if (theDate) {
+      (rv[theDate] = rv[theDate] || []).push(x);
+    }
+
+    return rv;
+  }, {} as { [x: string]: typeof list });
+  const datesWithNotifications = Object.keys(byDate);
+
+  return (
+    <section>
+      {datesWithNotifications.map((d, i) => {
+        return (
+          <Fragment key={i}>
+            <DateSeparator>
+              <Time
+                timestamp={{ t_ms: Number.parseInt(d, 10) }}
+                format="dd MMMM yyyy"
+              />
+            </DateSeparator>
+            {byDate[d].map((n, i) => (
+              <NotificationItem
+                key={i}
+                info={n.info}
+                isRead={n.read}
+                timestamp={n.when}
+              />
+            ))}
+          </Fragment>
+        );
+      })}
+    </section>
+  );
+}
+
+function NotificationItem({
+  info,
+  isRead,
+  timestamp,
+}: {
+  info: AttentionInfo;
+  timestamp: AbsoluteTime;
+  isRead: boolean;
+}): VNode {
+  switch (info.type) {
+    case AttentionType.KycWithdrawal:
+      return (
+        <NotificationLayout
+          timestamp={timestamp}
+          href={Pages.balanceTransaction({ tid: info.transactionId })}
+          title="Withdrawal on hold"
+          subtitle="Know-your-customer validation is required"
+          iconPath={"K"}
+          isRead={isRead}
+        />
+      );
+    case AttentionType.MerchantRefund:
+      return (
+        <NotificationLayout
+          timestamp={timestamp}
+          href={Pages.balanceTransaction({ tid: info.transactionId })}
+          title="Merchant has refund your payment"
+          subtitle="Accept or deny refund"
+          iconPath={"K"}
+          isRead={isRead}
+        />
+      );
+    case AttentionType.BackupUnpaid:
+      return (
+        <NotificationLayout
+          timestamp={timestamp}
+          href={`${Pages.ctaPay}?talerPayUri=${info.talerUri}`}
+          title="Backup provider is unpaid"
+          subtitle="Complete the payment or remove the service provider"
+          iconPath={"K"}
+          isRead={isRead}
+        />
+      );
+    case AttentionType.AuditorDenominationsExpires:
+      return <div>not implemented</div>;
+    case AttentionType.AuditorKeyExpires:
+      return <div>not implemented</div>;
+    case AttentionType.AuditorTosChanged:
+      return <div>not implemented</div>;
+    case AttentionType.ExchangeDenominationsExpired:
+      return <div>not implemented</div>;
+    // case AttentionType.ExchangeDenominationsExpiresSoon:
+    //   return <div>not implemented</div>;
+    case AttentionType.ExchangeKeyExpired:
+      return <div>not implemented</div>;
+    // case AttentionType.ExchangeKeyExpiresSoon:
+    //   return <div>not implemented</div>;
+    case AttentionType.ExchangeTosChanged:
+      return <div>not implemented</div>;
+    case AttentionType.BackupExpiresSoon:
+      return <div>not implemented</div>;
+    case AttentionType.PushPaymentReceived:
+      return <div>not implemented</div>;
+    case AttentionType.PullPaymentPaid:
+      return <div>not implemented</div>;
+    default:
+      assertUnreachable(info);
+  }
+}
+
+function NotificationLayout(props: {
+  title: string;
+  href: string;
+  subtitle?: string;
+  timestamp: AbsoluteTime;
+  iconPath: string;
+  isRead: boolean;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  return (
+    <HistoryRow
+      href={props.href}
+      style={{
+        backgroundColor: props.isRead ? "lightcyan" : "inherit",
+        alignItems: "center",
+      }}
+    >
+      <Avatar
+        style={{
+          border: "solid gray 1px",
+          color: "gray",
+          boxSizing: "border-box",
+        }}
+      >
+        {props.iconPath}
+      </Avatar>
+      <Column>
+        <LargeText>
+          <div>{props.title}</div>
+          {props.subtitle && (
+            <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
+              {props.subtitle}
+            </div>
+          )}
+        </LargeText>
+        <SmallLightText style={{ marginTop: 5 }}>
+          <Time timestamp={props.timestamp} format="HH:mm" />
+        </SmallLightText>
+      </Column>
+      <Column>
+        <Grid>
+          <Button variant="outlined">
+            <i18n.Translate>Ignore</i18n.Translate>
+          </Button>
+        </Grid>
+      </Column>
+    </HistoryRow>
+  );
+}
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
index d55a25e78..854c14ac1 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -174,6 +174,7 @@ export const InactiveInsufficientBalance = 
createExample(TestedComponent, {
     paymentProposalIds: [],
     paymentStatus: {
       type: ProviderPaymentType.InsufficientBalance,
+      amount: "EUR:123",
     },
     terms: {
       annualFee: "EUR:0.1",
@@ -191,6 +192,7 @@ export const InactivePending = 
createExample(TestedComponent, {
     paymentProposalIds: [],
     paymentStatus: {
       type: ProviderPaymentType.Pending,
+      talerUri: "taler://pay/sad",
     },
     terms: {
       annualFee: "EUR:0.1",
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
index d9dd1d746..6dde30b39 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -36,9 +36,16 @@ import { wxApi } from "../wxApi.js";
 interface Props {
   pid: string;
   onBack: () => Promise<void>;
+  onPayProvider: (uri: string) => Promise<void>;
+  onWithdraw: (amount: string) => Promise<void>;
 }
 
-export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode 
{
+export function ProviderDetailPage({
+  pid: providerURL,
+  onBack,
+  onPayProvider,
+  onWithdraw,
+}: Props): VNode {
   const { i18n } = useTranslationContext();
   async function getProviderInfo(): Promise<ProviderInfo | null> {
     //create a first list of backup info by currency
@@ -71,11 +78,30 @@ export function ProviderDetailPage({ pid: providerURL, 
onBack }: Props): VNode {
       />
     );
   }
+  const info = state.response;
+  if (info === null) {
+    return (
+      <Fragment>
+        <section>
+          <p>
+            <i18n.Translate>
+              There is not known provider with url &quot;{providerURL}&quot;.
+            </i18n.Translate>
+          </p>
+        </section>
+        <footer>
+          <Button variant="contained" color="secondary" onClick={onBack}>
+            <i18n.Translate>See providers</i18n.Translate>
+          </Button>
+          <div />
+        </footer>
+      </Fragment>
+    );
+  }
 
   return (
     <ProviderView
-      url={providerURL}
-      info={state.response}
+      info={info}
       onSync={async () =>
         wxApi.wallet
           .call(WalletApiOperation.RunBackupCycle, {
@@ -83,6 +109,16 @@ export function ProviderDetailPage({ pid: providerURL, 
onBack }: Props): VNode {
           })
           .then()
       }
+      onPayProvider={async () => {
+        if (info.paymentStatus.type !== ProviderPaymentType.Pending) return;
+        if (!info.paymentStatus.talerUri) return;
+        onPayProvider(info.paymentStatus.talerUri);
+      }}
+      onWithdraw={async () => {
+        if (info.paymentStatus.type !== 
ProviderPaymentType.InsufficientBalance)
+          return;
+        onWithdraw(info.paymentStatus.amount);
+      }}
       onDelete={() =>
         wxApi.wallet
           .call(WalletApiOperation.RemoveBackupProvider, {
@@ -99,42 +135,25 @@ export function ProviderDetailPage({ pid: providerURL, 
onBack }: Props): VNode {
 }
 
 export interface ViewProps {
-  url: string;
-  info: ProviderInfo | null;
+  info: ProviderInfo;
   onDelete: () => Promise<void>;
   onSync: () => Promise<void>;
   onBack: () => Promise<void>;
   onExtend: () => Promise<void>;
+  onPayProvider: () => Promise<void>;
+  onWithdraw: () => Promise<void>;
 }
 
 export function ProviderView({
   info,
-  url,
   onDelete,
+  onPayProvider,
+  onWithdraw,
   onSync,
   onBack,
   onExtend,
 }: ViewProps): VNode {
   const { i18n } = useTranslationContext();
-  if (info === null) {
-    return (
-      <Fragment>
-        <section>
-          <p>
-            <i18n.Translate>
-              There is not known provider with url &quot;{url}&quot;.
-            </i18n.Translate>
-          </p>
-        </section>
-        <footer>
-          <Button variant="contained" color="secondary" onClick={onBack}>
-            <i18n.Translate>See providers</i18n.Translate>
-          </Button>
-          <div />
-        </footer>
-      </Fragment>
-    );
-  }
   const lb = info.lastSuccessfulBackupTimestamp
     ? AbsoluteTime.fromTimestamp(info.lastSuccessfulBackupTimestamp)
     : undefined;
@@ -230,6 +249,18 @@ export function ProviderView({
           <Button variant="contained" color="error" onClick={onDelete}>
             <i18n.Translate>Remove provider</i18n.Translate>
           </Button>
+          {info.paymentStatus.type === ProviderPaymentType.Pending &&
+          info.paymentStatus.talerUri ? (
+            <Button variant="contained" color="primary" 
onClick={onPayProvider}>
+              <i18n.Translate>Pay</i18n.Translate>
+            </Button>
+          ) : undefined}
+          {info.paymentStatus.type ===
+          ProviderPaymentType.InsufficientBalance ? (
+            <Button variant="contained" color="primary" onClick={onWithdraw}>
+              <i18n.Translate>Withdraw</i18n.Translate>
+            </Button>
+          ) : undefined}
         </div>
       </footer>
     </Fragment>
diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
index ef1295846..20de1a3c3 100644
--- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
@@ -36,6 +36,7 @@ import * as a17 from "./QrReader.stories.js";
 import * as a18 from "./DestinationSelection.stories.js";
 import * as a19 from "./ExchangeSelection/stories.js";
 import * as a20 from "./ManageAccount/stories.js";
+import * as a21 from "./Notifications/stories.js";
 
 export default [
   a1,
@@ -55,4 +56,5 @@ export default [
   a18,
   a19,
   a20,
+  a21,
 ];

-- 
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]