This commit is contained in:
Sebastian 2022-12-15 17:12:03 -03:00
parent f93bd51499
commit f1f8f818db
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
45 changed files with 997 additions and 800 deletions

View File

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

View File

@ -21,10 +21,8 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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): State {
{ exchangeUrl, onChange }: Props, const api = useBackendContext();
): 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

@ -23,7 +23,7 @@ import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { wxApi, WxApiType } from "../wxApi.js"; import { wxApi, WxApiType } from "../wxApi.js";
type Type = WxApiType type Type = WxApiType;
const initial = wxApi; const initial = wxApi;
@ -31,7 +31,7 @@ const Context = createContext<Type>(initial);
type Props = Partial<WxApiType> & { type Props = Partial<WxApiType> & {
children: ComponentChildren; children: ComponentChildren;
} };
export const BackendProvider = ({ export const BackendProvider = ({
wallet, wallet,
@ -39,12 +39,11 @@ export const BackendProvider = ({
listener, listener,
children, children,
}: Props): VNode => { }: Props): VNode => {
return h(Context.Provider, { return h(Context.Provider, {
value: { value: {
wallet: wallet ?? initial.wallet, wallet: wallet ?? initial.wallet,
background: background ?? initial.background, background: background ?? initial.background,
listener: listener ?? initial.listener listener: listener ?? initial.listener,
}, },
children, children,
}); });

View File

@ -20,10 +20,13 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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,
): State { amountStr,
const api = useBackendContext() cancel,
onSuccess,
}: Props): 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

@ -42,21 +42,26 @@ describe("Deposit CTA states", () => {
}, },
}; };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equals("loading"); props,
}, [
({ status, error }) => { ({ status }) => {
expect(status).equals("loading-uri"); expect(status).equals("loading");
},
({ status, error }) => {
expect(status).equals("loading-uri");
if (!error) expect.fail(); if (!error) expect.fail();
if (!error.hasError) expect.fail(); if (!error.hasError) expect.fail();
if (error.operational) expect.fail(); if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-DEPOSIT"); expect(error.message).eq("ERROR_NO-URI-FOR-DEPOSIT");
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -83,21 +88,26 @@ describe("Deposit CTA states", () => {
}, },
}; };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equals("loading"); props,
}, [
(state) => { ({ status }) => {
if (state.status !== "ready") expect.fail(); expect(status).equals("loading");
if (state.error) expect.fail(); },
expect(state.confirm.onClick).not.undefined; (state) => {
expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2")); if (state.status !== "ready") expect.fail();
expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2")); if (state.error) expect.fail();
expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1")); expect(state.confirm.onClick).not.undefined;
}, expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
], TestingContext) 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" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -29,11 +29,13 @@ import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js"; import { RecursiveState } from "../../utils/index.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,
): RecursiveState<State> { onClose,
onSuccess,
}: Props): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const api = useBackendContext() const api = useBackendContext();
const hook = useAsyncAsHook(() => const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}), api.wallet.call(WalletApiOperation.ListExchanges, {}),
@ -158,8 +160,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,7 +18,7 @@ import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
PreparePayResult, PreparePayResult,
TalerErrorDetail TalerErrorDetail,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";

View File

@ -29,10 +29,13 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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,
): State { onClose,
const api = useBackendContext() goToWalletManualWithdraw,
onSuccess,
}: Props): 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,7 +19,7 @@ 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";

View File

@ -28,11 +28,14 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.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,
): State { cancel,
goToWalletManualWithdraw,
onSuccess,
}: Props): State {
const [payErrMsg, setPayErrMsg] = useState<TalerError | undefined>(undefined); const [payErrMsg, setPayErrMsg] = useState<TalerError | undefined>(undefined);
const api = useBackendContext() 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

@ -45,22 +45,26 @@ describe("Payment CTA states", () => {
onSuccess: nullFunction, onSuccess: nullFunction,
}; };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
({ status, error }) => { expect(status).equals("loading");
expect(status).equals("loading-uri"); expect(error).undefined;
if (error === undefined) expect.fail(); },
expect(error.hasError).true; ({ status, error }) => {
expect(error.operational).false; expect(status).equals("loading-uri");
}, if (error === undefined) expect.fail();
], TestingContext) expect(error.hasError).true;
expect(error.operational).false;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
it("should response with no balance", async () => { it("should response with no balance", async () => {
@ -86,22 +90,27 @@ describe("Payment CTA states", () => {
{ balances: [] }, { balances: [] },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "no-balance-for-currency") { expect(error).undefined;
expect(state).eq({}); },
return; (state) => {
} if (state.status !== "no-balance-for-currency") {
expect(state.balance).undefined; expect(state).eq({});
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10")); return;
}, }
], TestingContext) expect(state.balance).undefined;
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -138,19 +147,24 @@ describe("Payment CTA states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "no-enough-balance") expect.fail(); expect(error).undefined;
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:5")); },
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10")); (state) => {
}, if (state.status !== "no-enough-balance") expect.fail();
], TestingContext) 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" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -187,25 +201,29 @@ describe("Payment CTA states", () => {
], ],
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") { expect(error).undefined;
expect(state).eq({}); },
return; (state) => {
} if (state.status !== "ready") {
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state).eq({});
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10")); return;
expect(state.payHandler.onClick).not.undefined; }
}, expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
], TestingContext) expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(state.payHandler.onClick).not.undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
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 () => {
@ -241,20 +259,25 @@ describe("Payment CTA states", () => {
], ],
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") expect.fail(); expect(error).undefined;
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); },
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); (state) => {
expect(state.payHandler.onClick).not.undefined; if (state.status !== "ready") expect.fail();
}, expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
], TestingContext) expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(state.payHandler.onClick).not.undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -297,26 +320,30 @@ describe("Payment CTA states", () => {
contractTerms: {}, contractTerms: {},
} as ConfirmPayResult); } as ConfirmPayResult);
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") { expect(error).undefined;
expect(state).eq({}); },
return; (state) => {
} if (state.status !== "ready") {
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state).eq({});
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); return;
if (state.payHandler.onClick === undefined) expect.fail(); }
state.payHandler.onClick(); expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
}, expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
], TestingContext) if (state.payHandler.onClick === undefined) expect.fail();
state.payHandler.onClick();
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
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 () => {
@ -357,40 +384,44 @@ describe("Payment CTA states", () => {
lastError: { code: 1 }, lastError: { code: 1 },
} as ConfirmPayResult); } as ConfirmPayResult);
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") expect.fail(); expect(error).undefined;
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); },
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); (state) => {
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); if (state.status !== "ready") expect.fail();
if (state.payHandler.onClick === undefined) expect.fail(); expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
state.payHandler.onClick(); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
}, // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
(state) => { if (state.payHandler.onClick === undefined) expect.fail();
if (state.status !== "ready") expect.fail(); state.payHandler.onClick();
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); },
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); (state) => {
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); if (state.status !== "ready") expect.fail();
expect(state.payHandler.onClick).undefined; expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
if (state.payHandler.error === undefined) expect.fail(); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
//FIXME: error message here is bad // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(state.payHandler.error.errorDetail.hint).eq( expect(state.payHandler.onClick).undefined;
"could not confirm payment", if (state.payHandler.error === undefined) expect.fail();
); //FIXME: error message here is bad
expect(state.payHandler.error.errorDetail.payResult).deep.equal({ expect(state.payHandler.error.errorDetail.hint).eq(
type: ConfirmPayResultType.Pending, "could not confirm payment",
lastError: { code: 1 }, );
}); expect(state.payHandler.error.errorDetail.payResult).deep.equal({
}, type: ConfirmPayResultType.Pending,
], TestingContext) lastError: { code: 1 },
});
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) 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 () => {
@ -455,30 +486,35 @@ describe("Payment CTA states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") expect.fail() expect(error).undefined;
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:10")); },
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); (state) => {
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); if (state.status !== "ready") expect.fail();
expect(state.payHandler.onClick).not.undefined; 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); handler.notifyEventFromWallet(NotificationType.CoinWithdrawn);
}, },
(state) => { (state) => {
if (state.status !== "ready") expect.fail() if (state.status !== "ready") expect.fail();
expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(state.payHandler.onClick).not.undefined; expect(state.payHandler.onClick).not.undefined;
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -19,10 +19,12 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useBackendContext } from "../../context/backend.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,
): State { onCancel,
const api = useBackendContext() onSuccess,
}: Props): State {
const api = useBackendContext();
if (!talerRecoveryUri) { if (!talerRecoveryUri) {
return { return {
status: "loading-uri", status: "loading-uri",

View File

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

View File

@ -21,10 +21,12 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState({
{ talerRefundUri, cancel, onSuccess }: Props, talerRefundUri,
): State { cancel,
const api = useBackendContext() onSuccess,
}: Props): State {
const api = useBackendContext();
const [ignored, setIgnored] = useState(false); const [ignored, setIgnored] = useState(false);
const info = useAsyncAsHook(async () => { const info = useAsyncAsHook(async () => {

View File

@ -22,12 +22,16 @@
import { import {
Amounts, Amounts,
NotificationType, NotificationType,
OrderShortInfo OrderShortInfo,
} 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 { tests } from "../../../../web-util/src/index.browser.js";
import { createWalletApiMock, mountHook, nullFunction } 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", () => {
@ -38,23 +42,28 @@ describe("Refund CTA states", () => {
talerRefundUri: undefined, talerRefundUri: undefined,
cancel: nullFunction, cancel: nullFunction,
onSuccess: nullFunction, onSuccess: nullFunction,
} };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
({ status, error }) => { expect(status).equals("loading");
expect(status).equals("loading-uri"); expect(error).undefined;
if (!error) expect.fail(); },
if (!error.hasError) expect.fail(); ({ status, error }) => {
if (error.operational) expect.fail(); expect(status).equals("loading-uri");
expect(error.message).eq("ERROR_NO-URI-FOR-REFUND"); if (!error) expect.fail();
}, if (!error.hasError) expect.fail();
], TestingContext) if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-REFUND");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -83,23 +92,28 @@ describe("Refund CTA states", () => {
} as OrderShortInfo, } as OrderShortInfo,
}); });
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") expect.fail(); expect(error).undefined;
if (state.error) expect.fail(); },
expect(state.accept.onClick).not.undefined; (state) => {
expect(state.ignore.onClick).not.undefined; if (state.status !== "ready") expect.fail();
expect(state.merchantName).eq("the merchant name"); if (state.error) expect.fail();
expect(state.orderId).eq("orderId1"); expect(state.accept.onClick).not.undefined;
expect(state.products).undefined; expect(state.ignore.onClick).not.undefined;
}, expect(state.merchantName).eq("the merchant name");
], TestingContext) expect(state.orderId).eq("orderId1");
expect(state.products).undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -132,30 +146,35 @@ describe("Refund CTA states", () => {
} as OrderShortInfo, } as OrderShortInfo,
}); });
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") expect.fail() expect(error).undefined;
if (state.error) expect.fail() },
expect(state.accept.onClick).not.undefined; (state) => {
expect(state.merchantName).eq("the merchant name"); if (state.status !== "ready") expect.fail();
expect(state.orderId).eq("orderId1"); if (state.error) expect.fail();
expect(state.products).undefined; 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(); if (state.ignore.onClick === undefined) expect.fail();
state.ignore.onClick(); state.ignore.onClick();
}, },
(state) => { (state) => {
if (state.status !== "ignored") expect.fail() if (state.status !== "ignored") expect.fail();
if (state.error) expect.fail() if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name"); expect(state.merchantName).eq("the merchant name");
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -220,42 +239,46 @@ describe("Refund CTA states", () => {
} as OrderShortInfo, } as OrderShortInfo,
}); });
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "in-progress") expect.fail() expect(error).undefined;
if (state.error) expect.fail(); },
expect(state.merchantName).eq("the merchant name"); (state) => {
expect(state.products).undefined; if (state.status !== "in-progress") expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")); if (state.error) expect.fail();
// expect(state.progress).closeTo(1 / 3, 0.01) 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); handler.notifyEventFromWallet(NotificationType.RefreshMelted);
}, },
(state) => { (state) => {
if (state.status !== "in-progress") expect.fail() if (state.status !== "in-progress") expect.fail();
if (state.error) expect.fail(); if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name"); expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined; expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(2 / 3, 0.01) // expect(state.progress).closeTo(2 / 3, 0.01)
handler.notifyEventFromWallet(NotificationType.RefreshMelted); handler.notifyEventFromWallet(NotificationType.RefreshMelted);
}, },
(state) => { (state) => {
if (state.status !== "ready") expect.fail() if (state.status !== "ready") expect.fail();
if (state.error) expect.fail(); if (state.error) expect.fail();
expect(state.merchantName).eq("the merchant name"); expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined; expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
},
],
TestingContext,
);
}, expect(hookBehavior).deep.equal({ result: "ok" });
], TestingContext)
expect(hookBehavior).deep.equal({ result: "ok" })
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

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

View File

@ -20,10 +20,12 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState({
{ talerTipUri, onCancel, onSuccess }: Props, talerTipUri,
): State { onCancel,
const api = useBackendContext() onSuccess,
}: Props): 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

@ -36,23 +36,28 @@ describe("Tip CTA states", () => {
talerTipUri: undefined, talerTipUri: undefined,
onCancel: nullFunction, onCancel: nullFunction,
onSuccess: nullFunction, onSuccess: nullFunction,
} };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
({ status, error }) => { expect(status).equals("loading");
expect(status).equals("loading-uri"); expect(error).undefined;
if (!error) expect.fail(); },
if (!error.hasError) expect.fail(); ({ status, error }) => {
if (error.operational) expect.fail(); expect(status).equals("loading-uri");
expect(error.message).eq("ERROR_NO-URI-FOR-TIP"); if (!error) expect.fail();
}, if (!error.hasError) expect.fail();
], TestingContext) if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-TIP");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -75,50 +80,58 @@ describe("Tip CTA states", () => {
talerTipUri: "taler://tip/asd", talerTipUri: "taler://tip/asd",
onCancel: nullFunction, onCancel: nullFunction,
onSuccess: nullFunction, onSuccess: nullFunction,
} };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") { expect(error).undefined;
expect(state).eq({ status: "ready" }); },
return; (state) => {
} if (state.status !== "ready") {
if (state.error) expect.fail(); expect(state).eq({ status: "ready" });
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); return;
expect(state.merchantBaseUrl).eq("merchant url"); }
expect(state.exchangeBaseUrl).eq("exchange url"); if (state.error) expect.fail();
if (state.accept.onClick === undefined) 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); handler.addWalletCallResponse(WalletApiOperation.AcceptTip);
state.accept.onClick(); state.accept.onClick();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(
accepted: true, WalletApiOperation.PrepareTip,
exchangeBaseUrl: "exchange url", undefined,
merchantBaseUrl: "merchant url", {
tipAmountEffective: "EUR:1", accepted: true,
walletTipId: "tip_id", exchangeBaseUrl: "exchange url",
expirationTimestamp: { merchantBaseUrl: "merchant url",
t_s: 1, tipAmountEffective: "EUR:1",
}, walletTipId: "tip_id",
tipAmountRaw: "", expirationTimestamp: {
}); t_s: 1,
},
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" });
(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");
}); });
@ -140,25 +153,30 @@ describe("Tip CTA states", () => {
talerTipUri: "taler://tip/asd", talerTipUri: "taler://tip/asd",
onCancel: nullFunction, onCancel: nullFunction,
onSuccess: nullFunction, onSuccess: nullFunction,
} };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "ready") expect.fail(); expect(error).undefined;
if (state.error) expect.fail(); },
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); (state) => {
expect(state.merchantBaseUrl).eq("merchant url"); if (state.status !== "ready") expect.fail();
expect(state.exchangeBaseUrl).eq("exchange url"); 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 //FIXME: add ignore button
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -181,24 +199,28 @@ describe("Tip CTA states", () => {
talerTipUri: "taler://tip/asd", talerTipUri: "taler://tip/asd",
onCancel: nullFunction, onCancel: nullFunction,
onSuccess: nullFunction, onSuccess: nullFunction,
} };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status, error }) => { useComponentState,
expect(status).equals("loading"); props,
expect(error).undefined; [
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
if (state.status !== "accepted") expect.fail(); expect(error).undefined;
if (state.error) expect.fail(); },
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); (state) => {
expect(state.merchantBaseUrl).eq("merchant url"); if (state.status !== "accepted") expect.fail();
expect(state.exchangeBaseUrl).eq("exchange url"); if (state.error) expect.fail();
}, expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
], TestingContext) expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -17,7 +17,7 @@
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 { isFuture, parse } from "date-fns"; import { isFuture, parse } from "date-fns";
@ -26,10 +26,12 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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,
): State { onClose,
const api = useBackendContext() onSuccess,
}: Props): 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 +126,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,7 +17,7 @@
import { import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
TalerErrorDetail TalerErrorDetail,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";

View File

@ -18,7 +18,7 @@ import {
AbsoluteTime, AbsoluteTime,
Amounts, Amounts,
TalerErrorDetail, TalerErrorDetail,
TalerProtocolTimestamp TalerProtocolTimestamp,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -26,10 +26,12 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState({
{ talerPayPushUri, onClose, onSuccess }: Props, talerPayPushUri,
): State { onClose,
const api = useBackendContext() onSuccess,
}: Props): 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

@ -22,7 +22,7 @@ import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { import {
useComponentStateFromParams, useComponentStateFromParams,
useComponentStateFromURI useComponentStateFromURI,
} from "./state.js"; } from "./state.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";

View File

@ -19,7 +19,7 @@ import {
AmountJson, AmountJson,
Amounts, Amounts,
ExchangeListItem, ExchangeListItem,
ExchangeTosStatus ExchangeTosStatus,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -29,10 +29,12 @@ import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js"; import { RecursiveState } from "../../utils/index.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,
): RecursiveState<State> { cancel,
const api = useBackendContext() onSuccess,
}: PropsFromParams): 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,
@ -87,10 +89,12 @@ export function useComponentStateFromParams(
); );
} }
export function useComponentStateFromURI( export function useComponentStateFromURI({
{ talerWithdrawUri, cancel, onSuccess }: PropsFromURI, talerWithdrawUri,
): RecursiveState<State> { cancel,
const api = useBackendContext() onSuccess,
}: PropsFromURI): RecursiveState<State> {
const api = useBackendContext();
/** /**
* Ask the wallet about the withdraw URI * Ask the wallet about the withdraw URI
*/ */
@ -175,7 +179,7 @@ function exchangeSelectionState(
exchangeList: ExchangeListItem[], exchangeList: ExchangeListItem[],
defaultExchange: string | undefined, defaultExchange: string | undefined,
): RecursiveState<State> { ): RecursiveState<State> {
const api = useBackendContext() const api = useBackendContext();
const selectedExchange = useSelectedExchange({ const selectedExchange = useSelectedExchange({
currency: chosenAmount.currency, currency: chosenAmount.currency,
defaultExchange, defaultExchange,
@ -276,10 +280,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

@ -64,7 +64,7 @@ const exchanges: ExchangeListItem[] = [
const nullFunction = async (): Promise<void> => { const nullFunction = async (): Promise<void> => {
null; 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 () => {
@ -76,20 +76,25 @@ describe("Withdraw CTA states", () => {
onSuccess: nullFunction, onSuccess: nullFunction,
}; };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentStateFromURI,
expect(status).equals("loading"); props,
}, [
({ status, error }) => { ({ status }) => {
if (status != "uri-error") expect.fail(); expect(status).equals("loading");
if (!error) expect.fail(); },
if (!error.hasError) expect.fail(); ({ status, error }) => {
if (error.operational) expect.fail(); if (status != "uri-error") expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL"); if (!error) expect.fail();
}, if (!error.hasError) expect.fail();
], TestingContext) if (error.operational) expect.fail();
expect(error.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -110,17 +115,22 @@ describe("Withdraw CTA states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentStateFromURI,
expect(status).equals("loading"); props,
}, [
({ status, error }) => { ({ status }) => {
expect(status).equals("no-exchange"); expect(status).equals("loading");
expect(error).undefined; },
}, ({ status, error }) => {
], TestingContext) expect(status).equals("no-exchange");
expect(error).undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -153,27 +163,32 @@ describe("Withdraw CTA states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentStateFromURI,
expect(status).equals("loading"); props,
}, [
({ status, error }) => { ({ status }) => {
expect(status).equals("loading"); expect(status).equals("loading");
expect(error).undefined; },
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
expect(state.status).equals("success"); expect(error).undefined;
if (state.status !== "success") return; },
(state) => {
expect(state.status).equals("success");
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined; expect(state.doWithdrawal.onClick).not.undefined;
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -221,39 +236,44 @@ describe("Withdraw CTA states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentStateFromURI, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentStateFromURI,
expect(status).equals("loading"); props,
}, [
({ status, error }) => { ({ status }) => {
expect(status).equals("loading"); expect(status).equals("loading");
expect(error).undefined; },
}, ({ status, error }) => {
(state) => { expect(status).equals("loading");
expect(state.status).equals("success"); expect(error).undefined;
if (state.status !== "success") return; },
(state) => {
expect(state.status).equals("success");
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).undefined; expect(state.doWithdrawal.onClick).undefined;
state.onTosUpdate(); state.onTosUpdate();
}, },
(state) => { (state) => {
expect(state.status).equals("success"); expect(state.status).equals("success");
if (state.status !== "success") return; if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined; expect(state.doWithdrawal.onClick).not.undefined;
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -25,9 +25,11 @@ export function useAutoOpenPermissions(): ToggleHandler {
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, api.background).catch((e) => { return handleAutoOpenPerm(enabled, setEnabled, api.background).catch(
setError(TalerError.fromException(e)); (e) => {
}); setError(TalerError.fromException(e));
},
);
}; };
useEffect(() => { useEffect(() => {

View File

@ -28,7 +28,7 @@ export function useBackupDeviceName(): BackupDeviceName {
name: "", name: "",
update: () => Promise.resolve(), update: () => Promise.resolve(),
}); });
const api = useBackendContext() const api = useBackendContext();
useEffect(() => { useEffect(() => {
async function run(): Promise<void> { async function run(): Promise<void> {

View File

@ -23,12 +23,14 @@ import { platform } from "../platform/api.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 api = useBackendContext();
const toggle = async (): Promise<void> => { const toggle = async (): Promise<void> => {
return handleClipboardPerm(enabled, setEnabled, api.background).catch((e) => { return handleClipboardPerm(enabled, setEnabled, api.background).catch(
setError(TalerError.fromException(e)); (e) => {
}); setError(TalerError.fromException(e));
},
);
}; };
useEffect(() => { useEffect(() => {

View File

@ -444,8 +444,8 @@ function registerTalerHeaderListener(
info: chrome.tabs.TabChangeInfo, info: chrome.tabs.TabChangeInfo,
): Promise<void> { ): Promise<void> {
if (tabId < 0) return; if (tabId < 0) return;
const tabLocationHasBeenUpdated = info.status === "complete" const tabLocationHasBeenUpdated = info.status === "complete";
const tabTitleHasBeenUpdated = info.title !== undefined const tabTitleHasBeenUpdated = info.title !== undefined;
if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) { if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) {
const uri = await findTalerUriInTab(tabId); const uri = await findTalerUriInTab(tabId);
if (!uri) return; if (!uri) return;
@ -543,26 +543,26 @@ function setAlertedIcon(): void {
interface OffscreenCanvasRenderingContext2D interface OffscreenCanvasRenderingContext2D
extends CanvasState, extends CanvasState,
CanvasTransform, CanvasTransform,
CanvasCompositing, CanvasCompositing,
CanvasImageSmoothing, CanvasImageSmoothing,
CanvasFillStrokeStyles, CanvasFillStrokeStyles,
CanvasShadowStyles, CanvasShadowStyles,
CanvasFilters, CanvasFilters,
CanvasRect, CanvasRect,
CanvasDrawPath, CanvasDrawPath,
CanvasUserInterface, CanvasUserInterface,
CanvasText, CanvasText,
CanvasDrawImage, CanvasDrawImage,
CanvasImageData, CanvasImageData,
CanvasPathDrawingStyles, CanvasPathDrawingStyles,
CanvasTextDrawingStyles, CanvasTextDrawingStyles,
CanvasPath { CanvasPath {
readonly canvas: OffscreenCanvas; readonly canvas: OffscreenCanvas;
} }
declare const OffscreenCanvasRenderingContext2D: { declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D; prototype: OffscreenCanvasRenderingContext2D;
new(): OffscreenCanvasRenderingContext2D; new (): OffscreenCanvasRenderingContext2D;
}; };
interface OffscreenCanvas extends EventTarget { interface OffscreenCanvas extends EventTarget {
@ -575,7 +575,7 @@ interface OffscreenCanvas extends EventTarget {
} }
declare const OffscreenCanvas: { declare const OffscreenCanvas: {
prototype: OffscreenCanvas; prototype: OffscreenCanvas;
new(width: number, height: number): OffscreenCanvas; new (width: number, height: number): OffscreenCanvas;
}; };
function createCanvas(size: number): OffscreenCanvas { function createCanvas(size: number): OffscreenCanvas {

View File

@ -34,18 +34,21 @@ setupI18n("en", { en: {} });
setupPlatform(chromeAPI); setupPlatform(chromeAPI);
describe("All the examples:", () => { describe("All the examples:", () => {
const cms = parseGroupImport({ popup, wallet, cta, mui, components }) const cms = parseGroupImport({ popup, wallet, cta, mui, components });
cms.forEach(group => { cms.forEach((group) => {
describe(`Example for group "${group.title}:"`, () => { describe(`Example for group "${group.title}:"`, () => {
group.list.forEach(component => { group.list.forEach((component) => {
describe(`Component ${component.name}:`, () => { describe(`Component ${component.name}:`, () => {
component.examples.forEach(example => { component.examples.forEach((example) => {
it(`should render example: ${example.name}`, () => { it(`should render example: ${example.name}`, () => {
renderNodeOrBrowser(example.render.component, example.render.props) renderNodeOrBrowser(
}) example.render.component,
}) example.render.props,
}) );
}) });
}) });
}) });
});
});
});
}); });

View File

@ -54,7 +54,7 @@ export function createExample<Props>(
return { return {
component: Render, component: Render,
props: evaluatedProps props: evaluatedProps,
}; };
} }
@ -74,7 +74,7 @@ export function createExampleWithCustomContext<Props, ContextProps>(
return { return {
component: WithContext, component: WithContext,
props: evaluatedProps props: evaluatedProps,
}; };
} }
@ -253,7 +253,7 @@ type Subscriptions = {
export function createWalletApiMock(): { export function createWalletApiMock(): {
handler: MockHandler; handler: MockHandler;
TestingContext: FunctionalComponent<{ children: ComponentChildren }> TestingContext: FunctionalComponent<{ children: ComponentChildren }>;
} { } {
const calls = new Array<CallRecord>(); const calls = new Array<CallRecord>();
const subscriptions: Subscriptions = {}; const subscriptions: Subscriptions = {};
@ -342,8 +342,8 @@ export function createWalletApiMock(): {
callback: cb callback: cb
? cb ? cb
: () => { : () => {
null; null;
}, },
}); });
return handler; return handler;
}, },
@ -358,13 +358,21 @@ export function createWalletApiMock(): {
}, },
}; };
function TestingContext({ children }: { children: ComponentChildren }): VNode { function TestingContext({
return create(BackendProvider, { children,
wallet: mock.wallet, }: {
background: mock.background, children: ComponentChildren;
listener: mock.listener, }): VNode {
return create(
BackendProvider,
{
wallet: mock.wallet,
background: mock.background,
listener: mock.listener,
children,
},
children, children,
}, children) );
} }
return { handler, TestingContext }; return { handler, TestingContext };

View File

@ -14,22 +14,21 @@
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 { import { TalerErrorDetail } from "@gnu-taler/taler-util";
TalerErrorDetail
} 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";
import { HookError } from "../../hooks/useAsyncAsHook.js"; 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 { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { import {
ConfirmProviderView, LoadingUriView, ConfirmProviderView,
SelectProviderView LoadingUriView,
SelectProviderView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {

View File

@ -17,11 +17,11 @@
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 { useBackendContext } from "../../context/backend.js";
@ -106,47 +106,50 @@ 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],
); );
return state; return state;
} }
export function useComponentState( export function useComponentState({
{ currency, onBack, onComplete, onPaymentRequired }: Props, currency,
): State { onBack,
const api = useBackendContext() onComplete,
onPaymentRequired,
}: Props): 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 +226,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

@ -21,9 +21,7 @@
import { expect } from "chai"; import { expect } from "chai";
import { tests } from "../../../../web-util/src/index.browser.js"; import { tests } from "../../../../web-util/src/index.browser.js";
import { import { createWalletApiMock, nullFunction } from "../../test-utils.js";
createWalletApiMock, nullFunction
} from "../../test-utils.js";
import { Props } from "./index.js"; import { Props } from "./index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
@ -34,21 +32,24 @@ 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, TestingContext } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
(state) => { useComponentState,
expect(state.status).equal("select-provider"); props,
if (state.status !== "select-provider") return; [
expect(state.name.value).eq(""); (state) => {
expect(state.url.value).eq(""); expect(state.status).equal("select-provider");
}, if (state.status !== "select-provider") return;
], TestingContext) expect(state.name.value).eq("");
expect(state.url.value).eq("");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

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

View File

@ -21,7 +21,7 @@ import {
KnownBankAccountsInfo, KnownBankAccountsInfo,
parsePaytoUri, parsePaytoUri,
PaytoUri, PaytoUri,
stringifyPaytoUri stringifyPaytoUri,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -29,10 +29,13 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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,
): State { currency: currencyStr,
const api = useBackendContext() onCancel,
onSuccess,
}: Props): 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 +58,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>();
@ -162,7 +165,11 @@ 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.wallet); const result = await getFeeForAmount(
currentAccount,
newAmount,
api.wallet,
);
setAmount(newAmount); setAmount(newAmount);
setFee(result); setFee(result);
} catch (e) { } catch (e) {
@ -185,8 +192,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

View File

@ -23,14 +23,12 @@ 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 { tests } from "../../../../web-util/src/index.browser.js";
import { import { createWalletApiMock, nullFunction } from "../../test-utils.js";
createWalletApiMock, nullFunction
} from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
@ -71,16 +69,21 @@ describe("DepositPage states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equal("loading"); props,
}, [
({ status }) => { ({ status }) => {
expect(status).equal("no-enough-balance"); expect(status).equal("loading");
}, },
], TestingContext) ({ status }) => {
expect(status).equal("no-enough-balance");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -107,16 +110,21 @@ describe("DepositPage states", () => {
}, },
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equal("loading"); props,
}, [
({ status }) => { ({ status }) => {
expect(status).equal("no-accounts"); expect(status).equal("loading");
}, },
], TestingContext) ({ status }) => {
expect(status).equal("no-accounts");
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -161,24 +169,29 @@ describe("DepositPage states", () => {
withoutFee(), withoutFee(),
); );
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equal("loading"); props,
}, [
({ status }) => { ({ status }) => {
expect(status).equal("loading"); expect(status).equal("loading");
}, },
(state) => { ({ status }) => {
if (state.status !== "ready") expect.fail(); expect(status).equal("loading");
expect(state.cancelHandler.onClick).not.undefined; },
expect(state.currency).eq(currency); (state) => {
expect(state.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); if (state.status !== "ready") expect.fail();
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(state.cancelHandler.onClick).not.undefined;
expect(state.depositHandler.onClick).undefined; expect(state.currency).eq(currency);
}, expect(state.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
], TestingContext) expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.depositHandler.onClick).undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -218,37 +231,42 @@ describe("DepositPage states", () => {
const accountSelected = stringifyPaytoUri(ibanPayto.uri); const accountSelected = stringifyPaytoUri(ibanPayto.uri);
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equal("loading"); props,
}, [
({ status }) => { ({ status }) => {
expect(status).equal("loading"); expect(status).equal("loading");
}, },
(state) => { ({ status }) => {
if (state.status !== "ready") expect.fail(); expect(status).equal("loading");
expect(state.cancelHandler.onClick).not.undefined; },
expect(state.currency).eq(currency); (state) => {
expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); if (state.status !== "ready") expect.fail();
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(state.cancelHandler.onClick).not.undefined;
expect(state.depositHandler.onClick).undefined; expect(state.currency).eq(currency);
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
expect(state.account.onChange).not.undefined; 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;
state.account.onChange!(accountSelected); state.account.onChange!(accountSelected);
}, },
(state) => { (state) => {
if (state.status !== "ready") expect.fail(); if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined; expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency); expect(state.currency).eq(currency);
expect(state.account.value).eq(accountSelected); expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(state.depositHandler.onClick).undefined; expect(state.depositHandler.onClick).undefined;
}, },
], TestingContext) ],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
@ -292,52 +310,58 @@ describe("DepositPage states", () => {
const accountSelected = stringifyPaytoUri(ibanPayto.uri); const accountSelected = stringifyPaytoUri(ibanPayto.uri);
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equal("loading"); props,
}, [
({ status }) => { ({ status }) => {
expect(status).equal("loading"); expect(status).equal("loading");
}, },
(state) => { ({ status }) => {
if (state.status !== "ready") expect.fail(); expect(status).equal("loading");
expect(state.cancelHandler.onClick).not.undefined; },
expect(state.currency).eq(currency); (state) => {
expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); if (state.status !== "ready") expect.fail();
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(state.cancelHandler.onClick).not.undefined;
expect(state.depositHandler.onClick).undefined; expect(state.currency).eq(currency);
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
expect(state.account.onChange).not.undefined; 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;
state.account.onChange!(accountSelected); state.account.onChange!(accountSelected);
}, },
(state) => { (state) => {
if (state.status !== "ready") expect.fail(); if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined; expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency); expect(state.currency).eq(currency);
expect(state.account.value).eq(accountSelected); expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(state.depositHandler.onClick).undefined; expect(state.depositHandler.onClick).undefined;
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(state.amount.onInput).not.undefined; expect(state.amount.onInput).not.undefined;
if (!state.amount.onInput) return; if (!state.amount.onInput) return;
state.amount.onInput(Amounts.parseOrThrow("EUR:10")); state.amount.onInput(Amounts.parseOrThrow("EUR:10"));
}, },
(state) => { (state) => {
if (state.status !== "ready") expect.fail(); if (state.status !== "ready") expect.fail();
expect(state.cancelHandler.onClick).not.undefined; expect(state.cancelHandler.onClick).not.undefined;
expect(state.currency).eq(currency); expect(state.currency).eq(currency);
expect(state.account.value).eq(accountSelected); expect(state.account.value).eq(accountSelected);
expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(state.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); expect(state.totalToDeposit).deep.eq(
expect(state.depositHandler.onClick).not.undefined; Amounts.parseOrThrow(`${currency}:7`),
}, );
], TestingContext) expect(state.depositHandler.onClick).not.undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

@ -22,10 +22,8 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { assertUnreachable, RecursiveState } from "../../utils/index.js"; import { assertUnreachable, RecursiveState } from "../../utils/index.js";
import { Contact, Props, State } from "./index.js"; import { Contact, Props, State } from "./index.js";
export function useComponentState( export function useComponentState(props: Props): RecursiveState<State> {
props: Props, const api = useBackendContext();
): RecursiveState<State> {
const api = useBackendContext()
const parsedInitialAmount = !props.amount const parsedInitialAmount = !props.amount
? undefined ? undefined
: Amounts.parse(props.amount); : Amounts.parse(props.amount);
@ -41,22 +39,22 @@ export function useComponentState(
const previous: Contact[] = true const previous: Contact[] = true
? [] ? []
: [ : [
{ {
name: "International Bank", name: "International Bank",
icon_type: 'bank', icon_type: "bank",
description: "account ending with 3454", description: "account ending with 3454",
}, },
{ {
name: "Max", name: "Max",
icon_type: 'bank', icon_type: "bank",
description: "account ending with 3454", description: "account ending with 3454",
}, },
{ {
name: "Alex", name: "Alex",
icon_type: 'bank', icon_type: "bank",
description: "account ending with 3454", description: "account ending with 3454",
}, },
]; ];
if (!amount) { if (!amount) {
return () => { return () => {
@ -114,15 +112,15 @@ export function useComponentState(
onClick: invalid onClick: invalid
? undefined ? undefined
: async () => { : async () => {
props.goToWalletBankDeposit(currencyAndAmount); props.goToWalletBankDeposit(currencyAndAmount);
}, },
}, },
goToWallet: { goToWallet: {
onClick: invalid onClick: invalid
? undefined ? undefined
: async () => { : async () => {
props.goToWalletWalletSend(currencyAndAmount); props.goToWalletWalletSend(currencyAndAmount);
}, },
}, },
amountHandler: { amountHandler: {
onInput: async (s) => setAmount(s), onInput: async (s) => setAmount(s),
@ -144,15 +142,15 @@ export function useComponentState(
onClick: invalid onClick: invalid
? undefined ? undefined
: async () => { : async () => {
props.goToWalletManualWithdraw(currencyAndAmount); props.goToWalletManualWithdraw(currencyAndAmount);
}, },
}, },
goToWallet: { goToWallet: {
onClick: invalid onClick: invalid
? undefined ? undefined
: async () => { : async () => {
props.goToWalletWalletInvoice(currencyAndAmount); props.goToWalletWalletInvoice(currencyAndAmount);
}, },
}, },
amountHandler: { amountHandler: {
onInput: async (s) => setAmount(s), onInput: async (s) => setAmount(s),

View File

@ -23,7 +23,7 @@ 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";
@ -59,33 +59,39 @@ describe("Destination selection states", () => {
goToWalletWalletInvoice: nullFunction, goToWalletWalletInvoice: nullFunction,
}; };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
({ status }) => { useComponentState,
expect(status).equal("loading"); props,
}, [
(state) => { ({ status }) => {
if (state.status !== "select-currency") expect.fail(); expect(status).equal("loading");
if (state.error) expect.fail(); },
expect(state.currencies).deep.eq({ (state) => {
ARS: "ARS", if (state.status !== "select-currency") expect.fail();
"": "Select a currency", if (state.error) expect.fail();
}); expect(state.currencies).deep.eq({
ARS: "ARS",
"": "Select a currency",
});
state.onCurrencySelected(exchangeArs.currency!); state.onCurrencySelected(exchangeArs.currency!);
}, },
(state) => { (state) => {
if (state.status !== "ready") expect.fail(); if (state.status !== "ready") expect.fail();
if (state.error) expect.fail(); if (state.error) expect.fail();
expect(state.goToBank.onClick).eq(undefined); expect(state.goToBank.onClick).eq(undefined);
expect(state.goToWallet.onClick).eq(undefined); expect(state.goToWallet.onClick).eq(undefined);
expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:0")); expect(state.amountHandler.value).deep.eq(
}, Amounts.parseOrThrow("ARS:0"),
], TestingContext) );
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
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 () => {
@ -98,22 +104,28 @@ describe("Destination selection states", () => {
amount: "ARS:2", amount: "ARS:2",
}; };
const hookBehavior = await tests.hookBehaveLikeThis(useComponentState, props, [ const hookBehavior = await tests.hookBehaveLikeThis(
// ({ status }) => { useComponentState,
// expect(status).equal("loading"); props,
// }, [
(state) => { // ({ status }) => {
if (state.status !== "ready") expect.fail(); // expect(status).equal("loading");
if (state.error) expect.fail(); // },
expect(state.goToBank.onClick).not.eq(undefined); (state) => {
expect(state.goToWallet.onClick).not.eq(undefined); 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);
expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:2")); expect(state.amountHandler.value).deep.eq(
}, Amounts.parseOrThrow("ARS:2"),
], TestingContext) );
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" }) expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty"); expect(handler.getCallingQueueState()).eq("empty");
}); });
}); });

View File

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

View File

@ -17,17 +17,20 @@
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 { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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,
): State { onSelection,
const api = useBackendContext() list: exchanges,
currentExchange,
}: Props): State {
const api = useBackendContext();
const initialValue = exchanges.findIndex( const initialValue = exchanges.findIndex(
(e) => e.exchangeBaseUrl === currentExchange, (e) => e.exchangeBaseUrl === currentExchange,
); );
@ -52,14 +55,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

@ -20,7 +20,7 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
ButtonHandler, ButtonHandler,
SelectFieldHandler, SelectFieldHandler,
TextFieldHandler TextFieldHandler,
} from "../../mui/handlers.js"; } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";

View File

@ -17,7 +17,7 @@
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";
@ -25,10 +25,12 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.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,
): State { onAccountAdded,
const api = useBackendContext() onCancel,
}: Props): State {
const api = useBackendContext();
const hook = useAsyncAsHook(() => const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }), api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
); );

View File

@ -21,7 +21,7 @@ import { compose, StateViewMap } from "../../utils/index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, ReadyView } from "./views.js"; import { LoadingUriView, ReadyView } from "./views.js";
export type Props = object export type Props = object;
export type State = State.Loading | State.LoadingUriError | State.Ready; export type State = State.Loading | State.LoadingUriError | State.Ready;

View File

@ -20,7 +20,7 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState(p: Props): State { export function useComponentState(p: Props): State {
const api = useBackendContext() 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

@ -156,8 +156,8 @@ export type WxApiType = {
background: BackgroundApiClient; background: BackgroundApiClient;
listener: { listener: {
onUpdateNotification: typeof onUpdateNotification; onUpdateNotification: typeof onUpdateNotification;
} };
} };
export const wxApi = { export const wxApi = {
wallet: new WxWalletCoreApiClient(), wallet: new WxWalletCoreApiClient(),