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",

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(
{
talerDepositUri: undefined,
amountStr: undefined,
cancel: async () => {
null;
},
},
{
prepareRefund: async () => ({}), prepareRefund: async () => ({}),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}), onUpdateNotification: async () => ({}),
} as any), } 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(
{
talerDepositUri: "payto://refund/asdasdas",
amountStr: "EUR:1",
cancel: async () => {
null;
},
},
{
prepareDeposit: async () => prepareDeposit: async () =>
({ ({
effectiveDepositAmount: Amounts.parseOrThrow("EUR:1"), effectiveDepositAmount: Amounts.parseOrThrow("EUR:1"),
totalDepositCost: Amounts.parseOrThrow("EUR:1.2"), totalDepositCost: Amounts.parseOrThrow("EUR:1.2"),
} as PrepareDepositResponse as any), } as PrepareDepositResponse as any),
createDepositGroup: async () => ({}), createDepositGroup: async () => ({}),
} as any), } 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(
{
talerPayUri: undefined,
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -98,7 +105,13 @@ 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(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => preparePay: async () =>
({ ({
@ -109,7 +122,8 @@ describe("Payment CTA states", () => {
({ ({
balances: [], balances: [],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -133,7 +147,13 @@ 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(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => preparePay: async () =>
({ ({
@ -148,7 +168,8 @@ describe("Payment CTA states", () => {
}, },
], ],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -172,7 +193,13 @@ 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(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => preparePay: async () =>
({ ({
@ -188,7 +215,8 @@ describe("Payment CTA states", () => {
}, },
], ],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -214,7 +242,13 @@ 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(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => preparePay: async () =>
({ ({
@ -230,7 +264,8 @@ describe("Payment CTA states", () => {
}, },
], ],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -256,7 +291,13 @@ 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(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => preparePay: async () =>
({ ({
@ -277,7 +318,8 @@ describe("Payment CTA states", () => {
type: ConfirmPayResultType.Done, type: ConfirmPayResultType.Done,
contractTerms: {}, contractTerms: {},
} as Partial<ConfirmPayResult>), } as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -317,7 +359,13 @@ 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(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => preparePay: async () =>
({ ({
@ -338,7 +386,8 @@ describe("Payment CTA states", () => {
type: ConfirmPayResultType.Pending, type: ConfirmPayResultType.Pending,
lastError: { code: 1 }, lastError: { code: 1 },
} as Partial<ConfirmPayResult>), } as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any), } as Partial<typeof wxApi> as any,
),
); );
{ {
@ -393,7 +442,13 @@ describe("Payment CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, { useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: subscriptions.saveSubscription, onUpdateNotification: subscriptions.saveSubscription,
preparePay: async () => preparePay: async () =>
({ ({
@ -409,7 +464,8 @@ describe("Payment CTA states", () => {
}, },
], ],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any), } 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(
{
talerRefundUri: undefined,
cancel: async () => {
null;
},
},
{
prepareRefund: async () => ({}), prepareRefund: async () => ({}),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}), onUpdateNotification: async () => ({}),
} as any), } as any,
),
); );
{ {
@ -64,7 +73,14 @@ 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(
{
talerRefundUri: "taler://refund/asdasdas",
cancel: async () => {
null;
},
},
{
prepareRefund: async () => prepareRefund: async () =>
({ ({
effectivePaid: "EUR:2", effectivePaid: "EUR:2",
@ -84,7 +100,8 @@ describe("Refund CTA states", () => {
} as PrepareRefundResult as any), } as PrepareRefundResult as any),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}), onUpdateNotification: async () => ({}),
} as any), } as any,
),
); );
{ {
@ -113,7 +130,14 @@ 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(
{
talerRefundUri: "taler://refund/asdasdas",
cancel: async () => {
null;
},
},
{
prepareRefund: async () => prepareRefund: async () =>
({ ({
effectivePaid: "EUR:2", effectivePaid: "EUR:2",
@ -133,7 +157,8 @@ describe("Refund CTA states", () => {
} as PrepareRefundResult as any), } as PrepareRefundResult as any),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}), onUpdateNotification: async () => ({}),
} as any), } as any,
),
); );
{ {
@ -189,7 +214,14 @@ describe("Refund CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerRefundUri: "taler://refund/asdasdas", cancel: async () => { null } }, { useComponentState(
{
talerRefundUri: "taler://refund/asdasdas",
cancel: async () => {
null;
},
},
{
prepareRefund: async () => prepareRefund: async () =>
({ ({
awaiting: Amounts.stringify(awaiting), awaiting: Amounts.stringify(awaiting),
@ -209,7 +241,8 @@ describe("Refund CTA states", () => {
} as PrepareRefundResult as any), } as PrepareRefundResult as any),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: subscriptions.saveSubscription, onUpdateNotification: subscriptions.saveSubscription,
} as any), } 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(
{
talerTipUri: undefined,
onCancel: async () => {
null;
},
},
{
prepareTip: async () => ({}), prepareTip: async () => ({}),
acceptTip: async () => ({}), acceptTip: async () => ({}),
} as any), } as any,
),
); );
{ {
@ -62,7 +68,14 @@ describe("Tip CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, { useComponentState(
{
talerTipUri: "taler://tip/asd",
onCancel: async () => {
null;
},
},
{
prepareTip: async () => prepareTip: async () =>
({ ({
accepted: tipAccepted, accepted: tipAccepted,
@ -74,7 +87,8 @@ describe("Tip CTA states", () => {
acceptTip: async () => { acceptTip: async () => {
tipAccepted = true; tipAccepted = true;
}, },
} as any), } as any,
),
); );
{ {
@ -114,7 +128,14 @@ 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(
{
talerTipUri: "taler://tip/asd",
onCancel: async () => {
null;
},
},
{
prepareTip: async () => prepareTip: async () =>
({ ({
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
@ -123,7 +144,8 @@ describe("Tip CTA states", () => {
walletTipId: "tip_id", walletTipId: "tip_id",
} as PrepareTipResult as any), } as PrepareTipResult as any),
acceptTip: async () => ({}), acceptTip: async () => ({}),
} as any), } as any,
),
); );
{ {
@ -160,7 +182,14 @@ 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(
{
talerTipUri: "taler://tip/asd",
onCancel: async () => {
null;
},
},
{
prepareTip: async () => prepareTip: async () =>
({ ({
accepted: true, accepted: true,
@ -170,7 +199,8 @@ describe("Tip CTA states", () => {
walletTipId: "tip_id", walletTipId: "tip_id",
} as PrepareTipResult as any), } as PrepareTipResult as any),
acceptTip: async () => ({}), acceptTip: async () => ({}),
} as any), } 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 {
@ -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, list: ageRestrictionOptions,
value: String(ageRestricted), value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
} : undefined; }
: undefined;
return { return {
status: "success", status: "success",
@ -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 {
@ -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, list: ageRestrictionOptions,
value: String(ageRestricted), value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
} : undefined; }
: undefined;
return { return {
status: "success", status: "success",
@ -432,4 +451,3 @@ export function useComponentStateFromURI(
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(
{
talerWithdrawUri: undefined,
cancel: async () => {
null;
},
},
{
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: "ARS:2", amount: "ARS:2",
possibleExchanges: exchanges, possibleExchanges: exchanges,
}), }),
} as any), } 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(
{
talerWithdrawUri: "taler-withdraw://",
cancel: async () => {
null;
},
},
{
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: "EUR:2", amount: "EUR:2",
possibleExchanges: [], possibleExchanges: [],
}), }),
} as any), } as any,
),
); );
{ {
@ -128,12 +144,19 @@ 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(
{
talerWithdrawUri: "taler-withdraw://",
cancel: async () => {
null;
},
},
{
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: "ARS:2", amount: "ARS:2",
possibleExchanges: exchanges, possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
}), }),
getExchangeWithdrawalInfo: getExchangeWithdrawalInfo:
async (): Promise<ExchangeWithdrawDetails> => async (): Promise<ExchangeWithdrawDetails> =>
@ -147,7 +170,8 @@ describe("Withdraw CTA states", () => {
acceptedEtag: "v1", acceptedEtag: "v1",
currentEtag: "v1", currentEtag: "v1",
}), }),
} as any), } as any,
),
); );
{ {
@ -194,12 +218,19 @@ 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(
{
talerWithdrawUri: "taler-withdraw://",
cancel: async () => {
null;
},
},
{
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: "ARS:2", amount: "ARS:2",
possibleExchanges: exchanges, possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
}), }),
getExchangeWithdrawalInfo: getExchangeWithdrawalInfo:
async (): Promise<ExchangeWithdrawDetails> => async (): Promise<ExchangeWithdrawDetails> =>
@ -214,7 +245,8 @@ describe("Withdraw CTA states", () => {
currentEtag: "v2", currentEtag: "v2",
}), }),
setExchangeTosAccepted: async () => ({}), setExchangeTosAccepted: async () => ({}),
} as any), } 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,7 +119,8 @@ 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 = `${
getPaddingRight(container) + scrollbarSize
}px`; }px`;
// .mui-fixed is a global helper. // .mui-fixed is a global helper.
@ -131,7 +132,8 @@ 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 = `${
getPaddingRight(element) + scrollbarSize
}px`; }px`;
}); });
} }

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 = {
@ -534,7 +544,7 @@ interface OffscreenCanvasRenderingContext2D
} }
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"
}`, }`,
); );
} }

View File

@ -111,10 +111,12 @@ 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 } =

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) => {