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

@ -465,4 +465,4 @@ export const punycode = {
encode: encode,
toASCII: toASCII,
toUnicode: toUnicode,
};
};

View File

@ -241,12 +241,18 @@ export interface ConfirmPayResultPending {
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 const codecForConfirmPayResultPending =
(): Codec<ConfirmPayResultPending> =>
buildCodecForObject<ConfirmPayResultPending>()
.property("lastError", codecForAny())
.property("lastError", codecOptional(codecForTalerErrorDetail()))
.property("transactionId", codecForString())
.property("type", codecForConstString(ConfirmPayResultType.Pending))
.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
* background, but actually blocks the caller until the operation is done.
*/
export class SynchronousCryptoWorkerFactoryPlain implements CryptoWorkerFactory {
export class SynchronousCryptoWorkerFactoryPlain
implements CryptoWorkerFactory
{
startWorker(): CryptoWorker {
return new SynchronousCryptoWorkerPlain();
}

View File

@ -29,15 +29,18 @@ import {
AmountString,
BackupRecovery,
buildCodecForObject,
buildCodecForUnion,
bytesToString,
canonicalizeBaseUrl,
canonicalJson,
Codec,
codecForAmountString,
codecForBoolean,
codecForConstString,
codecForList,
codecForNumber,
codecForString,
codecForTalerErrorDetail,
codecOptional,
ConfirmPayResultType,
decodeCrock,
@ -78,6 +81,7 @@ import {
WalletBackupConfState,
} from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
@ -232,12 +236,6 @@ function deriveBlobSecret(bc: WalletBackupConfState): Uint8Array {
interface BackupForProviderArgs {
backupProviderBaseUrl: string;
/**
* Should we attempt one more upload after trying
* to pay?
*/
retryAfterPayment: boolean;
}
function getNextBackupTimestamp(): TalerProtocolTimestamp {
@ -253,7 +251,7 @@ function getNextBackupTimestamp(): TalerProtocolTimestamp {
async function runBackupCycleForProvider(
ws: InternalWalletState,
args: BackupForProviderArgs,
): Promise<OperationAttemptResult> {
): Promise<OperationAttemptResult<unknown, { talerUri: string }>> {
const provider = await ws.db
.mktx((x) => [x.backupProviders])
.runReadOnly(async (tx) => {
@ -339,57 +337,34 @@ async function runBackupCycleForProvider(
if (!talerUri) {
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
.mktx((x) => [x.backupProviders, x.operationRetries])
.runReadWrite(async (tx) => {
const provRec = await tx.backupProviders.get(provider.baseUrl);
checkDbInvariant(!!provRec);
const ids = new Set(provRec.paymentProposalIds);
ids.add(proposalId);
provRec.paymentProposalIds = Array.from(ids).sort();
provRec.currentPaymentProposalId = proposalId;
// FIXME: allocate error code for this!
await tx.backupProviders.put(provRec);
const opId = RetryTags.forBackup(provRec);
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
logger.warn("backup provider not found anymore");
return;
}
const opId = RetryTags.forBackup(prov);
await scheduleRetryInTx(ws, tx, opId);
prov.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 {
type: OperationAttemptResultType.Pending,
result: undefined,
result: {
talerUri,
},
};
}
@ -442,10 +417,7 @@ async function runBackupCycleForProvider(
});
logger.info("processed existing backup");
// Now upload our own, merged backup.
return await runBackupCycleForProvider(ws, {
...args,
retryAfterPayment: false,
});
return await runBackupCycleForProvider(ws, args);
}
// Some other response that we did not expect!
@ -477,7 +449,6 @@ export async function processBackupForProvider(
return await runBackupCycleForProvider(ws, {
backupProviderBaseUrl: provider.baseUrl,
retryAfterPayment: true,
});
}
@ -540,12 +511,11 @@ export async function runBackupCycle(
for (const provider of providers) {
await runBackupCycleForProvider(ws, {
backupProviderBaseUrl: provider.baseUrl,
retryAfterPayment: true,
});
}
}
interface SyncTermsOfServiceResponse {
export interface SyncTermsOfServiceResponse {
// maximum backup size supported
storage_limit_in_megabytes: number;
@ -557,7 +527,7 @@ interface SyncTermsOfServiceResponse {
version: string;
}
const codecForSyncTermsOfServiceResponse =
export const codecForSyncTermsOfServiceResponse =
(): Codec<SyncTermsOfServiceResponse> =>
buildCodecForObject<SyncTermsOfServiceResponse>()
.property("storage_limit_in_megabytes", codecForNumber())
@ -584,10 +554,58 @@ export const codecForAddBackupProviderRequest =
.property("activate", codecOptional(codecForBoolean()))
.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(
ws: InternalWalletState,
req: AddBackupProviderRequest,
): Promise<void> {
): Promise<AddBackupProviderResponse> {
logger.info(`adding backup provider ${j2s(req)}`);
await provideBackupState(ws);
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
@ -618,6 +636,7 @@ export async function addBackupProvider(
.mktx((x) => [x.backupProviders])
.runReadWrite(async (tx) => {
let state: BackupProviderState;
//FIXME: what is the difference provisional and ready?
if (req.activate) {
state = {
tag: BackupProviderStateTag.Ready,
@ -641,6 +660,39 @@ export async function addBackupProvider(
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> {

View File

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

View File

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

View File

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

View File

@ -25,7 +25,6 @@
* Imports.
*/
import { h, VNode } from "preact";
import { JustInDevMode } from "./components/JustInDevMode.js";
import {
NavigationHeader,
NavigationHeaderHolder,
@ -138,14 +137,9 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
>
<i18n.Translate>Balance</i18n.Translate>
</a>
<JustInDevMode>
<a
href={Pages.backup}
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate>
</a>
</JustInDevMode>
<a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate>
</a>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
<a href={Pages.qr}>
<SvgIcon
@ -177,18 +171,16 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
>
<i18n.Translate>Balance</i18n.Translate>
</a>
<JustInDevMode>
<a
href={Pages.backup}
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate>
</a>
<a
href={Pages.backup}
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate>
</a>
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate>
</a>
</JustInDevMode>
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate>
</a>
<div
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);
useEffect(() => {
if (!divRef.current) return;
const qr = qrcode(0, "L");
const qr = qrcode(0, "H");
qr.addData(text);
qr.make();
divRef.current.innerHTML = qr.createSvgTag({

View File

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

View File

@ -35,10 +35,13 @@ export function useComponentState(
* For the exchange selected, bring the status of the terms of service
*/
const terms = useAsyncAsHook(async () => {
const exchangeTos = await api.wallet.call(WalletApiOperation.GetExchangeTos, {
exchangeBaseUrl: exchangeUrl,
acceptedFormat: ["text/xml"]
})
const exchangeTos = await api.wallet.call(
WalletApiOperation.GetExchangeTos,
{
exchangeBaseUrl: exchangeUrl,
acceptedFormat: ["text/xml"],
},
);
const state = buildTermsOfServiceState(exchangeTos);
@ -78,14 +81,14 @@ export function useComponentState(
if (accepted) {
api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
exchangeBaseUrl: exchangeUrl,
etag: state.version
})
etag: state.version,
});
} else {
// mark as not accepted
api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
exchangeBaseUrl: exchangeUrl,
etag: undefined
})
etag: undefined,
});
}
// setAccepted(accepted);
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 a4 from "./ShowFullContractTermPopup.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: {
devMode: !!value,
devModeToggle: {
value, button: {}
}
value,
button: {},
},
},
children,
});
@ -57,7 +58,7 @@ export const DevContextProviderForTesting = ({
export const DevContextProvider = ({ children }: { children: any }): VNode => {
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
children =
children.length === 1 && typeof children === "function"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,13 +29,17 @@ export function useComponentState(
const info = useAsyncAsHook(async () => {
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 };
});
useEffect(() => api.listener.onUpdateNotification(
[NotificationType.RefreshMelted],
info?.retry)
useEffect(() =>
api.listener.onUpdateNotification(
[NotificationType.RefreshMelted],
info?.retry,
),
);
if (!info) {
@ -52,7 +56,7 @@ export function useComponentState(
const doAccept = async (): Promise<void> => {
const res = await api.wallet.call(WalletApiOperation.ApplyRefund, {
talerRefundUri: uri
talerRefundUri: uri,
});
onSuccess(res.transactionId);

View File

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

View File

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

View File

@ -26,7 +26,9 @@ export function useComponentState(
): State {
const tipInfo = useAsyncAsHook(async () => {
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 };
});
@ -46,7 +48,9 @@ export function useComponentState(
const { tip } = tipInfo.response;
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
tipInfo.retry();

View File

@ -65,11 +65,10 @@ describe("Tip CTA states", () => {
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty")
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be ready for accepting the tip", async () => {
const { handler, mock } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
@ -79,9 +78,9 @@ describe("Tip CTA states", () => {
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
expirationTimestamp: {
t_s: 1
t_s: 1,
},
tipAmountRaw: ""
tipAmountRaw: "",
});
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
@ -112,7 +111,7 @@ describe("Tip CTA states", () => {
const state = pullLastResultOrThrow();
if (state.status !== "ready") {
expect(state).eq({ status: "ready" })
expect(state).eq({ status: "ready" });
return;
}
if (state.error) expect.fail();
@ -132,9 +131,9 @@ describe("Tip CTA states", () => {
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
expirationTimestamp: {
t_s: 1
t_s: 1,
},
tipAmountRaw: ""
tipAmountRaw: "",
});
expect(await waitForStateUpdate()).true;
@ -142,7 +141,7 @@ describe("Tip CTA states", () => {
const state = pullLastResultOrThrow();
if (state.status !== "accepted") {
expect(state).eq({ status: "accepted" })
expect(state).eq({ status: "accepted" });
return;
}
if (state.error) expect.fail();
@ -151,7 +150,7 @@ describe("Tip CTA states", () => {
expect(state.exchangeBaseUrl).eq("exchange url");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty")
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be ignored after clicking the ignore button", async () => {
@ -165,7 +164,7 @@ describe("Tip CTA states", () => {
expirationTimestamp: {
t_s: 1,
},
tipAmountRaw: ""
tipAmountRaw: "",
});
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
@ -203,7 +202,7 @@ describe("Tip CTA states", () => {
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty")
expect(handler.getCallingQueueState()).eq("empty");
});
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");
}
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/>
*/
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 { format, isFuture, parse } from "date-fns";
import { useState } from "preact/hooks";
@ -29,52 +33,57 @@ export function useComponentState(
const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>()
const [timestamp, setTimestamp] = useState<string | undefined>();
const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
const hook = useAsyncAsHook(async () => {
const resp = await api.wallet.call(WalletApiOperation.PreparePeerPushPayment, {
amount: amountStr
})
return resp
})
const resp = await api.wallet.call(
WalletApiOperation.PreparePeerPushPayment,
{
amount: amountStr,
},
);
return resp;
});
if (!hook) {
return {
status: "loading",
error: undefined
}
error: undefined,
};
}
if (hook.hasError) {
return {
status: "loading-uri",
error: hook
}
error: hook,
};
}
const { amountEffective, amountRaw } = hook.response
const debitAmount = Amounts.parseOrThrow(amountRaw)
const toBeReceived = Amounts.parseOrThrow(amountEffective)
const { amountEffective, amountRaw } = hook.response;
const debitAmount = Amounts.parseOrThrow(amountRaw);
const toBeReceived = Amounts.parseOrThrow(amountEffective);
let purse_expiration: TalerProtocolTimestamp | undefined = undefined
let purse_expiration: TalerProtocolTimestamp | 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 (Number.isNaN(t.getTime())) {
timestampError = 'Should have the format "dd/MM/yyyy"'
timestampError = 'Should have the format "dd/MM/yyyy"';
} else {
if (!isFuture(t)) {
timestampError = 'Should be in the future'
timestampError = "Should be in the future";
} else {
purse_expiration = {
t_s: t.getTime() / 1000
}
t_s: t.getTime() / 1000,
};
}
}
}
@ -82,13 +91,16 @@ export function useComponentState(
async function accept(): Promise<void> {
if (!subject || !purse_expiration) return;
try {
const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPushPayment, {
partialContractTerms: {
summary: subject,
amount: amountStr,
purse_expiration
const resp = await api.wallet.call(
WalletApiOperation.InitiatePeerPushPayment,
{
partialContractTerms: {
summary: subject,
amount: amountStr,
purse_expiration,
},
},
});
);
onSuccess(resp.transactionId);
} catch (e) {
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 {
status: "ready",
@ -107,7 +120,12 @@ export function useComponentState(
onClick: onClose,
},
subject: {
error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined,
error:
subject === undefined
? undefined
: !subject
? "Can't be empty"
: undefined,
value: subject ?? "",
onInput: async (e) => setSubject(e),
},
@ -115,8 +133,8 @@ export function useComponentState(
error: timestampError,
value: timestamp === undefined ? "" : timestamp,
onInput: async (e) => {
setTimestamp(e)
}
setTimestamp(e);
},
},
create: {
onClick: unableToCreate ? undefined : accept,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,10 +32,15 @@ export function useBackupDeviceName(): BackupDeviceName {
useEffect(() => {
async function run(): Promise<void> {
//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> {
await wxApi.wallet.call(WalletApiOperation.SetWalletDeviceId, { walletDeviceId: newName });
await wxApi.wallet.call(WalletApiOperation.SetWalletDeviceId, {
walletDeviceId: newName,
});
setStatus((old) => ({ ...old, name: newName }));
}

View File

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

View File

@ -30,7 +30,10 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
useEffect(() => {
async function run(): Promise<void> {
//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(
(p) => p.syncProviderBaseUrl === url,
@ -40,7 +43,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function sync(): Promise<void> {
if (info) {
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> {
if (info) {
await wxApi.wallet.call(WalletApiOperation.RemoveBackupProvider, {
provider: info.syncProviderBaseUrl
provider: info.syncProviderBaseUrl,
});
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ setupI18n("en", { en: {} });
setupPlatform(chromeAPI);
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) => {
const Component = (st as any)[k];
if (k === "default" || !Component) return;

View File

@ -15,7 +15,12 @@
*/
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 {
ComponentChildren,
Fragment,
@ -207,7 +212,8 @@ export function mountHook<T extends object>(
export const nullFunction: any = () => null;
interface MockHandler {
addWalletCallResponse<Op extends WalletCoreOpKeys>(operation: Op,
addWalletCallResponse<Op extends WalletCoreOpKeys>(
operation: Op,
payload?: Partial<WalletCoreRequestType<Op>>,
response?: WalletCoreResponseType<Op>,
callback?: () => void,
@ -220,16 +226,16 @@ interface MockHandler {
type CallRecord = WalletCallRecord | BackgroundCallRecord;
interface WalletCallRecord {
source: "wallet"
source: "wallet";
callback: () => void;
operation: WalletCoreOpKeys,
payload?: WalletCoreRequestType<WalletCoreOpKeys>,
response?: WalletCoreResponseType<WalletCoreOpKeys>,
operation: WalletCoreOpKeys;
payload?: WalletCoreRequestType<WalletCoreOpKeys>;
response?: WalletCoreResponseType<WalletCoreOpKeys>;
}
interface BackgroundCallRecord {
source: "background"
name: string,
args: any,
source: "background";
name: string;
args: any;
response: any;
}
@ -237,78 +243,112 @@ type Subscriptions = {
[key in NotificationType]?: VoidFunction;
};
export function createWalletApiMock(): { handler: MockHandler, mock: typeof wxApi } {
const calls = new Array<CallRecord>()
export function createWalletApiMock(): {
handler: MockHandler;
mock: typeof wxApi;
} {
const calls = new Array<CallRecord>();
const subscriptions: Subscriptions = {};
const mock: typeof wxApi = {
wallet: new Proxy<WalletCoreApiClient>({} as any, {
get(target, name, receiver) {
const functionName = String(name)
const functionName = String(name);
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>) {
const next = calls.shift()
return function (
operation: WalletCoreOpKeys,
payload: WalletCoreRequestType<WalletCoreOpKeys>,
) {
const next = calls.shift();
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") {
throw Error(`wallet operation expected`)
throw Error(`wallet operation expected`);
}
if (operation !== next.operation) {
//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: {
onUpdateNotification(mTypes: NotificationType[], callback: (() => void) | undefined): (() => void) {
mTypes.forEach(m => {
subscriptions[m] = callback
})
return nullFunction
}
onUpdateNotification(
mTypes: NotificationType[],
callback: (() => void) | undefined,
): () => void {
mTypes.forEach((m) => {
subscriptions[m] = callback;
});
return nullFunction;
},
},
background: new Proxy<BackgroundApiClient>({} as any, {
get(target, name, receiver) {
const functionName = String(name);
return function (...args: any) {
const next = calls.shift()
const next = calls.shift();
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) {
//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 = {
addWalletCallResponse(operation, payload, response, cb) {
calls.push({ source: "wallet", operation, payload, response, callback: cb ? cb : () => { null } })
return handler
calls.push({
source: "wallet",
operation,
payload,
response,
callback: cb
? cb
: () => {
null;
},
});
return handler;
},
notifyEventFromWallet(event: NotificationType): void {
const callback = subscriptions[event]
if (!callback) throw Error(`Expected to have a subscription for ${event}`);
const callback = subscriptions[event];
if (!callback)
throw Error(`Expected to have a subscription for ${event}`);
return callback();
},
getCallingQueueState() {
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(
`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();
};
}
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 { InvoicePayPage } from "../cta/InvoicePay/index.js";
import { RecoveryPage } from "../cta/Recovery/index.js";
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
export function Application(): VNode {
const [globalNotification, setGlobalNotification] = useState<
@ -221,7 +222,13 @@ export function Application(): VNode {
/>
<Route
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)}
/>

View File

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

View File

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

View File

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

View File

@ -15,7 +15,10 @@
*/
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 { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
@ -41,15 +44,23 @@ export function useComponentState(
exchanges.length == 0 ? undefined : exchanges[selectedIdx];
const selected = !selectedExchange
? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { exchangeBaseUrl: selectedExchange.exchangeBaseUrl });
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
});
const initialExchange =
selectedIdx === initialValue ? undefined : exchanges[initialValue];
const original = !initialExchange
? 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]);
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>}
/>
);
}
return (
<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 {
ButtonHandler,
SelectFieldHandler,
TextFieldHandler
TextFieldHandler,
} from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
@ -58,13 +58,13 @@ export namespace State {
alias: TextFieldHandler;
onAccountAdded: ButtonHandler;
onCancel: ButtonHandler;
accountByType: AccountByType,
deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>,
accountByType: AccountByType;
deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>;
}
}
export type AccountByType = {
[key: string]: KnownBankAccountsInfo[]
[key: string]: KnownBankAccountsInfo[];
};
const viewMapping: StateViewMap<State> = {

View File

@ -14,7 +14,11 @@
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 { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -25,7 +29,9 @@ export function useComponentState(
{ currency, onAccountAdded, onCancel }: Props,
api: typeof wxApi,
): State {
const hook = useAsyncAsHook(() => api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }));
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
);
const [payto, setPayto] = useState("");
const [alias, setAlias] = useState("");
@ -61,34 +67,34 @@ export function useComponentState(
const normalizedPayto = stringifyPaytoUri(uri);
await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, {
alias, currency, payto: normalizedPayto
alias,
currency,
payto: normalizedPayto,
});
onAccountAdded(payto);
}
const paytoUriError =
found
? "that account is already present"
: undefined;
const paytoUriError = 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 = {
iban: [],
bitcoin: [],
"x-taler-bank": [],
}
};
hook.response.accounts.forEach(acc => {
accountByType[acc.uri.targetType].push(acc)
hook.response.accounts.forEach((acc) => {
accountByType[acc.uri.targetType].push(acc);
});
async function deleteAccount(account: KnownBankAccountsInfo): Promise<void> {
const payto = stringifyPaytoUri(account.uri);
await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, {
payto
})
hook?.retry()
payto,
});
hook?.retry();
}
return {

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { QrReaderPage } from "./QrReader.js";
export default {
title: "wallet/qr",
title: "wallet/qr reader",
};
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 a6 from "./ExchangeAddSetUrl.stories.js";
import * as a7 from "./History.stories.js";
import * as a8 from "./ProviderAddConfirmProvider.stories.js";
import * as a9 from "./ProviderAddSetUrl.stories.js";
import * as a8 from "./AddBackupProvider/stories.js";
import * as a10 from "./ProviderDetail.stories.js";
import * as a11 from "./ReserveCreated.stories.js";
import * as a12 from "./Settings.stories.js";
@ -47,7 +46,6 @@ export default [
a6,
a7,
a8,
a9,
a10,
a11,
a12,

View File

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