This commit is contained in:
Sebastian 2022-09-16 14:29:35 -03:00
parent 860f10e6f0
commit 6ddb2de842
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
43 changed files with 1015 additions and 736 deletions

View File

@ -23,11 +23,9 @@ import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { CompletedView, LoadingUriView, ReadyView } from "./views.js"; import { CompletedView, LoadingUriView, ReadyView } from "./views.js";
export interface Props { export interface Props {
talerDepositUri: string | undefined, talerDepositUri: string | undefined;
amountStr: AmountString | undefined, amountStr: AmountString | undefined;
cancel: () => Promise<void>; cancel: () => Promise<void>;
} }
@ -38,7 +36,6 @@ export type State =
| State.Completed; | State.Completed;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -63,10 +60,14 @@ export namespace State {
} }
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
"loading": Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
completed: CompletedView, completed: CompletedView,
ready: ReadyView, ready: ReadyView,
}; };
export const DepositPage = compose("Deposit", (p: Props) => useComponentState(p, wxApi), viewMapping) export const DepositPage = compose(
"Deposit",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, CreateDepositGroupResponse } from "@gnu-taler/taler-util"; import { Amounts, CreateDepositGroupResponse } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -41,7 +40,7 @@ export function useComponentState(
return { deposit, uri: talerDepositUri, amount }; return { deposit, uri: talerDepositUri, amount };
}); });
if (!info) return { status: "loading", error: undefined } if (!info) return { status: "loading", error: undefined };
if (info.hasError) { if (info.hasError) {
return { return {
status: "loading-uri", status: "loading-uri",
@ -74,4 +73,4 @@ export function useComponentState(
effective: deposit.effectiveDepositAmount, effective: deposit.effectiveDepositAmount,
cancel, cancel,
}; };
} }

View File

@ -19,9 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { import { Amounts, PrepareDepositResponse } from "@gnu-taler/taler-util";
Amounts, PrepareDepositResponse
} from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../../test-utils.js"; import { mountHook } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
@ -30,11 +28,20 @@ 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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerDepositUri: undefined, amountStr: undefined, cancel: async () => { null } }, { useComponentState(
prepareRefund: async () => ({}), {
applyRefund: async () => ({}), talerDepositUri: undefined,
onUpdateNotification: async () => ({}), amountStr: undefined,
} as any), cancel: async () => {
null;
},
},
{
prepareRefund: async () => ({}),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({}),
} as any,
),
); );
{ {
@ -61,14 +68,23 @@ describe("Deposit CTA states", () => {
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerDepositUri: "payto://refund/asdasdas", amountStr: "EUR:1", cancel: async () => { null } }, { useComponentState(
prepareDeposit: async () => {
({ talerDepositUri: "payto://refund/asdasdas",
effectiveDepositAmount: Amounts.parseOrThrow("EUR:1"), amountStr: "EUR:1",
totalDepositCost: Amounts.parseOrThrow("EUR:1.2"), cancel: async () => {
} as PrepareDepositResponse as any), null;
createDepositGroup: async () => ({}), },
} as any), },
{
prepareDeposit: async () =>
({
effectiveDepositAmount: Amounts.parseOrThrow("EUR:1"),
totalDepositCost: Amounts.parseOrThrow("EUR:1.2"),
} as PrepareDepositResponse as any),
createDepositGroup: async () => ({}),
} as any,
),
); );
{ {

View File

@ -35,7 +35,6 @@ export type State =
| State.Ready; | State.Ready;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -59,8 +58,8 @@ export namespace State {
status: "ready"; status: "ready";
create: ButtonHandler; create: ButtonHandler;
subject: TextFieldHandler; subject: TextFieldHandler;
toBeReceived: AmountJson, toBeReceived: AmountJson;
chosenAmount: AmountJson, chosenAmount: AmountJson;
exchangeUrl: string; exchangeUrl: string;
invalid: boolean; invalid: boolean;
error: undefined; error: undefined;
@ -71,10 +70,12 @@ export namespace State {
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"created": CreatedView, created: CreatedView,
"ready": ReadyView, ready: ReadyView,
}; };
export const InvoiceCreatePage = compose(
export const InvoiceCreatePage = compose("InvoiceCreatePage", (p: Props) => useComponentState(p, wxApi), viewMapping) "InvoiceCreatePage",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -25,21 +25,22 @@ export function useComponentState(
{ amount: amountStr, onClose }: Props, { amount: amountStr, onClose }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const amount = Amounts.parseOrThrow(amountStr) const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState("");
const [talerUri, setTalerUri] = useState("") const [talerUri, setTalerUri] = useState("");
const hook = useAsyncAsHook(api.listExchanges); const hook = useAsyncAsHook(api.listExchanges);
const [exchangeIdx, setExchangeIdx] = useState("0") const [exchangeIdx, setExchangeIdx] = useState("0");
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
error: undefined, error: undefined,
} };
} }
if (hook.hasError) { if (hook.hasError) {
return { return {
@ -54,62 +55,65 @@ export function useComponentState(
talerUri, talerUri,
error: undefined, error: undefined,
cancel: { cancel: {
onClick: onClose onClick: onClose,
}, },
copyToClipboard: { copyToClipboard: {
onClick: async () => { onClick: async () => {
navigator.clipboard.writeText(talerUri); navigator.clipboard.writeText(talerUri);
} },
}, },
} };
} }
const exchanges = hook.response.exchanges.filter(e => e.currency === amount.currency); const exchanges = hook.response.exchanges.filter(
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }), {} as Record<string, string>) (e) => e.currency === amount.currency,
);
const exchangeMap = exchanges.reduce(
(prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }),
{} as Record<string, string>,
);
const selected = exchanges[Number(exchangeIdx)]; const selected = exchanges[Number(exchangeIdx)];
async function accept(): Promise<string> { async function accept(): Promise<string> {
try { try {
const resp = await api.initiatePeerPullPayment({ const resp = await api.initiatePeerPullPayment({
amount: Amounts.stringify(amount), amount: Amounts.stringify(amount),
exchangeBaseUrl: selected.exchangeBaseUrl, exchangeBaseUrl: selected.exchangeBaseUrl,
partialContractTerms: { partialContractTerms: {
summary: subject summary: subject,
} },
}) });
return resp.talerUri return resp.talerUri;
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
setOperationError(e.errorDetail) setOperationError(e.errorDetail);
} }
console.error(e) console.error(e);
throw Error("error trying to accept") throw Error("error trying to accept");
} }
} }
return { return {
status: "ready", status: "ready",
subject: { subject: {
error: !subject ? "cant be empty" : undefined, error: !subject ? "cant be empty" : undefined,
value: subject, value: subject,
onInput: async (e) => setSubject(e) onInput: async (e) => setSubject(e),
}, },
invalid: !subject || Amounts.isZero(amount), invalid: !subject || Amounts.isZero(amount),
exchangeUrl: selected.exchangeBaseUrl, exchangeUrl: selected.exchangeBaseUrl,
create: { create: {
onClick: async () => { onClick: async () => {
const uri = await accept(); const uri = await accept();
setTalerUri(uri) setTalerUri(uri);
} },
}, },
cancel: { cancel: {
onClick: onClose onClick: onClose,
}, },
chosenAmount: amount, chosenAmount: amount,
toBeReceived: amount, toBeReceived: amount,
error: undefined, error: undefined,
operationError operationError,
} };
} }

View File

@ -22,10 +22,7 @@
import { expect } from "chai"; import { expect } from "chai";
describe("test description", () => { describe("test description", () => {
it("should assert", () => { it("should assert", () => {
expect([]).deep.equals([]);
expect([]).deep.equals([])
}); });
}) });

View File

@ -14,7 +14,12 @@
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 { AbsoluteTime, AmountJson, PreparePayResult, TalerErrorDetail } from "@gnu-taler/taler-util"; import {
AbsoluteTime,
AmountJson,
PreparePayResult,
TalerErrorDetail,
} 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";
@ -37,7 +42,6 @@ export type State =
| State.Ready; | State.Ready;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -52,20 +56,20 @@ export namespace State {
error: undefined; error: undefined;
uri: string; uri: string;
cancel: ButtonHandler; cancel: ButtonHandler;
amount: AmountJson, amount: AmountJson;
goToWalletManualWithdraw: (currency: string) => Promise<void>; goToWalletManualWithdraw: (currency: string) => Promise<void>;
summary: string | undefined, summary: string | undefined;
expiration: AbsoluteTime | undefined, expiration: AbsoluteTime | undefined;
operationError?: TalerErrorDetail; operationError?: TalerErrorDetail;
payStatus: PreparePayResult; payStatus: PreparePayResult;
} }
export interface NoBalanceForCurrency extends BaseInfo { export interface NoBalanceForCurrency extends BaseInfo {
status: "no-balance-for-currency" status: "no-balance-for-currency";
balance: undefined; balance: undefined;
} }
export interface NoEnoughBalance extends BaseInfo { export interface NoEnoughBalance extends BaseInfo {
status: "no-enough-balance" status: "no-enough-balance";
balance: AmountJson; balance: AmountJson;
} }
@ -82,9 +86,11 @@ const viewMapping: StateViewMap<State> = {
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"no-balance-for-currency": ReadyView, "no-balance-for-currency": ReadyView,
"no-enough-balance": ReadyView, "no-enough-balance": ReadyView,
"ready": ReadyView, ready: ReadyView,
}; };
export const InvoicePayPage = compose(
export const InvoicePayPage = compose("InvoicePayPage", (p: Props) => useComponentState(p, wxApi), viewMapping) "InvoicePayPage",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,15 @@
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 { AbsoluteTime, Amounts, NotificationType, PreparePayResult, PreparePayResultType, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import {
AbsoluteTime,
Amounts,
NotificationType,
PreparePayResult,
PreparePayResultType,
TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
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 { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -27,11 +35,11 @@ export function useComponentState(
): State { ): State {
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const p2p = await api.checkPeerPullPayment({ const p2p = await api.checkPeerPullPayment({
talerUri: talerPayPullUri talerUri: talerPayPullUri,
}) });
const balance = await api.getBalance(); const balance = await api.getBalance();
return { p2p, balance } return { p2p, balance };
}) });
useEffect(() => { useEffect(() => {
api.onUpdateNotification([NotificationType.CoinWithdrawn], () => { api.onUpdateNotification([NotificationType.CoinWithdrawn], () => {
@ -39,13 +47,15 @@ export function useComponentState(
}); });
}); });
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
error: undefined, error: undefined,
} };
} }
if (hook.hasError) { if (hook.hasError) {
return { return {
@ -56,13 +66,17 @@ export function useComponentState(
// const { payStatus } = hook.response.p2p; // const { payStatus } = hook.response.p2p;
const { amount: purseAmount, contractTerms, peerPullPaymentIncomingId } = hook.response.p2p const {
amount: purseAmount,
contractTerms,
peerPullPaymentIncomingId,
} = hook.response.p2p;
const amountStr: string = contractTerms?.amount;
const amountStr: string = contractTerms?.amount const amount = Amounts.parseOrThrow(amountStr);
const amount = Amounts.parseOrThrow(amountStr) const summary: string | undefined = contractTerms?.summary;
const summary: string | undefined = contractTerms?.summary const expiration: TalerProtocolTimestamp | undefined =
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration contractTerms?.purse_expiration;
const foundBalance = hook.response.balance.balances.find( const foundBalance = hook.response.balance.balances.find(
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency, (b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
@ -71,35 +85,32 @@ export function useComponentState(
const paymentPossible: PreparePayResult = { const paymentPossible: PreparePayResult = {
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
proposalId: "fakeID", proposalId: "fakeID",
contractTerms: { contractTerms: {} as any,
} as any,
contractTermsHash: "asd", contractTermsHash: "asd",
amountRaw: hook.response.p2p.amount, amountRaw: hook.response.p2p.amount,
amountEffective: hook.response.p2p.amount, amountEffective: hook.response.p2p.amount,
noncePriv: "", noncePriv: "",
} as PreparePayResult } as PreparePayResult;
const insufficientBalance: PreparePayResult = { const insufficientBalance: PreparePayResult = {
status: PreparePayResultType.InsufficientBalance, status: PreparePayResultType.InsufficientBalance,
proposalId: "fakeID", proposalId: "fakeID",
contractTerms: { contractTerms: {} as any,
} as any,
amountRaw: hook.response.p2p.amount, amountRaw: hook.response.p2p.amount,
noncePriv: "", noncePriv: "",
} };
const baseResult = { const baseResult = {
uri: talerPayPullUri, uri: talerPayPullUri,
cancel: { cancel: {
onClick: onClose onClick: onClose,
}, },
amount, amount,
goToWalletManualWithdraw, goToWalletManualWithdraw,
summary, summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined, expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
operationError, operationError,
} };
if (!foundBalance) { if (!foundBalance) {
return { return {
@ -108,20 +119,21 @@ export function useComponentState(
balance: undefined, balance: undefined,
...baseResult, ...baseResult,
payStatus: insufficientBalance, payStatus: insufficientBalance,
} };
} }
const foundAmount = Amounts.parseOrThrow(foundBalance.available); const foundAmount = Amounts.parseOrThrow(foundBalance.available);
//FIXME: should use pay result type since it check for coins exceptions //FIXME: should use pay result type since it check for coins exceptions
if (Amounts.cmp(foundAmount, amount) < 0) { //payStatus.status === PreparePayResultType.InsufficientBalance) { if (Amounts.cmp(foundAmount, amount) < 0) {
//payStatus.status === PreparePayResultType.InsufficientBalance) {
return { return {
status: 'no-enough-balance', status: "no-enough-balance",
error: undefined, error: undefined,
balance: foundAmount, balance: foundAmount,
...baseResult, ...baseResult,
payStatus: insufficientBalance, payStatus: insufficientBalance,
} };
} }
// if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { // if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
@ -135,19 +147,18 @@ export function useComponentState(
async function accept(): Promise<void> { async function accept(): Promise<void> {
try { try {
const resp = await api.acceptPeerPullPayment({ const resp = await api.acceptPeerPullPayment({
peerPullPaymentIncomingId peerPullPaymentIncomingId,
}) });
await onClose() await onClose();
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
setOperationError(e.errorDetail) setOperationError(e.errorDetail);
} }
console.error(e) console.error(e);
throw Error("error trying to accept") throw Error("error trying to accept");
} }
} }
return { return {
status: "ready", status: "ready",
error: undefined, error: undefined,
@ -155,7 +166,7 @@ export function useComponentState(
payStatus: paymentPossible, payStatus: paymentPossible,
balance: foundAmount, balance: foundAmount,
accept: { accept: {
onClick: accept onClick: accept,
}, },
} };
} }

View File

@ -22,10 +22,7 @@
import { expect } from "chai"; import { expect } from "chai";
describe("test description", () => { describe("test description", () => {
it("should assert", () => { it("should assert", () => {
expect([]).deep.equals([]);
expect([]).deep.equals([])
}); });
}) });

View File

@ -14,7 +14,14 @@
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 { AmountJson, ConfirmPayResult, PreparePayResult, PreparePayResultAlreadyConfirmed, PreparePayResultInsufficientBalance, PreparePayResultPaymentPossible } from "@gnu-taler/taler-util"; import {
AmountJson,
ConfirmPayResult,
PreparePayResult,
PreparePayResultAlreadyConfirmed,
PreparePayResultInsufficientBalance,
PreparePayResultPaymentPossible,
} from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } 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";
@ -24,8 +31,6 @@ import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { LoadingUriView, BaseView } from "./views.js"; import { LoadingUriView, BaseView } from "./views.js";
export interface Props { export interface Props {
talerPayUri?: string; talerPayUri?: string;
goToWalletManualWithdraw: (amount?: string) => Promise<void>; goToWalletManualWithdraw: (amount?: string) => Promise<void>;
@ -42,7 +47,6 @@ export type State =
| State.Confirmed; | State.Confirmed;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -60,12 +64,12 @@ export namespace State {
cancel: () => Promise<void>; cancel: () => Promise<void>;
} }
export interface NoBalanceForCurrency extends BaseInfo { export interface NoBalanceForCurrency extends BaseInfo {
status: "no-balance-for-currency" status: "no-balance-for-currency";
payStatus: PreparePayResult; payStatus: PreparePayResult;
balance: undefined; balance: undefined;
} }
export interface NoEnoughBalance extends BaseInfo { export interface NoEnoughBalance extends BaseInfo {
status: "no-enough-balance" status: "no-enough-balance";
payStatus: PreparePayResult; payStatus: PreparePayResult;
balance: AmountJson; balance: AmountJson;
} }
@ -101,4 +105,8 @@ const viewMapping: StateViewMap<State> = {
ready: BaseView, ready: BaseView,
}; };
export const PaymentPage = compose("Payment", (p: Props) => useComponentState(p, wxApi), viewMapping) export const PaymentPage = compose(
"Payment",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,8 +14,15 @@
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 { AmountJson, Amounts, ConfirmPayResult, ConfirmPayResultType, NotificationType, PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util"; AmountJson,
Amounts,
ConfirmPayResult,
ConfirmPayResultType,
NotificationType,
PreparePayResultType,
TalerErrorCode,
} from "@gnu-taler/taler-util";
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 { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -82,8 +89,9 @@ export function useComponentState(
uri: hook.response.uri, uri: hook.response.uri,
amount, amount,
error: undefined, error: undefined,
cancel, goToWalletManualWithdraw cancel,
} goToWalletManualWithdraw,
};
if (!foundBalance) { if (!foundBalance) {
return { return {
@ -91,7 +99,7 @@ export function useComponentState(
balance: undefined, balance: undefined,
payStatus, payStatus,
...baseResult, ...baseResult,
} };
} }
const foundAmount = Amounts.parseOrThrow(foundBalance.available); const foundAmount = Amounts.parseOrThrow(foundBalance.available);
@ -109,11 +117,11 @@ export function useComponentState(
if (payStatus.status === PreparePayResultType.InsufficientBalance) { if (payStatus.status === PreparePayResultType.InsufficientBalance) {
return { return {
status: 'no-enough-balance', status: "no-enough-balance",
balance: foundAmount, balance: foundAmount,
payStatus, payStatus,
...baseResult, ...baseResult,
} };
} }
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
@ -125,7 +133,6 @@ export function useComponentState(
}; };
} }
async function doPayment(): Promise<void> { async function doPayment(): Promise<void> {
try { try {
if (payStatus.status !== "payment-possible") { if (payStatus.status !== "payment-possible") {
@ -169,8 +176,6 @@ export function useComponentState(
payHandler, payHandler,
payStatus, payStatus,
...baseResult, ...baseResult,
balance: foundAmount balance: foundAmount,
}; };
} }

View File

@ -70,9 +70,16 @@ 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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: undefined, cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
} as Partial<typeof wxApi> as any), talerPayUri: undefined,
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -98,18 +105,25 @@ describe("Payment CTA states", () => {
it("should response with no balance", async () => { it("should response with no balance", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:10", goToWalletManualWithdraw: nullFunction,
status: PreparePayResultType.InsufficientBalance, },
} as Partial<PreparePayResult>), {
getBalance: async () => onUpdateNotification: nullFunction,
({ preparePay: async () =>
balances: [], ({
} as Partial<BalancesResponse>), amountRaw: "USD:10",
} as Partial<typeof wxApi> as any), status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -133,22 +147,29 @@ describe("Payment CTA states", () => {
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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:10", goToWalletManualWithdraw: nullFunction,
status: PreparePayResultType.InsufficientBalance, },
} as Partial<PreparePayResult>), {
getBalance: async () => onUpdateNotification: nullFunction,
({ preparePay: async () =>
balances: [ ({
{ amountRaw: "USD:10",
available: "USD:5", status: PreparePayResultType.InsufficientBalance,
}, } as Partial<PreparePayResult>),
], getBalance: async () =>
} as Partial<BalancesResponse>), ({
} as Partial<typeof wxApi> as any), balances: [
{
available: "USD:5",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -172,23 +193,30 @@ describe("Payment CTA states", () => {
it("should be able to pay (without fee)", async () => { it("should be able to pay (without fee)", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:10", goToWalletManualWithdraw: nullFunction,
amountEffective: "USD:10", },
status: PreparePayResultType.PaymentPossible, {
} as Partial<PreparePayResult>), onUpdateNotification: nullFunction,
getBalance: async () => preparePay: async () =>
({ ({
balances: [ amountRaw: "USD:10",
{ amountEffective: "USD:10",
available: "USD:15", status: PreparePayResultType.PaymentPossible,
}, } as Partial<PreparePayResult>),
], getBalance: async () =>
} as Partial<BalancesResponse>), ({
} as Partial<typeof wxApi> as any), balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -214,23 +242,30 @@ describe("Payment CTA states", () => {
it("should be able to pay (with fee)", async () => { it("should be able to pay (with fee)", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:9", goToWalletManualWithdraw: nullFunction,
amountEffective: "USD:10", },
status: PreparePayResultType.PaymentPossible, {
} as Partial<PreparePayResult>), onUpdateNotification: nullFunction,
getBalance: async () => preparePay: async () =>
({ ({
balances: [ amountRaw: "USD:9",
{ amountEffective: "USD:10",
available: "USD:15", status: PreparePayResultType.PaymentPossible,
}, } as Partial<PreparePayResult>),
], getBalance: async () =>
} as Partial<BalancesResponse>), ({
} as Partial<typeof wxApi> as any), balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -256,28 +291,35 @@ describe("Payment CTA states", () => {
it("should get confirmation done after pay successfully", async () => { it("should get confirmation done after pay successfully", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:9", goToWalletManualWithdraw: nullFunction,
amountEffective: "USD:10", },
status: PreparePayResultType.PaymentPossible, {
} as Partial<PreparePayResult>), onUpdateNotification: nullFunction,
getBalance: async () => preparePay: async () =>
({ ({
balances: [ amountRaw: "USD:9",
{ amountEffective: "USD:10",
available: "USD:15", status: PreparePayResultType.PaymentPossible,
}, } as Partial<PreparePayResult>),
], getBalance: async () =>
} as Partial<BalancesResponse>), ({
confirmPay: async () => balances: [
({ {
type: ConfirmPayResultType.Done, available: "USD:15",
contractTerms: {}, },
} as Partial<ConfirmPayResult>), ],
} as Partial<typeof wxApi> as any), } as Partial<BalancesResponse>),
confirmPay: async () =>
({
type: ConfirmPayResultType.Done,
contractTerms: {},
} as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -317,28 +359,35 @@ describe("Payment CTA states", () => {
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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: nullFunction, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:9", goToWalletManualWithdraw: nullFunction,
amountEffective: "USD:10", },
status: PreparePayResultType.PaymentPossible, {
} as Partial<PreparePayResult>), onUpdateNotification: nullFunction,
getBalance: async () => preparePay: async () =>
({ ({
balances: [ amountRaw: "USD:9",
{ amountEffective: "USD:10",
available: "USD:15", status: PreparePayResultType.PaymentPossible,
}, } as Partial<PreparePayResult>),
], getBalance: async () =>
} as Partial<BalancesResponse>), ({
confirmPay: async () => balances: [
({ {
type: ConfirmPayResultType.Pending, available: "USD:15",
lastError: { code: 1 }, },
} as Partial<ConfirmPayResult>), ],
} as Partial<typeof wxApi> as any), } as Partial<BalancesResponse>),
confirmPay: async () =>
({
type: ConfirmPayResultType.Pending,
lastError: { code: 1 },
} as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any,
),
); );
{ {
@ -393,23 +442,30 @@ describe("Payment CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
onUpdateNotification: subscriptions.saveSubscription, {
preparePay: async () => talerPayUri: "taller://pay",
({ cancel: nullFunction,
amountRaw: "USD:9", goToWalletManualWithdraw: nullFunction,
amountEffective: "USD:10", },
status: PreparePayResultType.PaymentPossible, {
} as Partial<PreparePayResult>), onUpdateNotification: subscriptions.saveSubscription,
getBalance: async () => preparePay: async () =>
({ ({
balances: [ amountRaw: "USD:9",
{ amountEffective: "USD:10",
available: Amounts.stringify(availableBalance), status: PreparePayResultType.PaymentPossible,
}, } as Partial<PreparePayResult>),
], getBalance: async () =>
} as Partial<BalancesResponse>), ({
} as Partial<typeof wxApi> as any), balances: [
{
available: Amounts.stringify(availableBalance),
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
); );
{ {

View File

@ -21,9 +21,13 @@ import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { CompletedView, IgnoredView, InProgressView, LoadingUriView, ReadyView } from "./views.js"; import {
CompletedView,
IgnoredView,
InProgressView,
LoadingUriView,
ReadyView,
} from "./views.js";
export interface Props { export interface Props {
talerRefundUri?: string; talerRefundUri?: string;
@ -39,7 +43,6 @@ export type State =
| State.Completed; | State.Completed;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -75,13 +78,11 @@ export namespace State {
export interface InProgress extends BaseInfo { export interface InProgress extends BaseInfo {
status: "in-progress"; status: "in-progress";
error: undefined; error: undefined;
} }
export interface Completed extends BaseInfo { export interface Completed extends BaseInfo {
status: "completed"; status: "completed";
error: undefined; error: undefined;
} }
} }
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
@ -93,4 +94,8 @@ const viewMapping: StateViewMap<State> = {
ready: ReadyView, ready: ReadyView,
}; };
export const RefundPage = compose("Refund", (p: Props) => useComponentState(p, wxApi), viewMapping) export const RefundPage = compose(
"Refund",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, NotificationType } from "@gnu-taler/taler-util"; import { Amounts, NotificationType } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -40,7 +39,7 @@ export function useComponentState(
}); });
if (!info) { if (!info) {
return { status: "loading", error: undefined } return { status: "loading", error: undefined };
} }
if (info.hasError) { if (info.hasError) {
return { return {
@ -67,7 +66,7 @@ export function useComponentState(
products: info.response.refund.info.products, products: info.response.refund.info.products,
awaitingAmount: Amounts.parseOrThrow(refund.awaiting), awaitingAmount: Amounts.parseOrThrow(refund.awaiting),
error: undefined, error: undefined,
} };
if (ignored) { if (ignored) {
return { return {

View File

@ -21,8 +21,9 @@
import { import {
AmountJson, AmountJson,
Amounts, NotificationType, Amounts,
PrepareRefundResult NotificationType,
PrepareRefundResult,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../../test-utils.js"; import { mountHook } from "../../test-utils.js";
@ -33,11 +34,19 @@ 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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerRefundUri: undefined, cancel: async () => { null } }, { useComponentState(
prepareRefund: async () => ({}), {
applyRefund: async () => ({}), talerRefundUri: undefined,
onUpdateNotification: async () => ({}), cancel: async () => {
} as any), null;
},
},
{
prepareRefund: async () => ({}),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({}),
} as any,
),
); );
{ {
@ -64,27 +73,35 @@ describe("Refund CTA states", () => {
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerRefundUri: "taler://refund/asdasdas", cancel: async () => { null } }, { useComponentState(
prepareRefund: async () => {
({ talerRefundUri: "taler://refund/asdasdas",
effectivePaid: "EUR:2", cancel: async () => {
awaiting: "EUR:2", null;
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 () => ({}), prepareRefund: async () =>
} as any), ({
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,
),
); );
{ {
@ -113,27 +130,35 @@ describe("Refund CTA states", () => {
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerRefundUri: "taler://refund/asdasdas", cancel: async () => { null } }, { useComponentState(
prepareRefund: async () => {
({ talerRefundUri: "taler://refund/asdasdas",
effectivePaid: "EUR:2", cancel: async () => {
awaiting: "EUR:2", null;
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 () => ({}), prepareRefund: async () =>
} as any), ({
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,
),
); );
{ {
@ -189,27 +214,35 @@ describe("Refund CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerRefundUri: "taler://refund/asdasdas", cancel: async () => { null } }, { useComponentState(
prepareRefund: async () => {
({ talerRefundUri: "taler://refund/asdasdas",
awaiting: Amounts.stringify(awaiting), cancel: async () => {
effectivePaid: "EUR:2", null;
gone: "EUR:0",
granted: Amounts.stringify(granted),
pending,
proposalId: "1",
info: {
contractTermsHash: "123",
merchant: {
name: "the merchant name",
},
orderId: "orderId1",
summary: "the summary",
}, },
} as PrepareRefundResult as any), },
applyRefund: async () => ({}), {
onUpdateNotification: subscriptions.saveSubscription, prepareRefund: async () =>
} as any), ({
awaiting: Amounts.stringify(awaiting),
effectivePaid: "EUR:2",
gone: "EUR:0",
granted: Amounts.stringify(granted),
pending,
proposalId: "1",
info: {
contractTermsHash: "123",
merchant: {
name: "the merchant name",
},
orderId: "orderId1",
summary: "the summary",
},
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: subscriptions.saveSubscription,
} as any,
),
); );
{ {

View File

@ -20,13 +20,14 @@ import { HookError } from "../../hooks/useAsyncAsHook.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 * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js";
Props as TermsOfServiceSectionProps
} from "../TermsOfServiceSection.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { AcceptedView, IgnoredView, LoadingUriView, ReadyView } from "./views.js"; import {
AcceptedView,
IgnoredView,
LoadingUriView,
ReadyView,
} from "./views.js";
export interface Props { export interface Props {
talerTipUri?: string; talerTipUri?: string;
@ -42,7 +43,6 @@ export type State =
| State.Ignored; | State.Ignored;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -77,9 +77,13 @@ export namespace State {
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"accepted": AcceptedView, accepted: AcceptedView,
"ignored": IgnoredView, ignored: IgnoredView,
"ready": ReadyView, ready: ReadyView,
}; };
export const TipPage = compose("Tip", (p: Props) => useComponentState(p, wxApi), viewMapping) export const TipPage = compose(
"Tip",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -37,7 +36,7 @@ export function useComponentState(
return { return {
status: "loading", status: "loading",
error: undefined, error: undefined,
} };
} }
if (tipInfo.hasError) { if (tipInfo.hasError) {
return { return {
@ -59,9 +58,9 @@ export function useComponentState(
amount: Amounts.parseOrThrow(tip.tipAmountEffective), amount: Amounts.parseOrThrow(tip.tipAmountEffective),
error: undefined, error: undefined,
cancel: { cancel: {
onClick: onCancel onClick: onCancel,
} },
} };
if (tipIgnored) { if (tipIgnored) {
return { return {
@ -85,4 +84,3 @@ export function useComponentState(
}, },
}; };
} }

View File

@ -19,9 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { import { Amounts, PrepareTipResult } from "@gnu-taler/taler-util";
Amounts, PrepareTipResult
} from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../../test-utils.js"; import { mountHook } from "../../test-utils.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
@ -30,10 +28,18 @@ 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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: undefined, onCancel: async () => { null } }, { useComponentState(
prepareTip: async () => ({}), {
acceptTip: async () => ({}), talerTipUri: undefined,
} as any), onCancel: async () => {
null;
},
},
{
prepareTip: async () => ({}),
acceptTip: async () => ({}),
} as any,
),
); );
{ {
@ -62,19 +68,27 @@ describe("Tip CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, { useComponentState(
prepareTip: async () => {
({ talerTipUri: "taler://tip/asd",
accepted: tipAccepted, onCancel: async () => {
exchangeBaseUrl: "exchange url", null;
merchantBaseUrl: "merchant url", },
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => {
tipAccepted = true;
}, },
} as any), {
prepareTip: async () =>
({
accepted: tipAccepted,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => {
tipAccepted = true;
},
} as any,
),
); );
{ {
@ -114,16 +128,24 @@ describe("Tip CTA states", () => {
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, { useComponentState(
prepareTip: async () => {
({ talerTipUri: "taler://tip/asd",
exchangeBaseUrl: "exchange url", onCancel: async () => {
merchantBaseUrl: "merchant url", null;
tipAmountEffective: "EUR:1", },
walletTipId: "tip_id", },
} as PrepareTipResult as any), {
acceptTip: async () => ({}), prepareTip: async () =>
} as any), ({
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => ({}),
} as any,
),
); );
{ {
@ -160,17 +182,25 @@ describe("Tip CTA states", () => {
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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, { useComponentState(
prepareTip: async () => {
({ talerTipUri: "taler://tip/asd",
accepted: true, onCancel: async () => {
exchangeBaseUrl: "exchange url", null;
merchantBaseUrl: "merchant url", },
tipAmountEffective: "EUR:1", },
walletTipId: "tip_id", {
} as PrepareTipResult as any), prepareTip: async () =>
acceptTip: async () => ({}), ({
} as any), accepted: true,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => ({}),
} as any,
),
); );
{ {

View File

@ -35,7 +35,6 @@ export type State =
| State.Ready; | State.Ready;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -59,9 +58,9 @@ export namespace State {
status: "ready"; status: "ready";
invalid: boolean; invalid: boolean;
create: ButtonHandler; create: ButtonHandler;
toBeReceived: AmountJson, toBeReceived: AmountJson;
chosenAmount: AmountJson, chosenAmount: AmountJson;
subject: TextFieldHandler, subject: TextFieldHandler;
error: undefined; error: undefined;
operationError?: TalerErrorDetail; operationError?: TalerErrorDetail;
} }
@ -70,10 +69,12 @@ export namespace State {
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"created": CreatedView, created: CreatedView,
"ready": ReadyView, ready: ReadyView,
}; };
export const TransferCreatePage = compose(
export const TransferCreatePage = compose("TransferCreatePage", (p: Props) => useComponentState(p, wxApi), viewMapping) "TransferCreatePage",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -24,11 +24,13 @@ export function useComponentState(
{ amount: amountStr, onClose }: Props, { amount: amountStr, onClose }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const amount = Amounts.parseOrThrow(amountStr) const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState("");
const [talerUri, setTalerUri] = useState("") const [talerUri, setTalerUri] = useState("");
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (talerUri) { if (talerUri) {
return { return {
@ -41,28 +43,26 @@ export function useComponentState(
copyToClipboard: { copyToClipboard: {
onClick: async () => { onClick: async () => {
navigator.clipboard.writeText(talerUri); navigator.clipboard.writeText(talerUri);
} },
}, },
} };
} }
async function accept(): Promise<string> { async function accept(): Promise<string> {
try { try {
const resp = await api.initiatePeerPushPayment({ const resp = await api.initiatePeerPushPayment({
amount: Amounts.stringify(amount), amount: Amounts.stringify(amount),
partialContractTerms: { partialContractTerms: {
summary: subject summary: subject,
} },
}) });
return resp.talerUri return resp.talerUri;
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
setOperationError(e.errorDetail) setOperationError(e.errorDetail);
} }
console.error(e) console.error(e);
throw Error("error trying to accept") throw Error("error trying to accept");
} }
} }
return { return {
@ -74,17 +74,17 @@ export function useComponentState(
subject: { subject: {
error: !subject ? "cant be empty" : undefined, error: !subject ? "cant be empty" : undefined,
value: subject, value: subject,
onInput: async (e) => setSubject(e) onInput: async (e) => setSubject(e),
}, },
create: { create: {
onClick: async () => { onClick: async () => {
const uri = await accept(); const uri = await accept();
setTalerUri(uri) setTalerUri(uri);
} },
}, },
chosenAmount: amount, chosenAmount: amount,
toBeReceived: amount, toBeReceived: amount,
error: undefined, error: undefined,
operationError operationError,
} };
} }

View File

@ -22,10 +22,7 @@
import { expect } from "chai"; import { expect } from "chai";
describe("test description", () => { describe("test description", () => {
it("should assert", () => { it("should assert", () => {
expect([]).deep.equals([]);
expect([]).deep.equals([])
}); });
}) });

View File

@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { AbsoluteTime, AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import {
AbsoluteTime,
AmountJson,
TalerErrorDetail,
} 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";
@ -28,13 +32,9 @@ export interface Props {
onClose: () => Promise<void>; onClose: () => Promise<void>;
} }
export type State = export type State = State.Loading | State.LoadingUriError | State.Ready;
| State.Loading
| State.LoadingUriError
| State.Ready;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -51,7 +51,7 @@ export namespace State {
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";
amount: AmountJson, amount: AmountJson;
summary: string | undefined; summary: string | undefined;
expiration: AbsoluteTime | undefined; expiration: AbsoluteTime | undefined;
error: undefined; error: undefined;
@ -63,9 +63,11 @@ export namespace State {
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"ready": ReadyView, ready: ReadyView,
}; };
export const TransferPickupPage = compose(
export const TransferPickupPage = compose("TransferPickupPage", (p: Props) => useComponentState(p, wxApi), viewMapping) "TransferPickupPage",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,12 @@
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 { AbsoluteTime, Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import {
AbsoluteTime,
Amounts,
TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -28,15 +33,17 @@ export function useComponentState(
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.checkPeerPushPayment({ return await api.checkPeerPushPayment({
talerUri: talerPayPushUri, talerUri: talerPayPushUri,
}) });
}, []) }, []);
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
error: undefined, error: undefined,
} };
} }
if (hook.hasError) { if (hook.hasError) {
return { return {
@ -45,24 +52,29 @@ export function useComponentState(
}; };
} }
const { amount: purseAmount, contractTerms, peerPushPaymentIncomingId } = hook.response const {
amount: purseAmount,
contractTerms,
peerPushPaymentIncomingId,
} = hook.response;
const amount: string = contractTerms?.amount const amount: string = contractTerms?.amount;
const summary: string | undefined = contractTerms?.summary const summary: string | undefined = contractTerms?.summary;
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration const expiration: TalerProtocolTimestamp | undefined =
contractTerms?.purse_expiration;
async function accept(): Promise<void> { async function accept(): Promise<void> {
try { try {
const resp = await api.acceptPeerPushPayment({ const resp = await api.acceptPeerPushPayment({
peerPushPaymentIncomingId peerPushPaymentIncomingId,
}) });
await onClose() await onClose();
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
setOperationError(e.errorDetail) setOperationError(e.errorDetail);
} }
console.error(e) console.error(e);
throw Error("error trying to accept") throw Error("error trying to accept");
} }
} }
return { return {
@ -70,13 +82,13 @@ export function useComponentState(
amount: Amounts.parseOrThrow(amount), amount: Amounts.parseOrThrow(amount),
error: undefined, error: undefined,
accept: { accept: {
onClick: accept onClick: accept,
}, },
summary, summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined, expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
cancel: { cancel: {
onClick: onClose onClick: onClose,
}, },
operationError operationError,
} };
} }

View File

@ -22,10 +22,7 @@
import { expect } from "chai"; import { expect } from "chai";
describe("test description", () => { describe("test description", () => {
it("should assert", () => { it("should assert", () => {
expect([]).deep.equals([]);
expect([]).deep.equals([])
}); });
}) });

View File

@ -20,12 +20,18 @@ import { HookError } from "../../hooks/useAsyncAsHook.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 * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js";
import { import {
Props as TermsOfServiceSectionProps useComponentStateFromParams,
} from "../TermsOfServiceSection.js"; useComponentStateFromURI,
import { useComponentStateFromParams, useComponentStateFromURI } from "./state.js"; } from "./state.js";
import { CompletedView, LoadingExchangeView, LoadingInfoView, LoadingUriView, SuccessView } from "./views.js"; import {
CompletedView,
LoadingExchangeView,
LoadingInfoView,
LoadingUriView,
SuccessView,
} from "./views.js";
export interface PropsFromURI { export interface PropsFromURI {
talerWithdrawUri: string | undefined; talerWithdrawUri: string | undefined;
@ -46,7 +52,6 @@ export type State =
| State.Completed; | State.Completed;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -99,5 +104,13 @@ const viewMapping: StateViewMap<State> = {
success: SuccessView, success: SuccessView,
}; };
export const WithdrawPageFromURI = compose("WithdrawPageFromURI", (p: PropsFromURI) => useComponentStateFromURI(p, wxApi), viewMapping) export const WithdrawPageFromURI = compose(
export const WithdrawPageFromParams = compose("WithdrawPageFromParams", (p: PropsFromParams) => useComponentStateFromParams(p, wxApi), viewMapping) "WithdrawPageFromURI",
(p: PropsFromURI) => useComponentStateFromURI(p, wxApi),
viewMapping,
);
export const WithdrawPageFromParams = compose(
"WithdrawPageFromParams",
(p: PropsFromParams) => useComponentStateFromParams(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -27,7 +26,6 @@ export function useComponentStateFromParams(
{ amount, cancel }: PropsFromParams, { amount, cancel }: PropsFromParams,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const [ageRestricted, setAgeRestricted] = useState(0); const [ageRestricted, setAgeRestricted] = useState(0);
const exchangeHook = useAsyncAsHook(api.listExchanges); const exchangeHook = useAsyncAsHook(api.listExchanges);
@ -40,14 +38,20 @@ export function useComponentStateFromParams(
const chosenAmount = Amounts.parseOrThrow(amount); const chosenAmount = Amounts.parseOrThrow(amount);
// get the first exchange with the currency as the default one // get the first exchange with the currency as the default one
const exchange = exchangeHookDep ? exchangeHookDep.exchanges.find(e => e.currency === chosenAmount.currency) : undefined const exchange = exchangeHookDep
? exchangeHookDep.exchanges.find(
(e) => e.currency === chosenAmount.currency,
)
: undefined;
/** /**
* For the exchange selected, bring the status of the terms of service * For the exchange selected, bring the status of the terms of service
*/ */
const terms = useAsyncAsHook(async () => { const terms = useAsyncAsHook(async () => {
if (!exchange) return undefined if (!exchange) return undefined;
const exchangeTos = await api.getExchangeTos(exchange.exchangeBaseUrl, ["text/xml"]); const exchangeTos = await api.getExchangeTos(exchange.exchangeBaseUrl, [
"text/xml",
]);
const state = buildTermsOfServiceState(exchangeTos); const state = buildTermsOfServiceState(exchangeTos);
@ -59,7 +63,7 @@ export function useComponentStateFromParams(
* about the withdrawal * about the withdrawal
*/ */
const amountHook = useAsyncAsHook(async () => { const amountHook = useAsyncAsHook(async () => {
if (!exchange) return undefined if (!exchange) return undefined;
const info = await api.getExchangeWithdrawalInfo({ const info = await api.getExchangeWithdrawalInfo({
exchangeBaseUrl: exchange.exchangeBaseUrl, exchangeBaseUrl: exchange.exchangeBaseUrl,
@ -71,9 +75,12 @@ export function useComponentStateFromParams(
const withdrawAmount = { const withdrawAmount = {
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw), raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective), effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
} };
return { amount: withdrawAmount, ageRestrictionOptions: info.ageRestrictionOptions }; return {
amount: withdrawAmount,
ageRestrictionOptions: info.ageRestrictionOptions,
};
}, [exchangeHookDep]); }, [exchangeHookDep]);
const [reviewing, setReviewing] = useState<boolean>(false); const [reviewing, setReviewing] = useState<boolean>(false);
@ -85,7 +92,7 @@ export function useComponentStateFromParams(
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false); const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
const [withdrawCompleted, setWithdrawCompleted] = useState<boolean>(false); const [withdrawCompleted, setWithdrawCompleted] = useState<boolean>(false);
if (!exchangeHook) return { status: "loading", error: undefined } if (!exchangeHook) return { status: "loading", error: undefined };
if (exchangeHook.hasError) { if (exchangeHook.hasError) {
return { return {
status: "loading-uri", status: "loading-uri",
@ -125,7 +132,7 @@ export function useComponentStateFromParams(
} }
if (!amountHook) { if (!amountHook) {
return { status: "loading", error: undefined } return { status: "loading", error: undefined };
} }
if (amountHook.hasError) { if (amountHook.hasError) {
return { return {
@ -149,8 +156,8 @@ export function useComponentStateFromParams(
const { state: termsState } = (!terms const { state: termsState } = (!terms
? undefined ? undefined
: terms.hasError : terms.hasError
? undefined ? undefined
: terms.response) || { state: undefined }; : terms.response) || { state: undefined };
async function onAccept(accepted: boolean): Promise<void> { async function onAccept(accepted: boolean): Promise<void> {
if (!termsState || !exchange) return; if (!termsState || !exchange) return;
@ -173,21 +180,25 @@ export function useComponentStateFromParams(
termsState !== undefined && termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new"); (termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions = amountHook.response. const ageRestrictionOptions =
ageRestrictionOptions?. amountHook.response.ageRestrictionOptions?.reduce(
reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {} as Record<string, string>) (p, c) => ({ ...p, [c]: `under ${c}` }),
{} as Record<string, string>,
);
const ageRestrictionEnabled = ageRestrictionOptions !== undefined const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
if (ageRestrictionEnabled) { if (ageRestrictionEnabled) {
ageRestrictionOptions["0"] = "Not restricted"; ageRestrictionOptions["0"] = "Not restricted";
} }
//TODO: calculate based on exchange info //TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled ? { const ageRestriction = ageRestrictionEnabled
list: ageRestrictionOptions, ? {
value: String(ageRestricted), list: ageRestrictionOptions,
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), value: String(ageRestricted),
} : undefined; onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
}
: undefined;
return { return {
status: "success", status: "success",
@ -207,12 +218,12 @@ export function useComponentStateFromParams(
tosProps: !termsState tosProps: !termsState
? undefined ? undefined
: { : {
onAccept, onAccept,
onReview: setReviewing, onReview: setReviewing,
reviewed: reviewed, reviewed: reviewed,
reviewing: reviewing, reviewing: reviewing,
terms: termsState, terms: termsState,
}, },
mustAcceptFirst, mustAcceptFirst,
cancel, cancel,
}; };
@ -233,7 +244,7 @@ export function useComponentStateFromURI(
const uriInfo = await api.getWithdrawalDetailsForUri({ const uriInfo = await api.getWithdrawalDetailsForUri({
talerWithdrawUri, talerWithdrawUri,
}); });
const { amount, defaultExchangeBaseUrl } = uriInfo const { amount, defaultExchangeBaseUrl } = uriInfo;
return { amount, thisExchange: defaultExchangeBaseUrl }; return { amount, thisExchange: defaultExchangeBaseUrl };
}); });
@ -245,14 +256,15 @@ export function useComponentStateFromURI(
? undefined ? undefined
: uriInfoHook.response; : uriInfoHook.response;
/** /**
* For the exchange selected, bring the status of the terms of service * For the exchange selected, bring the status of the terms of service
*/ */
const terms = useAsyncAsHook(async () => { const terms = useAsyncAsHook(async () => {
if (!uriHookDep?.thisExchange) return false; if (!uriHookDep?.thisExchange) return false;
const exchangeTos = await api.getExchangeTos(uriHookDep.thisExchange, ["text/xml"]); const exchangeTos = await api.getExchangeTos(uriHookDep.thisExchange, [
"text/xml",
]);
const state = buildTermsOfServiceState(exchangeTos); const state = buildTermsOfServiceState(exchangeTos);
@ -276,9 +288,12 @@ export function useComponentStateFromURI(
const withdrawAmount = { const withdrawAmount = {
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw), raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective), effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
} };
return { amount: withdrawAmount, ageRestrictionOptions: info.ageRestrictionOptions }; return {
amount: withdrawAmount,
ageRestrictionOptions: info.ageRestrictionOptions,
};
}, [uriHookDep]); }, [uriHookDep]);
const [reviewing, setReviewing] = useState<boolean>(false); const [reviewing, setReviewing] = useState<boolean>(false);
@ -290,7 +305,7 @@ export function useComponentStateFromURI(
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false); const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
const [withdrawCompleted, setWithdrawCompleted] = useState<boolean>(false); const [withdrawCompleted, setWithdrawCompleted] = useState<boolean>(false);
if (!uriInfoHook) return { status: "loading", error: undefined } if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) { if (uriInfoHook.hasError) {
return { return {
status: "loading-uri", status: "loading-uri",
@ -298,7 +313,7 @@ export function useComponentStateFromURI(
}; };
} }
const { amount, thisExchange } = uriInfoHook.response const { amount, thisExchange } = uriInfoHook.response;
const chosenAmount = Amounts.parseOrThrow(amount); const chosenAmount = Amounts.parseOrThrow(amount);
@ -339,7 +354,7 @@ export function useComponentStateFromURI(
} }
if (!amountHook) { if (!amountHook) {
return { status: "loading", error: undefined } return { status: "loading", error: undefined };
} }
if (amountHook.hasError) { if (amountHook.hasError) {
return { return {
@ -363,8 +378,8 @@ export function useComponentStateFromURI(
const { state: termsState } = (!terms const { state: termsState } = (!terms
? undefined ? undefined
: terms.hasError : terms.hasError
? undefined ? undefined
: terms.response) || { state: undefined }; : terms.response) || { state: undefined };
async function onAccept(accepted: boolean): Promise<void> { async function onAccept(accepted: boolean): Promise<void> {
if (!termsState || !thisExchange) return; if (!termsState || !thisExchange) return;
@ -387,21 +402,25 @@ export function useComponentStateFromURI(
termsState !== undefined && termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new"); (termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions = amountHook.response. const ageRestrictionOptions =
ageRestrictionOptions?. amountHook.response.ageRestrictionOptions?.reduce(
reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {} as Record<string, string>) (p, c) => ({ ...p, [c]: `under ${c}` }),
{} as Record<string, string>,
);
const ageRestrictionEnabled = ageRestrictionOptions !== undefined const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
if (ageRestrictionEnabled) { if (ageRestrictionEnabled) {
ageRestrictionOptions["0"] = "Not restricted"; ageRestrictionOptions["0"] = "Not restricted";
} }
//TODO: calculate based on exchange info //TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled ? { const ageRestriction = ageRestrictionEnabled
list: ageRestrictionOptions, ? {
value: String(ageRestricted), list: ageRestrictionOptions,
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), value: String(ageRestricted),
} : undefined; onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
}
: undefined;
return { return {
status: "success", status: "success",
@ -422,14 +441,13 @@ export function useComponentStateFromURI(
tosProps: !termsState tosProps: !termsState
? undefined ? undefined
: { : {
onAccept, onAccept,
onReview: setReviewing, onReview: setReviewing,
reviewed: reviewed, reviewed: reviewed,
reviewing: reviewing, reviewing: reviewing,
terms: termsState, terms: termsState,
}, },
mustAcceptFirst, mustAcceptFirst,
cancel, cancel,
}; };
} }

View File

@ -62,13 +62,21 @@ 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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentStateFromURI({ talerWithdrawUri: undefined, cancel: async () => { null } }, { useComponentStateFromURI(
listExchanges: async () => ({ exchanges }), {
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ talerWithdrawUri: undefined,
amount: "ARS:2", cancel: async () => {
possibleExchanges: exchanges, null;
}), },
} as any), },
{
listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: "ARS:2",
possibleExchanges: exchanges,
}),
} as any,
),
); );
{ {
@ -94,13 +102,21 @@ describe("Withdraw CTA states", () => {
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 { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentStateFromURI({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { useComponentStateFromURI(
listExchanges: async () => ({ exchanges }), {
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ talerWithdrawUri: "taler-withdraw://",
amount: "EUR:2", cancel: async () => {
possibleExchanges: [], null;
}), },
} as any), },
{
listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: "EUR:2",
possibleExchanges: [],
}),
} as any,
),
); );
{ {
@ -128,26 +144,34 @@ describe("Withdraw CTA states", () => {
it("should be able to withdraw if tos are ok", async () => { it("should be able to withdraw if tos are ok", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentStateFromURI({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { useComponentStateFromURI(
listExchanges: async () => ({ exchanges }), {
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ talerWithdrawUri: "taler-withdraw://",
amount: "ARS:2", cancel: async () => {
possibleExchanges: exchanges, null;
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl },
}), },
getExchangeWithdrawalInfo: {
async (): Promise<ExchangeWithdrawDetails> => listExchanges: async () => ({ exchanges }),
({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
withdrawalAmountRaw: "ARS:2", amount: "ARS:2",
withdrawalAmountEffective: "ARS:2", possibleExchanges: exchanges,
} as any), defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({ }),
contentType: "text", getExchangeWithdrawalInfo:
content: "just accept", async (): Promise<ExchangeWithdrawDetails> =>
acceptedEtag: "v1", ({
currentEtag: "v1", withdrawalAmountRaw: "ARS:2",
}), withdrawalAmountEffective: "ARS:2",
} as any), } as any),
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
contentType: "text",
content: "just accept",
acceptedEtag: "v1",
currentEtag: "v1",
}),
} as any,
),
); );
{ {
@ -194,27 +218,35 @@ describe("Withdraw CTA states", () => {
it("should be accept the tos before withdraw", async () => { it("should be accept the tos before withdraw", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentStateFromURI({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { useComponentStateFromURI(
listExchanges: async () => ({ exchanges }), {
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ talerWithdrawUri: "taler-withdraw://",
amount: "ARS:2", cancel: async () => {
possibleExchanges: exchanges, null;
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl },
}), },
getExchangeWithdrawalInfo: {
async (): Promise<ExchangeWithdrawDetails> => listExchanges: async () => ({ exchanges }),
({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
withdrawalAmountRaw: "ARS:2", amount: "ARS:2",
withdrawalAmountEffective: "ARS:2", possibleExchanges: exchanges,
} as any), defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({ }),
contentType: "text", getExchangeWithdrawalInfo:
content: "just accept", async (): Promise<ExchangeWithdrawDetails> =>
acceptedEtag: "v1", ({
currentEtag: "v2", withdrawalAmountRaw: "ARS:2",
}), withdrawalAmountEffective: "ARS:2",
setExchangeTosAccepted: async () => ({}), } as any),
} as any), getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
contentType: "text",
content: "just accept",
acceptedEtag: "v1",
currentEtag: "v2",
}),
setExchangeTosAccepted: async () => ({}),
} as any,
),
); );
{ {

View File

@ -55,7 +55,9 @@ async function handleClipboardPerm(
// as the result of an input event ... // as the result of an input event ...
let granted: boolean; let granted: boolean;
try { try {
granted = await platform.getPermissionsApi().requestClipboardPermissions(); granted = await platform
.getPermissionsApi()
.requestClipboardPermissions();
} catch (lastError) { } catch (lastError) {
onChange(false); onChange(false);
throw lastError; throw lastError;

View File

@ -20,7 +20,6 @@ import { h, VNode } from "preact";
import { expect } from "chai"; import { expect } from "chai";
describe("useTalerActionURL hook", () => { describe("useTalerActionURL hook", () => {
it("should be set url to undefined when dismiss", async () => { it("should be set url to undefined when dismiss", async () => {
const ctx = ({ children }: { children: any }): VNode => { const ctx = ({ children }: { children: any }): VNode => {
return h(IoCProviderForTesting, { return h(IoCProviderForTesting, {
@ -46,7 +45,7 @@ describe("useTalerActionURL hook", () => {
const [url, setDismissed] = getLastResultOrThrow(); const [url, setDismissed] = getLastResultOrThrow();
expect(url).deep.equals({ expect(url).deep.equals({
location: "clipboard", location: "clipboard",
uri: "qwe" uri: "qwe",
}); });
setDismissed(true); setDismissed(true);
} }

View File

@ -19,7 +19,7 @@ import { useIocContext } from "../context/iocContext.js";
export interface UriLocation { export interface UriLocation {
uri: string; uri: string;
location: "clipboard" | "activeTab" location: "clipboard" | "activeTab";
} }
export function useTalerActionURL(): [ export function useTalerActionURL(): [
@ -37,7 +37,7 @@ export function useTalerActionURL(): [
if (clipUri) { if (clipUri) {
setTalerActionUrl({ setTalerActionUrl({
location: "clipboard", location: "clipboard",
uri: clipUri uri: clipUri,
}); });
return; return;
} }
@ -45,7 +45,7 @@ export function useTalerActionURL(): [
if (tabUri) { if (tabUri) {
setTalerActionUrl({ setTalerActionUrl({
location: "activeTab", location: "activeTab",
uri: tabUri uri: tabUri,
}); });
return; return;
} }

View File

@ -119,8 +119,9 @@ function handleContainer(containerInfo: Container, props: ManagedModalProps) {
el: container, el: container,
}); });
// Use computed style, here to get the real padding to add our scrollbar width. // Use computed style, here to get the real padding to add our scrollbar width.
container.style.paddingRight = `${getPaddingRight(container) + scrollbarSize container.style.paddingRight = `${
}px`; getPaddingRight(container) + scrollbarSize
}px`;
// .mui-fixed is a global helper. // .mui-fixed is a global helper.
const fixedElements = const fixedElements =
@ -131,8 +132,9 @@ function handleContainer(containerInfo: Container, props: ManagedModalProps) {
property: "padding-right", property: "padding-right",
el: element, el: element,
}); });
element.style.paddingRight = `${getPaddingRight(element) + scrollbarSize element.style.paddingRight = `${
}px`; getPaddingRight(element) + scrollbarSize
}px`;
}); });
} }
@ -142,7 +144,7 @@ function handleContainer(containerInfo: Container, props: ManagedModalProps) {
const containerWindow = ownerWindow(container); const containerWindow = ownerWindow(container);
const scrollContainer = const scrollContainer =
parent?.nodeName === "HTML" && parent?.nodeName === "HTML" &&
containerWindow.getComputedStyle(parent).overflowY === "scroll" containerWindow.getComputedStyle(parent).overflowY === "scroll"
? parent ? parent
: container; : container;

View File

@ -176,13 +176,13 @@ export interface PlatformAPI {
findTalerUriInClipboard(): Promise<string | undefined>; findTalerUriInClipboard(): Promise<string | undefined>;
/** /**
* Used from the frontend to send commands to the wallet * Used from the frontend to send commands to the wallet
* *
* @param operation * @param operation
* @param payload * @param payload
* *
* @return response from the backend * @return response from the backend
*/ */
sendMessageToWalletBackground( sendMessageToWalletBackground(
operation: string, operation: string,
payload: any, payload: any,

View File

@ -109,7 +109,7 @@ export async function requestClipboardPermissions(): Promise<boolean> {
rej(le); rej(le);
} }
res(resp); res(resp);
}) });
}); });
} }
@ -130,13 +130,13 @@ type HeaderListenerFunc = (
) => void; ) => void;
let currentHeaderListener: HeaderListenerFunc | undefined = undefined; let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
type TabListenerFunc = ( type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
tabId: number, info: chrome.tabs.TabChangeInfo,
) => void;
let currentTabListener: TabListenerFunc | undefined = undefined; let currentTabListener: TabListenerFunc | undefined = undefined;
export function containsTalerHeaderListener(): boolean { export function containsTalerHeaderListener(): boolean {
return currentHeaderListener !== undefined || currentTabListener !== undefined; return (
currentHeaderListener !== undefined || currentTabListener !== undefined
);
} }
export async function removeHostPermissions(): Promise<boolean> { export async function removeHostPermissions(): Promise<boolean> {
@ -147,9 +147,11 @@ export async function removeHostPermissions(): Promise<boolean> {
) { ) {
chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener); chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
} }
if (currentTabListener && if (
chrome?.tabs?.onUpdated?.hasListener(currentTabListener)) { currentTabListener &&
chrome.tabs.onUpdated.removeListener(currentTabListener) chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
) {
chrome.tabs.onUpdated.removeListener(currentTabListener);
} }
currentHeaderListener = undefined; currentHeaderListener = undefined;
@ -413,20 +415,25 @@ function registerTalerHeaderListener(
.map((h) => h.value) .map((h) => h.value)
.filter((value): value is string => !!value); .filter((value): value is string => !!value);
if (values.length > 0) { if (values.length > 0) {
logger.info(`Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}`) logger.info(
`Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}`,
);
callback(details.tabId, values[0]); callback(details.tabId, values[0]);
} }
} }
return; return;
} }
async function tabListener(tabId: number, info: chrome.tabs.TabChangeInfo): Promise<void> { async function tabListener(
tabId: number,
info: chrome.tabs.TabChangeInfo,
): Promise<void> {
if (tabId < 0) return; if (tabId < 0) return;
if (info.status !== "complete") return; if (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId); const uri = await findTalerUriInTab(tabId);
if (!uri) return; if (!uri) return;
logger.info(`Found a Taler URI in the tab ${tabId}`) logger.info(`Found a Taler URI in the tab ${tabId}`);
callback(tabId, uri) callback(tabId, uri);
} }
const prevHeaderListener = currentHeaderListener; const prevHeaderListener = currentHeaderListener;
@ -442,14 +449,18 @@ function registerTalerHeaderListener(
) { ) {
chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener); chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
} }
if (prevTabListener && chrome?.tabs?.onUpdated?.hasListener(prevTabListener)) { if (
chrome.tabs.onUpdated.removeListener(prevTabListener) prevTabListener &&
chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
) {
chrome.tabs.onUpdated.removeListener(prevTabListener);
} }
//if the result was positive, add the headerListener //if the result was positive, add the headerListener
if (result) { if (result) {
const headersEvent: chrome.webRequest.WebResponseHeadersEvent | undefined = const headersEvent:
chrome?.webRequest?.onHeadersReceived; | chrome.webRequest.WebResponseHeadersEvent
| undefined = chrome?.webRequest?.onHeadersReceived;
if (headersEvent) { if (headersEvent) {
headersEvent.addListener(headerListener, { urls: ["<all_urls>"] }, [ headersEvent.addListener(headerListener, { urls: ["<all_urls>"] }, [
"responseHeaders", "responseHeaders",
@ -472,7 +483,6 @@ function registerTalerHeaderListener(
} }
}); });
}); });
} }
const alertIcons = { const alertIcons = {
@ -515,26 +525,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 {
@ -547,7 +557,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 {
@ -727,20 +737,23 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
} }
async function timeout(ms: number): Promise<void> { async function timeout(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
async function findTalerUriInClipboard(): Promise<string | undefined> { async function findTalerUriInClipboard(): Promise<string | undefined> {
try { try {
//It looks like clipboard promise does not return, so we need a timeout //It looks like clipboard promise does not return, so we need a timeout
const textInClipboard = await Promise.any([ const textInClipboard = await Promise.any([
timeout(100), timeout(100),
window.navigator.clipboard.readText() window.navigator.clipboard.readText(),
]) ]);
if (!textInClipboard) return; if (!textInClipboard) return;
return textInClipboard.startsWith("taler://") || textInClipboard.startsWith("taler+http://") ? textInClipboard : undefined return textInClipboard.startsWith("taler://") ||
textInClipboard.startsWith("taler+http://")
? textInClipboard
: undefined;
} catch (e) { } catch (e) {
logger.error("could not read clipboard", e) logger.error("could not read clipboard", e);
return undefined return undefined;
} }
} }

View File

@ -31,7 +31,8 @@ function getJsonIfOk(r: Response): Promise<any> {
} }
throw new Error( throw new Error(
`Try another server: (${r.status}) ${r.statusText || "internal server error" `Try another server: (${r.status}) ${
r.statusText || "internal server error"
}`, }`,
); );
} }
@ -102,10 +103,10 @@ export function buildTermsOfServiceStatus(
return !content return !content
? "notfound" ? "notfound"
: !acceptedVersion : !acceptedVersion
? "new" ? "new"
: acceptedVersion !== currentVersion : acceptedVersion !== currentVersion
? "changed" ? "changed"
: "accepted"; : "accepted";
} }
function parseTermsOfServiceContent( function parseTermsOfServiceContent(

View File

@ -62,9 +62,9 @@ describe("DepositPage states", () => {
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:0` }], balances: [{ available: `${currency}:0` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: {} }), listKnownBankAccounts: async () => ({ accounts: {} }),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any),
); );
@ -89,9 +89,9 @@ describe("DepositPage states", () => {
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:1` }], balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: {} }), listKnownBankAccounts: async () => ({ accounts: {} }),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any),
); );
@ -111,19 +111,21 @@ describe("DepositPage states", () => {
await assertNoPendingUpdate(); await assertNoPendingUpdate();
}); });
const ibanPayto_str = "payto://iban/ES8877998399652238" const ibanPayto_str = "payto://iban/ES8877998399652238";
const ibanPayto = { ibanPayto_str: parsePaytoUri(ibanPayto_str)! }; const ibanPayto = { ibanPayto_str: parsePaytoUri(ibanPayto_str)! };
const talerBankPayto_str = "payto://x-taler-bank/ES8877998399652238" const talerBankPayto_str = "payto://x-taler-bank/ES8877998399652238";
const talerBankPayto = { talerBankPayto_str: parsePaytoUri(talerBankPayto_str)! }; const talerBankPayto = {
talerBankPayto_str: parsePaytoUri(talerBankPayto_str)!,
};
it("should have status 'ready' but unable to deposit ", async () => { it("should have status 'ready' but unable to deposit ", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:1` }], balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: ibanPayto }), listKnownBankAccounts: async () => ({ accounts: ibanPayto }),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any),
); );
@ -153,9 +155,9 @@ describe("DepositPage states", () => {
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:1` }], balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: ibanPayto }), listKnownBankAccounts: async () => ({ accounts: ibanPayto }),
getFeeForDeposit: withoutFee, getFeeForDeposit: withoutFee,
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any),
@ -202,9 +204,9 @@ describe("DepositPage states", () => {
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:1` }], balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: ibanPayto }), listKnownBankAccounts: async () => ({ accounts: ibanPayto }),
getFeeForDeposit: withSomeFee, getFeeForDeposit: withSomeFee,
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any),
@ -252,9 +254,9 @@ describe("DepositPage states", () => {
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:1` }], balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ listKnownBankAccounts: async () => ({
accounts: { ...ibanPayto, ...talerBankPayto }, accounts: { ...ibanPayto, ...talerBankPayto },
}), }),
@ -338,9 +340,9 @@ describe("DepositPage states", () => {
mountHook(() => mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => getBalance: async () =>
({ ({
balances: [{ available: `${currency}:15` }], balances: [{ available: `${currency}:15` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: ibanPayto }), listKnownBankAccounts: async () => ({ accounts: ibanPayto }),
getFeeForDeposit: withSomeFee, getFeeForDeposit: withSomeFee,
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any),

View File

@ -25,13 +25,9 @@ export interface Props {
p: string; p: string;
} }
export type State = export type State = State.Loading | State.LoadingUriError | State.Ready;
| State.Loading
| State.LoadingUriError
| State.Ready;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -54,10 +50,11 @@ export namespace State {
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"ready": ReadyView, ready: ReadyView,
}; };
export const ComponentName = compose(
export const ComponentName = compose("ComponentName", (p: Props) => useComponentState(p, wxApi), viewMapping) "ComponentName",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -17,12 +17,9 @@
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState({ p }: Props, api: typeof wxApi): State {
{ p }: Props,
api: typeof wxApi,
): State {
return { return {
status: "ready", status: "ready",
error: undefined, error: undefined,
} };
} }

View File

@ -22,10 +22,7 @@
import { expect } from "chai"; import { expect } from "chai";
describe("test description", () => { describe("test description", () => {
it("should assert", () => { it("should assert", () => {
expect([]).deep.equals([]);
expect([]).deep.equals([])
}); });
}) });

View File

@ -14,16 +14,25 @@
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 { FeeDescription, FeeDescriptionPair, AbsoluteTime, ExchangeFullDetails, OperationMap } from "@gnu-taler/taler-util"; import {
FeeDescription,
FeeDescriptionPair,
AbsoluteTime,
ExchangeFullDetails,
OperationMap,
} 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, 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 * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { ComparingView, LoadingUriView, NoExchangesView, ReadyView } from "./views.js"; import {
ComparingView,
LoadingUriView,
NoExchangesView,
ReadyView,
} from "./views.js";
export interface Props { export interface Props {
currency?: string; currency?: string;
@ -39,7 +48,6 @@ export type State =
| State.NoExchanges; | State.NoExchanges;
export namespace State { export namespace State {
export interface Loading { export interface Loading {
status: "loading"; status: "loading";
error: undefined; error: undefined;
@ -75,13 +83,16 @@ export namespace State {
} }
} }
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"comparing": ComparingView, comparing: ComparingView,
"no-exchanges": NoExchangesView, "no-exchanges": NoExchangesView,
"ready": ReadyView, ready: ReadyView,
}; };
export const ExchangeSelectionPage = compose("ExchangeSelectionPage", (p: Props) => useComponentState(p, wxApi), viewMapping) export const ExchangeSelectionPage = compose(
"ExchangeSelectionPage",
(p: Props) => useComponentState(p, wxApi),
viewMapping,
);

View File

@ -14,7 +14,6 @@
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 { FeeDescription, OperationMap } from "@gnu-taler/taler-util"; import { FeeDescription, OperationMap } from "@gnu-taler/taler-util";
import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core"; import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -26,26 +25,32 @@ export function useComponentState(
{ onCancel, onSelection, currency }: Props, { onCancel, onSelection, currency }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const initialValue = 0 const initialValue = 0;
const [value, setValue] = useState(String(initialValue)); const [value, setValue] = useState(String(initialValue));
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const { exchanges } = await api.listExchanges() const { exchanges } = await api.listExchanges();
const selectedIdx = parseInt(value, 10) const selectedIdx = parseInt(value, 10);
const selectedExchange = exchanges.length == 0 ? undefined : exchanges[selectedIdx] const selectedExchange =
const selected = !selectedExchange ? undefined : await api.getExchangeDetailedInfo(selectedExchange.exchangeBaseUrl) exchanges.length == 0 ? undefined : exchanges[selectedIdx];
const selected = !selectedExchange
? undefined
: await api.getExchangeDetailedInfo(selectedExchange.exchangeBaseUrl);
const initialExchange = selectedIdx === initialValue ? undefined : exchanges[initialValue] const initialExchange =
const original = !initialExchange ? undefined : await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl) selectedIdx === initialValue ? undefined : exchanges[initialValue];
return { exchanges, selected, original } const original = !initialExchange
? undefined
: await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl);
return { exchanges, selected, original };
}); });
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
error: undefined, error: undefined,
} };
} }
if (hook.hasError) { if (hook.hasError) {
return { return {
@ -60,11 +65,14 @@ export function useComponentState(
//!selected <=> exchanges.length === 0 //!selected <=> exchanges.length === 0
return { return {
status: "no-exchanges", status: "no-exchanges",
error: undefined error: undefined,
} };
} }
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>) const exchangeMap = exchanges.reduce(
(prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }),
{} as Record<string, string>,
);
if (!original) { if (!original) {
// !original <=> selected == original // !original <=> selected == original
@ -74,24 +82,36 @@ export function useComponentState(
list: exchangeMap, list: exchangeMap,
value: value, value: value,
onChange: async (v) => { onChange: async (v) => {
setValue(v) setValue(v);
} },
}, },
error: undefined, error: undefined,
onClose: { onClose: {
onClick: onCancel onClick: onCancel,
}, },
selected, selected,
timeline: selected.feesDescription timeline: selected.feesDescription,
} };
} }
const pairTimeline: OperationMap<FeeDescription[]> = { const pairTimeline: OperationMap<FeeDescription[]> = {
deposit: createDenominationPairTimeline(selected.feesDescription.deposit, original.feesDescription.deposit), deposit: createDenominationPairTimeline(
refresh: createDenominationPairTimeline(selected.feesDescription.refresh, original.feesDescription.refresh), selected.feesDescription.deposit,
refund: createDenominationPairTimeline(selected.feesDescription.refund, original.feesDescription.refund), original.feesDescription.deposit,
withdraw: createDenominationPairTimeline(selected.feesDescription.withdraw, original.feesDescription.withdraw), ),
} refresh: createDenominationPairTimeline(
selected.feesDescription.refresh,
original.feesDescription.refresh,
),
refund: createDenominationPairTimeline(
selected.feesDescription.refund,
original.feesDescription.refund,
),
withdraw: createDenominationPairTimeline(
selected.feesDescription.withdraw,
original.feesDescription.withdraw,
),
};
return { return {
status: "comparing", status: "comparing",
@ -99,23 +119,21 @@ export function useComponentState(
list: exchangeMap, list: exchangeMap,
value: value, value: value,
onChange: async (v) => { onChange: async (v) => {
setValue(v) setValue(v);
} },
}, },
error: undefined, error: undefined,
onReset: { onReset: {
onClick: async () => { onClick: async () => {
setValue(String(initialValue)) setValue(String(initialValue));
} },
}, },
onSelect: { onSelect: {
onClick: async () => { onClick: async () => {
onSelection(selected.exchangeBaseUrl) onSelection(selected.exchangeBaseUrl);
} },
}, },
selected, selected,
pairTimeline, pairTimeline,
} };
} }

View File

@ -19,8 +19,5 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { import { AbsoluteTime, Amounts, DenominationInfo } from "@gnu-taler/taler-util";
AbsoluteTime,
Amounts, DenominationInfo
} from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";

View File

@ -261,9 +261,11 @@ export function listExchanges(): Promise<ExchangesListResponse> {
return callBackend("listExchanges", {}); return callBackend("listExchanges", {});
} }
export function getExchangeDetailedInfo(exchangeBaseUrl: string): Promise<ExchangeFullDetails> { export function getExchangeDetailedInfo(
exchangeBaseUrl: string,
): Promise<ExchangeFullDetails> {
return callBackend("getExchangeDetailedInfo", { return callBackend("getExchangeDetailedInfo", {
exchangeBaseUrl exchangeBaseUrl,
}); });
} }
@ -538,6 +540,6 @@ export function acceptPeerPullPayment(
export function getTransactionById(tid: string): Promise<Transaction> { export function getTransactionById(tid: string): Promise<Transaction> {
return callBackend("getTransactionById", { return callBackend("getTransactionById", {
transactionId: tid transactionId: tid,
}) });
} }

View File

@ -344,7 +344,6 @@ export async function wxMain(): Promise<void> {
console.error(e); console.error(e);
} }
// On platforms that support it, also listen to external // On platforms that support it, also listen to external
// modification of permissions. // modification of permissions.
platform.getPermissionsApi().addPermissionsListener((perm, lastError) => { platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {