From 2e2cf4049a771c82fcc520686de3ace7603baa05 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 15 Jan 2024 17:34:19 -0300 Subject: fixes #8083 --- .../test-withdrawal-notify-before-tx.ts | 80 ++++++++++++++++++++++ .../src/integrationtests/testrunner.ts | 2 + .../taler-util/src/http-client/bank-integration.ts | 8 +++ packages/taler-util/src/http-impl.node.ts | 2 +- packages/taler-util/src/http-impl.qtart.ts | 2 +- packages/taler-util/src/notifications.ts | 8 +++ packages/taler-util/src/transactions-types.ts | 10 +++ packages/taler-util/src/wallet-types.ts | 32 ++++++--- .../src/operations/transactions.ts | 62 +++++++++++++++-- .../taler-wallet-core/src/operations/withdraw.ts | 75 ++++++++++++-------- packages/taler-wallet-core/src/wallet-api-types.ts | 22 ++++-- packages/taler-wallet-core/src/wallet.ts | 16 +++-- .../src/cta/Withdraw/index.ts | 10 ++- .../src/cta/Withdraw/state.ts | 49 +++++++++++-- .../src/cta/Withdraw/stories.tsx | 19 ++++- .../src/cta/Withdraw/test.ts | 22 ++++++ .../src/cta/Withdraw/views.tsx | 29 +++++++- .../taler-wallet-webextension/src/utils/index.ts | 9 ++- .../taler-wallet-webextension/src/wxBackend.ts | 12 ++-- packages/web-util/src/utils/http-impl.browser.ts | 19 ++++- packages/web-util/src/utils/http-impl.sw.ts | 19 ++++- 21 files changed, 435 insertions(+), 72 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts (limited to 'packages') diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts new file mode 100644 index 000000000..fc36b8fc3 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-notify-before-tx.ts @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2020 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 + */ + +/** + * Imports. + */ +import { NotificationType, TalerCorebankApiClient } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runWithdrawalNotifyBeforeTxTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange } = + await createSimpleTestkudosEnvironmentV2(t); + + // Create a withdrawal operation + + const bankAccessApiClient = new TalerCorebankApiClient( + bank.corebankApiBaseUrl, + ); + const user = await bankAccessApiClient.createRandomBankUser(); + bankAccessApiClient.setAuth(user); + const wop = await bankAccessApiClient.createWithdrawalOperation( + user.username, + "TESTKUDOS:10", + ); + + // Hand it to the wallet + const r1 = await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + notifyChangeFromPendingTimeoutMs: 10000 + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + // Withdraw + + // Abort it + // const api = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); + // const token = await api.getAuthenticationAPI(user.username).createAccessToken(user.password, { + // scope: "readwrite", + // }) + // t.assertTrue(token.type !== "fail") + + // const confirm = await api.confirmWithdrawalById({ + // username: user.username, + // token: token.body.access_token, + // }, wop.withdrawal_id) + // t.assertTrue(confirm.type !== "fail") + + await walletClient.waitForNotificationCond((x) => { + return ( + x.type === NotificationType.WithdrawalOperationTransition && + x.operationId === r1.operationId && + x.state === "confirmed" + ); + }); + + await t.shutdown(); +} + +runWithdrawalNotifyBeforeTxTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 1d8353acf..6a8eb9504 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -97,6 +97,7 @@ import { runMultiExchangeTest } from "./test-multiexchange.js"; import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js"; import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js"; import { runPaymentDeletedTest } from "./test-payment-deleted.js"; +import { runWithdrawalNotifyBeforeTxTest } from "./test-withdrawal-notify-before-tx.js"; /** * Test runner. @@ -172,6 +173,7 @@ const allTests: TestMainFunction[] = [ runWalletDblessTest, runWallettestingTest, runWithdrawalAbortBankTest, + // runWithdrawalNotifyBeforeTxTest, runWithdrawalBankIntegratedTest, runWithdrawalFakebankTest, runWithdrawalFeesTest, diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts index 8131b36b6..757f1f897 100644 --- a/packages/taler-util/src/http-client/bank-integration.ts +++ b/packages/taler-util/src/http-client/bank-integration.ts @@ -1,6 +1,7 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; +import { LibtoolVersion } from "../libtool-version.js"; import { FailCasesByMethod, ResultByMethod, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; import { TalerErrorCode } from "../taler-error-codes.js"; import { codecForTalerErrorDetail } from "../wallet-types.js"; @@ -19,6 +20,8 @@ export type TalerBankIntegrationErrorsByMethod => + buildCodecForObject() + .property("talerWithdrawUri", codecForString()) + .build("WithdrawalTransactionByURIRequest"); + export const codecForTransactionsRequest = (): Codec => buildCodecForObject() .property("currency", codecOptional(codecForString())) diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 8ccc93c38..583d5dff5 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -46,7 +46,7 @@ import { codecOptional, renderContext, } from "./codec.js"; -import { CurrencySpecification } from "./index.js"; +import { CurrencySpecification, WithdrawalOperationStatus } from "./index.js"; import { VersionMatchResult } from "./libtool-version.js"; import { PaytoUri } from "./payto.js"; import { AgeCommitmentProof } from "./taler-crypto.js"; @@ -71,10 +71,12 @@ import { } from "./taler-types.js"; import { AbsoluteTime, + Duration, TalerPreciseTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, codecForAbsoluteTime, + codecForDuration, codecForTimestamp, } from "./time.js"; import { @@ -575,11 +577,11 @@ export interface CoinDumpJson { withdrawal_reserve_pub: string | undefined; coin_status: CoinStatus; spend_allocation: - | { - id: string; - amount: AmountString; - } - | undefined; + | { + id: string; + amount: AmountString; + } + | undefined; /** * Information about the age restriction */ @@ -942,13 +944,14 @@ export interface PreparePayResultAlreadyConfirmed { } export interface BankWithdrawDetails { - selectionDone: boolean; - transferDone: boolean; + status: WithdrawalOperationStatus, amount: AmountJson; senderWire?: string; suggestedExchange?: string; confirmTransferUrl?: string; wireTypes: string[]; + operationId: string, + apiBaseUrl: string, } export interface AcceptWithdrawalResponse { @@ -1799,6 +1802,7 @@ export const codecForApplyRefundFromPurchaseIdRequest = export interface GetWithdrawalDetailsForUriRequest { talerWithdrawUri: string; restrictAge?: number; + notifyChangeFromPendingTimeoutMs?: number; } export const codecForGetWithdrawalDetailsForUri = @@ -1806,6 +1810,7 @@ export const codecForGetWithdrawalDetailsForUri = buildCodecForObject() .property("talerWithdrawUri", codecForString()) .property("restrictAge", codecOptional(codecForNumber())) + .property("notifyChangeFromPendingTimeoutMs", codecOptional(codecForNumber())) .build("GetWithdrawalDetailsForUriRequest"); export interface ListKnownBankAccountsRequest { @@ -2185,6 +2190,9 @@ export interface TxIdResponse { } export interface WithdrawUriInfoResponse { + operationId: string; + status: WithdrawalOperationStatus, + confirmTransferUrl?: string; amount: AmountString; defaultExchangeBaseUrl?: string; possibleExchanges: ExchangeListItem[]; @@ -2193,6 +2201,14 @@ export interface WithdrawUriInfoResponse { export const codecForWithdrawUriInfoResponse = (): Codec => buildCodecForObject() + .property("operationId", codecForString()) + .property("confirmTransferUrl", codecOptional(codecForString())) + .property("status", codecForEither( + codecForConstString("pending"), + codecForConstString("selected"), + codecForConstString("aborted"), + codecForConstString("confirmed"), + )) .property("amount", codecForAmountString()) .property("defaultExchangeBaseUrl", codecOptional(codecForString())) .property("possibleExchanges", codecForList(codecForExchangeListItem())) diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 908aa540a..d93396ca5 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -41,7 +41,9 @@ import { TransactionsResponse, TransactionState, TransactionType, + TransactionWithdrawal, WalletContractData, + WithdrawalTransactionByURIRequest, WithdrawalType, } from "@gnu-taler/taler-util"; import { @@ -520,7 +522,7 @@ function buildTransactionForPeerPullCredit( const silentWithdrawalErrorForInvoice = wsrOrt?.lastError && wsrOrt.lastError.code === - TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && + TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => { return ( e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR && @@ -550,10 +552,10 @@ function buildTransactionForPeerPullCredit( kycUrl: pullCredit.kycUrl, ...(wsrOrt?.lastError ? { - error: silentWithdrawalErrorForInvoice - ? undefined - : wsrOrt.lastError, - } + error: silentWithdrawalErrorForInvoice + ? undefined + : wsrOrt.lastError, + } : {}), }; } @@ -641,7 +643,7 @@ function buildTransactionForPeerPushCredit( function buildTransactionForBankIntegratedWithdraw( wgRecord: WithdrawalGroupRecord, ort?: OperationRetryRecord, -): Transaction { +): TransactionWithdrawal { if (wgRecord.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) throw Error(""); @@ -676,7 +678,7 @@ function buildTransactionForManualWithdraw( withdrawalGroup: WithdrawalGroupRecord, exchangeDetails: ExchangeWireDetails, ort?: OperationRetryRecord, -): Transaction { +): TransactionWithdrawal { if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) throw Error(""); @@ -948,6 +950,52 @@ async function buildTransactionForPurchase( }; } +export async function getWithdrawalTransactionByUri( + ws: InternalWalletState, + request: WithdrawalTransactionByURIRequest, +): Promise { + return await ws.db + .mktx((x) => [ + x.withdrawalGroups, + x.exchangeDetails, + x.exchanges, + x.operationRetries, + ]) + .runReadWrite(async (tx) => { + const withdrawalGroupRecord = await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get( + request.talerWithdrawUri, + ); + + if (!withdrawalGroupRecord) { + return undefined; + } + + const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord); + const ort = await tx.operationRetries.get(opId); + + if ( + withdrawalGroupRecord.wgInfo.withdrawalType === + WithdrawalRecordType.BankIntegrated + ) { + return buildTransactionForBankIntegratedWithdraw( + withdrawalGroupRecord, + ort, + ); + } + const exchangeDetails = await getExchangeWireDetailsInTx( + tx, + withdrawalGroupRecord.exchangeBaseUrl, + ); + if (!exchangeDetails) throw Error("not exchange details"); + + return buildTransactionForManualWithdraw( + withdrawalGroupRecord, + exchangeDetails, + ort, + ); + }); +} + /** * Retrieve the full event history for this wallet. */ diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 58df75964..6c7e8c37a 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -45,6 +45,7 @@ import { LibtoolVersion, Logger, NotificationType, + TalerBankIntegrationHttpClient, TalerError, TalerErrorCode, TalerErrorDetail, @@ -556,19 +557,11 @@ export async function getBankWithdrawalInfo( throw Error(`can't parse URL ${talerWithdrawUri}`); } - const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl); + const bankApi = new TalerBankIntegrationHttpClient(uriResult.bankIntegrationApiBaseUrl, http); - const configResp = await http.fetch(configReqUrl.href); - const config = await readSuccessResponseJsonOrThrow( - configResp, - codecForIntegrationBankConfig(), - ); + const { body: config } = await bankApi.getConfig() - const versionRes = LibtoolVersion.compare( - WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, - config.version, - ); - if (versionRes?.compatible != true) { + if (!bankApi.isCompatible(config.version)) { throw TalerError.fromDetail( TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE, { @@ -579,29 +572,24 @@ export async function getBankWithdrawalInfo( ); } - const reqUrl = new URL( - `withdrawal-operation/${uriResult.withdrawalOperationId}`, - uriResult.bankIntegrationApiBaseUrl, - ); - - logger.info(`bank withdrawal status URL: ${reqUrl.href}}`); + const resp = await bankApi.getWithdrawalOperationById(uriResult.withdrawalOperationId) - const resp = await http.fetch(reqUrl.href); - const status = await readSuccessResponseJsonOrThrow( - resp, - codecForWithdrawOperationStatusResponse(), - ); + if (resp.type === "fail") { + throw TalerError.fromUncheckedDetail(resp.detail); + } + const { body: status } = resp logger.info(`bank withdrawal operation status: ${j2s(status)}`); return { + operationId: uriResult.withdrawalOperationId, + apiBaseUrl: uriResult.bankIntegrationApiBaseUrl, amount: Amounts.parseOrThrow(status.amount), confirmTransferUrl: status.confirm_transfer_url, - selectionDone: status.selection_done, senderWire: status.sender_wire, suggestedExchange: status.suggested_exchange, - transferDone: status.transfer_done, wireTypes: status.wire_types, + status: status.status, }; } @@ -1226,8 +1214,7 @@ export async function updateWithdrawalDenoms( denom.verificationStatus === DenominationVerificationStatus.Unverified ) { logger.trace( - `Validating denomination (${current + 1}/${ - denominations.length + `Validating denomination (${current + 1}/${denominations.length }) signature of ${denom.denomPubHash}`, ); let valid = false; @@ -1872,7 +1859,7 @@ export async function getExchangeWithdrawalInfo( ) { logger.warn( `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` + - `(exchange has ${exchange.protocolVersionRange}), checking for updates`, + `(exchange has ${exchange.protocolVersionRange}), checking for updates`, ); } } @@ -1915,6 +1902,7 @@ export async function getExchangeWithdrawalInfo( export interface GetWithdrawalDetailsForUriOpts { restrictAge?: number; + notifyChangeFromPendingTimeoutMs?: number; } /** @@ -1957,7 +1945,40 @@ export async function getWithdrawalDetailsForUri( ); }); + if (info.status === "pending" && opts.notifyChangeFromPendingTimeoutMs !== undefined) { + const bankApi = new TalerBankIntegrationHttpClient(info.apiBaseUrl, ws.http); + console.log( + `waiting operation (${info.operationId}) to change from pending`, + ); + bankApi.getWithdrawalOperationById(info.operationId, { + old_state: "pending", + timeoutMs: opts.notifyChangeFromPendingTimeoutMs + }).then(resp => { + console.log( + `operation (${info.operationId}) to change to ${JSON.stringify(resp, undefined, 2)}`, + ); + if (resp.type === "fail") { + //not found, this is rare since the previous request succeed + ws.notify({ + type: NotificationType.WithdrawalOperationTransition, + operationId: info.operationId, + state: info.status, + }) + return; + } + + ws.notify({ + type: NotificationType.WithdrawalOperationTransition, + operationId: info.operationId, + state: resp.body.status, + }); + }) + } + return { + operationId: info.operationId, + confirmTransferUrl: info.confirmTransferUrl, + status: info.status, amount: Amounts.stringify(info.amount), defaultExchangeBaseUrl: info.suggestedExchange, possibleExchanges, diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 7ac347b6d..7d3dc86a3 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -128,6 +128,8 @@ import { WalletCurrencyInfo, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, + WithdrawalTransactionByURIRequest, + TransactionWithdrawal, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, @@ -154,6 +156,7 @@ export enum WalletApiOperation { AddExchange = "addExchange", GetTransactions = "getTransactions", GetTransactionById = "getTransactionById", + GetWithdrawalTransactionByUri = "getWithdrawalTransactionByUri", TestingGetSampleTransactions = "testingGetSampleTransactions", ListExchanges = "listExchanges", GetExchangeEntryByUrl = "getExchangeEntryByUrl", @@ -377,6 +380,12 @@ export type GetTransactionByIdOp = { response: Transaction; }; +export type GetWithdrawalTransactionByUriOp = { + op: WalletApiOperation.GetWithdrawalTransactionByUri; + request: WithdrawalTransactionByURIRequest; + response: TransactionWithdrawal | undefined; +}; + export type RetryPendingNowOp = { op: WalletApiOperation.RetryPendingNow; request: EmptyObject; @@ -1124,6 +1133,7 @@ export type WalletOperations = { [WalletApiOperation.GetTransactions]: GetTransactionsOp; [WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp; [WalletApiOperation.GetTransactionById]: GetTransactionByIdOp; + [WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp; [WalletApiOperation.RetryPendingNow]: RetryPendingNowOp; [WalletApiOperation.GetPendingOperations]: GetPendingTasksOp; [WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests; @@ -1219,10 +1229,10 @@ type Primitives = string | number | boolean; type RecursivePartial = { [P in keyof T]?: T[P] extends Array - ? Array> - : T[P] extends Array - ? Array - : T[P] extends object - ? RecursivePartial - : T[P]; + ? Array> + : T[P] extends Array + ? Array + : T[P] extends object + ? RecursivePartial + : T[P]; } & object; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index d6da2250a..1a876b2c8 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -264,6 +264,7 @@ import { failTransaction, getTransactionById, getTransactions, + getWithdrawalTransactionByUri, parseTransactionIdentifier, resumeTransaction, retryTransaction, @@ -725,9 +726,9 @@ async function dumpCoins(ws: InternalWalletState): Promise { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } @@ -938,6 +939,10 @@ async function dispatchRequestInternal( const req = codecForTransactionByIdRequest().decode(payload); return await getTransactionById(ws, req); } + case WalletApiOperation.GetWithdrawalTransactionByUri: { + const req = codecForGetWithdrawalDetailsForUri().decode(payload); + return await getWithdrawalTransactionByUri(ws, req); + } case WalletApiOperation.AddExchange: { const req = codecForAddExchangeRequest().decode(payload); await fetchFreshExchange(ws, req.exchangeBaseUrl, { @@ -997,7 +1002,10 @@ async function dispatchRequestInternal( } case WalletApiOperation.GetWithdrawalDetailsForUri: { const req = codecForGetWithdrawalDetailsForUri().decode(payload); - return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri); + return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri, { + notifyChangeFromPendingTimeoutMs: req.notifyChangeFromPendingTimeoutMs, + restrictAge: req.restrictAge, + }); } case WalletApiOperation.AcceptManualWithdrawal: { const req = codecForAcceptManualWithdrawalRequet().decode(payload); diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts index 04713f3c4..1f8745a5d 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts @@ -38,7 +38,7 @@ import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { ErrorAlert } from "../../context/alert.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js"; -import { SelectAmountView, SuccessView } from "./views.js"; +import { FinalStateOperation, SelectAmountView, SuccessView } from "./views.js"; export interface PropsFromURI { talerWithdrawUri: string | undefined; @@ -60,6 +60,7 @@ export type State = | SelectExchangeState.NoExchangeFound | SelectExchangeState.Selecting | State.SelectAmount + | State.AlreadyCompleted | State.Success; export namespace State { @@ -80,6 +81,12 @@ export namespace State { amount: AmountFieldHandler; currency: string; } + export interface AlreadyCompleted { + status: "already-completed"; + operationState: "confirmed" | "aborted" | "selected"; + confirmTransferUrl?: string, + error: undefined; + } export type Success = { status: "success"; @@ -116,6 +123,7 @@ const viewMapping: StateViewMap = { "no-exchange-found": NoExchangesView, "selecting-exchange": ExchangeSelectionPage, success: SuccessView, + "already-completed": FinalStateOperation, }; export const WithdrawPageFromURI = compose( diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index 7bff13e51..bf460834d 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -21,11 +21,12 @@ import { ExchangeFullDetails, ExchangeListItem, ExchangeTosStatus, + NotificationType, TalerError, parseWithdrawExchangeUri, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useState, useMemo, useCallback } from "preact/hooks"; import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -208,17 +209,40 @@ export function useComponentStateFromURI({ WalletApiOperation.GetWithdrawalDetailsForUri, { talerWithdrawUri, + notifyChangeFromPendingTimeoutMs: 30 * 1000 }, ); - const { amount, defaultExchangeBaseUrl, possibleExchanges } = uriInfo; + const { amount, defaultExchangeBaseUrl, possibleExchanges, operationId, confirmTransferUrl, status } = uriInfo; + const transaction = await api.wallet.call( + WalletApiOperation.GetWithdrawalTransactionByUri, + { talerWithdrawUri }, + ); return { talerWithdrawUri, + operationId, + status, + transaction, + confirmTransferUrl, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, exchanges: possibleExchanges, }; }); + const readyToListen = uriInfoHook && !uriInfoHook.hasError + + useEffect(() => { + if (!uriInfoHook) { + return; + } + return api.listener.onUpdateNotification( + [NotificationType.WithdrawalOperationTransition], + () => { + uriInfoHook.retry() + }, + ); + }, [readyToListen]); + if (!uriInfoHook) return { status: "loading", error: undefined }; if (uriInfoHook.hasError) { @@ -257,8 +281,20 @@ export function useComponentStateFromURI({ }; } - return () => - exchangeSelectionState( + if (uriInfoHook.response.status !== "pending") { + if (uriInfoHook.response.transaction) { + onSuccess(uriInfoHook.response.transaction.transactionId) + } + return { + status: "already-completed", + operationState: uriInfoHook.response.status, + confirmTransferUrl: uriInfoHook.response.confirmTransferUrl, + error: undefined, + } + } + + return useCallback(() => { + return exchangeSelectionState( doManagedWithdraw, cancel, onSuccess, @@ -267,6 +303,7 @@ export function useComponentStateFromURI({ exchangeList, defaultExchange, ); + }, []) } type ManualOrManagedWithdrawFunction = ( @@ -294,7 +331,7 @@ function exchangeSelectionState( return selectedExchange; } - return (): State.Success | State.LoadingUriError | State.Loading => { + return useCallback((): State.Success | State.LoadingUriError | State.Loading => { const { i18n } = useTranslationContext(); const { pushAlertOnError } = useAlertContext(); const [ageRestricted, setAgeRestricted] = useState(0); @@ -428,5 +465,5 @@ function exchangeSelectionState( }, cancel, }; - }; + }, []); } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx index a3127fafc..29f39054f 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx @@ -23,7 +23,7 @@ import { CurrencySpecification, ExchangeListItem } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { nullFunction } from "../../mui/handlers.js"; // import { TermsState } from "../../utils/index.js"; -import { SuccessView } from "./views.js"; +import { SuccessView, FinalStateOperation } from "./views.js"; export default { title: "withdraw", @@ -67,6 +67,23 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, { chooseCurrencies: [], }); +export const AlreadyAborted = tests.createExample(FinalStateOperation, { + error: undefined, + status: "already-completed", + operationState: "aborted" +}); +export const AlreadySelected = tests.createExample(FinalStateOperation, { + error: undefined, + status: "already-completed", + operationState: "selected" +}); +export const AlreadyConfirmed = tests.createExample(FinalStateOperation, { + error: undefined, + status: "already-completed", + operationState: "confirmed" +}); + + export const WithSomeFee = tests.createExample(SuccessView, { error: undefined, status: "success", diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index 3493415d9..f90f7bed7 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -111,10 +111,19 @@ describe("Withdraw CTA states", () => { WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { + status: "pending", + operationId: "123", amount: "EUR:2" as AmountString, possibleExchanges: [], }, ); + handler.addWalletCallResponse( + WalletApiOperation.GetWithdrawalTransactionByUri, + undefined, + { + transactionId: "123" + } as any, + ); const hookBehavior = await tests.hookBehaveLikeThis( useComponentStateFromURI, @@ -147,11 +156,20 @@ describe("Withdraw CTA states", () => { WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { + status: "pending", + operationId: "123", amount: "ARS:2" as AmountString, possibleExchanges: exchanges, defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, }, ); + handler.addWalletCallResponse( + WalletApiOperation.GetWithdrawalTransactionByUri, + undefined, + { + transactionId: "123" + } as any, + ); handler.addWalletCallResponse( WalletApiOperation.GetWithdrawalDetailsForAmount, undefined, @@ -217,6 +235,8 @@ describe("Withdraw CTA states", () => { WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { + status: "pending", + operationId: "123", amount: "ARS:2" as AmountString, possibleExchanges: exchangeWithNewTos, defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl, @@ -245,6 +265,8 @@ describe("Withdraw CTA states", () => { WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { + status: "pending", + operationId: "123", amount: "ARS:2" as AmountString, possibleExchanges: exchanges, defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx index 748b65817..bd9f75696 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -21,7 +21,7 @@ import { Amount } from "../../components/Amount.js"; import { Part } from "../../components/Part.js"; import { QR } from "../../components/QR.js"; import { SelectList } from "../../components/SelectList.js"; -import { Input, LinkSuccess, SvgIcon } from "../../components/styled/index.js"; +import { Input, LinkSuccess, SvgIcon, WarningBox } from "../../components/styled/index.js"; import { TermsOfService } from "../../components/TermsOfService/index.js"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Button } from "../../mui/Button.js"; @@ -35,6 +35,33 @@ import { State } from "./index.js"; import { Grid } from "../../mui/Grid.js"; import { AmountField } from "../../components/AmountField.js"; +export function FinalStateOperation(state: State.AlreadyCompleted): VNode { + const { i18n } = useTranslationContext(); + + switch (state.operationState) { + case "confirmed": return +
+ This operation has already been completed by another wallet. +
+
+ case "aborted": return +
+ This operation has already been aborted +
+
+ case "selected": return +
+ This operation has already been used by another wallet. +
+
+ It can be confirmed in  + this page + +
+
+ } +} + export function SuccessView(state: State.Success): VNode { const { i18n } = useTranslationContext(); // const currentTosVersionIsAccepted = diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts index ad4eabf15..d83e6f472 100644 --- a/packages/taler-wallet-webextension/src/utils/index.ts +++ b/packages/taler-wallet-webextension/src/utils/index.ts @@ -15,6 +15,7 @@ */ import { createElement, VNode } from "preact"; +import { useCallback, useMemo } from "preact/hooks"; function getJsonIfOk(r: Response): Promise { if (r.ok) { @@ -26,8 +27,7 @@ function getJsonIfOk(r: Response): Promise { } throw new Error( - `Try another server: (${r.status}) ${ - r.statusText || "internal server error" + `Try another server: (${r.status}) ${r.statusText || "internal server error" }`, ); } @@ -89,6 +89,7 @@ export function compose( ): (p: PType) => VNode { function withHook(stateHook: () => RecursiveState): () => VNode { function TheComponent(): VNode { + //if the function is the same, do not compute const state = stateHook(); if (typeof state === "function") { @@ -102,7 +103,9 @@ export function compose( } // TheComponent.name = `${name}`; - return TheComponent; + return useMemo(() => { + return TheComponent + }, [stateHook]); } return (p: PType) => { diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index a194de0ff..1ecd66f05 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -272,11 +272,15 @@ async function reinitWallet(): Promise { let timer; if (platform.useServiceWorkerAsBackgroundProcess()) { - httpLib = new ServiceWorkerHttpLib(); + httpLib = new ServiceWorkerHttpLib({ + // enableThrottling: false, + }); cryptoWorker = new SynchronousCryptoWorkerFactoryPlain(); timer = new SetTimeoutTimerAPI(); } else { - httpLib = new BrowserHttpLib(); + httpLib = new BrowserHttpLib({ + // enableThrottling: false, + }); // We could (should?) use the BrowserCryptoWorkerFactory here, // but right now we don't, to have less platform differences. // cryptoWorker = new BrowserCryptoWorkerFactory(); @@ -409,9 +413,9 @@ async function toggleHeaderListener( platform.registerTalerHeaderListener(); return { newValue: true }; } catch (e) { - logger.error("FAIL to toggle",e) + logger.error("FAIL to toggle", e) } - return { newValue: false } + return { newValue: false } } const rem = await platform.getPermissionsApi().removeHostPermissions(); diff --git a/packages/web-util/src/utils/http-impl.browser.ts b/packages/web-util/src/utils/http-impl.browser.ts index 18140ef13..5d65c3903 100644 --- a/packages/web-util/src/utils/http-impl.browser.ts +++ b/packages/web-util/src/utils/http-impl.browser.ts @@ -33,6 +33,7 @@ import { getDefaultHeaders, encodeBody, DEFAULT_REQUEST_TIMEOUT_MS, + HttpLibArgs, } from "@gnu-taler/taler-util/http"; const logger = new Logger("browserHttpLib"); @@ -44,6 +45,12 @@ const logger = new Logger("browserHttpLib"); export class BrowserHttpLib implements HttpRequestLibrary { private throttle = new RequestThrottler(); private throttlingEnabled = true; + private requireTls = false; + + constructor(args?: HttpLibArgs) { + this.throttlingEnabled = args?.enableThrottling ?? true; + this.requireTls = args?.requireTls ?? false; + } fetch( requestUrl: string, @@ -55,8 +62,8 @@ export class BrowserHttpLib implements HttpRequestLibrary { const requestTimeout = options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS); + const parsedUrl = new URL(requestUrl); if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) { - const parsedUrl = new URL(requestUrl); throw TalerError.fromDetail( TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, { @@ -67,6 +74,16 @@ export class BrowserHttpLib implements HttpRequestLibrary { `request to origin ${parsedUrl.origin} was throttled`, ); } + if (this.requireTls && parsedUrl.protocol !== "https:") { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_NETWORK_ERROR, + { + requestMethod: requestMethod, + requestUrl: requestUrl, + }, + `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`, + ); + } let myBody: ArrayBuffer | undefined = requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH" diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts index 3c269e695..2ae4ccd86 100644 --- a/packages/web-util/src/utils/http-impl.sw.ts +++ b/packages/web-util/src/utils/http-impl.sw.ts @@ -27,6 +27,7 @@ import { import { DEFAULT_REQUEST_TIMEOUT_MS, Headers, + HttpLibArgs, HttpRequestLibrary, HttpRequestOptions, HttpResponse, @@ -41,6 +42,12 @@ import { export class ServiceWorkerHttpLib implements HttpRequestLibrary { private throttle = new RequestThrottler(); private throttlingEnabled = true; + private requireTls = false; + + public constructor(args?: HttpLibArgs) { + this.throttlingEnabled = args?.enableThrottling ?? true; + this.requireTls = args?.requireTls ?? false; + } async fetch( requestUrl: string, @@ -52,8 +59,8 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary { const requestTimeout = options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS); + const parsedUrl = new URL(requestUrl); if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) { - const parsedUrl = new URL(requestUrl); throw TalerError.fromDetail( TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, { @@ -64,6 +71,16 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary { `request to origin ${parsedUrl.origin} was throttled`, ); } + if (this.requireTls && parsedUrl.protocol !== "https:") { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_NETWORK_ERROR, + { + requestMethod: requestMethod, + requestUrl: requestUrl, + }, + `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`, + ); + } let myBody: ArrayBuffer | undefined = requestMethod === "POST" ? encodeBody(requestBody) : undefined; -- cgit v1.2.3