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 { CompletedView, LoadingUriView, ReadyView } from "./views.js";
export interface Props {
talerDepositUri: string | undefined,
amountStr: AmountString | undefined,
talerDepositUri: string | undefined;
amountStr: AmountString | undefined;
cancel: () => Promise<void>;
}
@ -38,7 +36,6 @@ export type State =
| State.Completed;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -63,10 +60,14 @@ export namespace State {
}
const viewMapping: StateViewMap<State> = {
"loading": Loading,
loading: Loading,
"loading-uri": LoadingUriView,
completed: CompletedView,
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/>
*/
import { Amounts, CreateDepositGroupResponse } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -41,7 +40,7 @@ export function useComponentState(
return { deposit, uri: talerDepositUri, amount };
});
if (!info) return { status: "loading", error: undefined }
if (!info) return { status: "loading", error: undefined };
if (info.hasError) {
return {
status: "loading-uri",

View File

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

View File

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

View File

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

View File

@ -22,10 +22,7 @@
import { expect } from "chai";
describe("test description", () => {
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/>
*/
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 { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
@ -37,7 +42,6 @@ export type State =
| State.Ready;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -52,20 +56,20 @@ export namespace State {
error: undefined;
uri: string;
cancel: ButtonHandler;
amount: AmountJson,
amount: AmountJson;
goToWalletManualWithdraw: (currency: string) => Promise<void>;
summary: string | undefined,
expiration: AbsoluteTime | undefined,
summary: string | undefined;
expiration: AbsoluteTime | undefined;
operationError?: TalerErrorDetail;
payStatus: PreparePayResult;
}
export interface NoBalanceForCurrency extends BaseInfo {
status: "no-balance-for-currency"
status: "no-balance-for-currency";
balance: undefined;
}
export interface NoEnoughBalance extends BaseInfo {
status: "no-enough-balance"
status: "no-enough-balance";
balance: AmountJson;
}
@ -82,9 +86,11 @@ const viewMapping: StateViewMap<State> = {
"loading-uri": LoadingUriView,
"no-balance-for-currency": ReadyView,
"no-enough-balance": ReadyView,
"ready": ReadyView,
ready: ReadyView,
};
export const InvoicePayPage = compose("InvoicePayPage", (p: Props) => useComponentState(p, wxApi), viewMapping)
export const InvoicePayPage = compose(
"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/>
*/
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 { useEffect, useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -27,11 +35,11 @@ export function useComponentState(
): State {
const hook = useAsyncAsHook(async () => {
const p2p = await api.checkPeerPullPayment({
talerUri: talerPayPullUri
})
talerUri: talerPayPullUri,
});
const balance = await api.getBalance();
return { p2p, balance }
})
return { p2p, balance };
});
useEffect(() => {
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) {
return {
status: "loading",
error: undefined,
}
};
}
if (hook.hasError) {
return {
@ -56,13 +66,17 @@ export function useComponentState(
// 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 amount = Amounts.parseOrThrow(amountStr)
const summary: string | undefined = contractTerms?.summary
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration
const amountStr: string = contractTerms?.amount;
const amount = Amounts.parseOrThrow(amountStr);
const summary: string | undefined = contractTerms?.summary;
const expiration: TalerProtocolTimestamp | undefined =
contractTerms?.purse_expiration;
const foundBalance = hook.response.balance.balances.find(
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
@ -71,35 +85,32 @@ export function useComponentState(
const paymentPossible: PreparePayResult = {
status: PreparePayResultType.PaymentPossible,
proposalId: "fakeID",
contractTerms: {
} as any,
contractTerms: {} as any,
contractTermsHash: "asd",
amountRaw: hook.response.p2p.amount,
amountEffective: hook.response.p2p.amount,
noncePriv: "",
} as PreparePayResult
} as PreparePayResult;
const insufficientBalance: PreparePayResult = {
status: PreparePayResultType.InsufficientBalance,
proposalId: "fakeID",
contractTerms: {
} as any,
contractTerms: {} as any,
amountRaw: hook.response.p2p.amount,
noncePriv: "",
}
};
const baseResult = {
uri: talerPayPullUri,
cancel: {
onClick: onClose
onClick: onClose,
},
amount,
goToWalletManualWithdraw,
summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
operationError,
}
};
if (!foundBalance) {
return {
@ -108,20 +119,21 @@ export function useComponentState(
balance: undefined,
...baseResult,
payStatus: insufficientBalance,
}
};
}
const foundAmount = Amounts.parseOrThrow(foundBalance.available);
//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 {
status: 'no-enough-balance',
status: "no-enough-balance",
error: undefined,
balance: foundAmount,
...baseResult,
payStatus: insufficientBalance,
}
};
}
// if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
@ -135,19 +147,18 @@ export function useComponentState(
async function accept(): Promise<void> {
try {
const resp = await api.acceptPeerPullPayment({
peerPullPaymentIncomingId
})
await onClose()
peerPullPaymentIncomingId,
});
await onClose();
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail)
setOperationError(e.errorDetail);
}
console.error(e)
throw Error("error trying to accept")
console.error(e);
throw Error("error trying to accept");
}
}
return {
status: "ready",
error: undefined,
@ -155,7 +166,7 @@ export function useComponentState(
payStatus: paymentPossible,
balance: foundAmount,
accept: {
onClick: accept
onClick: accept,
},
}
};
}

View File

@ -22,10 +22,7 @@
import { expect } from "chai";
describe("test description", () => {
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/>
*/
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 { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
@ -24,8 +31,6 @@ import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { LoadingUriView, BaseView } from "./views.js";
export interface Props {
talerPayUri?: string;
goToWalletManualWithdraw: (amount?: string) => Promise<void>;
@ -42,7 +47,6 @@ export type State =
| State.Confirmed;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -60,12 +64,12 @@ export namespace State {
cancel: () => Promise<void>;
}
export interface NoBalanceForCurrency extends BaseInfo {
status: "no-balance-for-currency"
status: "no-balance-for-currency";
payStatus: PreparePayResult;
balance: undefined;
}
export interface NoEnoughBalance extends BaseInfo {
status: "no-enough-balance"
status: "no-enough-balance";
payStatus: PreparePayResult;
balance: AmountJson;
}
@ -101,4 +105,8 @@ const viewMapping: StateViewMap<State> = {
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/>
*/
import { AmountJson, Amounts, ConfirmPayResult, ConfirmPayResultType, NotificationType, PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util";
import {
AmountJson,
Amounts,
ConfirmPayResult,
ConfirmPayResultType,
NotificationType,
PreparePayResultType,
TalerErrorCode,
} from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -82,8 +89,9 @@ export function useComponentState(
uri: hook.response.uri,
amount,
error: undefined,
cancel, goToWalletManualWithdraw
}
cancel,
goToWalletManualWithdraw,
};
if (!foundBalance) {
return {
@ -91,7 +99,7 @@ export function useComponentState(
balance: undefined,
payStatus,
...baseResult,
}
};
}
const foundAmount = Amounts.parseOrThrow(foundBalance.available);
@ -109,11 +117,11 @@ export function useComponentState(
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
return {
status: 'no-enough-balance',
status: "no-enough-balance",
balance: foundAmount,
payStatus,
...baseResult,
}
};
}
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
@ -125,7 +133,6 @@ export function useComponentState(
};
}
async function doPayment(): Promise<void> {
try {
if (payStatus.status !== "payment-possible") {
@ -169,8 +176,6 @@ export function useComponentState(
payHandler,
payStatus,
...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 () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: undefined, cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: undefined,
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
} as Partial<typeof wxApi> as any,
),
);
{
@ -98,18 +105,25 @@ describe("Payment CTA states", () => {
it("should response with no balance", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:10",
status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:10",
status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
);
{
@ -133,22 +147,29 @@ describe("Payment CTA states", () => {
it("should not be able to pay if there is no enough balance", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:10",
status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:5",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:10",
status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:5",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
);
{
@ -172,23 +193,30 @@ describe("Payment CTA states", () => {
it("should be able to pay (without fee)", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:10",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:10",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
);
{
@ -214,23 +242,30 @@ describe("Payment CTA states", () => {
it("should be able to pay (with fee)", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
);
{
@ -256,28 +291,35 @@ describe("Payment CTA states", () => {
it("should get confirmation done after pay successfully", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
confirmPay: async () =>
({
type: ConfirmPayResultType.Done,
contractTerms: {},
} as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
confirmPay: async () =>
({
type: ConfirmPayResultType.Done,
contractTerms: {},
} as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any,
),
);
{
@ -317,28 +359,35 @@ describe("Payment CTA states", () => {
it("should not stay in ready state after pay with error", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
confirmPay: async () =>
({
type: ConfirmPayResultType.Pending,
lastError: { code: 1 },
} as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: nullFunction,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: "USD:15",
},
],
} as Partial<BalancesResponse>),
confirmPay: async () =>
({
type: ConfirmPayResultType.Pending,
lastError: { code: 1 },
} as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any,
),
);
{
@ -393,23 +442,30 @@ describe("Payment CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState({ talerPayUri: "taller://pay", cancel: nullFunction, goToWalletManualWithdraw: nullFunction }, {
onUpdateNotification: subscriptions.saveSubscription,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: Amounts.stringify(availableBalance),
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any),
useComponentState(
{
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
},
{
onUpdateNotification: subscriptions.saveSubscription,
preparePay: async () =>
({
amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>),
getBalance: async () =>
({
balances: [
{
available: Amounts.stringify(availableBalance),
},
],
} as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any,
),
);
{

View File

@ -21,9 +21,13 @@ import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.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 {
talerRefundUri?: string;
@ -39,7 +43,6 @@ export type State =
| State.Completed;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -75,13 +78,11 @@ export namespace State {
export interface InProgress extends BaseInfo {
status: "in-progress";
error: undefined;
}
export interface Completed extends BaseInfo {
status: "completed";
error: undefined;
}
}
const viewMapping: StateViewMap<State> = {
@ -93,4 +94,8 @@ const viewMapping: StateViewMap<State> = {
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/>
*/
import { Amounts, NotificationType } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -40,7 +39,7 @@ export function useComponentState(
});
if (!info) {
return { status: "loading", error: undefined }
return { status: "loading", error: undefined };
}
if (info.hasError) {
return {
@ -67,7 +66,7 @@ export function useComponentState(
products: info.response.refund.info.products,
awaitingAmount: Amounts.parseOrThrow(refund.awaiting),
error: undefined,
}
};
if (ignored) {
return {

View File

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

View File

@ -20,13 +20,14 @@ import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.js";
import {
Props as TermsOfServiceSectionProps
} from "../TermsOfServiceSection.js";
import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.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 {
talerTipUri?: string;
@ -42,7 +43,6 @@ export type State =
| State.Ignored;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -77,9 +77,13 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"accepted": AcceptedView,
"ignored": IgnoredView,
"ready": ReadyView,
accepted: AcceptedView,
ignored: IgnoredView,
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/>
*/
import { Amounts } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -37,7 +36,7 @@ export function useComponentState(
return {
status: "loading",
error: undefined,
}
};
}
if (tipInfo.hasError) {
return {
@ -59,9 +58,9 @@ export function useComponentState(
amount: Amounts.parseOrThrow(tip.tipAmountEffective),
error: undefined,
cancel: {
onClick: onCancel
}
}
onClick: onCancel,
},
};
if (tipIgnored) {
return {
@ -85,4 +84,3 @@ export function useComponentState(
},
};
}

View File

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

View File

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

View File

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

View File

@ -22,10 +22,7 @@
import { expect } from "chai";
describe("test description", () => {
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/>
*/
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 { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
@ -28,13 +32,9 @@ export interface Props {
onClose: () => Promise<void>;
}
export type State =
| State.Loading
| State.LoadingUriError
| State.Ready;
export type State = State.Loading | State.LoadingUriError | State.Ready;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -51,7 +51,7 @@ export namespace State {
}
export interface Ready extends BaseInfo {
status: "ready";
amount: AmountJson,
amount: AmountJson;
summary: string | undefined;
expiration: AbsoluteTime | undefined;
error: undefined;
@ -63,9 +63,11 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"ready": ReadyView,
ready: ReadyView,
};
export const TransferPickupPage = compose("TransferPickupPage", (p: Props) => useComponentState(p, wxApi), viewMapping)
export const TransferPickupPage = compose(
"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/>
*/
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 { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -28,15 +33,17 @@ export function useComponentState(
const hook = useAsyncAsHook(async () => {
return await api.checkPeerPushPayment({
talerUri: talerPayPushUri,
})
}, [])
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
});
}, []);
const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (!hook) {
return {
status: "loading",
error: undefined,
}
};
}
if (hook.hasError) {
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 summary: string | undefined = contractTerms?.summary
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration
const amount: string = contractTerms?.amount;
const summary: string | undefined = contractTerms?.summary;
const expiration: TalerProtocolTimestamp | undefined =
contractTerms?.purse_expiration;
async function accept(): Promise<void> {
try {
const resp = await api.acceptPeerPushPayment({
peerPushPaymentIncomingId
})
await onClose()
peerPushPaymentIncomingId,
});
await onClose();
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail)
setOperationError(e.errorDetail);
}
console.error(e)
throw Error("error trying to accept")
console.error(e);
throw Error("error trying to accept");
}
}
return {
@ -70,13 +82,13 @@ export function useComponentState(
amount: Amounts.parseOrThrow(amount),
error: undefined,
accept: {
onClick: accept
onClick: accept,
},
summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
cancel: {
onClick: onClose
onClick: onClose,
},
operationError
}
operationError,
};
}

View File

@ -22,10 +22,7 @@
import { expect } from "chai";
describe("test description", () => {
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 { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.js";
import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js";
import {
Props as TermsOfServiceSectionProps
} from "../TermsOfServiceSection.js";
import { useComponentStateFromParams, useComponentStateFromURI } from "./state.js";
import { CompletedView, LoadingExchangeView, LoadingInfoView, LoadingUriView, SuccessView } from "./views.js";
useComponentStateFromParams,
useComponentStateFromURI,
} from "./state.js";
import {
CompletedView,
LoadingExchangeView,
LoadingInfoView,
LoadingUriView,
SuccessView,
} from "./views.js";
export interface PropsFromURI {
talerWithdrawUri: string | undefined;
@ -46,7 +52,6 @@ export type State =
| State.Completed;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -99,5 +104,13 @@ const viewMapping: StateViewMap<State> = {
success: SuccessView,
};
export const WithdrawPageFromURI = compose("WithdrawPageFromURI", (p: PropsFromURI) => useComponentStateFromURI(p, wxApi), viewMapping)
export const WithdrawPageFromParams = compose("WithdrawPageFromParams", (p: PropsFromParams) => useComponentStateFromParams(p, wxApi), viewMapping)
export const WithdrawPageFromURI = compose(
"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/>
*/
import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
@ -27,7 +26,6 @@ export function useComponentStateFromParams(
{ amount, cancel }: PropsFromParams,
api: typeof wxApi,
): State {
const [ageRestricted, setAgeRestricted] = useState(0);
const exchangeHook = useAsyncAsHook(api.listExchanges);
@ -40,14 +38,20 @@ export function useComponentStateFromParams(
const chosenAmount = Amounts.parseOrThrow(amount);
// 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
*/
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);
@ -59,7 +63,7 @@ export function useComponentStateFromParams(
* about the withdrawal
*/
const amountHook = useAsyncAsHook(async () => {
if (!exchange) return undefined
if (!exchange) return undefined;
const info = await api.getExchangeWithdrawalInfo({
exchangeBaseUrl: exchange.exchangeBaseUrl,
@ -71,9 +75,12 @@ export function useComponentStateFromParams(
const withdrawAmount = {
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
}
};
return { amount: withdrawAmount, ageRestrictionOptions: info.ageRestrictionOptions };
return {
amount: withdrawAmount,
ageRestrictionOptions: info.ageRestrictionOptions,
};
}, [exchangeHookDep]);
const [reviewing, setReviewing] = useState<boolean>(false);
@ -85,7 +92,7 @@ export function useComponentStateFromParams(
const [doingWithdraw, setDoingWithdraw] = 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) {
return {
status: "loading-uri",
@ -125,7 +132,7 @@ export function useComponentStateFromParams(
}
if (!amountHook) {
return { status: "loading", error: undefined }
return { status: "loading", error: undefined };
}
if (amountHook.hasError) {
return {
@ -149,8 +156,8 @@ export function useComponentStateFromParams(
const { state: termsState } = (!terms
? undefined
: terms.hasError
? undefined
: terms.response) || { state: undefined };
? undefined
: terms.response) || { state: undefined };
async function onAccept(accepted: boolean): Promise<void> {
if (!termsState || !exchange) return;
@ -173,21 +180,25 @@ export function useComponentStateFromParams(
termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions = amountHook.response.
ageRestrictionOptions?.
reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {} as Record<string, string>)
const ageRestrictionOptions =
amountHook.response.ageRestrictionOptions?.reduce(
(p, c) => ({ ...p, [c]: `under ${c}` }),
{} as Record<string, string>,
);
const ageRestrictionEnabled = ageRestrictionOptions !== undefined
const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
if (ageRestrictionEnabled) {
ageRestrictionOptions["0"] = "Not restricted";
}
//TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled ? {
list: ageRestrictionOptions,
value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
} : undefined;
const ageRestriction = ageRestrictionEnabled
? {
list: ageRestrictionOptions,
value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
}
: undefined;
return {
status: "success",
@ -207,12 +218,12 @@ export function useComponentStateFromParams(
tosProps: !termsState
? undefined
: {
onAccept,
onReview: setReviewing,
reviewed: reviewed,
reviewing: reviewing,
terms: termsState,
},
onAccept,
onReview: setReviewing,
reviewed: reviewed,
reviewing: reviewing,
terms: termsState,
},
mustAcceptFirst,
cancel,
};
@ -233,7 +244,7 @@ export function useComponentStateFromURI(
const uriInfo = await api.getWithdrawalDetailsForUri({
talerWithdrawUri,
});
const { amount, defaultExchangeBaseUrl } = uriInfo
const { amount, defaultExchangeBaseUrl } = uriInfo;
return { amount, thisExchange: defaultExchangeBaseUrl };
});
@ -245,14 +256,15 @@ export function useComponentStateFromURI(
? undefined
: uriInfoHook.response;
/**
* For the exchange selected, bring the status of the terms of service
*/
const terms = useAsyncAsHook(async () => {
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);
@ -276,9 +288,12 @@ export function useComponentStateFromURI(
const withdrawAmount = {
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
}
};
return { amount: withdrawAmount, ageRestrictionOptions: info.ageRestrictionOptions };
return {
amount: withdrawAmount,
ageRestrictionOptions: info.ageRestrictionOptions,
};
}, [uriHookDep]);
const [reviewing, setReviewing] = useState<boolean>(false);
@ -290,7 +305,7 @@ export function useComponentStateFromURI(
const [doingWithdraw, setDoingWithdraw] = 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) {
return {
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);
@ -339,7 +354,7 @@ export function useComponentStateFromURI(
}
if (!amountHook) {
return { status: "loading", error: undefined }
return { status: "loading", error: undefined };
}
if (amountHook.hasError) {
return {
@ -363,8 +378,8 @@ export function useComponentStateFromURI(
const { state: termsState } = (!terms
? undefined
: terms.hasError
? undefined
: terms.response) || { state: undefined };
? undefined
: terms.response) || { state: undefined };
async function onAccept(accepted: boolean): Promise<void> {
if (!termsState || !thisExchange) return;
@ -387,21 +402,25 @@ export function useComponentStateFromURI(
termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions = amountHook.response.
ageRestrictionOptions?.
reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {} as Record<string, string>)
const ageRestrictionOptions =
amountHook.response.ageRestrictionOptions?.reduce(
(p, c) => ({ ...p, [c]: `under ${c}` }),
{} as Record<string, string>,
);
const ageRestrictionEnabled = ageRestrictionOptions !== undefined
const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
if (ageRestrictionEnabled) {
ageRestrictionOptions["0"] = "Not restricted";
}
//TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled ? {
list: ageRestrictionOptions,
value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
} : undefined;
const ageRestriction = ageRestrictionEnabled
? {
list: ageRestrictionOptions,
value: String(ageRestricted),
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
}
: undefined;
return {
status: "success",
@ -422,14 +441,13 @@ export function useComponentStateFromURI(
tosProps: !termsState
? undefined
: {
onAccept,
onReview: setReviewing,
reviewed: reviewed,
reviewing: reviewing,
terms: termsState,
},
onAccept,
onReview: setReviewing,
reviewed: reviewed,
reviewing: reviewing,
terms: termsState,
},
mustAcceptFirst,
cancel,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ export async function requestClipboardPermissions(): Promise<boolean> {
rej(le);
}
res(resp);
})
});
});
}
@ -130,13 +130,13 @@ type HeaderListenerFunc = (
) => void;
let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
type TabListenerFunc = (
tabId: number, info: chrome.tabs.TabChangeInfo,
) => void;
type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
let currentTabListener: TabListenerFunc | undefined = undefined;
export function containsTalerHeaderListener(): boolean {
return currentHeaderListener !== undefined || currentTabListener !== undefined;
return (
currentHeaderListener !== undefined || currentTabListener !== undefined
);
}
export async function removeHostPermissions(): Promise<boolean> {
@ -147,9 +147,11 @@ export async function removeHostPermissions(): Promise<boolean> {
) {
chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
}
if (currentTabListener &&
chrome?.tabs?.onUpdated?.hasListener(currentTabListener)) {
chrome.tabs.onUpdated.removeListener(currentTabListener)
if (
currentTabListener &&
chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
) {
chrome.tabs.onUpdated.removeListener(currentTabListener);
}
currentHeaderListener = undefined;
@ -413,20 +415,25 @@ function registerTalerHeaderListener(
.map((h) => h.value)
.filter((value): value is string => !!value);
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]);
}
}
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 (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId);
if (!uri) return;
logger.info(`Found a Taler URI in the tab ${tabId}`)
callback(tabId, uri)
logger.info(`Found a Taler URI in the tab ${tabId}`);
callback(tabId, uri);
}
const prevHeaderListener = currentHeaderListener;
@ -442,14 +449,18 @@ function registerTalerHeaderListener(
) {
chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
}
if (prevTabListener && chrome?.tabs?.onUpdated?.hasListener(prevTabListener)) {
chrome.tabs.onUpdated.removeListener(prevTabListener)
if (
prevTabListener &&
chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
) {
chrome.tabs.onUpdated.removeListener(prevTabListener);
}
//if the result was positive, add the headerListener
if (result) {
const headersEvent: chrome.webRequest.WebResponseHeadersEvent | undefined =
chrome?.webRequest?.onHeadersReceived;
const headersEvent:
| chrome.webRequest.WebResponseHeadersEvent
| undefined = chrome?.webRequest?.onHeadersReceived;
if (headersEvent) {
headersEvent.addListener(headerListener, { urls: ["<all_urls>"] }, [
"responseHeaders",
@ -472,7 +483,6 @@ function registerTalerHeaderListener(
}
});
});
}
const alertIcons = {
@ -515,26 +525,26 @@ function setAlertedIcon(): void {
interface OffscreenCanvasRenderingContext2D
extends CanvasState,
CanvasTransform,
CanvasCompositing,
CanvasImageSmoothing,
CanvasFillStrokeStyles,
CanvasShadowStyles,
CanvasFilters,
CanvasRect,
CanvasDrawPath,
CanvasUserInterface,
CanvasText,
CanvasDrawImage,
CanvasImageData,
CanvasPathDrawingStyles,
CanvasTextDrawingStyles,
CanvasPath {
CanvasTransform,
CanvasCompositing,
CanvasImageSmoothing,
CanvasFillStrokeStyles,
CanvasShadowStyles,
CanvasFilters,
CanvasRect,
CanvasDrawPath,
CanvasUserInterface,
CanvasText,
CanvasDrawImage,
CanvasImageData,
CanvasPathDrawingStyles,
CanvasTextDrawingStyles,
CanvasPath {
readonly canvas: OffscreenCanvas;
}
declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D;
new(): OffscreenCanvasRenderingContext2D;
new (): OffscreenCanvasRenderingContext2D;
};
interface OffscreenCanvas extends EventTarget {
@ -547,7 +557,7 @@ interface OffscreenCanvas extends EventTarget {
}
declare const OffscreenCanvas: {
prototype: OffscreenCanvas;
new(width: number, height: number): OffscreenCanvas;
new (width: number, height: 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> {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function findTalerUriInClipboard(): Promise<string | undefined> {
try {
//It looks like clipboard promise does not return, so we need a timeout
const textInClipboard = await Promise.any([
timeout(100),
window.navigator.clipboard.readText()
])
window.navigator.clipboard.readText(),
]);
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) {
logger.error("could not read clipboard", e)
return undefined
logger.error("could not read clipboard", e);
return undefined;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,7 @@
import { expect } from "chai";
describe("test description", () => {
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/>
*/
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 { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.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 {
currency?: string;
@ -39,7 +48,6 @@ export type State =
| State.NoExchanges;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
@ -75,13 +83,16 @@ export namespace State {
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"comparing": ComparingView,
comparing: ComparingView,
"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/>
*/
import { FeeDescription, OperationMap } from "@gnu-taler/taler-util";
import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
@ -26,26 +25,32 @@ export function useComponentState(
{ onCancel, onSelection, currency }: Props,
api: typeof wxApi,
): State {
const initialValue = 0
const initialValue = 0;
const [value, setValue] = useState(String(initialValue));
const hook = useAsyncAsHook(async () => {
const { exchanges } = await api.listExchanges()
const { exchanges } = await api.listExchanges();
const selectedIdx = parseInt(value, 10)
const selectedExchange = exchanges.length == 0 ? undefined : exchanges[selectedIdx]
const selected = !selectedExchange ? undefined : await api.getExchangeDetailedInfo(selectedExchange.exchangeBaseUrl)
const selectedIdx = parseInt(value, 10);
const selectedExchange =
exchanges.length == 0 ? undefined : exchanges[selectedIdx];
const selected = !selectedExchange
? undefined
: await api.getExchangeDetailedInfo(selectedExchange.exchangeBaseUrl);
const initialExchange = selectedIdx === initialValue ? undefined : exchanges[initialValue]
const original = !initialExchange ? undefined : await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl)
return { exchanges, selected, original }
const initialExchange =
selectedIdx === initialValue ? undefined : exchanges[initialValue];
const original = !initialExchange
? undefined
: await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl);
return { exchanges, selected, original };
});
if (!hook) {
return {
status: "loading",
error: undefined,
}
};
}
if (hook.hasError) {
return {
@ -60,11 +65,14 @@ export function useComponentState(
//!selected <=> exchanges.length === 0
return {
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) {
// !original <=> selected == original
@ -74,24 +82,36 @@ export function useComponentState(
list: exchangeMap,
value: value,
onChange: async (v) => {
setValue(v)
}
setValue(v);
},
},
error: undefined,
onClose: {
onClick: onCancel
onClick: onCancel,
},
selected,
timeline: selected.feesDescription
}
timeline: selected.feesDescription,
};
}
const pairTimeline: OperationMap<FeeDescription[]> = {
deposit: createDenominationPairTimeline(selected.feesDescription.deposit, original.feesDescription.deposit),
refresh: createDenominationPairTimeline(selected.feesDescription.refresh, original.feesDescription.refresh),
refund: createDenominationPairTimeline(selected.feesDescription.refund, original.feesDescription.refund),
withdraw: createDenominationPairTimeline(selected.feesDescription.withdraw, original.feesDescription.withdraw),
}
deposit: createDenominationPairTimeline(
selected.feesDescription.deposit,
original.feesDescription.deposit,
),
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 {
status: "comparing",
@ -99,23 +119,21 @@ export function useComponentState(
list: exchangeMap,
value: value,
onChange: async (v) => {
setValue(v)
}
setValue(v);
},
},
error: undefined,
onReset: {
onClick: async () => {
setValue(String(initialValue))
}
setValue(String(initialValue));
},
},
onSelect: {
onClick: async () => {
onSelection(selected.exchangeBaseUrl)
}
onSelection(selected.exchangeBaseUrl);
},
},
selected,
pairTimeline,
}
};
}

View File

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

View File

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

View File

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