wxApi from context and using the new testing sdk

This commit is contained in:
Sebastian 2022-12-15 17:11:24 -03:00
parent 8d8d71807d
commit f93bd51499
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
67 changed files with 982 additions and 1424 deletions

View File

@ -9,7 +9,7 @@
"private": false, "private": false,
"scripts": { "scripts": {
"clean": "rimraf dist lib tsconfig.tsbuildinfo", "clean": "rimraf dist lib tsconfig.tsbuildinfo",
"test": "pnpm compile && mocha 'dist/**/*.test.js' 'dist/**/test.js'", "test": "pnpm compile && mocha --require source-map-support/register 'dist/**/*.test.js' 'dist/**/test.js'",
"test:coverage": "nyc pnpm test", "test:coverage": "nyc pnpm test",
"compile": "tsc && ./build-fast-with-linaria.mjs", "compile": "tsc && ./build-fast-with-linaria.mjs",
"prepare": "pnpm compile", "prepare": "pnpm compile",

View File

@ -24,20 +24,20 @@
/** /**
* Imports. * Imports.
*/ */
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { JustInDevMode } from "./components/JustInDevMode.js";
import { import {
NavigationHeader, NavigationHeader,
NavigationHeaderHolder, NavigationHeaderHolder,
SvgIcon, SvgIcon,
} from "./components/styled/index.js"; } from "./components/styled/index.js";
import { useBackendContext } from "./context/backend.js";
import { useTranslationContext } from "./context/translation.js"; import { useTranslationContext } from "./context/translation.js";
import settingsIcon from "./svg/settings_black_24dp.svg";
import qrIcon from "./svg/qr_code_24px.svg";
import warningIcon from "./svg/warning_24px.svg";
import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
import { wxApi } from "./wxApi.js"; import qrIcon from "./svg/qr_code_24px.svg";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import settingsIcon from "./svg/settings_black_24dp.svg";
import { JustInDevMode } from "./components/JustInDevMode.js"; import warningIcon from "./svg/warning_24px.svg";
/** /**
* List of pages used by the wallet * List of pages used by the wallet
@ -133,13 +133,8 @@ export const Pages = {
), ),
}; };
export function PopupNavBar({ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
path = "", const api = useBackendContext();
}: {
path?: string;
}): // api: typeof wxApi,
VNode {
const api = wxApi; //FIXME: as parameter
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.wallet.call( return await api.wallet.call(
WalletApiOperation.GetUserAttentionUnreadCount, WalletApiOperation.GetUserAttentionUnreadCount,
@ -194,7 +189,7 @@ VNode {
export function WalletNavBar({ path = "" }: { path?: string }): VNode { export function WalletNavBar({ path = "" }: { path?: string }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const api = wxApi; //FIXME: as parameter const api = useBackendContext();
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.wallet.call( return await api.wallet.call(
WalletApiOperation.GetUserAttentionUnreadCount, WalletApiOperation.GetUserAttentionUnreadCount,

View File

@ -22,11 +22,11 @@ import {
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, JSX, VNode } from "preact"; import { Fragment, h, JSX, VNode } from "preact";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Avatar } from "../mui/Avatar.js"; import { Avatar } from "../mui/Avatar.js";
import { Typography } from "../mui/Typography.js"; import { Typography } from "../mui/Typography.js";
import { wxApi } from "../wxApi.js";
import Banner from "./Banner.js"; import Banner from "./Banner.js";
import { Time } from "./Time.js"; import { Time } from "./Time.js";
@ -35,12 +35,13 @@ interface Props extends JSX.HTMLAttributes {
} }
export function PendingTransactions({ goToTransaction }: Props): VNode { export function PendingTransactions({ goToTransaction }: Props): VNode {
const api = useBackendContext();
const state = useAsyncAsHook(() => const state = useAsyncAsHook(() =>
wxApi.wallet.call(WalletApiOperation.GetTransactions, {}), api.wallet.call(WalletApiOperation.GetTransactions, {}),
); );
useEffect(() => { useEffect(() => {
return wxApi.listener.onUpdateNotification( return api.listener.onUpdateNotification(
[NotificationType.WithdrawGroupFinished], [NotificationType.WithdrawGroupFinished],
state?.retry, state?.retry,
); );

View File

@ -25,11 +25,11 @@ import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js"; import { LoadingError } from "../components/LoadingError.js";
import { Modal } from "../components/Modal.js"; import { Modal } from "../components/Modal.js";
import { Time } from "../components/Time.js"; import { Time } from "../components/Time.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../mui/handlers.js"; import { ButtonHandler } from "../mui/handlers.js";
import { compose, StateViewMap } from "../utils/index.js"; import { compose, StateViewMap } from "../utils/index.js";
import { wxApi } from "../wxApi.js";
import { Amount } from "./Amount.js"; import { Amount } from "./Amount.js";
import { Link } from "./styled/index.js"; import { Link } from "./styled/index.js";
@ -98,7 +98,8 @@ interface Props {
proposalId: string; proposalId: string;
} }
function useComponentState({ proposalId }: Props, api: typeof wxApi): State { function useComponentState({ proposalId }: Props): State {
const api = useBackendContext();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
if (!show) return undefined; if (!show) return undefined;
@ -139,7 +140,7 @@ const viewMapping: StateViewMap<State> = {
export const ShowFullContractTermPopup = compose( export const ShowFullContractTermPopup = compose(
"ShowFullContractTermPopup", "ShowFullContractTermPopup",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -18,7 +18,6 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ToggleHandler } from "../../mui/handlers.js"; import { ToggleHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { TermsState } from "./utils.js"; import { TermsState } from "./utils.js";
import { import {
@ -26,7 +25,7 @@ import {
LoadingUriView, LoadingUriView,
ShowButtonsAcceptedTosView, ShowButtonsAcceptedTosView,
ShowButtonsNonAcceptedTosView, ShowButtonsNonAcceptedTosView,
ShowTosContentView, ShowTosContentView
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -89,6 +88,6 @@ const viewMapping: StateViewMap<State> = {
export const TermsOfService = compose( export const TermsOfService = compose(
"TermsOfService", "TermsOfService",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -16,15 +16,15 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
import { buildTermsOfServiceState } from "./utils.js"; import { buildTermsOfServiceState } from "./utils.js";
export function useComponentState( export function useComponentState(
{ exchangeUrl, onChange }: Props, { exchangeUrl, onChange }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const readOnly = !onChange; const readOnly = !onChange;
const [showContent, setShowContent] = useState<boolean>(readOnly); const [showContent, setShowContent] = useState<boolean>(readOnly);
const [errorAccepting, setErrorAccepting] = useState<Error | undefined>( const [errorAccepting, setErrorAccepting] = useState<Error | undefined>(

View File

@ -0,0 +1,53 @@
/*
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 { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { wxApi, WxApiType } from "../wxApi.js";
type Type = WxApiType
const initial = wxApi;
const Context = createContext<Type>(initial);
type Props = Partial<WxApiType> & {
children: ComponentChildren;
}
export const BackendProvider = ({
wallet,
background,
listener,
children,
}: Props): VNode => {
return h(Context.Provider, {
value: {
wallet: wallet ?? initial.wallet,
background: background ?? initial.background,
listener: listener ?? initial.listener
},
children,
});
};
export const useBackendContext = (): Type => useContext(Context);

View File

@ -19,7 +19,6 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -64,6 +63,6 @@ const viewMapping: StateViewMap<State> = {
export const DepositPage = compose( export const DepositPage = compose(
"Deposit", "Deposit",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -16,14 +16,14 @@
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerDepositUri, amountStr, cancel, onSuccess }: Props, { talerDepositUri, amountStr, cancel, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const info = useAsyncAsHook(async () => { const info = useAsyncAsHook(async () => {
if (!talerDepositUri) throw Error("ERROR_NO-URI-FOR-DEPOSIT"); if (!talerDepositUri) throw Error("ERROR_NO-URI-FOR-DEPOSIT");
if (!amountStr) throw Error("ERROR_NO-AMOUNT-FOR-DEPOSIT"); if (!amountStr) throw Error("ERROR_NO-AMOUNT-FOR-DEPOSIT");

View File

@ -20,16 +20,18 @@
*/ */
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../../test-utils.js";
import { createWalletApiMock } from "../../test-utils.js"; import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { Props } from "./index.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
describe("Deposit CTA states", () => { describe("Deposit CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = {
const props: Props = {
talerDepositUri: undefined, talerDepositUri: undefined,
amountStr: undefined, amountStr: undefined,
cancel: async () => { cancel: async () => {
@ -39,32 +41,28 @@ describe("Deposit CTA states", () => {
null; null;
}, },
}; };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const { status } = pullLastResultOrThrow(); ({ status }) => {
expect(status).equals("loading"); expect(status).equals("loading");
} },
({ status, error }) => {
expect(status).equals("loading-uri");
expect(await waitForStateUpdate()).true; if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-DEPOSIT");
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading-uri");
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-DEPOSIT");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.PrepareDeposit, WalletApiOperation.PrepareDeposit,
undefined, undefined,
@ -73,6 +71,7 @@ describe("Deposit CTA states", () => {
totalDepositCost: "EUR:1.2", totalDepositCost: "EUR:1.2",
}, },
); );
const props = { const props = {
talerDepositUri: "payto://refund/asdasdas", talerDepositUri: "payto://refund/asdasdas",
amountStr: "EUR:1", amountStr: "EUR:1",
@ -84,28 +83,21 @@ describe("Deposit CTA states", () => {
}, },
}; };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status }) => {
expect(status).equals("loading");
},
(state) => {
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.confirm.onClick).not.undefined;
expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2"));
expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status } = pullLastResultOrThrow();
expect(status).equals("loading");
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.confirm.onClick).not.undefined;
expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2"));
expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -22,7 +22,6 @@ import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js"; import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -78,6 +77,6 @@ const viewMapping: StateViewMap<State> = {
export const InvoiceCreatePage = compose( export const InvoiceCreatePage = compose(
"InvoiceCreatePage", "InvoiceCreatePage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -23,17 +23,17 @@ import {
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { isFuture, parse } from "date-fns"; import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js"; import { RecursiveState } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ amount: amountStr, onClose, onSuccess }: Props, { amount: amountStr, onClose, onSuccess }: Props,
api: typeof wxApi,
): RecursiveState<State> { ): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const api = useBackendContext()
const hook = useAsyncAsHook(() => const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}), api.wallet.call(WalletApiOperation.ListExchanges, {}),
@ -158,8 +158,8 @@ export function useComponentState(
subject === undefined subject === undefined
? undefined ? undefined
: !subject : !subject
? "Can't be empty" ? "Can't be empty"
: undefined, : undefined,
value: subject ?? "", value: subject ?? "",
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },

View File

@ -18,13 +18,12 @@ import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
PreparePayResult, PreparePayResult,
TalerErrorDetail, TalerErrorDetail
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -92,6 +91,6 @@ const viewMapping: StateViewMap<State> = {
export const InvoicePayPage = compose( export const InvoicePayPage = compose(
"InvoicePayPage", "InvoicePayPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -25,14 +25,14 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerPayPullUri, onClose, goToWalletManualWithdraw, onSuccess }: Props, { talerPayPullUri, onClose, goToWalletManualWithdraw, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const p2p = await api.wallet.call(WalletApiOperation.CheckPeerPullPayment, { const p2p = await api.wallet.call(WalletApiOperation.CheckPeerPullPayment, {
talerUri: talerPayPullUri, talerUri: talerPayPullUri,

View File

@ -19,13 +19,12 @@ import {
PreparePayResult, PreparePayResult,
PreparePayResultAlreadyConfirmed, PreparePayResultAlreadyConfirmed,
PreparePayResultInsufficientBalance, PreparePayResultInsufficientBalance,
PreparePayResultPaymentPossible, PreparePayResultPaymentPossible
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { BaseView, LoadingUriView } from "./views.js"; import { BaseView, LoadingUriView } from "./views.js";
@ -96,6 +95,6 @@ const viewMapping: StateViewMap<State> = {
export const PaymentPage = compose( export const PaymentPage = compose(
"Payment", "Payment",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -23,16 +23,16 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerPayUri, cancel, goToWalletManualWithdraw, onSuccess }: Props, { talerPayUri, cancel, goToWalletManualWithdraw, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const [payErrMsg, setPayErrMsg] = useState<TalerError | undefined>(undefined); const [payErrMsg, setPayErrMsg] = useState<TalerError | undefined>(undefined);
const api = useBackendContext()
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT"); if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT");

View File

@ -30,54 +30,46 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { tests } from "../../../../web-util/src/index.browser.js";
import { mountHook, nullFunction } from "../../test-utils.js"; import { mountHook, nullFunction } from "../../test-utils.js";
import { createWalletApiMock } from "../../test-utils.js"; import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
describe("Payment CTA states", () => { describe("Payment CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: undefined, talerPayUri: undefined,
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const { status, error } = pullLastResultOrThrow(); ({ status, error }) => {
expect(status).equals("loading"); expect(status).equals("loading");
expect(error).undefined; expect(error).undefined;
} },
({ status, error }) => {
expect(status).equals("loading-uri");
if (error === undefined) expect.fail();
expect(error.hasError).true;
expect(error.operational).false;
},
], TestingContext)
expect(await waitForStateUpdate()).true; expect(hookBehavior).deep.equal({ result: "ok" })
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading-uri");
if (error === undefined) expect.fail();
expect(error.hasError).true;
expect(error.operational).false;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should response with no balance", async () => { it("should response with no balance", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
@ -94,41 +86,34 @@ describe("Payment CTA states", () => {
{ balances: [] }, { balances: [] },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "no-balance-for-currency") {
expect(state).eq({});
return;
}
expect(state.balance).undefined;
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "no-balance-for-currency") {
expect(r).eq({});
return;
}
expect(r.balance).undefined;
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should not be able to pay if there is no enough balance", async () => { it("should not be able to pay if there is no enough balance", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri, WalletApiOperation.PreparePayForUri,
undefined, undefined,
@ -153,38 +138,31 @@ describe("Payment CTA states", () => {
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "no-enough-balance") expect.fail();
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:5"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "no-enough-balance") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:5"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be able to pay (without fee)", async () => { it("should be able to pay (without fee)", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri, WalletApiOperation.PreparePayForUri,
undefined, undefined,
@ -209,42 +187,36 @@ describe("Payment CTA states", () => {
], ],
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "ready") {
expect(state).eq({});
return;
}
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(state.payHandler.onClick).not.undefined;
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") {
expect(r).eq({});
return;
}
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.payHandler.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be able to pay (with fee)", async () => { it("should be able to pay (with fee)", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri, WalletApiOperation.PreparePayForUri,
undefined, undefined,
@ -269,39 +241,32 @@ describe("Payment CTA states", () => {
], ],
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "ready") expect.fail();
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(state.payHandler.onClick).not.undefined;
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.payHandler.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should get confirmation done after pay successfully", async () => { it("should get confirmation done after pay successfully", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri, WalletApiOperation.PreparePayForUri,
undefined, undefined,
@ -332,35 +297,30 @@ describe("Payment CTA states", () => {
contractTerms: {}, contractTerms: {},
} as ConfirmPayResult); } as ConfirmPayResult);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "ready") {
expect(state).eq({});
return;
}
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
if (state.payHandler.onClick === undefined) expect.fail();
state.payHandler.onClick();
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") {
expect(r).eq({});
return;
}
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
if (r.payHandler.onClick === undefined) expect.fail();
r.payHandler.onClick();
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should not stay in ready state after pay with error", async () => { it("should not stay in ready state after pay with error", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
@ -397,62 +357,50 @@ describe("Payment CTA states", () => {
lastError: { code: 1 }, lastError: { code: 1 },
} as ConfirmPayResult); } as ConfirmPayResult);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
{ expect(error).undefined;
const { status, error } = pullLastResultOrThrow(); },
expect(status).equals("loading"); (state) => {
expect(error).undefined; if (state.status !== "ready") expect.fail();
} expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(await waitForStateUpdate()).true; // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
if (state.payHandler.onClick === undefined) expect.fail();
{ state.payHandler.onClick();
const r = pullLastResultOrThrow(); },
if (r.status !== "ready") expect.fail(); (state) => {
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15")); if (state.status !== "ready") expect.fail();
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9")); expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
if (r.payHandler.onClick === undefined) expect.fail(); // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
r.payHandler.onClick(); expect(state.payHandler.onClick).undefined;
} if (state.payHandler.error === undefined) expect.fail();
//FIXME: error message here is bad
expect(await waitForStateUpdate()).true; expect(state.payHandler.error.errorDetail.hint).eq(
"could not confirm payment",
{ );
const r = pullLastResultOrThrow(); expect(state.payHandler.error.errorDetail.payResult).deep.equal({
if (r.status !== "ready") expect.fail(); type: ConfirmPayResultType.Pending,
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15")); lastError: { code: 1 },
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9")); });
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); },
expect(r.payHandler.onClick).undefined; ], TestingContext)
if (r.payHandler.error === undefined) expect.fail();
//FIXME: error message here is bad
expect(r.payHandler.error.errorDetail.hint).eq(
"could not confirm payment",
);
expect(r.payHandler.error.errorDetail.payResult).deep.equal({
type: ConfirmPayResultType.Pending,
lastError: { code: 1 },
});
}
await assertNoPendingUpdate();
expect(hookBehavior).deep.equal({ result: "ok" })
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should update balance if a coins is withdraw", async () => { it("should update balance if a coins is withdraw", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerPayUri: "taller://pay", talerPayUri: "taller://pay",
cancel: nullFunction, cancel: nullFunction,
goToWalletManualWithdraw: nullFunction, goToWalletManualWithdraw: nullFunction,
onSuccess: async () => { onSuccess: nullFunction,
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
@ -507,46 +455,30 @@ describe("Payment CTA states", () => {
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "ready") expect.fail()
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(state.payHandler.onClick).not.undefined;
{ handler.notifyEventFromWallet(NotificationType.CoinWithdrawn);
const { status, error } = pullLastResultOrThrow(); },
expect(status).equals("loading"); (state) => {
expect(error).undefined; if (state.status !== "ready") expect.fail()
} expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(state.payHandler.onClick).not.undefined;
},
], TestingContext)
expect(await waitForStateUpdate()).true; expect(hookBehavior).deep.equal({ result: "ok" })
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") {
expect(r).eq({});
return;
}
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(r.payHandler.onClick).not.undefined;
handler.notifyEventFromWallet(NotificationType.CoinWithdrawn);
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") {
expect(r).eq({});
return;
}
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(r.payHandler.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -18,7 +18,6 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -60,6 +59,6 @@ const viewMapping: StateViewMap<State> = {
export const RecoveryPage = compose( export const RecoveryPage = compose(
"Recovery", "Recovery",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -16,13 +16,13 @@
import { parseRecoveryUri } from "@gnu-taler/taler-util"; import { parseRecoveryUri } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { wxApi } from "../../wxApi.js"; import { useBackendContext } from "../../context/backend.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerRecoveryUri, onCancel, onSuccess }: Props, { talerRecoveryUri, onCancel, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
if (!talerRecoveryUri) { if (!talerRecoveryUri) {
return { return {
status: "loading-uri", status: "loading-uri",
@ -48,7 +48,7 @@ export function useComponentState(
const recovery = info; const recovery = info;
async function recoverBackup(): Promise<void> { async function recoverBackup(): Promise<void> {
await wxApi.wallet.call(WalletApiOperation.ImportBackupRecovery, { await api.wallet.call(WalletApiOperation.ImportBackupRecovery, {
recovery, recovery,
}); });
onSuccess(); onSuccess();

View File

@ -19,13 +19,12 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { import {
IgnoredView, IgnoredView,
InProgressView, InProgressView,
LoadingUriView, LoadingUriView,
ReadyView, ReadyView
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -90,6 +89,6 @@ const viewMapping: StateViewMap<State> = {
export const RefundPage = compose( export const RefundPage = compose(
"Refund", "Refund",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -17,14 +17,14 @@
import { Amounts, NotificationType } from "@gnu-taler/taler-util"; import { Amounts, NotificationType } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerRefundUri, cancel, onSuccess }: Props, { talerRefundUri, cancel, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const [ignored, setIgnored] = useState(false); const [ignored, setIgnored] = useState(false);
const info = useAsyncAsHook(async () => { const info = useAsyncAsHook(async () => {

View File

@ -20,75 +20,50 @@
*/ */
import { import {
AmountJson,
Amounts, Amounts,
NotificationType, NotificationType,
OrderShortInfo, OrderShortInfo
PrepareRefundResult,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../../test-utils.js"; import { tests } from "../../../../web-util/src/index.browser.js";
import { createWalletApiMock } from "../../test-utils.js"; import { createWalletApiMock, mountHook, nullFunction } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
describe("Refund CTA states", () => { describe("Refund CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const props = {
mountHook(() => talerRefundUri: undefined,
useComponentState( cancel: nullFunction,
{ onSuccess: nullFunction,
talerRefundUri: undefined,
cancel: async () => {
null;
},
onSuccess: async () => {
null;
},
},
mock,
// {
// prepareRefund: async () => ({}),
// applyRefund: async () => ({}),
// onUpdateNotification: async () => ({}),
// } as any,
),
);
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
} }
expect(await waitForStateUpdate()).true; const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
({ status, error }) => {
expect(status).equals("loading-uri");
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-REFUND");
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading-uri");
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-REFUND");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerRefundUri: "taler://refund/asdasdas", talerRefundUri: "taler://refund/asdasdas",
cancel: async () => { cancel: nullFunction,
null; onSuccess: nullFunction,
},
onSuccess: async () => {
null;
},
}; };
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
@ -108,61 +83,28 @@ describe("Refund CTA states", () => {
} as OrderShortInfo, } as OrderShortInfo,
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => ({ status, error }) => {
useComponentState( expect(status).equals("loading");
props, expect(error).undefined;
mock, },
// { (state) => {
// prepareRefund: async () => if (state.status !== "ready") expect.fail();
// ({ if (state.error) expect.fail();
// effectivePaid: "EUR:2", expect(state.accept.onClick).not.undefined;
// awaiting: "EUR:2", expect(state.ignore.onClick).not.undefined;
// gone: "EUR:0", expect(state.merchantName).eq("the merchant name");
// granted: "EUR:0", expect(state.orderId).eq("orderId1");
// pending: false, expect(state.products).undefined;
// proposalId: "1", },
// info: { ], TestingContext)
// contractTermsHash: "123",
// merchant: {
// name: "the merchant name",
// },
// orderId: "orderId1",
// summary: "the summary",
// },
// } as PrepareRefundResult as any),
// applyRefund: async () => ({}),
// onUpdateNotification: async () => ({}),
// } as any,
),
);
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.accept.onClick).not.undefined;
expect(state.ignore.onClick).not.undefined;
expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq("orderId1");
expect(state.products).undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerRefundUri: "taler://refund/asdasdas", talerRefundUri: "taler://refund/asdasdas",
cancel: async () => { cancel: async () => {
@ -189,102 +131,36 @@ describe("Refund CTA states", () => {
summary: "the summary", summary: "the summary",
} as OrderShortInfo, } as OrderShortInfo,
}); });
// handler.addWalletCall(WalletApiOperation.ApplyRefund)
// handler.addWalletCall(WalletApiOperation.PrepareRefund, undefined, {
// awaiting: "EUR:1",
// effectivePaid: "EUR:2",
// gone: "EUR:0",
// granted: "EUR:1",
// pending: true,
// proposalId: "1",
// info: {
// contractTermsHash: "123",
// merchant: {
// name: "the merchant name",
// },
// orderId: "orderId1",
// summary: "the summary",
// } as OrderShortInfo,
// })
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(
props,
mock,
// {
// prepareRefund: async () =>
// ({
// effectivePaid: "EUR:2",
// awaiting: "EUR:2",
// gone: "EUR:0",
// granted: "EUR:0",
// pending: false,
// proposalId: "1",
// info: {
// contractTermsHash: "123",
// merchant: {
// name: "the merchant name",
// },
// orderId: "orderId1",
// summary: "the summary",
// },
// } as PrepareRefundResult as any),
// applyRefund: async () => ({}),
// onUpdateNotification: async () => ({}),
// } as any,
),
);
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const { status, error } = pullLastResultOrThrow(); ({ status, error }) => {
expect(status).equals("loading"); expect(status).equals("loading");
expect(error).undefined; expect(error).undefined;
} },
(state) => {
if (state.status !== "ready") expect.fail()
if (state.error) expect.fail()
expect(state.accept.onClick).not.undefined;
expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq("orderId1");
expect(state.products).undefined;
expect(await waitForStateUpdate()).true; if (state.ignore.onClick === undefined) expect.fail();
state.ignore.onClick();
},
(state) => {
if (state.status !== "ignored") expect.fail()
if (state.error) expect.fail()
expect(state.merchantName).eq("the merchant name");
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const state = pullLastResultOrThrow();
if (state.status !== "ready") {
expect(state).eq({});
return;
}
if (state.error) {
expect(state).eq({});
return;
}
expect(state.accept.onClick).not.undefined;
expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq("orderId1");
expect(state.products).undefined;
if (state.ignore.onClick === undefined) expect.fail();
state.ignore.onClick();
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
if (state.status !== "ignored") {
expect(state).eq({});
return;
}
if (state.error) {
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 () => { it("should be in progress when doing refresh", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerRefundUri: "taler://refund/asdasdas", talerRefundUri: "taler://refund/asdasdas",
cancel: async () => { cancel: async () => {
@ -344,67 +220,42 @@ describe("Refund CTA states", () => {
} as OrderShortInfo, } as OrderShortInfo,
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "in-progress") expect.fail()
if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(1 / 3, 0.01)
{ handler.notifyEventFromWallet(NotificationType.RefreshMelted);
const { status, error } = pullLastResultOrThrow(); },
expect(status).equals("loading"); (state) => {
expect(error).undefined; if (state.status !== "in-progress") expect.fail()
} if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(2 / 3, 0.01)
expect(await waitForStateUpdate()).true; handler.notifyEventFromWallet(NotificationType.RefreshMelted);
},
(state) => {
if (state.status !== "ready") expect.fail()
if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
{ },
const state = pullLastResultOrThrow(); ], TestingContext)
if (state.status !== "in-progress") { expect(hookBehavior).deep.equal({ result: "ok" })
expect(state).eq({});
return;
}
if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(1 / 3, 0.01)
handler.notifyEventFromWallet(NotificationType.RefreshMelted);
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
if (state.status !== "in-progress") {
expect(state).eq({});
return;
}
if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(2 / 3, 0.01)
handler.notifyEventFromWallet(NotificationType.RefreshMelted);
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
if (state.status !== "ready") {
expect(state).eq({});
return;
}
if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -19,13 +19,12 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { import {
AcceptedView, AcceptedView,
IgnoredView, IgnoredView,
LoadingUriView, LoadingUriView,
ReadyView, ReadyView
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -84,6 +83,6 @@ const viewMapping: StateViewMap<State> = {
export const TipPage = compose( export const TipPage = compose(
"Tip", "Tip",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -16,14 +16,14 @@
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerTipUri, onCancel, onSuccess }: Props, { talerTipUri, onCancel, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const tipInfo = useAsyncAsHook(async () => { const tipInfo = useAsyncAsHook(async () => {
if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP"); if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP");
const tip = await api.wallet.call(WalletApiOperation.PrepareTip, { const tip = await api.wallet.call(WalletApiOperation.PrepareTip, {

View File

@ -22,54 +22,42 @@
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../../test-utils.js"; import { tests } from "../../../../web-util/src/index.browser.js";
import { mountHook, nullFunction } from "../../test-utils.js";
import { createWalletApiMock } from "../../test-utils.js"; import { createWalletApiMock } from "../../test-utils.js";
import { Props } from "./index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
describe("Tip CTA states", () => { describe("Tip CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const props: Props = {
mountHook(() => talerTipUri: undefined,
useComponentState( onCancel: nullFunction,
{ onSuccess: nullFunction,
talerTipUri: undefined,
onCancel: async () => {
null;
},
onSuccess: async () => {
null;
},
},
mock,
),
);
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
} }
expect(await waitForStateUpdate()).true; const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
({ status, error }) => {
expect(status).equals("loading-uri");
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-TIP");
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading-uri");
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-TIP");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ready for accepting the tip", async () => { it("should be ready for accepting the tip", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
accepted: false, accepted: false,
@ -83,78 +71,59 @@ describe("Tip CTA states", () => {
tipAmountRaw: "", tipAmountRaw: "",
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const props: Props = {
mountHook(() => talerTipUri: "taler://tip/asd",
useComponentState( onCancel: nullFunction,
{ onSuccess: nullFunction,
talerTipUri: "taler://tip/asd",
onCancel: async () => {
null;
},
onSuccess: async () => {
null;
},
},
mock,
),
);
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
} }
expect(await waitForStateUpdate()).true; const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
{ expect(status).equals("loading");
const state = pullLastResultOrThrow(); expect(error).undefined;
if (state.status !== "ready") {
expect(state).eq({ status: "ready" });
return;
}
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
if (state.accept.onClick === undefined) expect.fail();
handler.addWalletCallResponse(WalletApiOperation.AcceptTip);
state.accept.onClick();
}
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
accepted: true,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
expirationTimestamp: {
t_s: 1,
}, },
tipAmountRaw: "", (state) => {
}); if (state.status !== "ready") {
expect(await waitForStateUpdate()).true; expect(state).eq({ status: "ready" });
return;
}
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
if (state.accept.onClick === undefined) expect.fail();
{ handler.addWalletCallResponse(WalletApiOperation.AcceptTip);
const state = pullLastResultOrThrow(); state.accept.onClick();
if (state.status !== "accepted") { handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
expect(state).eq({ status: "accepted" }); accepted: true,
return; exchangeBaseUrl: "exchange url",
} merchantBaseUrl: "merchant url",
if (state.error) expect.fail(); tipAmountEffective: "EUR:1",
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); walletTipId: "tip_id",
expect(state.merchantBaseUrl).eq("merchant url"); expirationTimestamp: {
expect(state.exchangeBaseUrl).eq("exchange url"); t_s: 1,
} },
await assertNoPendingUpdate(); tipAmountRaw: "",
});
},
(state) => {
if (state.status !== "accepted") expect.fail()
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
},
], TestingContext)
expect(hookBehavior).deep.equal({ result: "ok" })
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be ignored after clicking the ignore button", async () => { it.skip("should be ignored after clicking the ignore button", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
@ -167,46 +136,34 @@ describe("Tip CTA states", () => {
tipAmountRaw: "", tipAmountRaw: "",
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const props: Props = {
mountHook(() => talerTipUri: "taler://tip/asd",
useComponentState( onCancel: nullFunction,
{ onSuccess: nullFunction,
talerTipUri: "taler://tip/asd",
onCancel: async () => {
null;
},
onSuccess: async () => {
null;
},
},
mock,
),
);
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
} }
expect(await waitForStateUpdate()).true; const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
{ //FIXME: add ignore button
const state = pullLastResultOrThrow(); },
], TestingContext)
if (state.status !== "ready") expect.fail(); expect(hookBehavior).deep.equal({ result: "ok" })
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should render accepted if the tip has been used previously", async () => { it("should render accepted if the tip has been used previously", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
accepted: true, accepted: true,
@ -220,40 +177,28 @@ describe("Tip CTA states", () => {
tipAmountRaw: "", tipAmountRaw: "",
}); });
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const props: Props = {
mountHook(() => talerTipUri: "taler://tip/asd",
useComponentState( onCancel: nullFunction,
{ onSuccess: nullFunction,
talerTipUri: "taler://tip/asd",
onCancel: async () => {
null;
},
onSuccess: async () => {
null;
},
},
mock,
),
);
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
} }
expect(await waitForStateUpdate()).true; const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
if (state.status !== "accepted") expect.fail();
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const state = pullLastResultOrThrow();
if (state.status !== "accepted") expect.fail();
if (state.error) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -19,7 +19,6 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js"; import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -66,6 +65,6 @@ const viewMapping: StateViewMap<State> = {
export const TransferCreatePage = compose( export const TransferCreatePage = compose(
"TransferCreatePage", "TransferCreatePage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -17,19 +17,19 @@
import { import {
Amounts, Amounts,
TalerErrorDetail, TalerErrorDetail,
TalerProtocolTimestamp, TalerProtocolTimestamp
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { format, isFuture, parse } from "date-fns"; import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ amount: amountStr, onClose, onSuccess }: Props, { amount: amountStr, onClose, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState<string | undefined>(); const [subject, setSubject] = useState<string | undefined>();
@ -124,8 +124,8 @@ export function useComponentState(
subject === undefined subject === undefined
? undefined ? undefined
: !subject : !subject
? "Can't be empty" ? "Can't be empty"
: undefined, : undefined,
value: subject ?? "", value: subject ?? "",
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },

View File

@ -17,13 +17,12 @@
import { import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
TalerErrorDetail, TalerErrorDetail
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -69,6 +68,6 @@ const viewMapping: StateViewMap<State> = {
export const TransferPickupPage = compose( export const TransferPickupPage = compose(
"TransferPickupPage", "TransferPickupPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -18,18 +18,18 @@ import {
AbsoluteTime, AbsoluteTime,
Amounts, Amounts,
TalerErrorDetail, TalerErrorDetail,
TalerProtocolTimestamp, TalerProtocolTimestamp
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerPayPushUri, onClose, onSuccess }: Props, { talerPayPushUri, onClose, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.wallet.call(WalletApiOperation.CheckPeerPushPayment, { return await api.wallet.call(WalletApiOperation.CheckPeerPushPayment, {
talerUri: talerPayPushUri, talerUri: talerPayPushUri,

View File

@ -20,10 +20,9 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { import {
useComponentStateFromParams, useComponentStateFromParams,
useComponentStateFromURI, useComponentStateFromURI
} from "./state.js"; } from "./state.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
@ -96,11 +95,11 @@ const viewMapping: StateViewMap<State> = {
export const WithdrawPageFromURI = compose( export const WithdrawPageFromURI = compose(
"WithdrawPageFromURI", "WithdrawPageFromURI",
(p: PropsFromURI) => useComponentStateFromURI(p, wxApi), (p: PropsFromURI) => useComponentStateFromURI(p),
viewMapping, viewMapping,
); );
export const WithdrawPageFromParams = compose( export const WithdrawPageFromParams = compose(
"WithdrawPageFromParams", "WithdrawPageFromParams",
(p: PropsFromParams) => useComponentStateFromParams(p, wxApi), (p: PropsFromParams) => useComponentStateFromParams(p),
viewMapping, viewMapping,
); );

View File

@ -19,20 +19,20 @@ import {
AmountJson, AmountJson,
Amounts, Amounts,
ExchangeListItem, ExchangeListItem,
ExchangeTosStatus, ExchangeTosStatus
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js"; import { RecursiveState } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { PropsFromParams, PropsFromURI, State } from "./index.js"; import { PropsFromParams, PropsFromURI, State } from "./index.js";
export function useComponentStateFromParams( export function useComponentStateFromParams(
{ amount, cancel, onSuccess }: PropsFromParams, { amount, cancel, onSuccess }: PropsFromParams,
api: typeof wxApi,
): RecursiveState<State> { ): RecursiveState<State> {
const api = useBackendContext()
const uriInfoHook = useAsyncAsHook(async () => { const uriInfoHook = useAsyncAsHook(async () => {
const exchanges = await api.wallet.call( const exchanges = await api.wallet.call(
WalletApiOperation.ListExchanges, WalletApiOperation.ListExchanges,
@ -84,14 +84,13 @@ export function useComponentStateFromParams(
chosenAmount, chosenAmount,
exchangeList, exchangeList,
undefined, undefined,
api,
); );
} }
export function useComponentStateFromURI( export function useComponentStateFromURI(
{ talerWithdrawUri, cancel, onSuccess }: PropsFromURI, { talerWithdrawUri, cancel, onSuccess }: PropsFromURI,
api: typeof wxApi,
): RecursiveState<State> { ): RecursiveState<State> {
const api = useBackendContext()
/** /**
* Ask the wallet about the withdraw URI * Ask the wallet about the withdraw URI
*/ */
@ -158,7 +157,6 @@ export function useComponentStateFromURI(
chosenAmount, chosenAmount,
exchangeList, exchangeList,
defaultExchange, defaultExchange,
api,
); );
} }
@ -176,8 +174,8 @@ function exchangeSelectionState(
chosenAmount: AmountJson, chosenAmount: AmountJson,
exchangeList: ExchangeListItem[], exchangeList: ExchangeListItem[],
defaultExchange: string | undefined, defaultExchange: string | undefined,
api: typeof wxApi,
): RecursiveState<State> { ): RecursiveState<State> {
const api = useBackendContext()
const selectedExchange = useSelectedExchange({ const selectedExchange = useSelectedExchange({
currency: chosenAmount.currency, currency: chosenAmount.currency,
defaultExchange, defaultExchange,
@ -278,10 +276,10 @@ function exchangeSelectionState(
//TODO: calculate based on exchange info //TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled const ageRestriction = ageRestrictionEnabled
? { ? {
list: ageRestrictionOptions, list: ageRestrictionOptions,
value: String(ageRestricted), value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
} }
: undefined; : undefined;
return { return {

View File

@ -27,6 +27,7 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { tests } from "../../../../web-util/src/index.browser.js";
import { mountHook } from "../../test-utils.js"; import { mountHook } from "../../test-utils.js";
import { createWalletApiMock } from "../../test-utils.js"; import { createWalletApiMock } from "../../test-utils.js";
import { useComponentStateFromURI } from "./state.js"; import { useComponentStateFromURI } from "./state.js";
@ -61,53 +62,45 @@ const exchanges: ExchangeListItem[] = [
} as Partial<ExchangeListItem> as ExchangeListItem, } as Partial<ExchangeListItem> as ExchangeListItem,
]; ];
const nullFunction = async (): Promise<void> => {
null;
}
describe("Withdraw CTA states", () => { describe("Withdraw CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerWithdrawUri: undefined, talerWithdrawUri: undefined,
cancel: async () => { cancel: nullFunction,
null; onSuccess: nullFunction,
},
onSuccess: async () => {
null;
},
}; };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentStateFromURI(props, mock));
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [
const { status } = pullLastResultOrThrow(); ({ status }) => {
expect(status).equals("loading"); expect(status).equals("loading");
} },
({ status, error }) => {
if (status != "uri-error") expect.fail();
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
},
], TestingContext)
expect(await waitForStateUpdate()).true; expect(hookBehavior).deep.equal({ result: "ok" })
{
const { status, error } = pullLastResultOrThrow();
if (status != "uri-error") expect.fail();
if (!error) expect.fail();
if (!error.hasError) expect.fail();
if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should tell the user that there is not known exchange", async () => { it("should tell the user that there is not known exchange", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerWithdrawUri: "taler-withdraw://", talerWithdrawUri: "taler-withdraw://",
cancel: async () => { cancel: nullFunction,
null; onSuccess: nullFunction,
},
onSuccess: async () => {
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForUri, WalletApiOperation.GetWithdrawalDetailsForUri,
undefined, undefined,
@ -117,39 +110,28 @@ describe("Withdraw CTA states", () => {
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [
mountHook(() => useComponentStateFromURI(props, mock)); ({ status }) => {
expect(status).equals("loading");
},
({ status, error }) => {
expect(status).equals("no-exchange");
expect(error).undefined;
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status } = pullLastResultOrThrow();
expect(status).equals("loading", "1");
}
expect(await waitForStateUpdate()).true;
{
const { status, error } = pullLastResultOrThrow();
expect(status).equals("no-exchange", "3");
expect(error).undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be able to withdraw if tos are ok", async () => { it("should be able to withdraw if tos are ok", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerWithdrawUri: "taler-withdraw://", talerWithdrawUri: "taler-withdraw://",
cancel: async () => { cancel: nullFunction,
null; onSuccess: nullFunction,
},
onSuccess: async () => {
null;
},
}; };
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForUri, WalletApiOperation.GetWithdrawalDetailsForUri,
undefined, undefined,
@ -171,54 +153,38 @@ describe("Withdraw CTA states", () => {
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [
mountHook(() => useComponentStateFromURI(props, mock)); ({ status }) => {
expect(status).equals("loading");
},
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
expect(state.status).equals("success");
if (state.status !== "success") return;
{ expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
const { status, error } = pullLastResultOrThrow(); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(status).equals("loading"); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(error).undefined;
}
expect(await waitForStateUpdate()).true; expect(state.doWithdrawal.onClick).not.undefined;
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status, error } = pullLastResultOrThrow();
expect(status).equals("loading");
expect(error).undefined;
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
expect(state.status).equals("success");
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should accept the tos before withdraw", async () => { it("should accept the tos before withdraw", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
talerWithdrawUri: "taler-withdraw://", talerWithdrawUri: "taler-withdraw://",
cancel: async () => { cancel: nullFunction,
null; onSuccess: nullFunction,
},
onSuccess: async () => {
null;
},
}; };
const exchangeWithNewTos = exchanges.map((e) => ({ const exchangeWithNewTos = exchanges.map((e) => ({
...e, ...e,
tosStatus: ExchangeTosStatus.New, tosStatus: ExchangeTosStatus.New,
@ -255,57 +221,39 @@ describe("Withdraw CTA states", () => {
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [
mountHook(() => useComponentStateFromURI(props, mock)); ({ status }) => {
expect(status).equals("loading");
},
({ status, error }) => {
expect(status).equals("loading");
expect(error).undefined;
},
(state) => {
expect(state.status).equals("success");
if (state.status !== "success") return;
{ expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
const { status, error } = pullLastResultOrThrow(); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(status).equals("loading"); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(error).undefined;
}
expect(await waitForStateUpdate()).true; expect(state.doWithdrawal.onClick).undefined;
{ state.onTosUpdate();
const { status, error } = pullLastResultOrThrow(); },
(state) => {
expect(state.status).equals("success");
if (state.status !== "success") return;
expect(status).equals("loading"); expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(error).undefined; expect(state.doWithdrawal.onClick).not.undefined;
} },
], TestingContext)
expect(await waitForStateUpdate()).true; expect(hookBehavior).deep.equal({ result: "ok" })
{
const state = pullLastResultOrThrow();
expect(state.status).equals("success");
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).undefined;
// updateAcceptedVersionToCurrentVersion();
state.onTosUpdate();
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
expect(state.status).equals("success");
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -16,22 +16,23 @@
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
import { wxApi } from "../wxApi.js";
export function useAutoOpenPermissions(): ToggleHandler { export function useAutoOpenPermissions(): ToggleHandler {
const api = useBackendContext();
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [error, setError] = useState<TalerError | undefined>(); const [error, setError] = useState<TalerError | undefined>();
const toggle = async (): Promise<void> => { const toggle = async (): Promise<void> => {
return handleAutoOpenPerm(enabled, setEnabled).catch((e) => { return handleAutoOpenPerm(enabled, setEnabled, api.background).catch((e) => {
setError(TalerError.fromException(e)); setError(TalerError.fromException(e));
}); });
}; };
useEffect(() => { useEffect(() => {
async function getValue(): Promise<void> { async function getValue(): Promise<void> {
const res = await wxApi.background.containsHeaderListener(); const res = await api.background.containsHeaderListener();
setEnabled(res.newValue); setEnabled(res.newValue);
} }
getValue(); getValue();
@ -48,6 +49,7 @@ export function useAutoOpenPermissions(): ToggleHandler {
async function handleAutoOpenPerm( async function handleAutoOpenPerm(
isEnabled: boolean, isEnabled: boolean,
onChange: (value: boolean) => void, onChange: (value: boolean) => void,
background: ReturnType<typeof useBackendContext>["background"],
): Promise<void> { ): Promise<void> {
if (!isEnabled) { if (!isEnabled) {
// We set permissions here, since apparently FF wants this to be done // We set permissions here, since apparently FF wants this to be done
@ -59,11 +61,11 @@ async function handleAutoOpenPerm(
onChange(false); onChange(false);
throw lastError; throw lastError;
} }
const res = await wxApi.background.toggleHeaderListener(granted); const res = await background.toggleHeaderListener(granted);
onChange(res.newValue); onChange(res.newValue);
} else { } else {
try { try {
await wxApi.background await background
.toggleHeaderListener(false) .toggleHeaderListener(false)
.then((r) => onChange(r.newValue)); .then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {

View File

@ -16,7 +16,7 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { wxApi } from "../wxApi.js"; import { useBackendContext } from "../context/backend.js";
export interface BackupDeviceName { export interface BackupDeviceName {
name: string; name: string;
@ -28,17 +28,18 @@ export function useBackupDeviceName(): BackupDeviceName {
name: "", name: "",
update: () => Promise.resolve(), update: () => Promise.resolve(),
}); });
const api = useBackendContext()
useEffect(() => { useEffect(() => {
async function run(): Promise<void> { async function run(): Promise<void> {
//create a first list of backup info by currency //create a first list of backup info by currency
const status = await wxApi.wallet.call( const status = await api.wallet.call(
WalletApiOperation.GetBackupInfo, WalletApiOperation.GetBackupInfo,
{}, {},
); );
async function update(newName: string): Promise<void> { async function update(newName: string): Promise<void> {
await wxApi.wallet.call(WalletApiOperation.SetWalletDeviceId, { await api.wallet.call(WalletApiOperation.SetWalletDeviceId, {
walletDeviceId: newName, walletDeviceId: newName,
}); });
setStatus((old) => ({ ...old, name: newName })); setStatus((old) => ({ ...old, name: newName }));

View File

@ -16,22 +16,24 @@
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
import { wxApi } from "../wxApi.js";
export function useClipboardPermissions(): ToggleHandler { export function useClipboardPermissions(): ToggleHandler {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [error, setError] = useState<TalerError | undefined>(); const [error, setError] = useState<TalerError | undefined>();
const api = useBackendContext()
const toggle = async (): Promise<void> => { const toggle = async (): Promise<void> => {
return handleClipboardPerm(enabled, setEnabled).catch((e) => { return handleClipboardPerm(enabled, setEnabled, api.background).catch((e) => {
setError(TalerError.fromException(e)); setError(TalerError.fromException(e));
}); });
}; };
useEffect(() => { useEffect(() => {
async function getValue(): Promise<void> { async function getValue(): Promise<void> {
const res = await wxApi.background.containsHeaderListener(); const res = await api.background.containsHeaderListener();
setEnabled(res.newValue); setEnabled(res.newValue);
} }
getValue(); getValue();
@ -49,6 +51,7 @@ export function useClipboardPermissions(): ToggleHandler {
async function handleClipboardPerm( async function handleClipboardPerm(
isEnabled: boolean, isEnabled: boolean,
onChange: (value: boolean) => void, onChange: (value: boolean) => void,
background: ReturnType<typeof useBackendContext>["background"],
): Promise<void> { ): Promise<void> {
if (!isEnabled) { if (!isEnabled) {
// We set permissions here, since apparently FF wants this to be done // We set permissions here, since apparently FF wants this to be done
@ -62,11 +65,10 @@ async function handleClipboardPerm(
onChange(false); onChange(false);
throw lastError; throw lastError;
} }
// const res = await wxApi.toggleHeaderListener(granted);
onChange(granted); onChange(granted);
} else { } else {
try { try {
await wxApi.background await background
.toggleHeaderListener(false) .toggleHeaderListener(false)
.then((r) => onChange(r.newValue)); .then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {

View File

@ -16,10 +16,11 @@
import { WalletDiagnostics } from "@gnu-taler/taler-util"; import { WalletDiagnostics } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { wxApi } from "../wxApi.js"; import { useBackendContext } from "../context/backend.js";
export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] { export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
const [timedOut, setTimedOut] = useState(false); const [timedOut, setTimedOut] = useState(false);
const api = useBackendContext();
const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>( const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
undefined, undefined,
); );
@ -33,7 +34,7 @@ export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
} }
}, 1000); }, 1000);
const doFetch = async (): Promise<void> => { const doFetch = async (): Promise<void> => {
const d = await wxApi.background.getDiagnostics(); const d = await api.background.getDiagnostics();
gotDiagnostics = true; gotDiagnostics = true;
setDiagnostics(d); setDiagnostics(d);
}; };

View File

@ -16,7 +16,7 @@
import { ProviderInfo, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { ProviderInfo, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { wxApi } from "../wxApi.js"; import { useBackendContext } from "../context/backend.js";
export interface ProviderStatus { export interface ProviderStatus {
info?: ProviderInfo; info?: ProviderInfo;
@ -26,11 +26,11 @@ export interface ProviderStatus {
export function useProviderStatus(url: string): ProviderStatus | undefined { export function useProviderStatus(url: string): ProviderStatus | undefined {
const [status, setStatus] = useState<ProviderStatus | undefined>(undefined); const [status, setStatus] = useState<ProviderStatus | undefined>(undefined);
const api = useBackendContext();
useEffect(() => { useEffect(() => {
async function run(): Promise<void> { async function run(): Promise<void> {
//create a first list of backup info by currency //create a first list of backup info by currency
const status = await wxApi.wallet.call( const status = await api.wallet.call(
WalletApiOperation.GetBackupInfo, WalletApiOperation.GetBackupInfo,
{}, {},
); );
@ -42,7 +42,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function sync(): Promise<void> { async function sync(): Promise<void> {
if (info) { if (info) {
await wxApi.wallet.call(WalletApiOperation.RunBackupCycle, { await api.wallet.call(WalletApiOperation.RunBackupCycle, {
providers: [info.syncProviderBaseUrl], providers: [info.syncProviderBaseUrl],
}); });
} }
@ -50,7 +50,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function remove(): Promise<void> { async function remove(): Promise<void> {
if (info) { if (info) {
await wxApi.wallet.call(WalletApiOperation.RemoveBackupProvider, { await api.wallet.call(WalletApiOperation.RemoveBackupProvider, {
provider: info.syncProviderBaseUrl, provider: info.syncProviderBaseUrl,
}); });
} }

View File

@ -15,22 +15,24 @@
*/ */
import { useState, useEffect } from "preact/hooks"; import { useState, useEffect } from "preact/hooks";
import { wxApi } from "../wxApi.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useBackendContext } from "../context/backend.js";
export function useWalletDevMode(): ToggleHandler { export function useWalletDevMode(): ToggleHandler {
const [enabled, setEnabled] = useState<undefined | boolean>(undefined); const [enabled, setEnabled] = useState<undefined | boolean>(undefined);
const [error, setError] = useState<TalerError | undefined>(); const [error, setError] = useState<TalerError | undefined>();
const api = useBackendContext();
const toggle = async (): Promise<void> => { const toggle = async (): Promise<void> => {
return handleOpen(enabled, setEnabled).catch((e) => { return handleOpen(enabled, setEnabled, api).catch((e) => {
setError(TalerError.fromException(e)); setError(TalerError.fromException(e));
}); });
}; };
useEffect(() => { useEffect(() => {
async function getValue(): Promise<void> { async function getValue(): Promise<void> {
const res = await wxApi.wallet.call(WalletApiOperation.GetVersion, {}); const res = await api.wallet.call(WalletApiOperation.GetVersion, {});
setEnabled(res.devMode); setEnabled(res.devMode);
} }
getValue(); getValue();
@ -47,9 +49,10 @@ export function useWalletDevMode(): ToggleHandler {
async function handleOpen( async function handleOpen(
currentValue: undefined | boolean, currentValue: undefined | boolean,
onChange: (value: boolean) => void, onChange: (value: boolean) => void,
api: ReturnType<typeof useBackendContext>,
): Promise<void> { ): Promise<void> {
const nextValue = !currentValue; const nextValue = !currentValue;
await wxApi.wallet.call(WalletApiOperation.SetDevMode, { await api.wallet.call(WalletApiOperation.SetDevMode, {
devModeEnabled: nextValue, devModeEnabled: nextValue,
}); });
onChange(nextValue); onChange(nextValue);

View File

@ -22,13 +22,13 @@ import { BalanceTable } from "../components/BalanceTable.js";
import { Loading } from "../components/Loading.js"; import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js"; import { LoadingError } from "../components/LoadingError.js";
import { MultiActionButton } from "../components/MultiActionButton.js"; import { MultiActionButton } from "../components/MultiActionButton.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { ButtonHandler } from "../mui/handlers.js"; import { ButtonHandler } from "../mui/handlers.js";
import { compose, StateViewMap } from "../utils/index.js"; import { compose, StateViewMap } from "../utils/index.js";
import { AddNewActionView } from "../wallet/AddNewActionView.js"; import { AddNewActionView } from "../wallet/AddNewActionView.js";
import { wxApi } from "../wxApi.js";
import { NoBalanceHelp } from "./NoBalanceHelp.js"; import { NoBalanceHelp } from "./NoBalanceHelp.js";
export interface Props { export interface Props {
@ -67,10 +67,12 @@ export namespace State {
} }
} }
function useComponentState( function useComponentState({
{ goToWalletDeposit, goToWalletHistory, goToWalletManualWithdraw }: Props, goToWalletDeposit,
api: typeof wxApi, goToWalletHistory,
): State { goToWalletManualWithdraw,
}: Props): State {
const api = useBackendContext();
const [addingAction, setAddingAction] = useState(false); const [addingAction, setAddingAction] = useState(false);
const state = useAsyncAsHook(() => const state = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.GetBalances, {}), api.wallet.call(WalletApiOperation.GetBalances, {}),
@ -128,7 +130,7 @@ const viewMapping: StateViewMap<State> = {
export const BalancePage = compose( export const BalancePage = compose(
"BalancePage", "BalancePage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -31,6 +31,7 @@ import {
VNode, VNode,
} from "preact"; } from "preact";
import { render as renderToString } from "preact-render-to-string"; import { render as renderToString } from "preact-render-to-string";
import { BackendProvider } from "./context/backend.js";
import { BackgroundApiClient, wxApi } from "./wxApi.js"; import { BackgroundApiClient, wxApi } from "./wxApi.js";
// When doing tests we want the requestAnimationFrame to be as fast as possible. // When doing tests we want the requestAnimationFrame to be as fast as possible.
@ -252,7 +253,7 @@ type Subscriptions = {
export function createWalletApiMock(): { export function createWalletApiMock(): {
handler: MockHandler; handler: MockHandler;
mock: typeof wxApi; TestingContext: FunctionalComponent<{ children: ComponentChildren }>
} { } {
const calls = new Array<CallRecord>(); const calls = new Array<CallRecord>();
const subscriptions: Subscriptions = {}; const subscriptions: Subscriptions = {};
@ -357,5 +358,14 @@ export function createWalletApiMock(): {
}, },
}; };
return { handler, mock }; function TestingContext({ children }: { children: ComponentChildren }): VNode {
return create(BackendProvider, {
wallet: mock.wallet,
background: mock.background,
listener: mock.listener,
children,
}, children)
}
return { handler, TestingContext };
} }

View File

@ -15,9 +15,7 @@
*/ */
import { import {
AmountJson, TalerErrorDetail
BackupBackupProviderTerms,
TalerErrorDetail,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { SyncTermsOfServiceResponse } from "@gnu-taler/taler-wallet-core"; import { SyncTermsOfServiceResponse } from "@gnu-taler/taler-wallet-core";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
@ -25,15 +23,13 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
ButtonHandler, ButtonHandler,
TextFieldHandler, TextFieldHandler,
ToggleHandler, ToggleHandler
} from "../../mui/handlers.js"; } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { import {
LoadingUriView, ConfirmProviderView, LoadingUriView,
SelectProviderView, SelectProviderView
ConfirmProviderView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -90,6 +86,6 @@ const viewMapping: StateViewMap<State> = {
export const AddBackupProviderPage = compose( export const AddBackupProviderPage = compose(
"AddBackupProvider", "AddBackupProvider",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -17,15 +17,15 @@
import { import {
canonicalizeBaseUrl, canonicalizeBaseUrl,
Codec, Codec,
TalerErrorDetail, TalerErrorDetail
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
codecForSyncTermsOfServiceResponse, codecForSyncTermsOfServiceResponse,
WalletApiOperation, WalletApiOperation
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { assertUnreachable } from "../../utils/index.js"; import { assertUnreachable } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
type UrlState<T> = UrlOk<T> | UrlError; type UrlState<T> = UrlOk<T> | UrlError;
@ -106,37 +106,37 @@ function useUrlState<T>(
constHref == undefined constHref == undefined
? undefined ? undefined
: async () => { : async () => {
const req = await fetch(constHref).catch((e) => { const req = await fetch(constHref).catch((e) => {
return setState({ return setState({
status: "network-error", status: "network-error",
href: constHref, href: constHref,
});
}); });
if (!req) return; });
if (!req) return;
if (req.status >= 400 && req.status < 500) { if (req.status >= 400 && req.status < 500) {
setState({ setState({
status: "client-error", status: "client-error",
code: req.status, code: req.status,
}); });
return; return;
} }
if (req.status > 500) { if (req.status > 500) {
setState({ setState({
status: "server-error", status: "server-error",
code: req.status, code: req.status,
}); });
return; return;
} }
const json = await req.json(); const json = await req.json();
try { try {
const result = codec.decode(json); const result = codec.decode(json);
setState({ status: "ok", result }); setState({ status: "ok", result });
} catch (e: any) { } catch (e: any) {
setState({ status: "parsing-error", json }); setState({ status: "parsing-error", json });
} }
}, },
[host, path], [host, path],
); );
@ -145,8 +145,8 @@ function useUrlState<T>(
export function useComponentState( export function useComponentState(
{ currency, onBack, onComplete, onPaymentRequired }: Props, { currency, onBack, onComplete, onPaymentRequired }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const [url, setHost] = useState<string | undefined>(); const [url, setHost] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>(); const [name, setName] = useState<string | undefined>();
const [tos, setTos] = useState(false); const [tos, setTos] = useState(false);
@ -223,8 +223,8 @@ export function useComponentState(
!urlState || urlState.status !== "ok" || !name !urlState || urlState.status !== "ok" || !name
? undefined ? undefined
: async () => { : async () => {
setShowConfirm(true); setShowConfirm(true);
}, },
}, },
urlOk: urlState?.status === "ok", urlOk: urlState?.status === "ok",
url: { url: {

View File

@ -19,12 +19,10 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { tests } from "../../../../web-util/src/index.browser.js";
import { import {
createWalletApiMock, createWalletApiMock, nullFunction
mountHook,
nullFunction,
} from "../../test-utils.js"; } from "../../test-utils.js";
import { Props } from "./index.js"; import { Props } from "./index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
@ -36,44 +34,21 @@ const props: Props = {
onPaymentRequired: nullFunction, onPaymentRequired: nullFunction,
}; };
describe("AddBackupProvider states", () => { describe("AddBackupProvider states", () => {
it("should start in 'select-provider' state", async () => { it("should start in 'select-provider' state", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
// handler.addWalletCallResponse( const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
// WalletApiOperation.ListKnownBankAccounts, (state) => {
// undefined, expect(state.status).equal("select-provider");
// { if (state.status !== "select-provider") return;
// accounts: [], expect(state.name.value).eq("");
// }, expect(state.url.value).eq("");
// ); },
], TestingContext)
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = expect(hookBehavior).deep.equal({ result: "ok" })
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"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -42,11 +42,11 @@ import {
SmallText, SmallText,
WarningBox, WarningBox,
} from "../components/styled/index.js"; } from "../components/styled/index.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { wxApi } from "../wxApi.js";
interface Props { interface Props {
onAddProvider: () => Promise<void>; onAddProvider: () => Promise<void>;
@ -107,8 +107,9 @@ export function ShowRecoveryInfo({
export function BackupPage({ onAddProvider }: Props): VNode { export function BackupPage({ onAddProvider }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const api = useBackendContext();
const status = useAsyncAsHook(() => const status = useAsyncAsHook(() =>
wxApi.wallet.call(WalletApiOperation.GetBackupInfo, {}), api.wallet.call(WalletApiOperation.GetBackupInfo, {}),
); );
const [recoveryInfo, setRecoveryInfo] = useState<string>(""); const [recoveryInfo, setRecoveryInfo] = useState<string>("");
if (!status) { if (!status) {
@ -124,7 +125,7 @@ export function BackupPage({ onAddProvider }: Props): VNode {
} }
async function getRecoveryInfo(): Promise<void> { async function getRecoveryInfo(): Promise<void> {
const r = await wxApi.wallet.call( const r = await api.wallet.call(
WalletApiOperation.ExportBackupRecovery, WalletApiOperation.ExportBackupRecovery,
{}, {},
); );
@ -158,7 +159,7 @@ export function BackupPage({ onAddProvider }: Props): VNode {
providers={providers} providers={providers}
onAddProvider={onAddProvider} onAddProvider={onAddProvider}
onSyncAll={async () => onSyncAll={async () =>
wxApi.wallet.call(WalletApiOperation.RunBackupCycle, {}).then() api.wallet.call(WalletApiOperation.RunBackupCycle, {}).then()
} }
onShowInfo={getRecoveryInfo} onShowInfo={getRecoveryInfo}
/> />

View File

@ -20,11 +20,9 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
AmountFieldHandler, AmountFieldHandler,
ButtonHandler, ButtonHandler,
SelectFieldHandler, SelectFieldHandler
TextFieldHandler,
} from "../../mui/handlers.js"; } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { ManageAccountPage } from "../ManageAccount/index.js"; import { ManageAccountPage } from "../ManageAccount/index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { import {
@ -32,7 +30,7 @@ import {
LoadingErrorView, LoadingErrorView,
NoAccountToDepositView, NoAccountToDepositView,
NoEnoughBalanceView, NoEnoughBalanceView,
ReadyView, ReadyView
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -119,6 +117,6 @@ const viewMapping: StateViewMap<State> = {
export const DepositPage = compose( export const DepositPage = compose(
"DepositPage", "DepositPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -21,18 +21,18 @@ import {
KnownBankAccountsInfo, KnownBankAccountsInfo,
parsePaytoUri, parsePaytoUri,
PaytoUri, PaytoUri,
stringifyPaytoUri, stringifyPaytoUri
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ amount: amountStr, currency: currencyStr, onCancel, onSuccess }: Props, { amount: amountStr, currency: currencyStr, onCancel, onSuccess }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr); const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr);
const currency = parsed !== undefined ? parsed.currency : currencyStr; const currency = parsed !== undefined ? parsed.currency : currencyStr;
@ -55,8 +55,8 @@ export function useComponentState(
parsed !== undefined parsed !== undefined
? parsed ? parsed
: currency !== undefined : currency !== undefined
? Amounts.zeroOfCurrency(currency) ? Amounts.zeroOfCurrency(currency)
: undefined; : undefined;
// const [accountIdx, setAccountIdx] = useState<number>(0); // const [accountIdx, setAccountIdx] = useState<number>(0);
const [amount, setAmount] = useState<AmountJson>(initialValue ?? ({} as any)); const [amount, setAmount] = useState<AmountJson>(initialValue ?? ({} as any));
const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
@ -134,7 +134,7 @@ export function useComponentState(
const currentAccount = !selectedAccount ? firstAccount : selectedAccount; const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
if (fee === undefined) { if (fee === undefined) {
getFeeForAmount(currentAccount, amount, api).then((initialFee) => { getFeeForAmount(currentAccount, amount, api.wallet).then((initialFee) => {
setFee(initialFee); setFee(initialFee);
}); });
return { return {
@ -149,7 +149,7 @@ export function useComponentState(
const uri = !accountStr ? undefined : parsePaytoUri(accountStr); const uri = !accountStr ? undefined : parsePaytoUri(accountStr);
if (uri) { if (uri) {
try { try {
const result = await getFeeForAmount(uri, amount, api); const result = await getFeeForAmount(uri, amount, api.wallet);
setSelectedAccount(uri); setSelectedAccount(uri);
setFee(result); setFee(result);
} catch (e) { } catch (e) {
@ -162,7 +162,7 @@ export function useComponentState(
async function updateAmount(newAmount: AmountJson): Promise<void> { async function updateAmount(newAmount: AmountJson): Promise<void> {
// const parsed = Amounts.parse(`${currency}:${numStr}`); // const parsed = Amounts.parse(`${currency}:${numStr}`);
try { try {
const result = await getFeeForAmount(currentAccount, newAmount, api); const result = await getFeeForAmount(currentAccount, newAmount, api.wallet);
setAmount(newAmount); setAmount(newAmount);
setFee(result); setFee(result);
} catch (e) { } catch (e) {
@ -185,8 +185,8 @@ export function useComponentState(
const amountError = !isDirty const amountError = !isDirty
? undefined ? undefined
: Amounts.cmp(balance, amount) === -1 : Amounts.cmp(balance, amount) === -1
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
: undefined; : undefined;
const unableToDeposit = const unableToDeposit =
Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
@ -243,11 +243,11 @@ export function useComponentState(
async function getFeeForAmount( async function getFeeForAmount(
p: PaytoUri, p: PaytoUri,
a: AmountJson, a: AmountJson,
api: typeof wxApi, wallet: ReturnType<typeof useBackendContext>["wallet"],
): Promise<DepositGroupFees> { ): Promise<DepositGroupFees> {
const depositPaytoUri = `payto://${p.targetType}/${p.targetPath}`; const depositPaytoUri = `payto://${p.targetType}/${p.targetPath}`;
const amount = Amounts.stringify(a); const amount = Amounts.stringify(a);
return await api.wallet.call(WalletApiOperation.GetFeeForDeposit, { return await wallet.call(WalletApiOperation.GetFeeForDeposit, {
amount, amount,
depositPaytoUri, depositPaytoUri,
}); });

View File

@ -23,14 +23,13 @@ import {
Amounts, Amounts,
DepositGroupFees, DepositGroupFees,
parsePaytoUri, parsePaytoUri,
stringifyPaytoUri, stringifyPaytoUri
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { tests } from "../../../../web-util/src/index.browser.js";
import { import {
createWalletApiMock, createWalletApiMock, nullFunction
mountHook,
nullFunction,
} from "../../test-utils.js"; } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
@ -50,7 +49,7 @@ const withSomeFee = (): DepositGroupFees => ({
describe("DepositPage states", () => { describe("DepositPage states", () => {
it("should have status 'no-enough-balance' when balance is empty", async () => { it("should have status 'no-enough-balance' when balance is empty", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { currency, onCancel: nullFunction, onSuccess: nullFunction }; const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
@ -72,27 +71,21 @@ describe("DepositPage states", () => {
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status }) => {
expect(status).equal("loading");
},
({ status }) => {
expect(status).equal("no-enough-balance");
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("no-enough-balance");
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => { it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { currency, onCancel: nullFunction, onSuccess: nullFunction }; const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
@ -113,22 +106,17 @@ describe("DepositPage states", () => {
accounts: [], accounts: [],
}, },
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const { status } = pullLastResultOrThrow(); ({ status }) => {
expect(status).equal("loading"); expect(status).equal("loading");
} },
({ status }) => {
expect(status).equal("no-accounts");
},
], TestingContext)
expect(await waitForStateUpdate()).true; expect(hookBehavior).deep.equal({ result: "ok" })
{
const r = pullLastResultOrThrow();
if (r.status !== "no-accounts") expect.fail();
// expect(r.cancelHandler.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -146,7 +134,7 @@ describe("DepositPage states", () => {
}; };
it("should have status 'ready' but unable to deposit ", async () => { it("should have status 'ready' but unable to deposit ", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { currency, onCancel: nullFunction, onSuccess: nullFunction }; const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
@ -173,37 +161,29 @@ describe("DepositPage states", () => {
withoutFee(), withoutFee(),
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
mountHook(() => useComponentState(props, mock)); ({ status }) => {
expect(status).equal("loading");
},
({ status }) => {
expect(status).equal("loading");
},
(state) => {
if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency);
expect(state.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.depositHandler.onClick).undefined;
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency);
expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.depositHandler.onClick).undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should not be able to deposit more than the balance ", async () => { it("should not be able to deposit more than the balance ", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { currency, onCancel: nullFunction, onSuccess: nullFunction }; const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
@ -230,139 +210,50 @@ describe("DepositPage states", () => {
withoutFee(), withoutFee(),
); );
handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit,
undefined,
withoutFee(),
);
handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit,
undefined,
withoutFee(),
);
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit, WalletApiOperation.GetFeeForDeposit,
undefined, undefined,
withoutFee(), withoutFee(),
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
const accountSelected = stringifyPaytoUri(ibanPayto.uri); const accountSelected = stringifyPaytoUri(ibanPayto.uri);
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const r = pullLastResultOrThrow(); ({ status }) => {
if (r.status !== "ready") expect.fail(); expect(status).equal("loading");
expect(r.cancelHandler.onClick).not.undefined; },
expect(r.currency).eq(currency); ({ status }) => {
expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); expect(status).equal("loading");
expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); },
expect(r.depositHandler.onClick).undefined; (state) => {
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); if (state.status !== "ready") expect.fail();
expect(r.account.onChange).not.undefined; expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency);
expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.depositHandler.onClick).undefined;
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(state.account.onChange).not.undefined;
r.account.onChange!(accountSelected); state.account.onChange!(accountSelected);
} },
(state) => {
if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency);
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(state.depositHandler.onClick).undefined;
},
], TestingContext)
expect(await waitForStateUpdate()).true; expect(hookBehavior).deep.equal({ result: "ok" })
expect(handler.getCallingQueueState()).eq("empty");
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency);
expect(r.account.value).eq(accountSelected);
expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.depositHandler.onClick).undefined;
}
await assertNoPendingUpdate();
}); });
// it("should calculate the fee upon entering amount ", async () => {
// const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
// mountHook(() =>
// useComponentState(
// { currency, onCancel: nullFunction, onSuccess: nullFunction },
// {
// getBalance: async () =>
// ({
// balances: [{ available: `${currency}:1` }],
// } as Partial<BalancesResponse>),
// listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
// getFeeForDeposit: withSomeFee,
// } as Partial<typeof wxApi> as any,
// ),
// );
// {
// const { status } = getLastResultOrThrow();
// expect(status).equal("loading");
// }
// await waitNextUpdate();
// {
// const r = getLastResultOrThrow();
// if (r.status !== "ready") expect.fail();
// expect(r.cancelHandler.onClick).not.undefined;
// expect(r.currency).eq(currency);
// expect(r.account.value).eq(accountSelected);
// expect(r.amount.value).eq("0");
// expect(r.depositHandler.onClick).undefined;
// expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
// r.amount.onInput("10");
// }
// expect(await waitForStateUpdate()).true;
// {
// const r = pullLastResultOrThrow();
// if (r.status !== "ready") expect.fail();
// expect(r.cancelHandler.onClick).not.undefined;
// expect(r.currency).eq(currency);
// expect(r.account.value).eq(accountSelected);
// expect(r.amount.value).eq("10");
// expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
// expect(r.depositHandler.onClick).undefined;
// r.amount.onInput("3");
// }
// expect(await waitForStateUpdate()).true;
// {
// const r = pullLastResultOrThrow();
// if (r.status !== "ready") expect.fail();
// expect(r.cancelHandler.onClick).not.undefined;
// expect(r.currency).eq(currency);
// expect(r.account.value).eq(accountSelected);
// expect(r.amount.value).eq("3");
// expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
// expect(r.depositHandler.onClick).not.undefined;
// }
// await assertNoPendingUpdate();
// expect(handler.getCallingQueueState()).eq("empty")
// });
it("should calculate the fee upon entering amount ", async () => { it("should calculate the fee upon entering amount ", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { currency, onCancel: nullFunction, onSuccess: nullFunction }; const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
@ -399,70 +290,54 @@ describe("DepositPage states", () => {
withSomeFee(), withSomeFee(),
); );
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true;
const accountSelected = stringifyPaytoUri(ibanPayto.uri); const accountSelected = stringifyPaytoUri(ibanPayto.uri);
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const r = pullLastResultOrThrow(); ({ status }) => {
if (r.status !== "ready") expect.fail(); expect(status).equal("loading");
expect(r.cancelHandler.onClick).not.undefined; },
expect(r.currency).eq(currency); ({ status }) => {
expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); expect(status).equal("loading");
expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); },
expect(r.depositHandler.onClick).undefined; (state) => {
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); if (state.status !== "ready") expect.fail();
expect(r.account.onChange).not.undefined; expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency);
expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.depositHandler.onClick).undefined;
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(state.account.onChange).not.undefined;
r.account.onChange!(accountSelected); state.account.onChange!(accountSelected);
} },
(state) => {
if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency);
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.depositHandler.onClick).undefined;
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(await waitForStateUpdate()).true; expect(state.amount.onInput).not.undefined;
if (!state.amount.onInput) return;
state.amount.onInput(Amounts.parseOrThrow("EUR:10"));
},
(state) => {
if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency);
expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(state.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
expect(state.depositHandler.onClick).not.undefined;
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const r = pullLastResultOrThrow();
if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency);
expect(r.account.value).eq(accountSelected);
expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.amount.onInput).not.undefined;
if (!r.amount.onInput) return;
r.amount.onInput(Amounts.parseOrThrow("EUR:10"));
}
expect(await waitForStateUpdate()).true;
{
const r = pullLastResultOrThrow();
if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency);
expect(r.account.value).eq(accountSelected);
expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
expect(r.depositHandler.onClick).not.undefined;
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -18,7 +18,6 @@ import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { AmountFieldHandler, ButtonHandler } from "../../mui/handlers.js"; import { AmountFieldHandler, ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView, SelectCurrencyView } from "./views.js"; import { LoadingUriView, ReadyView, SelectCurrencyView } from "./views.js";
@ -88,6 +87,6 @@ const viewMapping: StateViewMap<State> = {
export const DestinationSelectionPage = compose( export const DestinationSelectionPage = compose(
"DestinationSelectionPage", "DestinationSelectionPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -17,15 +17,15 @@
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { assertUnreachable, RecursiveState } from "../../utils/index.js"; import { assertUnreachable, RecursiveState } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { Contact, Props, State } from "./index.js"; import { Contact, Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
props: Props, props: Props,
api: typeof wxApi,
): RecursiveState<State> { ): RecursiveState<State> {
const api = useBackendContext()
const parsedInitialAmount = !props.amount const parsedInitialAmount = !props.amount
? undefined ? undefined
: Amounts.parse(props.amount); : Amounts.parse(props.amount);

View File

@ -23,11 +23,12 @@ import {
Amounts, Amounts,
ExchangeEntryStatus, ExchangeEntryStatus,
ExchangeListItem, ExchangeListItem,
ExchangeTosStatus, ExchangeTosStatus
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { createWalletApiMock, mountHook } from "../../test-utils.js"; import { tests } from "../../../../web-util/src/index.browser.js";
import { createWalletApiMock, nullFunction } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
const exchangeArs: ExchangeListItem = { const exchangeArs: ExchangeListItem = {
@ -42,7 +43,7 @@ const exchangeArs: ExchangeListItem = {
describe("Destination selection states", () => { describe("Destination selection states", () => {
it("should select currency if no amount specified", async () => { it("should select currency if no amount specified", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.ListExchanges, WalletApiOperation.ListExchanges,
@ -54,83 +55,65 @@ describe("Destination selection states", () => {
const props = { const props = {
type: "get" as const, type: "get" as const,
goToWalletManualWithdraw: () => { goToWalletManualWithdraw: nullFunction,
return null; goToWalletWalletInvoice: nullFunction,
},
goToWalletWalletInvoice: () => {
null;
},
}; };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const state = pullLastResultOrThrow(); ({ status }) => {
expect(status).equal("loading");
},
(state) => {
if (state.status !== "select-currency") expect.fail();
if (state.error) expect.fail();
expect(state.currencies).deep.eq({
ARS: "ARS",
"": "Select a currency",
});
if (state.status !== "loading") expect.fail(); state.onCurrencySelected(exchangeArs.currency!);
if (state.error) expect.fail(); },
} (state) => {
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.goToBank.onClick).eq(undefined);
expect(state.goToWallet.onClick).eq(undefined);
expect(await waitForStateUpdate()).true; expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:0"));
},
], TestingContext)
{ expect(hookBehavior).deep.equal({ result: "ok" })
const state = pullLastResultOrThrow();
if (state.status !== "select-currency") expect.fail();
if (state.error) expect.fail();
expect(state.currencies).deep.eq({
ARS: "ARS",
"": "Select a currency",
});
state.onCurrencySelected(exchangeArs.currency!);
}
expect(await waitForStateUpdate()).true;
{
const state = pullLastResultOrThrow();
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.goToBank.onClick).eq(undefined);
expect(state.goToWallet.onClick).eq(undefined);
expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:0"));
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should be possible to start with an amount specified in request params", async () => { it("should be possible to start with an amount specified in request params", async () => {
const { handler, mock } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const props = { const props = {
type: "get" as const, type: "get" as const,
goToWalletManualWithdraw: () => { goToWalletManualWithdraw: nullFunction,
return null; goToWalletWalletInvoice: nullFunction,
},
goToWalletWalletInvoice: () => {
null;
},
amount: "ARS:2", amount: "ARS:2",
}; };
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock));
{ const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [
const state = pullLastResultOrThrow(); // ({ status }) => {
// expect(status).equal("loading");
// },
(state) => {
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.goToBank.onClick).not.eq(undefined);
expect(state.goToWallet.onClick).not.eq(undefined);
if (state.status !== "ready") expect.fail(); expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:2"));
if (state.error) expect.fail(); },
expect(state.goToBank.onClick).not.eq(undefined); ], TestingContext)
expect(state.goToWallet.onClick).not.eq(undefined);
expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:2")); expect(hookBehavior).deep.equal({ result: "ok" })
}
await assertNoPendingUpdate();
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -31,12 +31,12 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { Diagnostics } from "../components/Diagnostics.js"; import { Diagnostics } from "../components/Diagnostics.js";
import { NotifyUpdateFadeOut } from "../components/styled/index.js"; import { NotifyUpdateFadeOut } from "../components/styled/index.js";
import { Time } from "../components/Time.js"; import { Time } from "../components/Time.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { useDiagnostics } from "../hooks/useDiagnostics.js"; import { useDiagnostics } from "../hooks/useDiagnostics.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js"; import { Grid } from "../mui/Grid.js";
import { wxApi } from "../wxApi.js";
export function DeveloperPage(): VNode { export function DeveloperPage(): VNode {
const [status, timedOut] = useDiagnostics(); const [status, timedOut] = useDiagnostics();
@ -45,13 +45,15 @@ export function DeveloperPage(): VNode {
//FIXME: waiting for retry notification make a always increasing loop of notifications //FIXME: waiting for retry notification make a always increasing loop of notifications
listenAllEvents.includes = (e) => e !== "waiting-for-retry"; // includes every event listenAllEvents.includes = (e) => e !== "waiting-for-retry"; // includes every event
const api = useBackendContext();
const response = useAsyncAsHook(async () => { const response = useAsyncAsHook(async () => {
const op = await wxApi.wallet.call( const op = await api.wallet.call(
WalletApiOperation.GetPendingOperations, WalletApiOperation.GetPendingOperations,
{}, {},
); );
const c = await wxApi.wallet.call(WalletApiOperation.DumpCoins, {}); const c = await api.wallet.call(WalletApiOperation.DumpCoins, {});
const ex = await wxApi.wallet.call(WalletApiOperation.ListExchanges, {}); const ex = await api.wallet.call(WalletApiOperation.ListExchanges, {});
return { return {
operations: op.pendingOperations, operations: op.pendingOperations,
coins: c.coins, coins: c.coins,
@ -60,10 +62,7 @@ export function DeveloperPage(): VNode {
}); });
useEffect(() => { useEffect(() => {
return wxApi.listener.onUpdateNotification( return api.listener.onUpdateNotification(listenAllEvents, response?.retry);
listenAllEvents,
response?.retry,
);
}); });
const nonResponse = { operations: [], coins: [], exchanges: [] }; const nonResponse = { operations: [], coins: [], exchanges: [] };
@ -82,7 +81,7 @@ export function DeveloperPage(): VNode {
coins={coins} coins={coins}
exchanges={exchanges} exchanges={exchanges}
onDownloadDatabase={async () => { onDownloadDatabase={async () => {
const db = await wxApi.wallet.call(WalletApiOperation.ExportDb, {}); const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
return JSON.stringify(db); return JSON.stringify(db);
}} }}
/> />
@ -135,9 +134,10 @@ export function View({
content, content,
}); });
} }
const api = useBackendContext();
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
async function onImportDatabase(str: string): Promise<void> { async function onImportDatabase(str: string): Promise<void> {
return wxApi.wallet.call(WalletApiOperation.ImportDb, { return api.wallet.call(WalletApiOperation.ImportDb, {
dump: JSON.parse(str), dump: JSON.parse(str),
}); });
} }
@ -177,7 +177,7 @@ export function View({
onClick={() => onClick={() =>
confirmReset( confirmReset(
i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`, i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`,
() => wxApi.background.resetDb(), () => api.background.resetDb(),
) )
} }
> >
@ -190,7 +190,7 @@ export function View({
onClick={() => onClick={() =>
confirmReset( confirmReset(
i18n.str`TESTING: This may delete all your coin, proceed with caution`, i18n.str`TESTING: This may delete all your coin, proceed with caution`,
() => wxApi.background.runGarbageCollector(), () => api.background.runGarbageCollector(),
) )
} }
> >

View File

@ -17,7 +17,6 @@
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -55,6 +54,6 @@ const viewMapping: StateViewMap<State> = {
export const ComponentName = compose( export const ComponentName = compose(
"ComponentName", "ComponentName",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -14,10 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState({ p }: Props, api: typeof wxApi): State { export function useComponentState({ p }: Props): State {
return { return {
status: "ready", status: "ready",
error: undefined, error: undefined,

View File

@ -21,9 +21,9 @@ import {
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { queryToSlashKeys } from "../utils/index.js"; import { queryToSlashKeys } from "../utils/index.js";
import { wxApi } from "../wxApi.js";
import { ExchangeAddConfirmPage } from "./ExchangeAddConfirm.js"; import { ExchangeAddConfirmPage } from "./ExchangeAddConfirm.js";
import { ExchangeSetUrlPage } from "./ExchangeSetUrl.js"; import { ExchangeSetUrlPage } from "./ExchangeSetUrl.js";
@ -37,8 +37,9 @@ export function ExchangeAddPage({ currency, onBack }: Props): VNode {
{ url: string; config: TalerConfigResponse } | undefined { url: string; config: TalerConfigResponse } | undefined
>(undefined); >(undefined);
const api = useBackendContext();
const knownExchangesResponse = useAsyncAsHook(() => const knownExchangesResponse = useAsyncAsHook(() =>
wxApi.wallet.call(WalletApiOperation.ListExchanges, {}), api.wallet.call(WalletApiOperation.ListExchanges, {}),
); );
const knownExchanges = !knownExchangesResponse const knownExchanges = !knownExchangesResponse
? [] ? []
@ -75,7 +76,7 @@ export function ExchangeAddPage({ currency, onBack }: Props): VNode {
url={verifying.url} url={verifying.url}
onCancel={onBack} onCancel={onBack}
onConfirm={async () => { onConfirm={async () => {
await wxApi.wallet.call(WalletApiOperation.AddExchange, { await api.wallet.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: canonicalizeBaseUrl(verifying.url), exchangeBaseUrl: canonicalizeBaseUrl(verifying.url),
forceUpdate: true, forceUpdate: true,
}); });

View File

@ -18,14 +18,13 @@ import {
DenomOperationMap, DenomOperationMap,
ExchangeFullDetails, ExchangeFullDetails,
ExchangeListItem, ExchangeListItem,
FeeDescriptionPair, FeeDescriptionPair
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { import {
ComparingView, ComparingView,
@ -33,7 +32,7 @@ import {
NoExchangesView, NoExchangesView,
PrivacyContentView, PrivacyContentView,
ReadyView, ReadyView,
TosContentView, TosContentView
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -106,6 +105,6 @@ const viewMapping: StateViewMap<State> = {
export const ExchangeSelectionPage = compose( export const ExchangeSelectionPage = compose(
"ExchangeSelectionPage", "ExchangeSelectionPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -17,17 +17,17 @@
import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util"; import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
import { import {
createPairTimeline, createPairTimeline,
WalletApiOperation, WalletApiOperation
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ onCancel, onSelection, list: exchanges, currentExchange }: Props, { onCancel, onSelection, list: exchanges, currentExchange }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const initialValue = exchanges.findIndex( const initialValue = exchanges.findIndex(
(e) => e.exchangeBaseUrl === currentExchange, (e) => e.exchangeBaseUrl === currentExchange,
); );
@ -52,14 +52,14 @@ export function useComponentState(
const selected = !selectedExchange const selected = !selectedExchange
? undefined ? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: selectedExchange.exchangeBaseUrl, exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
}); });
const original = !initialExchange const original = !initialExchange
? undefined ? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: initialExchange.exchangeBaseUrl, exchangeBaseUrl: initialExchange.exchangeBaseUrl,
}); });
return { return {
exchanges, exchanges,

View File

@ -33,13 +33,13 @@ import {
} from "../components/styled/index.js"; } from "../components/styled/index.js";
import { Time } from "../components/Time.js"; import { Time } from "../components/Time.js";
import { TransactionItem } from "../components/TransactionItem.js"; import { TransactionItem } from "../components/TransactionItem.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { NoBalanceHelp } from "../popup/NoBalanceHelp.js"; import { NoBalanceHelp } from "../popup/NoBalanceHelp.js";
import DownloadIcon from "../svg/download_24px.svg"; import DownloadIcon from "../svg/download_24px.svg";
import UploadIcon from "../svg/upload_24px.svg"; import UploadIcon from "../svg/upload_24px.svg";
import { wxApi } from "../wxApi.js";
interface Props { interface Props {
currency?: string; currency?: string;
@ -52,13 +52,14 @@ export function HistoryPage({
goToWalletDeposit, goToWalletDeposit,
}: Props): VNode { }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const api = useBackendContext();
const state = useAsyncAsHook(async () => ({ const state = useAsyncAsHook(async () => ({
b: await wxApi.wallet.call(WalletApiOperation.GetBalances, {}), b: await api.wallet.call(WalletApiOperation.GetBalances, {}),
tx: await wxApi.wallet.call(WalletApiOperation.GetTransactions, {}), tx: await api.wallet.call(WalletApiOperation.GetTransactions, {}),
})); }));
useEffect(() => { useEffect(() => {
return wxApi.listener.onUpdateNotification( return api.listener.onUpdateNotification(
[NotificationType.WithdrawGroupFinished], [NotificationType.WithdrawGroupFinished],
state?.retry, state?.retry,
); );

View File

@ -20,10 +20,9 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
ButtonHandler, ButtonHandler,
SelectFieldHandler, SelectFieldHandler,
TextFieldHandler, TextFieldHandler
} from "../../mui/handlers.js"; } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
@ -75,6 +74,6 @@ const viewMapping: StateViewMap<State> = {
export const ManageAccountPage = compose( export const ManageAccountPage = compose(
"ManageAccountPage", "ManageAccountPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -17,18 +17,18 @@
import { import {
KnownBankAccountsInfo, KnownBankAccountsInfo,
parsePaytoUri, parsePaytoUri,
stringifyPaytoUri, stringifyPaytoUri
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { AccountByType, Props, State } from "./index.js"; import { AccountByType, Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ currency, onAccountAdded, onCancel }: Props, { currency, onAccountAdded, onCancel }: Props,
api: typeof wxApi,
): State { ): State {
const api = useBackendContext()
const hook = useAsyncAsHook(() => const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }), api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
); );

View File

@ -18,11 +18,10 @@ import { UserAttentionUnreadList } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
export interface Props {} export type Props = object
export type State = State.Loading | State.LoadingUriError | State.Ready; export type State = State.Loading | State.LoadingUriError | State.Ready;
@ -56,6 +55,6 @@ const viewMapping: StateViewMap<State> = {
export const NotificationsPage = compose( export const NotificationsPage = compose(
"NotificationsPage", "NotificationsPage",
(p: Props) => useComponentState(p, wxApi), (p: Props) => useComponentState(p),
viewMapping, viewMapping,
); );

View File

@ -15,11 +15,12 @@
*/ */
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState({}: Props, api: typeof wxApi): State { export function useComponentState(p: Props): State {
const api = useBackendContext()
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.wallet.call( return await api.wallet.call(
WalletApiOperation.GetUserAttentionRequests, WalletApiOperation.GetUserAttentionRequests,

View File

@ -31,10 +31,10 @@ import {
SubTitle, SubTitle,
Title, Title,
} from "../components/styled/index.js"; } from "../components/styled/index.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { queryToSlashConfig } from "../utils/index.js"; import { queryToSlashConfig } from "../utils/index.js";
import { wxApi } from "../wxApi.js";
interface Props { interface Props {
currency: string; currency: string;
@ -46,7 +46,7 @@ export function ProviderAddPage({ onBack }: Props): VNode {
| { url: string; name: string; provider: BackupBackupProviderTerms } | { url: string; name: string; provider: BackupBackupProviderTerms }
| undefined | undefined
>(undefined); >(undefined);
const api = useBackendContext();
if (!verifying) { if (!verifying) {
return ( return (
<SetUrlView <SetUrlView
@ -70,7 +70,7 @@ export function ProviderAddPage({ onBack }: Props): VNode {
setVerifying(undefined); setVerifying(undefined);
}} }}
onConfirm={() => { onConfirm={() => {
return wxApi.wallet return api.wallet
.call(WalletApiOperation.AddBackupProvider, { .call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: verifying.url, backupProviderBaseUrl: verifying.url,
name: verifying.name, name: verifying.name,

View File

@ -28,10 +28,10 @@ import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js"; import { LoadingError } from "../components/LoadingError.js";
import { PaymentStatus, SmallLightText } from "../components/styled/index.js"; import { PaymentStatus, SmallLightText } from "../components/styled/index.js";
import { Time } from "../components/Time.js"; import { Time } from "../components/Time.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { wxApi } from "../wxApi.js";
interface Props { interface Props {
pid: string; pid: string;
@ -47,12 +47,10 @@ export function ProviderDetailPage({
onWithdraw, onWithdraw,
}: Props): VNode { }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const api = useBackendContext();
async function getProviderInfo(): Promise<ProviderInfo | null> { async function getProviderInfo(): Promise<ProviderInfo | null> {
//create a first list of backup info by currency //create a first list of backup info by currency
const status = await wxApi.wallet.call( const status = await api.wallet.call(WalletApiOperation.GetBackupInfo, {});
WalletApiOperation.GetBackupInfo,
{},
);
const providers = status.providers.filter( const providers = status.providers.filter(
(p) => p.syncProviderBaseUrl === providerURL, (p) => p.syncProviderBaseUrl === providerURL,
@ -103,7 +101,7 @@ export function ProviderDetailPage({
<ProviderView <ProviderView
info={info} info={info}
onSync={async () => onSync={async () =>
wxApi.wallet api.wallet
.call(WalletApiOperation.RunBackupCycle, { .call(WalletApiOperation.RunBackupCycle, {
providers: [providerURL], providers: [providerURL],
}) })
@ -120,7 +118,7 @@ export function ProviderDetailPage({
onWithdraw(info.paymentStatus.amount); onWithdraw(info.paymentStatus.amount);
}} }}
onDelete={() => onDelete={() =>
wxApi.wallet api.wallet
.call(WalletApiOperation.RemoveBackupProvider, { .call(WalletApiOperation.RemoveBackupProvider, {
provider: providerURL, provider: providerURL,
}) })

View File

@ -34,6 +34,7 @@ import {
SuccessText, SuccessText,
WarningText, WarningText,
} from "../components/styled/index.js"; } from "../components/styled/index.js";
import { useBackendContext } from "../context/backend.js";
import { useDevContext } from "../context/devContext.js"; import { useDevContext } from "../context/devContext.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
@ -43,7 +44,6 @@ import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
import { wxApi } from "../wxApi.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
@ -53,10 +53,11 @@ export function SettingsPage(): VNode {
const { devModeToggle } = useDevContext(); const { devModeToggle } = useDevContext();
const { name, update } = useBackupDeviceName(); const { name, update } = useBackupDeviceName();
const webex = platform.getWalletWebExVersion(); const webex = platform.getWalletWebExVersion();
const api = useBackendContext();
const exchangesHook = useAsyncAsHook(async () => { const exchangesHook = useAsyncAsHook(async () => {
const list = await wxApi.wallet.call(WalletApiOperation.ListExchanges, {}); const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
const version = await wxApi.wallet.call(WalletApiOperation.GetVersion, {}); const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
return { exchanges: list.exchanges, version }; return { exchanges: list.exchanges, version };
}); });
const { exchanges, version } = const { exchanges, version } =

View File

@ -60,11 +60,11 @@ import {
WarningBox, WarningBox,
} from "../components/styled/index.js"; } from "../components/styled/index.js";
import { Time } from "../components/Time.js"; import { Time } from "../components/Time.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { wxApi } from "../wxApi.js";
interface Props { interface Props {
tid: string; tid: string;
@ -76,17 +76,17 @@ export function TransactionPage({
goToWalletHistory, goToWalletHistory,
}: Props): VNode { }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const api = useBackendContext();
const state = useAsyncAsHook( const state = useAsyncAsHook(
() => () =>
wxApi.wallet.call(WalletApiOperation.GetTransactionById, { api.wallet.call(WalletApiOperation.GetTransactionById, {
transactionId, transactionId,
}), }),
[transactionId], [transactionId],
); );
useEffect(() => useEffect(() =>
wxApi.listener.onUpdateNotification( api.listener.onUpdateNotification(
[NotificationType.WithdrawGroupFinished], [NotificationType.WithdrawGroupFinished],
state?.retry, state?.retry,
), ),
@ -118,19 +118,19 @@ export function TransactionPage({
null; null;
}} }}
onDelete={async () => { onDelete={async () => {
await wxApi.wallet.call(WalletApiOperation.DeleteTransaction, { await api.wallet.call(WalletApiOperation.DeleteTransaction, {
transactionId, transactionId,
}); });
goToWalletHistory(currency); goToWalletHistory(currency);
}} }}
onRetry={async () => { onRetry={async () => {
await wxApi.wallet.call(WalletApiOperation.RetryTransaction, { await api.wallet.call(WalletApiOperation.RetryTransaction, {
transactionId, transactionId,
}); });
goToWalletHistory(currency); goToWalletHistory(currency);
}} }}
onRefund={async (purchaseId) => { onRefund={async (purchaseId) => {
await wxApi.wallet.call(WalletApiOperation.ApplyRefundFromPurchaseId, { await api.wallet.call(WalletApiOperation.ApplyRefundFromPurchaseId, {
purchaseId, purchaseId,
}); });
}} }}

View File

@ -151,6 +151,14 @@ function onUpdateNotification(
return platform.listenToWalletBackground(onNewMessage); return platform.listenToWalletBackground(onNewMessage);
} }
export type WxApiType = {
wallet: WalletCoreApiClient;
background: BackgroundApiClient;
listener: {
onUpdateNotification: typeof onUpdateNotification;
}
}
export const wxApi = { export const wxApi = {
wallet: new WxWalletCoreApiClient(), wallet: new WxWalletCoreApiClient(),
background: new BackgroundApiClient(), background: new BackgroundApiClient(),

View File

@ -246,7 +246,7 @@ interface HookTestResultError {
export async function hookBehaveLikeThis<T extends object, PropsType>( export async function hookBehaveLikeThis<T extends object, PropsType>(
hookFunction: (p: PropsType) => RecursiveState<T>, hookFunction: (p: PropsType) => RecursiveState<T>,
props: PropsType, props: PropsType,
checks: Array<(state: T) => void>, checks: Array<(state: Exclude<T, VoidFunction>) => void>,
Context?: ({ children }: { children: any }) => VNode | null, Context?: ({ children }: { children: any }) => VNode | null,
): Promise<HookTestResult> { ): Promise<HookTestResult> {
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =