aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/taleruri.ts17
-rw-r--r--packages/taler-util/src/wallet-types.ts189
-rw-r--r--packages/taler-wallet-core/src/db.ts27
-rw-r--r--packages/taler-wallet-core/src/operations/attention.ts145
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts140
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts43
-rw-r--r--packages/taler-wallet-core/src/util/assertUnreachable.ts2
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts37
-rw-r--r--packages/taler-wallet-core/src/wallet.ts19
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx45
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.stories.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/components/TransactionItem.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts1
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/index.ts17
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/state.ts8
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx25
-rw-r--r--packages/taler-wallet-webextension/src/popup/Application.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/index.ts61
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/state.ts48
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx58
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx220
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx81
-rw-r--r--packages/taler-wallet-webextension/src/wallet/index.stories.tsx2
29 files changed, 1182 insertions, 70 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",
@@ -749,6 +756,33 @@ export type WithdrawFakebankOp = {
/**
* 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.
+ */
export type GetPendingTasksOp = {
op: WalletApiOperation.GetPendingOperations;
request: EmptyObject;
@@ -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 {
@@ -148,6 +150,11 @@ import {
import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js";
import {
+ getUserAttentions,
+ getUserAttentionsUnreadCount,
+ markAttentionRequestAsRead,
+} from "./operations/attention.js";
+import {
getExchangeTosStatus,
makeExchangeListItem,
runOperationWithErrorReporting,
@@ -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-webextension/src/wallet/Notifications/test.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
new file mode 100644
index 000000000..eae4d4ca2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
@@ -0,0 +1,28 @@
+/*
+ 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 { 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,
];