fix #7411, also making the backup payment visible

This commit is contained in:
Sebastian 2022-11-16 16:04:52 -03:00
parent 53164dc47b
commit 1a63d56bfd
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
58 changed files with 4614 additions and 8439 deletions

View File

@ -241,12 +241,18 @@ export interface ConfirmPayResultPending {
lastError: TalerErrorDetail | undefined; lastError: TalerErrorDetail | undefined;
} }
export const codecForTalerErrorDetail = (): Codec<TalerErrorDetail> =>
buildCodecForObject<TalerErrorDetail>()
.property("code", codecForNumber())
.property("hint", codecOptional(codecForString()))
.build("TalerErrorDetail");
export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending; export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
export const codecForConfirmPayResultPending = export const codecForConfirmPayResultPending =
(): Codec<ConfirmPayResultPending> => (): Codec<ConfirmPayResultPending> =>
buildCodecForObject<ConfirmPayResultPending>() buildCodecForObject<ConfirmPayResultPending>()
.property("lastError", codecForAny()) .property("lastError", codecOptional(codecForTalerErrorDetail()))
.property("transactionId", codecForString()) .property("transactionId", codecForString())
.property("type", codecForConstString(ConfirmPayResultType.Pending)) .property("type", codecForConstString(ConfirmPayResultType.Pending))
.build("ConfirmPayResultPending"); .build("ConfirmPayResultPending");

View File

@ -25,7 +25,9 @@ import { SynchronousCryptoWorkerPlain } from "./synchronousWorkerPlain.js";
* The synchronous crypto worker produced by this factory doesn't run in the * The synchronous crypto worker produced by this factory doesn't run in the
* background, but actually blocks the caller until the operation is done. * background, but actually blocks the caller until the operation is done.
*/ */
export class SynchronousCryptoWorkerFactoryPlain implements CryptoWorkerFactory { export class SynchronousCryptoWorkerFactoryPlain
implements CryptoWorkerFactory
{
startWorker(): CryptoWorker { startWorker(): CryptoWorker {
return new SynchronousCryptoWorkerPlain(); return new SynchronousCryptoWorkerPlain();
} }

View File

@ -29,15 +29,18 @@ import {
AmountString, AmountString,
BackupRecovery, BackupRecovery,
buildCodecForObject, buildCodecForObject,
buildCodecForUnion,
bytesToString, bytesToString,
canonicalizeBaseUrl, canonicalizeBaseUrl,
canonicalJson, canonicalJson,
Codec, Codec,
codecForAmountString, codecForAmountString,
codecForBoolean, codecForBoolean,
codecForConstString,
codecForList, codecForList,
codecForNumber, codecForNumber,
codecForString, codecForString,
codecForTalerErrorDetail,
codecOptional, codecOptional,
ConfirmPayResultType, ConfirmPayResultType,
decodeCrock, decodeCrock,
@ -78,6 +81,7 @@ import {
WalletBackupConfState, WalletBackupConfState,
} from "../../db.js"; } from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
import { import {
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
readTalerErrorResponse, readTalerErrorResponse,
@ -232,12 +236,6 @@ function deriveBlobSecret(bc: WalletBackupConfState): Uint8Array {
interface BackupForProviderArgs { interface BackupForProviderArgs {
backupProviderBaseUrl: string; backupProviderBaseUrl: string;
/**
* Should we attempt one more upload after trying
* to pay?
*/
retryAfterPayment: boolean;
} }
function getNextBackupTimestamp(): TalerProtocolTimestamp { function getNextBackupTimestamp(): TalerProtocolTimestamp {
@ -253,7 +251,7 @@ function getNextBackupTimestamp(): TalerProtocolTimestamp {
async function runBackupCycleForProvider( async function runBackupCycleForProvider(
ws: InternalWalletState, ws: InternalWalletState,
args: BackupForProviderArgs, args: BackupForProviderArgs,
): Promise<OperationAttemptResult> { ): Promise<OperationAttemptResult<unknown, { talerUri: string }>> {
const provider = await ws.db const provider = await ws.db
.mktx((x) => [x.backupProviders]) .mktx((x) => [x.backupProviders])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
@ -339,57 +337,34 @@ async function runBackupCycleForProvider(
if (!talerUri) { if (!talerUri) {
throw Error("no taler URI available to pay provider"); throw Error("no taler URI available to pay provider");
} }
const res = await preparePayForUri(ws, talerUri);
let proposalId = res.proposalId;
let doPay = false;
switch (res.status) {
case PreparePayResultType.InsufficientBalance:
// FIXME: record in provider state!
logger.warn("insufficient balance to pay for backup provider");
proposalId = res.proposalId;
break;
case PreparePayResultType.PaymentPossible:
doPay = true;
break;
case PreparePayResultType.AlreadyConfirmed:
break;
}
// FIXME: check if the provider is overcharging us! //We can't delay downloading the proposal since we need the id
//FIXME: check download errors
const res = await preparePayForUri(ws, talerUri);
await ws.db await ws.db
.mktx((x) => [x.backupProviders, x.operationRetries]) .mktx((x) => [x.backupProviders, x.operationRetries])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const provRec = await tx.backupProviders.get(provider.baseUrl); const prov = await tx.backupProviders.get(provider.baseUrl);
checkDbInvariant(!!provRec); if (!prov) {
const ids = new Set(provRec.paymentProposalIds); logger.warn("backup provider not found anymore");
ids.add(proposalId); return;
provRec.paymentProposalIds = Array.from(ids).sort(); }
provRec.currentPaymentProposalId = proposalId; const opId = RetryTags.forBackup(prov);
// FIXME: allocate error code for this!
await tx.backupProviders.put(provRec);
const opId = RetryTags.forBackup(provRec);
await scheduleRetryInTx(ws, tx, opId); await scheduleRetryInTx(ws, tx, opId);
prov.currentPaymentProposalId = res.proposalId;
prov.state = {
tag: BackupProviderStateTag.Retrying,
};
await tx.backupProviders.put(prov);
}); });
if (doPay) {
const confirmRes = await confirmPay(ws, proposalId);
switch (confirmRes.type) {
case ConfirmPayResultType.Pending:
logger.warn("payment not yet finished yet");
break;
}
}
if (args.retryAfterPayment) {
return await runBackupCycleForProvider(ws, {
...args,
retryAfterPayment: false,
});
}
return { return {
type: OperationAttemptResultType.Pending, type: OperationAttemptResultType.Pending,
result: undefined, result: {
talerUri,
},
}; };
} }
@ -442,10 +417,7 @@ async function runBackupCycleForProvider(
}); });
logger.info("processed existing backup"); logger.info("processed existing backup");
// Now upload our own, merged backup. // Now upload our own, merged backup.
return await runBackupCycleForProvider(ws, { return await runBackupCycleForProvider(ws, args);
...args,
retryAfterPayment: false,
});
} }
// Some other response that we did not expect! // Some other response that we did not expect!
@ -477,7 +449,6 @@ export async function processBackupForProvider(
return await runBackupCycleForProvider(ws, { return await runBackupCycleForProvider(ws, {
backupProviderBaseUrl: provider.baseUrl, backupProviderBaseUrl: provider.baseUrl,
retryAfterPayment: true,
}); });
} }
@ -540,12 +511,11 @@ export async function runBackupCycle(
for (const provider of providers) { for (const provider of providers) {
await runBackupCycleForProvider(ws, { await runBackupCycleForProvider(ws, {
backupProviderBaseUrl: provider.baseUrl, backupProviderBaseUrl: provider.baseUrl,
retryAfterPayment: true,
}); });
} }
} }
interface SyncTermsOfServiceResponse { export interface SyncTermsOfServiceResponse {
// maximum backup size supported // maximum backup size supported
storage_limit_in_megabytes: number; storage_limit_in_megabytes: number;
@ -557,7 +527,7 @@ interface SyncTermsOfServiceResponse {
version: string; version: string;
} }
const codecForSyncTermsOfServiceResponse = export const codecForSyncTermsOfServiceResponse =
(): Codec<SyncTermsOfServiceResponse> => (): Codec<SyncTermsOfServiceResponse> =>
buildCodecForObject<SyncTermsOfServiceResponse>() buildCodecForObject<SyncTermsOfServiceResponse>()
.property("storage_limit_in_megabytes", codecForNumber()) .property("storage_limit_in_megabytes", codecForNumber())
@ -584,10 +554,58 @@ export const codecForAddBackupProviderRequest =
.property("activate", codecOptional(codecForBoolean())) .property("activate", codecOptional(codecForBoolean()))
.build("AddBackupProviderRequest"); .build("AddBackupProviderRequest");
export type AddBackupProviderResponse =
| AddBackupProviderOk
| AddBackupProviderPaymentRequired
| AddBackupProviderError;
interface AddBackupProviderOk {
status: "ok";
}
interface AddBackupProviderPaymentRequired {
status: "payment-required";
talerUri: string;
}
interface AddBackupProviderError {
status: "error";
error: TalerErrorDetail;
}
export const codecForAddBackupProviderOk = (): Codec<AddBackupProviderOk> =>
buildCodecForObject<AddBackupProviderOk>()
.property("status", codecForConstString("ok"))
.build("AddBackupProviderOk");
export const codecForAddBackupProviderPaymenrRequired =
(): Codec<AddBackupProviderPaymentRequired> =>
buildCodecForObject<AddBackupProviderPaymentRequired>()
.property("status", codecForConstString("payment-required"))
.property("talerUri", codecForString())
.build("AddBackupProviderPaymentRequired");
export const codecForAddBackupProviderError =
(): Codec<AddBackupProviderError> =>
buildCodecForObject<AddBackupProviderError>()
.property("status", codecForConstString("error"))
.property("error", codecForTalerErrorDetail())
.build("AddBackupProviderError");
export const codecForAddBackupProviderResponse =
(): Codec<AddBackupProviderResponse> =>
buildCodecForUnion<AddBackupProviderResponse>()
.discriminateOn("status")
.alternative("ok", codecForAddBackupProviderOk())
.alternative(
"payment-required",
codecForAddBackupProviderPaymenrRequired(),
)
.alternative("error", codecForAddBackupProviderError())
.build("AddBackupProviderResponse");
export async function addBackupProvider( export async function addBackupProvider(
ws: InternalWalletState, ws: InternalWalletState,
req: AddBackupProviderRequest, req: AddBackupProviderRequest,
): Promise<void> { ): Promise<AddBackupProviderResponse> {
logger.info(`adding backup provider ${j2s(req)}`); logger.info(`adding backup provider ${j2s(req)}`);
await provideBackupState(ws); await provideBackupState(ws);
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl); const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
@ -618,6 +636,7 @@ export async function addBackupProvider(
.mktx((x) => [x.backupProviders]) .mktx((x) => [x.backupProviders])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let state: BackupProviderState; let state: BackupProviderState;
//FIXME: what is the difference provisional and ready?
if (req.activate) { if (req.activate) {
state = { state = {
tag: BackupProviderStateTag.Ready, tag: BackupProviderStateTag.Ready,
@ -641,6 +660,39 @@ export async function addBackupProvider(
uids: [encodeCrock(getRandomBytes(32))], uids: [encodeCrock(getRandomBytes(32))],
}); });
}); });
return await runFirstBackupCycleForProvider(ws, {
backupProviderBaseUrl: canonUrl,
});
}
async function runFirstBackupCycleForProvider(
ws: InternalWalletState,
args: BackupForProviderArgs,
): Promise<AddBackupProviderResponse> {
const resp = await runBackupCycleForProvider(ws, args);
switch (resp.type) {
case OperationAttemptResultType.Error:
return {
status: "error",
error: resp.errorDetail,
};
case OperationAttemptResultType.Finished:
return {
status: "ok",
};
case OperationAttemptResultType.Longpoll:
throw Error(
"unexpected runFirstBackupCycleForProvider result (longpoll)",
);
case OperationAttemptResultType.Pending:
return {
status: "payment-required",
talerUri: resp.result.talerUri,
};
default:
assertUnreachable(resp);
}
} }
export async function restoreFromRecoverySecret(): Promise<void> { export async function restoreFromRecoverySecret(): Promise<void> {

View File

@ -1584,7 +1584,7 @@ export async function runPayForConfirmPay(
const numRetry = opRetry?.retryInfo.retryCounter ?? 0; const numRetry = opRetry?.retryInfo.retryCounter ?? 0;
if ( if (
res.errorDetail.code === res.errorDetail.code ===
TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR &&
numRetry < maxRetry numRetry < maxRetry
) { ) {
// Pretend the operation is pending instead of reporting // Pretend the operation is pending instead of reporting

View File

@ -106,6 +106,7 @@ import {
import { WalletContractData } from "./db.js"; import { WalletContractData } from "./db.js";
import { import {
AddBackupProviderRequest, AddBackupProviderRequest,
AddBackupProviderResponse,
BackupInfo, BackupInfo,
RemoveBackupProviderRequest, RemoveBackupProviderRequest,
RunBackupCycleRequest, RunBackupCycleRequest,
@ -519,7 +520,7 @@ export type ExportBackupOp = {
export type AddBackupProviderOp = { export type AddBackupProviderOp = {
op: WalletApiOperation.AddBackupProvider; op: WalletApiOperation.AddBackupProvider;
request: AddBackupProviderRequest; request: AddBackupProviderRequest;
response: EmptyObject; response: AddBackupProviderResponse;
}; };
export type RemoveBackupProviderOp = { export type RemoveBackupProviderOp = {

View File

@ -933,9 +933,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
ageCommitmentProof: c.ageCommitmentProof, ageCommitmentProof: c.ageCommitmentProof,
spend_allocation: c.spendAllocation spend_allocation: c.spendAllocation
? { ? {
amount: c.spendAllocation.amount, amount: c.spendAllocation.amount,
id: c.spendAllocation.id, id: c.spendAllocation.id,
} }
: undefined, : undefined,
}); });
} }
@ -1215,8 +1215,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
} }
case WalletApiOperation.AddBackupProvider: { case WalletApiOperation.AddBackupProvider: {
const req = codecForAddBackupProviderRequest().decode(payload); const req = codecForAddBackupProviderRequest().decode(payload);
await addBackupProvider(ws, req); return await addBackupProvider(ws, req);
return {};
} }
case WalletApiOperation.RunBackupCycle: { case WalletApiOperation.RunBackupCycle: {
const req = codecForRunBackupCycle().decode(payload); const req = codecForRunBackupCycle().decode(payload);

View File

@ -25,7 +25,6 @@
* Imports. * Imports.
*/ */
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { JustInDevMode } from "./components/JustInDevMode.js";
import { import {
NavigationHeader, NavigationHeader,
NavigationHeaderHolder, NavigationHeaderHolder,
@ -138,14 +137,9 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
> >
<i18n.Translate>Balance</i18n.Translate> <i18n.Translate>Balance</i18n.Translate>
</a> </a>
<JustInDevMode> <a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}>
<a <i18n.Translate>Backup</i18n.Translate>
href={Pages.backup} </a>
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate>
</a>
</JustInDevMode>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}> <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
<a href={Pages.qr}> <a href={Pages.qr}>
<SvgIcon <SvgIcon
@ -177,18 +171,16 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
> >
<i18n.Translate>Balance</i18n.Translate> <i18n.Translate>Balance</i18n.Translate>
</a> </a>
<JustInDevMode> <a
<a href={Pages.backup}
href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}
class={path.startsWith("/backup") ? "active" : ""} >
> <i18n.Translate>Backup</i18n.Translate>
<i18n.Translate>Backup</i18n.Translate> </a>
</a>
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}> <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate> <i18n.Translate>Dev</i18n.Translate>
</a> </a>
</JustInDevMode>
<div <div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }} style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}

View File

@ -0,0 +1,31 @@
/*
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 { createExample } from "../test-utils.js";
import { QR } from "./QR.js";
export default {
title: "wallet/qr",
};
export const Restore = createExample(QR, {
text: "taler://restore/6J0RZTJC6AV21WXK87BTE67WTHE9P2QSHF2BZXTP7PDZY2ARYBPG@sync1.demo.taler.net,sync2.demo.taler.net,sync1.demo.taler.net,sync3.demo.taler.net",
});

View File

@ -22,7 +22,7 @@ export function QR({ text }: { text: string }): VNode {
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (!divRef.current) return; if (!divRef.current) return;
const qr = qrcode(0, "L"); const qr = qrcode(0, "H");
qr.addData(text); qr.addData(text);
qr.make(); qr.make();
divRef.current.innerHTML = qr.createSvgTag({ divRef.current.innerHTML = qr.createSvgTag({

View File

@ -26,7 +26,7 @@ import {
LoadingUriView, LoadingUriView,
ShowButtonsAcceptedTosView, ShowButtonsAcceptedTosView,
ShowButtonsNonAcceptedTosView, ShowButtonsNonAcceptedTosView,
ShowTosContentView ShowTosContentView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {

View File

@ -35,10 +35,13 @@ export function useComponentState(
* For the exchange selected, bring the status of the terms of service * For the exchange selected, bring the status of the terms of service
*/ */
const terms = useAsyncAsHook(async () => { const terms = useAsyncAsHook(async () => {
const exchangeTos = await api.wallet.call(WalletApiOperation.GetExchangeTos, { const exchangeTos = await api.wallet.call(
exchangeBaseUrl: exchangeUrl, WalletApiOperation.GetExchangeTos,
acceptedFormat: ["text/xml"] {
}) exchangeBaseUrl: exchangeUrl,
acceptedFormat: ["text/xml"],
},
);
const state = buildTermsOfServiceState(exchangeTos); const state = buildTermsOfServiceState(exchangeTos);
@ -78,14 +81,14 @@ export function useComponentState(
if (accepted) { if (accepted) {
api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, { api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
exchangeBaseUrl: exchangeUrl, exchangeBaseUrl: exchangeUrl,
etag: state.version etag: state.version,
}) });
} else { } else {
// mark as not accepted // mark as not accepted
api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, { api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
exchangeBaseUrl: exchangeUrl, exchangeBaseUrl: exchangeUrl,
etag: undefined etag: undefined,
}) });
} }
// setAccepted(accepted); // setAccepted(accepted);
if (!readOnly) onChange(accepted); //external update if (!readOnly) onChange(accepted); //external update

View File

@ -24,5 +24,6 @@ import * as a2 from "./PendingTransactions.stories.js";
import * as a3 from "./Amount.stories.js"; import * as a3 from "./Amount.stories.js";
import * as a4 from "./ShowFullContractTermPopup.stories.js"; import * as a4 from "./ShowFullContractTermPopup.stories.js";
import * as a5 from "./TermsOfService/stories.js"; import * as a5 from "./TermsOfService/stories.js";
import * as a6 from "./QR.stories";
export default [a1, a2, a3, a4, a5]; export default [a1, a2, a3, a4, a5, a6];

View File

@ -48,8 +48,9 @@ export const DevContextProviderForTesting = ({
value: { value: {
devMode: !!value, devMode: !!value,
devModeToggle: { devModeToggle: {
value, button: {} value,
} button: {},
},
}, },
children, children,
}); });
@ -57,7 +58,7 @@ export const DevContextProviderForTesting = ({
export const DevContextProvider = ({ children }: { children: any }): VNode => { export const DevContextProvider = ({ children }: { children: any }): VNode => {
const devModeToggle = useWalletDevMode(); const devModeToggle = useWalletDevMode();
const value: Type = { devMode: !!devModeToggle.value, devModeToggle } const value: Type = { devMode: !!devModeToggle.value, devModeToggle };
//support for function as children, useful for getting the value right away //support for function as children, useful for getting the value right away
children = children =
children.length === 1 && typeof children === "function" children.length === 1 && typeof children === "function"

View File

@ -15,7 +15,11 @@
*/ */
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import {
Amounts,
TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { isFuture, parse } from "date-fns"; import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -32,7 +36,9 @@ export function useComponentState(
): RecursiveState<State> { ): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const hook = useAsyncAsHook(() => api.wallet.call(WalletApiOperation.ListExchanges, {})); const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
);
if (!hook) { if (!hook) {
return { return {
@ -51,7 +57,7 @@ export function useComponentState(
return () => { return () => {
const [subject, setSubject] = useState<string | undefined>(); const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>() const [timestamp, setTimestamp] = useState<string | undefined>();
const [operationError, setOperationError] = useState< const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined TalerErrorDetail | undefined
@ -70,45 +76,51 @@ export function useComponentState(
const exchange = selectedExchange.selected; const exchange = selectedExchange.selected;
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const resp = await api.wallet.call(WalletApiOperation.PreparePeerPullPayment, { const resp = await api.wallet.call(
amount: amountStr, WalletApiOperation.PreparePeerPullPayment,
exchangeBaseUrl: exchange.exchangeBaseUrl, {
}) amount: amountStr,
return resp exchangeBaseUrl: exchange.exchangeBaseUrl,
}) },
);
return resp;
});
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
error: undefined error: undefined,
} };
} }
if (hook.hasError) { if (hook.hasError) {
return { return {
status: "loading-uri", status: "loading-uri",
error: hook error: hook,
} };
} }
const { amountEffective, amountRaw } = hook.response const { amountEffective, amountRaw } = hook.response;
const requestAmount = Amounts.parseOrThrow(amountRaw) const requestAmount = Amounts.parseOrThrow(amountRaw);
const toBeReceived = Amounts.parseOrThrow(amountEffective) const toBeReceived = Amounts.parseOrThrow(amountEffective);
let purse_expiration: TalerProtocolTimestamp | undefined = undefined let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
let timestampError: string | undefined = undefined; let timestampError: string | undefined = undefined;
const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()) const t =
timestamp === undefined
? undefined
: parse(timestamp, "dd/MM/yyyy", new Date());
if (t !== undefined) { if (t !== undefined) {
if (Number.isNaN(t.getTime())) { if (Number.isNaN(t.getTime())) {
timestampError = 'Should have the format "dd/MM/yyyy"' timestampError = 'Should have the format "dd/MM/yyyy"';
} else { } else {
if (!isFuture(t)) { if (!isFuture(t)) {
timestampError = 'Should be in the future' timestampError = "Should be in the future";
} else { } else {
purse_expiration = { purse_expiration = {
t_s: t.getTime() / 1000 t_s: t.getTime() / 1000,
} };
} }
} }
} }
@ -116,14 +128,17 @@ export function useComponentState(
async function accept(): Promise<void> { async function accept(): Promise<void> {
if (!subject || !purse_expiration) return; if (!subject || !purse_expiration) return;
try { try {
const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPullPayment, { const resp = await api.wallet.call(
exchangeBaseUrl: exchange.exchangeBaseUrl, WalletApiOperation.InitiatePeerPullPayment,
partialContractTerms: { {
amount: Amounts.stringify(amount), exchangeBaseUrl: exchange.exchangeBaseUrl,
summary: subject, partialContractTerms: {
purse_expiration amount: Amounts.stringify(amount),
summary: subject,
purse_expiration,
},
}, },
}); );
onSuccess(resp.transactionId); onSuccess(resp.transactionId);
} catch (e) { } catch (e) {
@ -134,12 +149,18 @@ export function useComponentState(
throw Error("error trying to accept"); throw Error("error trying to accept");
} }
} }
const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration const unableToCreate =
!subject || Amounts.isZero(amount) || !purse_expiration;
return { return {
status: "ready", status: "ready",
subject: { subject: {
error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, error:
subject === undefined
? undefined
: !subject
? "Can't be empty"
: undefined,
value: subject ?? "", value: subject ?? "",
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },
@ -147,8 +168,8 @@ export function useComponentState(
error: timestampError, error: timestampError,
value: timestamp === undefined ? "" : timestamp, value: timestamp === undefined ? "" : timestamp,
onInput: async (e) => { onInput: async (e) => {
setTimestamp(e) setTimestamp(e);
} },
}, },
doSelectExchange: selectedExchange.doSelect, doSelectExchange: selectedExchange.doSelect,
exchangeUrl: exchange.exchangeBaseUrl, exchangeUrl: exchange.exchangeBaseUrl,

View File

@ -18,7 +18,7 @@ import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
PreparePayResult, PreparePayResult,
TalerErrorDetail TalerErrorDetail,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";

View File

@ -21,7 +21,7 @@ import {
PreparePayResult, PreparePayResult,
PreparePayResultType, PreparePayResultType,
TalerErrorDetail, TalerErrorDetail,
TalerProtocolTimestamp TalerProtocolTimestamp,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
@ -41,10 +41,12 @@ export function useComponentState(
return { p2p, balance }; return { p2p, balance };
}); });
useEffect(() => api.listener.onUpdateNotification( useEffect(() =>
[NotificationType.CoinWithdrawn], api.listener.onUpdateNotification(
hook?.retry [NotificationType.CoinWithdrawn],
)); hook?.retry,
),
);
const [operationError, setOperationError] = useState< const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined TalerErrorDetail | undefined
@ -63,10 +65,7 @@ export function useComponentState(
}; };
} }
const { const { contractTerms, peerPullPaymentIncomingId } = hook.response.p2p;
contractTerms,
peerPullPaymentIncomingId,
} = hook.response.p2p;
const amountStr: string = contractTerms?.amount; const amountStr: string = contractTerms?.amount;
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
@ -134,9 +133,12 @@ export function useComponentState(
async function accept(): Promise<void> { async function accept(): Promise<void> {
try { try {
const resp = await api.wallet.call(WalletApiOperation.AcceptPeerPullPayment, { const resp = await api.wallet.call(
peerPullPaymentIncomingId, WalletApiOperation.AcceptPeerPullPayment,
}); {
peerPullPaymentIncomingId,
},
);
onSuccess(resp.transactionId); onSuccess(resp.transactionId);
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {

View File

@ -15,8 +15,10 @@
*/ */
import { import {
AmountJson, PreparePayResult, AmountJson,
PreparePayResultAlreadyConfirmed, PreparePayResultPaymentPossible PreparePayResult,
PreparePayResultAlreadyConfirmed,
PreparePayResultPaymentPossible,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";

View File

@ -15,10 +15,11 @@
*/ */
import { import {
Amounts, ConfirmPayResultType, Amounts,
ConfirmPayResultType,
NotificationType, NotificationType,
PreparePayResultType, PreparePayResultType,
TalerErrorCode TalerErrorCode,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
@ -35,17 +36,24 @@ export function useComponentState(
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT"); if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT");
const payStatus = await api.wallet.call(WalletApiOperation.PreparePayForUri, { const payStatus = await api.wallet.call(
talerPayUri: talerPayUri WalletApiOperation.PreparePayForUri,
}); {
talerPayUri: talerPayUri,
},
);
const balance = await api.wallet.call(WalletApiOperation.GetBalances, {}); const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
return { payStatus, balance, uri: talerPayUri }; return { payStatus, balance, uri: talerPayUri };
}, []); }, []);
useEffect(() => api.listener.onUpdateNotification( useEffect(
[NotificationType.CoinWithdrawn], () =>
hook?.retry api.listener.onUpdateNotification(
), [hook]); [NotificationType.CoinWithdrawn],
hook?.retry,
),
[hook],
);
const hookResponse = !hook || hook.hasError ? undefined : hook.response; const hookResponse = !hook || hook.hasError ? undefined : hook.response;

View File

@ -20,11 +20,13 @@
*/ */
import { import {
Amounts, ConfirmPayResult, Amounts,
ConfirmPayResult,
ConfirmPayResultType, ConfirmPayResultType,
NotificationType, PreparePayResultInsufficientBalance, NotificationType,
PreparePayResultInsufficientBalance,
PreparePayResultPaymentPossible, PreparePayResultPaymentPossible,
PreparePayResultType PreparePayResultType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
@ -42,11 +44,9 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(props, mock),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -66,7 +66,7 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should response with no balance", async () => { it("should response with no balance", async () => {
@ -78,18 +78,24 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.InsufficientBalance, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:10", undefined,
} as PreparePayResultInsufficientBalance) {
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { balances: [] }) status: PreparePayResultType.InsufficientBalance,
amountRaw: "USD:10",
} as PreparePayResultInsufficientBalance,
);
handler.addWalletCallResponse(
WalletApiOperation.GetBalances,
{},
{ balances: [] },
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(props, mock),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -102,7 +108,7 @@ describe("Payment CTA states", () => {
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
if (r.status !== "no-balance-for-currency") { if (r.status !== "no-balance-for-currency") {
expect(r).eq({}) expect(r).eq({});
return; return;
} }
expect(r.balance).undefined; expect(r.balance).undefined;
@ -110,7 +116,7 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should not be able to pay if there is no enough balance", async () => { it("should not be able to pay if there is no enough balance", async () => {
@ -122,25 +128,33 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.InsufficientBalance, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:10", undefined,
} as PreparePayResultInsufficientBalance) {
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { status: PreparePayResultType.InsufficientBalance,
balances: [{ amountRaw: "USD:10",
available: "USD:5", } as PreparePayResultInsufficientBalance,
hasPendingTransactions: false, );
pendingIncoming: "USD:0", handler.addWalletCallResponse(
pendingOutgoing: "USD:0", WalletApiOperation.GetBalances,
requiresUserInput: false, {},
}] {
}) balances: [
{
available: "USD:5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(props, mock),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -158,7 +172,7 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be able to pay (without fee)", async () => { it("should be able to pay (without fee)", async () => {
@ -170,25 +184,33 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.PaymentPossible, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:10", undefined,
amountEffective: "USD:10", {
} as PreparePayResultPaymentPossible) status: PreparePayResultType.PaymentPossible,
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { amountRaw: "USD:10",
balances: [{ amountEffective: "USD:10",
available: "USD:15", } as PreparePayResultPaymentPossible,
hasPendingTransactions: false, );
pendingIncoming: "USD:0", handler.addWalletCallResponse(
pendingOutgoing: "USD:0", WalletApiOperation.GetBalances,
requiresUserInput: false, {},
}] {
}) balances: [
{
available: "USD:15",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(props, mock),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -201,8 +223,8 @@ describe("Payment CTA states", () => {
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
if (r.status !== "ready") { if (r.status !== "ready") {
expect(r).eq({}) expect(r).eq({});
return return;
} }
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10")); expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
@ -210,7 +232,7 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be able to pay (with fee)", async () => { it("should be able to pay (with fee)", async () => {
@ -222,29 +244,33 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.PaymentPossible, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:9", undefined,
amountEffective: "USD:10", {
} as PreparePayResultPaymentPossible) status: PreparePayResultType.PaymentPossible,
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { amountRaw: "USD:9",
balances: [{ amountEffective: "USD:10",
available: "USD:15", } as PreparePayResultPaymentPossible,
hasPendingTransactions: false, );
pendingIncoming: "USD:0", handler.addWalletCallResponse(
pendingOutgoing: "USD:0", WalletApiOperation.GetBalances,
requiresUserInput: false, {},
}] {
}) balances: [
{
available: "USD:15",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(
props,
mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -263,7 +289,7 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should get confirmation done after pay successfully", async () => { it("should get confirmation done after pay successfully", async () => {
@ -275,33 +301,39 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.PaymentPossible, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:9", undefined,
amountEffective: "USD:10", {
} as PreparePayResultPaymentPossible) status: PreparePayResultType.PaymentPossible,
amountRaw: "USD:9",
amountEffective: "USD:10",
} as PreparePayResultPaymentPossible,
);
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { handler.addWalletCallResponse(
balances: [{ WalletApiOperation.GetBalances,
available: "USD:15", {},
hasPendingTransactions: false, {
pendingIncoming: "USD:0", balances: [
pendingOutgoing: "USD:0", {
requiresUserInput: false, available: "USD:15",
}] hasPendingTransactions: false,
}) pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
handler.addWalletCallResponse(WalletApiOperation.ConfirmPay, undefined, { handler.addWalletCallResponse(WalletApiOperation.ConfirmPay, undefined, {
type: ConfirmPayResultType.Done, type: ConfirmPayResultType.Done,
contractTerms: {}, contractTerms: {},
} as ConfirmPayResult) } as ConfirmPayResult);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(
props, mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -314,7 +346,7 @@ describe("Payment CTA states", () => {
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
if (r.status !== "ready") { if (r.status !== "ready") {
expect(r).eq({}) expect(r).eq({});
return; return;
} }
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
@ -324,7 +356,7 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should not stay in ready state after pay with error", async () => { it("should not stay in ready state after pay with error", async () => {
@ -335,32 +367,38 @@ describe("Payment CTA states", () => {
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: nullFunction, onSuccess: nullFunction,
}; };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.PaymentPossible, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:9", undefined,
amountEffective: "USD:10", {
} as PreparePayResultPaymentPossible) status: PreparePayResultType.PaymentPossible,
amountRaw: "USD:9",
amountEffective: "USD:10",
} as PreparePayResultPaymentPossible,
);
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { handler.addWalletCallResponse(
balances: [{ WalletApiOperation.GetBalances,
available: "USD:15", {},
hasPendingTransactions: false, {
pendingIncoming: "USD:0", balances: [
pendingOutgoing: "USD:0", {
requiresUserInput: false, available: "USD:15",
}] hasPendingTransactions: false,
}) pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
handler.addWalletCallResponse(WalletApiOperation.ConfirmPay, undefined, { handler.addWalletCallResponse(WalletApiOperation.ConfirmPay, undefined, {
type: ConfirmPayResultType.Pending, type: ConfirmPayResultType.Pending,
lastError: { code: 1 }, lastError: { code: 1 },
} as ConfirmPayResult) } as ConfirmPayResult);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(
props, mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -368,7 +406,7 @@ describe("Payment CTA states", () => {
expect(error).undefined; expect(error).undefined;
} }
expect(await waitForStateUpdate()).true expect(await waitForStateUpdate()).true;
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
@ -380,7 +418,7 @@ describe("Payment CTA states", () => {
r.payHandler.onClick(); r.payHandler.onClick();
} }
expect(await waitForStateUpdate()).true expect(await waitForStateUpdate()).true;
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
@ -402,7 +440,7 @@ describe("Payment CTA states", () => {
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should update balance if a coins is withdraw", async () => { it("should update balance if a coins is withdraw", async () => {
@ -415,46 +453,62 @@ describe("Payment CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.PaymentPossible, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:9", undefined,
amountEffective: "USD:10", {
} as PreparePayResultPaymentPossible) status: PreparePayResultType.PaymentPossible,
amountRaw: "USD:9",
amountEffective: "USD:10",
} as PreparePayResultPaymentPossible,
);
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { handler.addWalletCallResponse(
balances: [{ WalletApiOperation.GetBalances,
available: "USD:10", {},
hasPendingTransactions: false, {
pendingIncoming: "USD:0", balances: [
pendingOutgoing: "USD:0", {
requiresUserInput: false, available: "USD:10",
}] hasPendingTransactions: false,
}) pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
handler.addWalletCallResponse(WalletApiOperation.PreparePayForUri, undefined, { handler.addWalletCallResponse(
status: PreparePayResultType.PaymentPossible, WalletApiOperation.PreparePayForUri,
amountRaw: "USD:9", undefined,
amountEffective: "USD:10", {
} as PreparePayResultPaymentPossible) status: PreparePayResultType.PaymentPossible,
amountRaw: "USD:9",
amountEffective: "USD:10",
} as PreparePayResultPaymentPossible,
);
handler.addWalletCallResponse(WalletApiOperation.GetBalances, {}, { handler.addWalletCallResponse(
balances: [{ WalletApiOperation.GetBalances,
available: "USD:15", {},
hasPendingTransactions: false, {
pendingIncoming: "USD:0", balances: [
pendingOutgoing: "USD:0", {
requiresUserInput: false, available: "USD:15",
}] hasPendingTransactions: false,
}) pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(
props, mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -467,8 +521,8 @@ describe("Payment CTA states", () => {
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
if (r.status !== "ready") { if (r.status !== "ready") {
expect(r).eq({}) expect(r).eq({});
return return;
} }
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10")); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9")); expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
@ -483,8 +537,8 @@ describe("Payment CTA states", () => {
{ {
const r = pullLastResultOrThrow(); const r = pullLastResultOrThrow();
if (r.status !== "ready") { if (r.status !== "ready") {
expect(r).eq({}) expect(r).eq({});
return return;
} }
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9")); expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
@ -493,6 +547,6 @@ describe("Payment CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -48,7 +48,9 @@ export function useComponentState(
const recovery = info; const recovery = info;
async function recoverBackup(): Promise<void> { async function recoverBackup(): Promise<void> {
await wxApi.wallet.call(WalletApiOperation.ImportBackupRecovery, { recovery }); await wxApi.wallet.call(WalletApiOperation.ImportBackupRecovery, {
recovery,
});
onSuccess(); onSuccess();
} }

View File

@ -25,7 +25,7 @@ import {
IgnoredView, IgnoredView,
InProgressView, InProgressView,
LoadingUriView, LoadingUriView,
ReadyView ReadyView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {

View File

@ -29,13 +29,17 @@ export function useComponentState(
const info = useAsyncAsHook(async () => { const info = useAsyncAsHook(async () => {
if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND"); if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
const refund = await api.wallet.call(WalletApiOperation.PrepareRefund, { talerRefundUri }); const refund = await api.wallet.call(WalletApiOperation.PrepareRefund, {
talerRefundUri,
});
return { refund, uri: talerRefundUri }; return { refund, uri: talerRefundUri };
}); });
useEffect(() => api.listener.onUpdateNotification( useEffect(() =>
[NotificationType.RefreshMelted], api.listener.onUpdateNotification(
info?.retry) [NotificationType.RefreshMelted],
info?.retry,
),
); );
if (!info) { if (!info) {
@ -52,7 +56,7 @@ export function useComponentState(
const doAccept = async (): Promise<void> => { const doAccept = async (): Promise<void> => {
const res = await api.wallet.call(WalletApiOperation.ApplyRefund, { const res = await api.wallet.call(WalletApiOperation.ApplyRefund, {
talerRefundUri: uri talerRefundUri: uri,
}); });
onSuccess(res.transactionId); onSuccess(res.transactionId);

View File

@ -21,7 +21,10 @@
import { import {
AmountJson, AmountJson,
Amounts, NotificationType, OrderShortInfo, PrepareRefundResult Amounts,
NotificationType,
OrderShortInfo,
PrepareRefundResult,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
@ -45,7 +48,7 @@ describe("Refund CTA states", () => {
null; null;
}, },
}, },
mock mock,
// { // {
// prepareRefund: async () => ({}), // prepareRefund: async () => ({}),
// applyRefund: async () => ({}), // applyRefund: async () => ({}),
@ -73,7 +76,7 @@ describe("Refund CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
@ -86,7 +89,7 @@ describe("Refund CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
awaiting: "EUR:2", awaiting: "EUR:2",
@ -103,12 +106,13 @@ describe("Refund CTA states", () => {
orderId: "orderId1", orderId: "orderId1",
summary: "the summary", summary: "the summary",
} as OrderShortInfo, } as OrderShortInfo,
}) });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState( useComponentState(
props, mock props,
mock,
// { // {
// prepareRefund: async () => // prepareRefund: async () =>
// ({ // ({
@ -154,7 +158,7 @@ describe("Refund CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
@ -167,7 +171,7 @@ describe("Refund CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
awaiting: "EUR:2", awaiting: "EUR:2",
@ -184,7 +188,7 @@ describe("Refund CTA states", () => {
orderId: "orderId1", orderId: "orderId1",
summary: "the summary", summary: "the summary",
} as OrderShortInfo, } as OrderShortInfo,
}) });
// handler.addWalletCall(WalletApiOperation.ApplyRefund) // handler.addWalletCall(WalletApiOperation.ApplyRefund)
// handler.addWalletCall(WalletApiOperation.PrepareRefund, undefined, { // handler.addWalletCall(WalletApiOperation.PrepareRefund, undefined, {
// awaiting: "EUR:1", // awaiting: "EUR:1",
@ -205,7 +209,8 @@ describe("Refund CTA states", () => {
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState( useComponentState(
props, mock props,
mock,
// { // {
// prepareRefund: async () => // prepareRefund: async () =>
// ({ // ({
@ -242,11 +247,11 @@ describe("Refund CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "ready") { if (state.status !== "ready") {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
if (state.error) { if (state.error) {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
expect(state.accept.onClick).not.undefined; expect(state.accept.onClick).not.undefined;
@ -264,18 +269,18 @@ describe("Refund CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "ignored") { if (state.status !== "ignored") {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
if (state.error) { if (state.error) {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
expect(state.merchantName).eq("the merchant name"); expect(state.merchantName).eq("the merchant name");
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be in progress when doing refresh", async () => { it("should be in progress when doing refresh", async () => {
@ -288,7 +293,7 @@ describe("Refund CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
awaiting: "EUR:2", awaiting: "EUR:2",
@ -305,7 +310,7 @@ describe("Refund CTA states", () => {
orderId: "orderId1", orderId: "orderId1",
summary: "the summary", summary: "the summary",
} as OrderShortInfo, } as OrderShortInfo,
}) });
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
awaiting: "EUR:1", awaiting: "EUR:1",
effectivePaid: "EUR:2", effectivePaid: "EUR:2",
@ -321,7 +326,7 @@ describe("Refund CTA states", () => {
orderId: "orderId1", orderId: "orderId1",
summary: "the summary", summary: "the summary",
} as OrderShortInfo, } as OrderShortInfo,
}) });
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
awaiting: "EUR:0", awaiting: "EUR:0",
effectivePaid: "EUR:2", effectivePaid: "EUR:2",
@ -337,14 +342,10 @@ describe("Refund CTA states", () => {
orderId: "orderId1", orderId: "orderId1",
summary: "the summary", summary: "the summary",
} as OrderShortInfo, } as OrderShortInfo,
}) });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentState(props, mock));
useComponentState(
props, mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -358,7 +359,7 @@ describe("Refund CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "in-progress") { if (state.status !== "in-progress") {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
if (state.error) expect.fail(); if (state.error) expect.fail();
@ -367,7 +368,7 @@ describe("Refund CTA states", () => {
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(1 / 3, 0.01) // expect(state.progress).closeTo(1 / 3, 0.01)
handler.notifyEventFromWallet(NotificationType.RefreshMelted) handler.notifyEventFromWallet(NotificationType.RefreshMelted);
} }
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
@ -376,7 +377,7 @@ describe("Refund CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "in-progress") { if (state.status !== "in-progress") {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
if (state.error) expect.fail(); if (state.error) expect.fail();
@ -385,7 +386,7 @@ describe("Refund CTA states", () => {
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(2 / 3, 0.01) // expect(state.progress).closeTo(2 / 3, 0.01)
handler.notifyEventFromWallet(NotificationType.RefreshMelted) handler.notifyEventFromWallet(NotificationType.RefreshMelted);
} }
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
@ -394,7 +395,7 @@ describe("Refund CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "ready") { if (state.status !== "ready") {
expect(state).eq({}) expect(state).eq({});
return; return;
} }
if (state.error) expect.fail(); if (state.error) expect.fail();
@ -404,6 +405,6 @@ describe("Refund CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -25,7 +25,7 @@ import {
AcceptedView, AcceptedView,
IgnoredView, IgnoredView,
LoadingUriView, LoadingUriView,
ReadyView ReadyView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {

View File

@ -26,7 +26,9 @@ export function useComponentState(
): State { ): State {
const tipInfo = useAsyncAsHook(async () => { const tipInfo = useAsyncAsHook(async () => {
if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP"); if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP");
const tip = await api.wallet.call(WalletApiOperation.PrepareTip, { talerTipUri }); const tip = await api.wallet.call(WalletApiOperation.PrepareTip, {
talerTipUri,
});
return { tip }; return { tip };
}); });
@ -46,7 +48,9 @@ export function useComponentState(
const { tip } = tipInfo.response; const { tip } = tipInfo.response;
const doAccept = async (): Promise<void> => { const doAccept = async (): Promise<void> => {
const res = await api.wallet.call(WalletApiOperation.AcceptTip, { walletTipId: tip.walletTipId }); const res = await api.wallet.call(WalletApiOperation.AcceptTip, {
walletTipId: tip.walletTipId,
});
//FIX: this may not be seen since we are moving to the success also //FIX: this may not be seen since we are moving to the success also
tipInfo.retry(); tipInfo.retry();

View File

@ -65,11 +65,10 @@ describe("Tip CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ready for accepting the tip", async () => { it("should be ready for accepting the tip", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, mock } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
@ -79,9 +78,9 @@ describe("Tip CTA states", () => {
tipAmountEffective: "EUR:1", tipAmountEffective: "EUR:1",
walletTipId: "tip_id", walletTipId: "tip_id",
expirationTimestamp: { expirationTimestamp: {
t_s: 1 t_s: 1,
}, },
tipAmountRaw: "" tipAmountRaw: "",
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
@ -112,7 +111,7 @@ describe("Tip CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "ready") { if (state.status !== "ready") {
expect(state).eq({ status: "ready" }) expect(state).eq({ status: "ready" });
return; return;
} }
if (state.error) expect.fail(); if (state.error) expect.fail();
@ -132,9 +131,9 @@ describe("Tip CTA states", () => {
tipAmountEffective: "EUR:1", tipAmountEffective: "EUR:1",
walletTipId: "tip_id", walletTipId: "tip_id",
expirationTimestamp: { expirationTimestamp: {
t_s: 1 t_s: 1,
}, },
tipAmountRaw: "" tipAmountRaw: "",
}); });
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
@ -142,7 +141,7 @@ describe("Tip CTA states", () => {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
if (state.status !== "accepted") { if (state.status !== "accepted") {
expect(state).eq({ status: "accepted" }) expect(state).eq({ status: "accepted" });
return; return;
} }
if (state.error) expect.fail(); if (state.error) expect.fail();
@ -151,7 +150,7 @@ describe("Tip CTA states", () => {
expect(state.exchangeBaseUrl).eq("exchange url"); expect(state.exchangeBaseUrl).eq("exchange url");
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
@ -165,7 +164,7 @@ describe("Tip CTA states", () => {
expirationTimestamp: { expirationTimestamp: {
t_s: 1, t_s: 1,
}, },
tipAmountRaw: "" tipAmountRaw: "",
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
@ -203,7 +202,7 @@ describe("Tip CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should render accepted if the tip has been used previously", async () => { it("should render accepted if the tip has been used previously", async () => {
@ -255,6 +254,6 @@ describe("Tip CTA states", () => {
expect(state.exchangeBaseUrl).eq("exchange url"); expect(state.exchangeBaseUrl).eq("exchange url");
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import {
Amounts,
TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { format, isFuture, parse } from "date-fns"; import { format, isFuture, parse } from "date-fns";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -29,52 +33,57 @@ export function useComponentState(
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState<string | undefined>(); const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>() const [timestamp, setTimestamp] = useState<string | undefined>();
const [operationError, setOperationError] = useState< const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined TalerErrorDetail | undefined
>(undefined); >(undefined);
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const resp = await api.wallet.call(WalletApiOperation.PreparePeerPushPayment, { const resp = await api.wallet.call(
amount: amountStr WalletApiOperation.PreparePeerPushPayment,
}) {
return resp amount: amountStr,
}) },
);
return resp;
});
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
error: undefined error: undefined,
} };
} }
if (hook.hasError) { if (hook.hasError) {
return { return {
status: "loading-uri", status: "loading-uri",
error: hook error: hook,
} };
} }
const { amountEffective, amountRaw } = hook.response const { amountEffective, amountRaw } = hook.response;
const debitAmount = Amounts.parseOrThrow(amountRaw) const debitAmount = Amounts.parseOrThrow(amountRaw);
const toBeReceived = Amounts.parseOrThrow(amountEffective) const toBeReceived = Amounts.parseOrThrow(amountEffective);
let purse_expiration: TalerProtocolTimestamp | undefined = undefined let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
let timestampError: string | undefined = undefined; let timestampError: string | undefined = undefined;
const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()) const t =
timestamp === undefined
? undefined
: parse(timestamp, "dd/MM/yyyy", new Date());
if (t !== undefined) { if (t !== undefined) {
if (Number.isNaN(t.getTime())) { if (Number.isNaN(t.getTime())) {
timestampError = 'Should have the format "dd/MM/yyyy"' timestampError = 'Should have the format "dd/MM/yyyy"';
} else { } else {
if (!isFuture(t)) { if (!isFuture(t)) {
timestampError = 'Should be in the future' timestampError = "Should be in the future";
} else { } else {
purse_expiration = { purse_expiration = {
t_s: t.getTime() / 1000 t_s: t.getTime() / 1000,
} };
} }
} }
} }
@ -82,13 +91,16 @@ export function useComponentState(
async function accept(): Promise<void> { async function accept(): Promise<void> {
if (!subject || !purse_expiration) return; if (!subject || !purse_expiration) return;
try { try {
const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPushPayment, { const resp = await api.wallet.call(
partialContractTerms: { WalletApiOperation.InitiatePeerPushPayment,
summary: subject, {
amount: amountStr, partialContractTerms: {
purse_expiration summary: subject,
amount: amountStr,
purse_expiration,
},
}, },
}); );
onSuccess(resp.transactionId); onSuccess(resp.transactionId);
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
@ -99,7 +111,8 @@ export function useComponentState(
} }
} }
const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration const unableToCreate =
!subject || Amounts.isZero(amount) || !purse_expiration;
return { return {
status: "ready", status: "ready",
@ -107,7 +120,12 @@ export function useComponentState(
onClick: onClose, onClick: onClose,
}, },
subject: { subject: {
error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, error:
subject === undefined
? undefined
: !subject
? "Can't be empty"
: undefined,
value: subject ?? "", value: subject ?? "",
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },
@ -115,8 +133,8 @@ export function useComponentState(
error: timestampError, error: timestampError,
value: timestamp === undefined ? "" : timestamp, value: timestamp === undefined ? "" : timestamp,
onInput: async (e) => { onInput: async (e) => {
setTimestamp(e) setTimestamp(e);
} },
}, },
create: { create: {
onClick: unableToCreate ? undefined : accept, onClick: unableToCreate ? undefined : accept,

View File

@ -17,7 +17,7 @@
import { import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
TalerErrorDetail TalerErrorDetail,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";

View File

@ -18,7 +18,7 @@ import {
AbsoluteTime, AbsoluteTime,
Amounts, Amounts,
TalerErrorDetail, TalerErrorDetail,
TalerProtocolTimestamp TalerProtocolTimestamp,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -52,10 +52,7 @@ export function useComponentState(
}; };
} }
const { const { contractTerms, peerPushPaymentIncomingId } = hook.response;
contractTerms,
peerPushPaymentIncomingId,
} = hook.response;
const amount: string = contractTerms?.amount; const amount: string = contractTerms?.amount;
const summary: string | undefined = contractTerms?.summary; const summary: string | undefined = contractTerms?.summary;
@ -64,9 +61,12 @@ export function useComponentState(
async function accept(): Promise<void> { async function accept(): Promise<void> {
try { try {
const resp = await api.wallet.call(WalletApiOperation.AcceptPeerPushPayment, { const resp = await api.wallet.call(
peerPushPaymentIncomingId, WalletApiOperation.AcceptPeerPushPayment,
}); {
peerPushPaymentIncomingId,
},
);
onSuccess(resp.transactionId); onSuccess(resp.transactionId);
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {

View File

@ -23,7 +23,7 @@ import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js"; import { wxApi } from "../../wxApi.js";
import { import {
useComponentStateFromParams, useComponentStateFromParams,
useComponentStateFromURI useComponentStateFromURI,
} from "./state.js"; } from "./state.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";

View File

@ -19,7 +19,7 @@ import {
AmountJson, AmountJson,
Amounts, Amounts,
ExchangeListItem, ExchangeListItem,
ExchangeTosStatus ExchangeTosStatus,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -35,7 +35,10 @@ export function useComponentStateFromParams(
api: typeof wxApi, api: typeof wxApi,
): RecursiveState<State> { ): RecursiveState<State> {
const uriInfoHook = useAsyncAsHook(async () => { const uriInfoHook = useAsyncAsHook(async () => {
const exchanges = await api.wallet.call(WalletApiOperation.ListExchanges, {}); const exchanges = await api.wallet.call(
WalletApiOperation.ListExchanges,
{},
);
return { amount: Amounts.parseOrThrow(amount), exchanges }; return { amount: Amounts.parseOrThrow(amount), exchanges };
}); });
@ -58,11 +61,14 @@ export function useComponentStateFromParams(
transactionId: string; transactionId: string;
confirmTransferUrl: string | undefined; confirmTransferUrl: string | undefined;
}> { }> {
const res = await api.wallet.call(WalletApiOperation.AcceptManualWithdrawal, { const res = await api.wallet.call(
exchangeBaseUrl: exchange, WalletApiOperation.AcceptManualWithdrawal,
amount: Amounts.stringify(chosenAmount), {
restrictAge: ageRestricted, exchangeBaseUrl: exchange,
}); amount: Amounts.stringify(chosenAmount),
restrictAge: ageRestricted,
},
);
return { return {
confirmTransferUrl: undefined, confirmTransferUrl: undefined,
transactionId: res.transactionId, transactionId: res.transactionId,
@ -93,9 +99,12 @@ export function useComponentStateFromURI(
const uriInfoHook = useAsyncAsHook(async () => { const uriInfoHook = useAsyncAsHook(async () => {
if (!talerWithdrawUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL"); if (!talerWithdrawUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL");
const uriInfo = await api.wallet.call(WalletApiOperation.GetWithdrawalDetailsForUri, { const uriInfo = await api.wallet.call(
talerWithdrawUri, WalletApiOperation.GetWithdrawalDetailsForUri,
}); {
talerWithdrawUri,
},
);
const { amount, defaultExchangeBaseUrl } = uriInfo; const { amount, defaultExchangeBaseUrl } = uriInfo;
return { return {
talerWithdrawUri, talerWithdrawUri,
@ -126,11 +135,14 @@ export function useComponentStateFromURI(
transactionId: string; transactionId: string;
confirmTransferUrl: string | undefined; confirmTransferUrl: string | undefined;
}> { }> {
const res = await api.wallet.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { const res = await api.wallet.call(
exchangeBaseUrl: exchange, WalletApiOperation.AcceptBankIntegratedWithdrawal,
talerWithdrawUri: uri, {
restrictAge: ageRestricted exchangeBaseUrl: exchange,
}); talerWithdrawUri: uri,
restrictAge: ageRestricted,
},
);
return { return {
confirmTransferUrl: res.confirmTransferUrl, confirmTransferUrl: res.confirmTransferUrl,
transactionId: res.transactionId, transactionId: res.transactionId,
@ -189,11 +201,14 @@ function exchangeSelectionState(
* about the withdrawal * about the withdrawal
*/ */
const amountHook = useAsyncAsHook(async () => { const amountHook = useAsyncAsHook(async () => {
const info = await api.wallet.call(WalletApiOperation.GetWithdrawalDetailsForAmount, { const info = await api.wallet.call(
exchangeBaseUrl: currentExchange.exchangeBaseUrl, WalletApiOperation.GetWithdrawalDetailsForAmount,
amount: Amounts.stringify(chosenAmount), {
restrictAge: ageRestricted, exchangeBaseUrl: currentExchange.exchangeBaseUrl,
}); amount: Amounts.stringify(chosenAmount),
restrictAge: ageRestricted,
},
);
const withdrawAmount = { const withdrawAmount = {
raw: Amounts.parseOrThrow(info.amountRaw), raw: Amounts.parseOrThrow(info.amountRaw),
@ -264,10 +279,10 @@ function exchangeSelectionState(
//TODO: calculate based on exchange info //TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled const ageRestriction = ageRestrictionEnabled
? { ? {
list: ageRestrictionOptions, list: ageRestrictionOptions,
value: String(ageRestricted), value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
} }
: undefined; : undefined;
return { return {

View File

@ -21,7 +21,9 @@
import { import {
Amounts, Amounts,
ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
@ -70,13 +72,9 @@ describe("Withdraw CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentStateFromURI(props, mock));
useComponentStateFromURI(
props, mock
),
);
{ {
const { status } = pullLastResultOrThrow(); const { status } = pullLastResultOrThrow();
@ -96,7 +94,7 @@ describe("Withdraw CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should tell the user that there is not known exchange", async () => { it("should tell the user that there is not known exchange", async () => {
@ -109,18 +107,18 @@ describe("Withdraw CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { handler.addWalletCallResponse(
amount: "EUR:2", WalletApiOperation.GetWithdrawalDetailsForUri,
possibleExchanges: [], undefined,
}) {
amount: "EUR:2",
possibleExchanges: [],
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentStateFromURI(props, mock));
useComponentStateFromURI(
props, mock
),
);
{ {
const { status } = pullLastResultOrThrow(); const { status } = pullLastResultOrThrow();
@ -138,7 +136,7 @@ describe("Withdraw CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be able to withdraw if tos are ok", async () => { it("should be able to withdraw if tos are ok", async () => {
@ -151,26 +149,30 @@ describe("Withdraw CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
handler.addWalletCallResponse(WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { handler.addWalletCallResponse(
amount: "ARS:2", WalletApiOperation.GetWithdrawalDetailsForUri,
possibleExchanges: exchanges, undefined,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl {
}) amount: "ARS:2",
handler.addWalletCallResponse(WalletApiOperation.GetWithdrawalDetailsForAmount, undefined, { possibleExchanges: exchanges,
amountRaw: "ARS:2", defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
amountEffective: "ARS:2", },
paytoUris: ["payto://"], );
tosAccepted: true, handler.addWalletCallResponse(
ageRestrictionOptions: [] WalletApiOperation.GetWithdrawalDetailsForAmount,
}) undefined,
{
amountRaw: "ARS:2",
amountEffective: "ARS:2",
paytoUris: ["payto://"],
tosAccepted: true,
ageRestrictionOptions: [],
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentStateFromURI(props, mock));
useComponentStateFromURI(
props, mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -203,7 +205,7 @@ describe("Withdraw CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should accept the tos before withdraw", async () => { it("should accept the tos before withdraw", async () => {
@ -216,38 +218,45 @@ describe("Withdraw CTA states", () => {
onSuccess: async () => { onSuccess: async () => {
null; null;
}, },
} };
const exchangeWithNewTos = exchanges.map((e) => ({ const exchangeWithNewTos = exchanges.map((e) => ({
...e, ...e,
tosStatus: ExchangeTosStatus.New, tosStatus: ExchangeTosStatus.New,
})); }));
handler.addWalletCallResponse(WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { handler.addWalletCallResponse(
amount: "ARS:2", WalletApiOperation.GetWithdrawalDetailsForUri,
possibleExchanges: exchangeWithNewTos, undefined,
defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl {
}) amount: "ARS:2",
handler.addWalletCallResponse(WalletApiOperation.GetWithdrawalDetailsForAmount, undefined, { possibleExchanges: exchangeWithNewTos,
amountRaw: "ARS:2", defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
amountEffective: "ARS:2", },
paytoUris: ["payto://"], );
tosAccepted: false, handler.addWalletCallResponse(
ageRestrictionOptions: [] WalletApiOperation.GetWithdrawalDetailsForAmount,
}) undefined,
{
amountRaw: "ARS:2",
amountEffective: "ARS:2",
paytoUris: ["payto://"],
tosAccepted: false,
ageRestrictionOptions: [],
},
);
handler.addWalletCallResponse(
handler.addWalletCallResponse(WalletApiOperation.GetWithdrawalDetailsForUri, undefined, { WalletApiOperation.GetWithdrawalDetailsForUri,
amount: "ARS:2", undefined,
possibleExchanges: exchanges, {
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl amount: "ARS:2",
}) possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
},
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() => useComponentStateFromURI(props, mock));
useComponentStateFromURI(
props, mock
),
);
{ {
const { status, error } = pullLastResultOrThrow(); const { status, error } = pullLastResultOrThrow();
@ -297,6 +306,6 @@ describe("Withdraw CTA states", () => {
} }
await assertNoPendingUpdate(); await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty") expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -63,7 +63,9 @@ async function handleAutoOpenPerm(
onChange(res.newValue); onChange(res.newValue);
} else { } else {
try { try {
await wxApi.background.toggleHeaderListener(false).then((r) => onChange(r.newValue)); await wxApi.background
.toggleHeaderListener(false)
.then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }

View File

@ -32,10 +32,15 @@ export function useBackupDeviceName(): BackupDeviceName {
useEffect(() => { useEffect(() => {
async function run(): Promise<void> { async function run(): Promise<void> {
//create a first list of backup info by currency //create a first list of backup info by currency
const status = await wxApi.wallet.call(WalletApiOperation.GetBackupInfo, {}); const status = await wxApi.wallet.call(
WalletApiOperation.GetBackupInfo,
{},
);
async function update(newName: string): Promise<void> { async function update(newName: string): Promise<void> {
await wxApi.wallet.call(WalletApiOperation.SetWalletDeviceId, { walletDeviceId: newName }); await wxApi.wallet.call(WalletApiOperation.SetWalletDeviceId, {
walletDeviceId: newName,
});
setStatus((old) => ({ ...old, name: newName })); setStatus((old) => ({ ...old, name: newName }));
} }

View File

@ -66,7 +66,9 @@ async function handleClipboardPerm(
onChange(granted); onChange(granted);
} else { } else {
try { try {
await wxApi.background.toggleHeaderListener(false).then((r) => onChange(r.newValue)); await wxApi.background
.toggleHeaderListener(false)
.then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }

View File

@ -30,7 +30,10 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
useEffect(() => { useEffect(() => {
async function run(): Promise<void> { async function run(): Promise<void> {
//create a first list of backup info by currency //create a first list of backup info by currency
const status = await wxApi.wallet.call(WalletApiOperation.GetBackupInfo, {}); const status = await wxApi.wallet.call(
WalletApiOperation.GetBackupInfo,
{},
);
const providers = status.providers.filter( const providers = status.providers.filter(
(p) => p.syncProviderBaseUrl === url, (p) => p.syncProviderBaseUrl === url,
@ -40,7 +43,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function sync(): Promise<void> { async function sync(): Promise<void> {
if (info) { if (info) {
await wxApi.wallet.call(WalletApiOperation.RunBackupCycle, { await wxApi.wallet.call(WalletApiOperation.RunBackupCycle, {
providers: [info.syncProviderBaseUrl] providers: [info.syncProviderBaseUrl],
}); });
} }
} }
@ -48,7 +51,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function remove(): Promise<void> { async function remove(): Promise<void> {
if (info) { if (info) {
await wxApi.wallet.call(WalletApiOperation.RemoveBackupProvider, { await wxApi.wallet.call(WalletApiOperation.RemoveBackupProvider, {
provider: info.syncProviderBaseUrl provider: info.syncProviderBaseUrl,
}); });
} }
} }

View File

@ -48,8 +48,10 @@ async function handleOpen(
currentValue: undefined | boolean, currentValue: undefined | boolean,
onChange: (value: boolean) => void, onChange: (value: boolean) => void,
): Promise<void> { ): Promise<void> {
const nextValue = !currentValue const nextValue = !currentValue;
await wxApi.wallet.call(WalletApiOperation.SetDevMode, { devModeEnabled: nextValue }); await wxApi.wallet.call(WalletApiOperation.SetDevMode, {
devModeEnabled: nextValue,
});
onChange(nextValue); onChange(nextValue);
return; return;
} }

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ setupI18n("en", { en: {} });
setupPlatform(chromeAPI); setupPlatform(chromeAPI);
function testThisStory(st: any): any { function testThisStory(st: any): any {
describe(`render examples for ${(st as any).default.title}`, () => { describe(`example "${(st as any).default.title}"`, () => {
Object.keys(st).forEach((k) => { Object.keys(st).forEach((k) => {
const Component = (st as any)[k]; const Component = (st as any)[k];
if (k === "default" || !Component) return; if (k === "default" || !Component) return;

View File

@ -15,7 +15,12 @@
*/ */
import { NotificationType } from "@gnu-taler/taler-util"; import { NotificationType } from "@gnu-taler/taler-util";
import { WalletCoreApiClient, WalletCoreOpKeys, WalletCoreRequestType, WalletCoreResponseType } from "@gnu-taler/taler-wallet-core"; import {
WalletCoreApiClient,
WalletCoreOpKeys,
WalletCoreRequestType,
WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
import { import {
ComponentChildren, ComponentChildren,
Fragment, Fragment,
@ -207,7 +212,8 @@ export function mountHook<T extends object>(
export const nullFunction: any = () => null; export const nullFunction: any = () => null;
interface MockHandler { interface MockHandler {
addWalletCallResponse<Op extends WalletCoreOpKeys>(operation: Op, addWalletCallResponse<Op extends WalletCoreOpKeys>(
operation: Op,
payload?: Partial<WalletCoreRequestType<Op>>, payload?: Partial<WalletCoreRequestType<Op>>,
response?: WalletCoreResponseType<Op>, response?: WalletCoreResponseType<Op>,
callback?: () => void, callback?: () => void,
@ -220,16 +226,16 @@ interface MockHandler {
type CallRecord = WalletCallRecord | BackgroundCallRecord; type CallRecord = WalletCallRecord | BackgroundCallRecord;
interface WalletCallRecord { interface WalletCallRecord {
source: "wallet" source: "wallet";
callback: () => void; callback: () => void;
operation: WalletCoreOpKeys, operation: WalletCoreOpKeys;
payload?: WalletCoreRequestType<WalletCoreOpKeys>, payload?: WalletCoreRequestType<WalletCoreOpKeys>;
response?: WalletCoreResponseType<WalletCoreOpKeys>, response?: WalletCoreResponseType<WalletCoreOpKeys>;
} }
interface BackgroundCallRecord { interface BackgroundCallRecord {
source: "background" source: "background";
name: string, name: string;
args: any, args: any;
response: any; response: any;
} }
@ -237,78 +243,112 @@ type Subscriptions = {
[key in NotificationType]?: VoidFunction; [key in NotificationType]?: VoidFunction;
}; };
export function createWalletApiMock(): { handler: MockHandler, mock: typeof wxApi } { export function createWalletApiMock(): {
const calls = new Array<CallRecord>() handler: MockHandler;
mock: typeof wxApi;
} {
const calls = new Array<CallRecord>();
const subscriptions: Subscriptions = {}; const subscriptions: Subscriptions = {};
const mock: typeof wxApi = { const mock: typeof wxApi = {
wallet: new Proxy<WalletCoreApiClient>({} as any, { wallet: new Proxy<WalletCoreApiClient>({} as any, {
get(target, name, receiver) { get(target, name, receiver) {
const functionName = String(name) const functionName = String(name);
if (functionName !== "call") { if (functionName !== "call") {
throw Error(`the only method in wallet api should be 'call': ${functionName}`) throw Error(
`the only method in wallet api should be 'call': ${functionName}`,
);
} }
return function (operation: WalletCoreOpKeys, payload: WalletCoreRequestType<WalletCoreOpKeys>) { return function (
const next = calls.shift() operation: WalletCoreOpKeys,
payload: WalletCoreRequestType<WalletCoreOpKeys>,
) {
const next = calls.shift();
if (!next) { if (!next) {
throw Error(`wallet operation was called but none was expected: ${operation} (${JSON.stringify(payload, undefined, 2)})`) throw Error(
`wallet operation was called but none was expected: ${operation} (${JSON.stringify(
payload,
undefined,
2,
)})`,
);
} }
if (next.source !== "wallet") { if (next.source !== "wallet") {
throw Error(`wallet operation expected`) throw Error(`wallet operation expected`);
} }
if (operation !== next.operation) { if (operation !== next.operation) {
//more checks, deep check payload //more checks, deep check payload
throw Error(`wallet operation doesn't match: expected ${next.operation} actual ${operation}`) throw Error(
`wallet operation doesn't match: expected ${next.operation} actual ${operation}`,
);
} }
next.callback() next.callback();
return next.response ?? {} return next.response ?? {};
} };
} },
}), }),
listener: { listener: {
onUpdateNotification(mTypes: NotificationType[], callback: (() => void) | undefined): (() => void) { onUpdateNotification(
mTypes.forEach(m => { mTypes: NotificationType[],
subscriptions[m] = callback callback: (() => void) | undefined,
}) ): () => void {
return nullFunction mTypes.forEach((m) => {
} subscriptions[m] = callback;
});
return nullFunction;
},
}, },
background: new Proxy<BackgroundApiClient>({} as any, { background: new Proxy<BackgroundApiClient>({} as any, {
get(target, name, receiver) { get(target, name, receiver) {
const functionName = String(name); const functionName = String(name);
return function (...args: any) { return function (...args: any) {
const next = calls.shift() const next = calls.shift();
if (!next) { if (!next) {
throw Error(`background operation was called but none was expected: ${functionName} (${JSON.stringify(args, undefined, 2)})`) throw Error(
`background operation was called but none was expected: ${functionName} (${JSON.stringify(
args,
undefined,
2,
)})`,
);
} }
if (next.source !== "background" || functionName !== next.name) { if (next.source !== "background" || functionName !== next.name) {
//more checks, deep check args //more checks, deep check args
throw Error(`background operation doesn't match`) throw Error(`background operation doesn't match`);
} }
return next.response return next.response;
} };
} },
}), }),
} };
const handler: MockHandler = { const handler: MockHandler = {
addWalletCallResponse(operation, payload, response, cb) { addWalletCallResponse(operation, payload, response, cb) {
calls.push({ source: "wallet", operation, payload, response, callback: cb ? cb : () => { null } }) calls.push({
return handler source: "wallet",
operation,
payload,
response,
callback: cb
? cb
: () => {
null;
},
});
return handler;
}, },
notifyEventFromWallet(event: NotificationType): void { notifyEventFromWallet(event: NotificationType): void {
const callback = subscriptions[event] const callback = subscriptions[event];
if (!callback) throw Error(`Expected to have a subscription for ${event}`); if (!callback)
throw Error(`Expected to have a subscription for ${event}`);
return callback(); return callback();
}, },
getCallingQueueState() { getCallingQueueState() {
return calls.length === 0 ? "empty" : `${calls.length} left`; return calls.length === 0 ? "empty" : `${calls.length} left`;
}, },
} };
return { handler, mock } return { handler, mock };
} }

View File

@ -26,7 +26,8 @@ function getJsonIfOk(r: Response): Promise<any> {
} }
throw new Error( throw new Error(
`Try another server: (${r.status}) ${r.statusText || "internal server error" `Try another server: (${r.status}) ${
r.statusText || "internal server error"
}`, }`,
); );
} }
@ -109,3 +110,7 @@ export function compose<SType extends { status: string }, PType>(
return h(); return h();
}; };
} }
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -0,0 +1,95 @@
/*
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 {
AmountJson,
BackupBackupProviderTerms,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
import { SyncTermsOfServiceResponse } from "@gnu-taler/taler-wallet-core";
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import {
ButtonHandler,
TextFieldHandler,
ToggleHandler,
} from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
import {
LoadingUriView,
SelectProviderView,
ConfirmProviderView,
} from "./views.js";
export interface Props {
currency: string;
onBack: () => Promise<void>;
onComplete: (pid: string) => Promise<void>;
onPaymentRequired: (uri: string) => Promise<void>;
}
export type State =
| State.Loading
| State.LoadingUriError
| State.ConfirmProvider
| State.SelectProvider;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
export interface LoadingUriError {
status: "loading-error";
error: HookError;
}
export interface ConfirmProvider {
status: "confirm-provider";
error: undefined | TalerErrorDetail;
url: string;
provider: SyncTermsOfServiceResponse;
tos: ToggleHandler;
onCancel: ButtonHandler;
onAccept: ButtonHandler;
}
export interface SelectProvider {
status: "select-provider";
url: TextFieldHandler;
urlOk: boolean;
name: TextFieldHandler;
onConfirm: ButtonHandler;
onCancel: ButtonHandler;
error: undefined | TalerErrorDetail;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-error": LoadingUriView,
"select-provider": SelectProviderView,
"confirm-provider": ConfirmProviderView,
};
export const AddBackupProviderPage = compose(
"AddBackupProvider",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -0,0 +1,260 @@
/*
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 {
canonicalizeBaseUrl,
Codec,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
import {
codecForSyncTermsOfServiceResponse,
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import { assertUnreachable } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
type UrlState<T> = UrlOk<T> | UrlError;
interface UrlOk<T> {
status: "ok";
result: T;
}
type UrlError =
| UrlNetworkError
| UrlClientError
| UrlServerError
| UrlParsingError
| UrlReadError;
interface UrlNetworkError {
status: "network-error";
href: string;
}
interface UrlClientError {
status: "client-error";
code: number;
}
interface UrlServerError {
status: "server-error";
code: number;
}
interface UrlParsingError {
status: "parsing-error";
json: any;
}
interface UrlReadError {
status: "url-error";
}
function useDebounceEffect(
time: number,
cb: undefined | (() => Promise<void>),
deps: Array<any>,
): void {
const [currentTimer, setCurrentTimer] = useState<any>();
useEffect(() => {
if (currentTimer !== undefined) clearTimeout(currentTimer);
if (cb !== undefined) {
const tid = setTimeout(cb, time);
setCurrentTimer(tid);
}
}, deps);
}
function useUrlState<T>(
host: string | undefined,
path: string,
codec: Codec<T>,
): UrlState<T> | undefined {
const [state, setState] = useState<UrlState<T> | undefined>();
let href: string | undefined;
try {
if (host) {
const isHttps =
host.startsWith("https://") && host.length > "https://".length;
const isHttp =
host.startsWith("http://") && host.length > "http://".length;
const withProto = isHttp || isHttps ? host : `https://${host}`;
const baseUrl = canonicalizeBaseUrl(withProto);
href = new URL(path, baseUrl).href;
}
} catch (e) {
setState({
status: "url-error",
});
}
const constHref = href;
useDebounceEffect(
500,
constHref == undefined
? undefined
: async () => {
const req = await fetch(constHref).catch((e) => {
return setState({
status: "network-error",
href: constHref,
});
});
if (!req) return;
if (req.status >= 400 && req.status < 500) {
setState({
status: "client-error",
code: req.status,
});
return;
}
if (req.status > 500) {
setState({
status: "server-error",
code: req.status,
});
return;
}
const json = await req.json();
try {
const result = codec.decode(json);
setState({ status: "ok", result });
} catch (e: any) {
setState({ status: "parsing-error", json });
}
},
[host, path],
);
return state;
}
export function useComponentState(
{ currency, onBack, onComplete, onPaymentRequired }: Props,
api: typeof wxApi,
): State {
const [url, setHost] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [tos, setTos] = useState(false);
const urlState = useUrlState(
url,
"config",
codecForSyncTermsOfServiceResponse(),
);
const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>();
const [showConfirm, setShowConfirm] = useState(false);
async function addBackupProvider() {
if (!url || !name) return;
const resp = await api.wallet.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: url,
name: name,
activate: true,
});
switch (resp.status) {
case "payment-required":
return onPaymentRequired(resp.talerUri);
case "error":
return setOperationError(resp.error);
case "ok":
return onComplete(url);
default:
assertUnreachable(resp);
}
}
if (showConfirm && urlState && urlState.status === "ok") {
return {
status: "confirm-provider",
error: operationError,
onAccept: {
onClick: !tos ? undefined : addBackupProvider,
},
onCancel: {
onClick: onBack,
},
provider: urlState.result,
tos: {
value: tos,
button: {
onClick: async () => setTos(!tos),
},
},
url: url ?? "",
};
}
return {
status: "select-provider",
error: undefined,
name: {
value: name || "",
onInput: async (e) => setName(e),
error:
name === undefined ? undefined : !name ? "Can't be empty" : undefined,
},
onCancel: {
onClick: onBack,
},
onConfirm: {
onClick:
!urlState || urlState.status !== "ok" || !name
? undefined
: async () => {
setShowConfirm(true);
},
},
urlOk: urlState?.status === "ok",
url: {
value: url || "",
onInput: async (e) => setHost(e),
error: errorString(urlState),
},
};
}
function errorString(state: undefined | UrlState<any>): string | undefined {
if (!state) return state;
switch (state.status) {
case "ok":
return undefined;
case "client-error": {
switch (state.code) {
case 404:
return "Not found";
case 401:
return "Unauthorized";
case 403:
return "Forbidden";
default:
return `Server says it a client error: ${state.code}.`;
}
}
case "server-error":
return `Server had a problem ${state.code}.`;
case "parsing-error":
return `Server response doesn't have the right format.`;
case "network-error":
return `Unable to connect to ${state.href}.`;
case "url-error":
return "URL is not complete";
}
}

View File

@ -0,0 +1,109 @@
/*
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 { createExample } from "../../test-utils.js";
import { ConfirmProviderView, SelectProviderView } from "./views.js";
export default {
title: "wallet/backup/confirm",
};
export const DemoService = createExample(ConfirmProviderView, {
url: "https://sync.demo.taler.net/",
provider: {
annual_fee: "KUDOS:0.1",
storage_limit_in_megabytes: 20,
version: "1",
},
tos: {
button: {},
},
onAccept: {},
onCancel: {},
});
export const FreeService = createExample(ConfirmProviderView, {
url: "https://sync.taler:9667/",
provider: {
annual_fee: "ARS:0",
storage_limit_in_megabytes: 20,
version: "1",
},
tos: {
button: {},
},
onAccept: {},
onCancel: {},
});
export const Initial = createExample(SelectProviderView, {
url: { value: "" },
name: { value: "" },
onCancel: {},
onConfirm: {},
});
export const WithValue = createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
},
name: {
value: "Demo backup service",
},
onCancel: {},
onConfirm: {},
});
export const WithConnectionError = createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
error: "Network error",
},
name: {
value: "Demo backup service",
},
onCancel: {},
onConfirm: {},
});
export const WithClientError = createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
error: "URL may not be right: (404) Not Found",
},
name: {
value: "Demo backup service",
},
onCancel: {},
onConfirm: {},
});
export const WithServerError = createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
error: "Try another server: (500) Internal Server Error",
},
name: {
value: "Demo backup service",
},
onCancel: {},
onConfirm: {},
});

View File

@ -0,0 +1,79 @@
/*
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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
import {
createWalletApiMock,
mountHook,
nullFunction,
} from "../../test-utils.js";
import { Props } from "./index.js";
import { useComponentState } from "./state.js";
const props: Props = {
currency: "KUDOS",
onBack: nullFunction,
onComplete: nullFunction,
onPaymentRequired: nullFunction,
};
describe("AddBackupProvider states", () => {
it("should start in 'select-provider' state", async () => {
const { handler, mock } = createWalletApiMock();
// handler.addWalletCallResponse(
// WalletApiOperation.ListKnownBankAccounts,
// undefined,
// {
// accounts: [],
// },
// );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{
const state = pullLastResultOrThrow();
expect(state.status).equal("select-provider");
if (state.status !== "select-provider") return;
expect(state.name.value).eq("");
expect(state.url.value).eq("");
}
//FIXME: this should not make an extra update
/**
* this may be due to useUrlState because is using an effect over
* a dependency with a timeout
*/
// NOTE: do not remove this comment, keeping as an example
// await waitForStateUpdate()
// {
// const state = pullLastResultOrThrow();
// expect(state.status).equal("select-provider");
// if (state.status !== "select-provider") return;
// expect(state.name.value).eq("")
// expect(state.url.value).eq("")
// }
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty");
});
});

View File

@ -0,0 +1,172 @@
/*
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 { Amounts } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { Checkbox } from "../../components/Checkbox.js";
import { LoadingError } from "../../components/LoadingError.js";
import {
LightText,
SmallLightText,
SubTitle,
TermsOfService,
Title,
} from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<LoadingError
title={<i18n.Translate>Could not load</i18n.Translate>}
error={error}
/>
);
}
export function ConfirmProviderView({
url,
provider,
tos,
onCancel,
onAccept,
}: State.ConfirmProvider): VNode {
const { i18n } = useTranslationContext();
const noFee = Amounts.isZero(provider.annual_fee);
return (
<Fragment>
<section>
<Title>
<i18n.Translate>Review terms of service</i18n.Translate>
</Title>
<div>
<i18n.Translate>Provider URL</i18n.Translate>:{" "}
<a href={url} target="_blank" rel="noreferrer">
{url}
</a>
</div>
<SmallLightText>
<i18n.Translate>
Please review and accept this provider&apos;s terms of service
</i18n.Translate>
</SmallLightText>
<SubTitle>
1. <i18n.Translate>Pricing</i18n.Translate>
</SubTitle>
<p>
{noFee ? (
<i18n.Translate>free of charge</i18n.Translate>
) : (
<i18n.Translate>
{provider.annual_fee} per year of service
</i18n.Translate>
)}
</p>
<SubTitle>
2. <i18n.Translate>Storage</i18n.Translate>
</SubTitle>
<p>
<i18n.Translate>
{provider.storage_limit_in_megabytes} megabytes of storage per year
of service
</i18n.Translate>
</p>
{/* replace with <TermsOfService /> */}
<Checkbox
label={<i18n.Translate>Accept terms of service</i18n.Translate>}
name="terms"
onToggle={tos.button.onClick}
enabled={tos.value}
/>
</section>
<footer>
<Button
variant="contained"
color="secondary"
onClick={onCancel.onClick}
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button variant="contained" color="primary" onClick={onAccept.onClick}>
{noFee ? (
<i18n.Translate>Add provider</i18n.Translate>
) : (
<i18n.Translate>Pay</i18n.Translate>
)}
</Button>
</footer>
</Fragment>
);
}
export function SelectProviderView({
url,
name,
urlOk,
onCancel,
onConfirm,
}: State.SelectProvider): VNode {
const { i18n } = useTranslationContext();
return (
<Fragment>
<section>
<Title>
<i18n.Translate>Add backup provider</i18n.Translate>
</Title>
<LightText>
<i18n.Translate>
Backup providers may charge for their service
</i18n.Translate>
</LightText>
<p>
<TextField
label={<i18n.Translate>URL</i18n.Translate>}
placeholder="https://"
color={urlOk ? "success" : undefined}
value={url.value}
error={url.error}
onChange={url.onInput}
/>
</p>
<p>
<TextField
label={<i18n.Translate>Name</i18n.Translate>}
placeholder="provider name"
value={name.value}
error={name.error}
onChange={name.onInput}
/>
</p>
</section>
<footer>
<Button
variant="contained"
color="secondary"
onClick={onCancel.onClick}
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button variant="contained" color="primary" onClick={onConfirm.onClick}>
<i18n.Translate>Next</i18n.Translate>
</Button>
</footer>
</Fragment>
);
}

View File

@ -65,6 +65,7 @@ import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
import { TransferPickupPage } from "../cta/TransferPickup/index.js"; import { TransferPickupPage } from "../cta/TransferPickup/index.js";
import { InvoicePayPage } from "../cta/InvoicePay/index.js"; import { InvoicePayPage } from "../cta/InvoicePay/index.js";
import { RecoveryPage } from "../cta/Recovery/index.js"; import { RecoveryPage } from "../cta/Recovery/index.js";
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
export function Application(): VNode { export function Application(): VNode {
const [globalNotification, setGlobalNotification] = useState< const [globalNotification, setGlobalNotification] = useState<
@ -221,7 +222,13 @@ export function Application(): VNode {
/> />
<Route <Route
path={Pages.backupProviderAdd} path={Pages.backupProviderAdd}
component={ProviderAddPage} component={AddBackupProviderPage}
onPaymentRequired={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
onComplete={(pid: string) =>
redirectTo(Pages.backupProviderDetail({ pid }))
}
onBack={() => redirectTo(Pages.backup)} onBack={() => redirectTo(Pages.backup)}
/> />

View File

@ -20,7 +20,7 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
ButtonHandler, ButtonHandler,
SelectFieldHandler, SelectFieldHandler,
TextFieldHandler TextFieldHandler,
} from "../../mui/handlers.js"; } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js"; import { wxApi } from "../../wxApi.js";
@ -31,7 +31,7 @@ import {
LoadingErrorView, LoadingErrorView,
NoAccountToDepositView, NoAccountToDepositView,
NoEnoughBalanceView, NoEnoughBalanceView,
ReadyView ReadyView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {

View File

@ -21,7 +21,7 @@ import {
KnownBankAccountsInfo, KnownBankAccountsInfo,
parsePaytoUri, parsePaytoUri,
PaytoUri, PaytoUri,
stringifyPaytoUri stringifyPaytoUri,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -37,10 +37,16 @@ export function useComponentState(
const currency = parsed !== undefined ? parsed.currency : currencyStr; const currency = parsed !== undefined ? parsed.currency : currencyStr;
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const { balances } = await api.wallet.call(WalletApiOperation.GetBalances, {}); const { balances } = await api.wallet.call(
const { accounts } = await api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { WalletApiOperation.GetBalances,
currency {},
}); );
const { accounts } = await api.wallet.call(
WalletApiOperation.ListKnownBankAccounts,
{
currency,
},
);
return { accounts, balances }; return { accounts, balances };
}); });
@ -120,13 +126,13 @@ export function useComponentState(
}, },
}; };
} }
const firstAccount = accounts[0].uri const firstAccount = accounts[0].uri;
const currentAccount = !selectedAccount ? firstAccount : selectedAccount; const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
if (fee === undefined && parsedAmount) { if (fee === undefined && parsedAmount) {
getFeeForAmount(currentAccount, parsedAmount, api).then(initialFee => { getFeeForAmount(currentAccount, parsedAmount, api).then((initialFee) => {
setFee(initialFee) setFee(initialFee);
}) });
return { return {
status: "loading", status: "loading",
error: undefined, error: undefined,
@ -177,10 +183,10 @@ export function useComponentState(
const amountError = !isDirty const amountError = !isDirty
? undefined ? undefined
: !parsedAmount : !parsedAmount
? "Invalid amount" ? "Invalid amount"
: Amounts.cmp(balance, parsedAmount) === -1 : Amounts.cmp(balance, parsedAmount) === -1
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
: undefined; : undefined;
const unableToDeposit = const unableToDeposit =
!parsedAmount || //no amount specified !parsedAmount || //no amount specified
@ -194,8 +200,9 @@ export function useComponentState(
const depositPaytoUri = stringifyPaytoUri(currentAccount); const depositPaytoUri = stringifyPaytoUri(currentAccount);
const amount = Amounts.stringify(parsedAmount); const amount = Amounts.stringify(parsedAmount);
await api.wallet.call(WalletApiOperation.CreateDepositGroup, { await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
amount, depositPaytoUri amount,
}) depositPaytoUri,
});
onSuccess(currency); onSuccess(currency);
} }
@ -242,8 +249,9 @@ async function getFeeForAmount(
const depositPaytoUri = `payto://${p.targetType}/${p.targetPath}`; const depositPaytoUri = `payto://${p.targetType}/${p.targetPath}`;
const amount = Amounts.stringify(a); const amount = Amounts.stringify(a);
return await api.wallet.call(WalletApiOperation.GetFeeForDeposit, { return await api.wallet.call(WalletApiOperation.GetFeeForDeposit, {
amount, depositPaytoUri amount,
}) depositPaytoUri,
});
} }
export function labelForAccountType(id: string) { export function labelForAccountType(id: string) {

View File

@ -18,7 +18,7 @@ import {
DenomOperationMap, DenomOperationMap,
ExchangeFullDetails, ExchangeFullDetails,
ExchangeListItem, ExchangeListItem,
FeeDescriptionPair FeeDescriptionPair,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
@ -33,7 +33,7 @@ import {
NoExchangesView, NoExchangesView,
PrivacyContentView, PrivacyContentView,
ReadyView, ReadyView,
TosContentView TosContentView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {

View File

@ -15,7 +15,10 @@
*/ */
import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util"; import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
import { createPairTimeline, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import {
createPairTimeline,
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js"; import { wxApi } from "../../wxApi.js";
@ -41,15 +44,23 @@ export function useComponentState(
exchanges.length == 0 ? undefined : exchanges[selectedIdx]; exchanges.length == 0 ? undefined : exchanges[selectedIdx];
const selected = !selectedExchange const selected = !selectedExchange
? undefined ? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { exchangeBaseUrl: selectedExchange.exchangeBaseUrl }); : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
});
const initialExchange = const initialExchange =
selectedIdx === initialValue ? undefined : exchanges[initialValue]; selectedIdx === initialValue ? undefined : exchanges[initialValue];
const original = !initialExchange const original = !initialExchange
? undefined ? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { exchangeBaseUrl: initialExchange.exchangeBaseUrl }); : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: initialExchange.exchangeBaseUrl,
});
return { exchanges, selected: selected?.exchange, original: original?.exchange }; return {
exchanges,
selected: selected?.exchange,
original: original?.exchange,
};
}, [value]); }, [value]);
const [showingTos, setShowingTos] = useState<string | undefined>(undefined); const [showingTos, setShowingTos] = useState<string | undefined>(undefined);

View File

@ -161,11 +161,14 @@ export function NoExchangesView({
title={<i18n.Translate>Could not find any exchange</i18n.Translate>} title={<i18n.Translate>Could not find any exchange</i18n.Translate>}
/> />
); );
} }
return ( return (
<ErrorMessage <ErrorMessage
title={<i18n.Translate>Could not find any exchange for the currency {currency}</i18n.Translate>} title={
<i18n.Translate>
Could not find any exchange for the currency {currency}
</i18n.Translate>
}
/> />
); );
} }

View File

@ -20,7 +20,7 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
ButtonHandler, ButtonHandler,
SelectFieldHandler, SelectFieldHandler,
TextFieldHandler TextFieldHandler,
} from "../../mui/handlers.js"; } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js"; import { wxApi } from "../../wxApi.js";
@ -58,13 +58,13 @@ export namespace State {
alias: TextFieldHandler; alias: TextFieldHandler;
onAccountAdded: ButtonHandler; onAccountAdded: ButtonHandler;
onCancel: ButtonHandler; onCancel: ButtonHandler;
accountByType: AccountByType, accountByType: AccountByType;
deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>, deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>;
} }
} }
export type AccountByType = { export type AccountByType = {
[key: string]: KnownBankAccountsInfo[] [key: string]: KnownBankAccountsInfo[];
}; };
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {

View File

@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { KnownBankAccountsInfo, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import {
KnownBankAccountsInfo,
parsePaytoUri,
stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -25,7 +29,9 @@ export function useComponentState(
{ currency, onAccountAdded, onCancel }: Props, { currency, onAccountAdded, onCancel }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const hook = useAsyncAsHook(() => api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency })); const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
);
const [payto, setPayto] = useState(""); const [payto, setPayto] = useState("");
const [alias, setAlias] = useState(""); const [alias, setAlias] = useState("");
@ -61,34 +67,34 @@ export function useComponentState(
const normalizedPayto = stringifyPaytoUri(uri); const normalizedPayto = stringifyPaytoUri(uri);
await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, { await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, {
alias, currency, payto: normalizedPayto alias,
currency,
payto: normalizedPayto,
}); });
onAccountAdded(payto); onAccountAdded(payto);
} }
const paytoUriError = const paytoUriError = found ? "that account is already present" : undefined;
found
? "that account is already present"
: undefined;
const unableToAdd = !type || !alias || paytoUriError !== undefined || uri === undefined; const unableToAdd =
!type || !alias || paytoUriError !== undefined || uri === undefined;
const accountByType: AccountByType = { const accountByType: AccountByType = {
iban: [], iban: [],
bitcoin: [], bitcoin: [],
"x-taler-bank": [], "x-taler-bank": [],
} };
hook.response.accounts.forEach(acc => { hook.response.accounts.forEach((acc) => {
accountByType[acc.uri.targetType].push(acc) accountByType[acc.uri.targetType].push(acc);
}); });
async function deleteAccount(account: KnownBankAccountsInfo): Promise<void> { async function deleteAccount(account: KnownBankAccountsInfo): Promise<void> {
const payto = stringifyPaytoUri(account.uri); const payto = stringifyPaytoUri(account.uri);
await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, { await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, {
payto payto,
}) });
hook?.retry() hook?.retry();
} }
return { return {

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { QrReaderPage } from "./QrReader.js"; import { QrReaderPage } from "./QrReader.js";
export default { export default {
title: "wallet/qr", title: "wallet/qr reader",
}; };
export const Reading = createExample(QrReaderPage, {}); export const Reading = createExample(QrReaderPage, {});

View File

@ -25,8 +25,7 @@ import * as a4 from "./DepositPage/stories.js";
import * as a5 from "./ExchangeAddConfirm.stories.js"; import * as a5 from "./ExchangeAddConfirm.stories.js";
import * as a6 from "./ExchangeAddSetUrl.stories.js"; import * as a6 from "./ExchangeAddSetUrl.stories.js";
import * as a7 from "./History.stories.js"; import * as a7 from "./History.stories.js";
import * as a8 from "./ProviderAddConfirmProvider.stories.js"; import * as a8 from "./AddBackupProvider/stories.js";
import * as a9 from "./ProviderAddSetUrl.stories.js";
import * as a10 from "./ProviderDetail.stories.js"; import * as a10 from "./ProviderDetail.stories.js";
import * as a11 from "./ReserveCreated.stories.js"; import * as a11 from "./ReserveCreated.stories.js";
import * as a12 from "./Settings.stories.js"; import * as a12 from "./Settings.stories.js";
@ -47,7 +46,6 @@ export default [
a6, a6,
a7, a7,
a8, a8,
a9,
a10, a10,
a11, a11,
a12, a12,

View File

@ -22,13 +22,17 @@
* Imports. * Imports.
*/ */
import { import {
CoreApiResponse, Logger, NotificationType, WalletDiagnostics CoreApiResponse,
Logger,
NotificationType,
WalletDiagnostics,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
TalerError, WalletCoreApiClient, TalerError,
WalletCoreApiClient,
WalletCoreOpKeys, WalletCoreOpKeys,
WalletCoreRequestType, WalletCoreRequestType,
WalletCoreResponseType WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { MessageFromBackend, platform } from "./platform/api.js"; import { MessageFromBackend, platform } from "./platform/api.js";
import { nullFunction } from "./test-utils.js"; import { nullFunction } from "./test-utils.js";
@ -107,7 +111,6 @@ export class WxWalletCoreApiClient implements WalletCoreApiClient {
} }
export class BackgroundApiClient { export class BackgroundApiClient {
public resetDb(): Promise<void> { public resetDb(): Promise<void> {
return callBackend("reset-db", {}); return callBackend("reset-db", {});
} }
@ -129,16 +132,16 @@ export class BackgroundApiClient {
public runGarbageCollector(): Promise<void> { public runGarbageCollector(): Promise<void> {
return callBackend("run-gc", {}); return callBackend("run-gc", {});
} }
} }
function onUpdateNotification( function onUpdateNotification(
messageTypes: Array<NotificationType>, messageTypes: Array<NotificationType>,
doCallback: undefined | (() => void), doCallback: undefined | (() => void),
): () => void { ): () => void {
//if no callback, then ignore //if no callback, then ignore
if (!doCallback) return () => { if (!doCallback)
return return () => {
}; return;
};
const onNewMessage = (message: MessageFromBackend): void => { const onNewMessage = (message: MessageFromBackend): void => {
const shouldNotify = messageTypes.includes(message.type); const shouldNotify = messageTypes.includes(message.type);
if (shouldNotify) { if (shouldNotify) {
@ -152,7 +155,6 @@ export const wxApi = {
wallet: new WxWalletCoreApiClient(), wallet: new WxWalletCoreApiClient(),
background: new BackgroundApiClient(), background: new BackgroundApiClient(),
listener: { listener: {
onUpdateNotification onUpdateNotification,
} },
} };