exchange selection for invoices and some fixes

This commit is contained in:
Sebastian 2022-09-20 20:26:41 -03:00
parent 7adaeff0a5
commit 859991a40c
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
9 changed files with 122 additions and 112 deletions

View File

@ -14,14 +14,19 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { LoadingUriView, ReadyView } from "./views.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { LoadingUriView, ReadyView } from "./views.js";
export interface Props {
amount: string;
@ -29,7 +34,12 @@ export interface Props {
onSuccess: (tx: string) => Promise<void>;
}
export type State = State.Loading | State.LoadingUriError | State.Ready;
export type State = State.Loading
| State.LoadingUriError
| State.Ready
| SelectExchangeState.Selecting
| SelectExchangeState.NoExchange
;
export namespace State {
export interface Loading {
@ -63,6 +73,8 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"no-exchange": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
ready: ReadyView,
};

View File

@ -14,26 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/* eslint-disable react-hooks/rules-of-hooks */
import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
type RecursiveState<S extends object> = S | (() => RecursiveState<S>)
export function useComponentState(
{ amount: amountStr, onClose, onSuccess }: Props,
api: typeof wxApi,
): State {
): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState("");
const hook = useAsyncAsHook(api.listExchanges);
const [exchangeIdx, setExchangeIdx] = useState("0");
const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (!hook) {
return {
@ -48,56 +46,68 @@ export function useComponentState(
};
}
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)];
const exchangeList = hook.response.exchanges
async function accept(): Promise<void> {
try {
const resp = await api.initiatePeerPullPayment({
amount: Amounts.stringify(amount),
exchangeBaseUrl: selected.exchangeBaseUrl,
partialContractTerms: {
summary: subject,
},
});
return () => {
const [subject, setSubject] = useState("");
onSuccess(resp.transactionId);
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail);
}
console.error(e);
throw Error("error trying to accept");
const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
const selectedExchange = useSelectedExchange({ currency: amount.currency, defaultExchange: undefined, list: exchangeList })
if (selectedExchange.status !== 'ready') {
return selectedExchange
}
const exchange = selectedExchange.selected
async function accept(): Promise<void> {
try {
const resp = await api.initiatePeerPullPayment({
amount: Amounts.stringify(amount),
exchangeBaseUrl: exchange.exchangeBaseUrl,
partialContractTerms: {
summary: subject,
},
});
onSuccess(resp.transactionId);
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail);
}
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),
},
doSelectExchange: selectedExchange.doSelect,
invalid: !subject || Amounts.isZero(amount),
exchangeUrl: exchange.exchangeBaseUrl,
create: {
onClick: accept,
},
cancel: {
onClick: onClose,
},
chosenAmount: amount,
toBeReceived: amount,
error: undefined,
operationError,
};
}
return {
status: "ready",
subject: {
error: !subject ? "cant be empty" : undefined,
value: subject,
onInput: async (e) => setSubject(e),
},
doSelectExchange: {
//FIX
},
invalid: !subject || Amounts.isZero(amount),
exchangeUrl: selected.exchangeBaseUrl,
create: {
onClick: accept,
},
cancel: {
onClick: onClose,
},
chosenAmount: amount,
toBeReceived: amount,
error: undefined,
operationError,
};
}

View File

@ -17,25 +17,25 @@
import { AmountJson } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.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 {
useComponentStateFromParams,
useComponentStateFromURI,
useComponentStateFromURI
} from "./state.js";
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import {
LoadingExchangeView,
LoadingInfoView,
LoadingUriView,
SuccessView,
SuccessView
} from "./views.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
export interface PropsFromURI {
talerWithdrawUri: string | undefined;
@ -52,8 +52,8 @@ export interface PropsFromParams {
export type State =
| State.Loading
| State.LoadingUriError
| State.LoadingExchangeError
| State.LoadingInfoError
| SelectExchangeState.NoExchange
| SelectExchangeState.Selecting
| State.Success;
@ -66,10 +66,6 @@ export namespace State {
status: "loading-error";
error: HookError;
}
export interface LoadingExchangeError {
status: "no-exchange";
error: undefined,
}
export interface LoadingInfoError {
status: "loading-info";
error: HookError;
@ -100,8 +96,8 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-error": LoadingUriView,
"no-exchange": LoadingExchangeView,
"loading-info": LoadingInfoView,
"no-exchange": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
success: SuccessView,
};

View File

@ -36,8 +36,6 @@ export function useComponentStateFromParams(
return { amount: Amounts.parseOrThrow(amount), exchanges };
});
console.log("uri info", uriInfoHook)
if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) {
@ -80,7 +78,6 @@ export function useComponentStateFromURI(
return { talerWithdrawUri, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, exchanges };
});
console.log("uri info", uriInfoHook)
if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) {
@ -111,20 +108,11 @@ type ManualOrManagedWithdrawFunction = (exchange: string, ageRestricted: number
function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, cancel: () => Promise<void>, onSuccess: (txid: string) => Promise<void>, talerWithdrawUri: string | undefined, chosenAmount: AmountJson, exchangeList: ExchangeListItem[], defaultExchange: string | undefined, api: typeof wxApi,): RecursiveState<State> {
//FIXME: use substates here
const selectedExchange = useSelectedExchange({ currency: chosenAmount.currency, defaultExchange, list: exchangeList })
if (selectedExchange.status === 'no-exchange') {
return {
status: "no-exchange",
error: undefined,
}
}
if (selectedExchange.status === 'selecting-exchange') {
if (selectedExchange.status !== 'ready') {
return selectedExchange
}
console.log("exchange selected", selectedExchange.selected)
return () => {
@ -142,7 +130,7 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can
return { state };
}, []);
console.log("terms", terms)
/**
* With the exchange and amount, ask the wallet the information
* about the withdrawal

View File

@ -53,16 +53,6 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
);
}
export function LoadingExchangeView(p: State.LoadingExchangeError): VNode {
const { i18n } = useTranslationContext();
return (
<ErrorMessage
title={<i18n.Translate>Could not get a default exchange, please check configuration</i18n.Translate>}
/>
);
}
export function LoadingInfoView({ error }: State.LoadingInfoError): VNode {
const { i18n } = useTranslationContext();

View File

@ -24,6 +24,7 @@ export namespace State {
export interface NoExchange {
status: "no-exchange"
error: undefined;
currency: string | undefined;
}
export interface Ready {
status: "ready",
@ -59,25 +60,27 @@ export function useSelectedExchange({ currency, defaultExchange, list }: Props):
return {
status: "no-exchange",
error: undefined,
currency: undefined,
}
}
const firstByCurrency = list.find((e) => e.currency === currency)
if (!firstByCurrency) {
const listCurrency = list.filter((e) => e.currency === currency)
if (!listCurrency.length) {
// there should be at least one exchange for this currency
return {
status: "no-exchange",
error: undefined,
currency,
}
}
if (isSelecting) {
const currentExchange = selectedExchange ?? defaultExchange ?? firstByCurrency.exchangeBaseUrl;
const currentExchange = selectedExchange ?? defaultExchange ?? listCurrency[0].exchangeBaseUrl;
return {
status: "selecting-exchange",
error: undefined,
list,
list: listCurrency,
currency,
currentExchange: currentExchange,
onSelection: async (exchangeBaseUrl: string) => {
@ -120,6 +123,6 @@ export function useSelectedExchange({ currency, defaultExchange, list }: Props):
doSelect: {
onClick: async () => setIsSelecting(true)
},
selected: firstByCurrency
selected: listCurrency[0]
}
}

View File

@ -41,13 +41,16 @@ export interface Props {
onCancel: () => Promise<void>;
onSelection: (exchange: string) => Promise<void>;
}
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
export type State =
| State.Loading
| State.LoadingUriError
| State.Ready
| State.Comparing
| State.NoExchanges;
| SelectExchangeState.NoExchange;
export namespace State {
export interface Loading {
@ -66,11 +69,6 @@ export namespace State {
error: undefined;
}
export interface NoExchanges {
status: "no-exchanges";
error: undefined;
}
export interface Ready extends BaseInfo {
status: "ready";
timeline: OperationMap<FeeDescription[]>;
@ -89,7 +87,7 @@ const viewMapping: StateViewMap<State> = {
loading: Loading,
"error-loading": ErrorLoadingView,
comparing: ComparingView,
"no-exchanges": NoExchangesView,
"no-exchange": NoExchangesView,
ready: ReadyView,
};

View File

@ -47,7 +47,7 @@ export function useComponentState(
? undefined
: await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl);
return { exchanges, selected, original };
});
}, [value]);
if (!hook) {
return {
@ -67,13 +67,14 @@ export function useComponentState(
if (!selected) {
//!selected <=> exchanges.length === 0
return {
status: "no-exchanges",
status: "no-exchange",
error: undefined,
currency: undefined,
};
}
const exchangeMap = exchanges.reduce(
(prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }),
(prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }),
{} as Record<string, string>,
);

View File

@ -31,6 +31,9 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import arrowDown from "../../svg/chevron-down.svg";
import { State } from "./index.js";
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
const ButtonGroup = styled.div`
& > button {
@ -112,11 +115,20 @@ export function ErrorLoadingView({ error }: State.LoadingUriError): VNode {
);
}
export function NoExchangesView(state: State.NoExchanges): VNode {
export function NoExchangesView({currency}: SelectExchangeState.NoExchange): VNode {
const { i18n } = useTranslationContext();
if (!currency) {
return (
<div>
<i18n.Translate>could not find any exchange</i18n.Translate>
</div>
);
}
return (
<div>
<i18n.Translate>no exchanges</i18n.Translate>
<i18n.Translate>could not find any exchange for the currency {currency}</i18n.Translate>
</div>
);
}