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/> 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 { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.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 { 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 * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { LoadingUriView, ReadyView } from "./views.js";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
export interface Props { export interface Props {
amount: string; amount: string;
@ -29,7 +34,12 @@ export interface Props {
onSuccess: (tx: string) => Promise<void>; 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 namespace State {
export interface Loading { export interface Loading {
@ -63,6 +73,8 @@ export namespace State {
const viewMapping: StateViewMap<State> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"no-exchange": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
ready: ReadyView, ready: ReadyView,
}; };

View File

@ -14,26 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/* eslint-disable react-hooks/rules-of-hooks */
import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
type RecursiveState<S extends object> = S | (() => RecursiveState<S>)
export function useComponentState( export function useComponentState(
{ amount: amountStr, onClose, onSuccess }: Props, { amount: amountStr, onClose, onSuccess }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState("");
const hook = useAsyncAsHook(api.listExchanges); const hook = useAsyncAsHook(api.listExchanges);
const [exchangeIdx, setExchangeIdx] = useState("0");
const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined
>(undefined);
if (!hook) { if (!hook) {
return { return {
@ -48,20 +46,29 @@ export function useComponentState(
}; };
} }
const exchanges = hook.response.exchanges.filter( const exchangeList = hook.response.exchanges
(e) => e.currency === amount.currency,
); return () => {
const exchangeMap = exchanges.reduce( const [subject, setSubject] = useState("");
(prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }),
{} as Record<string, string>, const [operationError, setOperationError] = useState<
); TalerErrorDetail | undefined
const selected = exchanges[Number(exchangeIdx)]; >(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> { async function accept(): Promise<void> {
try { try {
const resp = await api.initiatePeerPullPayment({ const resp = await api.initiatePeerPullPayment({
amount: Amounts.stringify(amount), amount: Amounts.stringify(amount),
exchangeBaseUrl: selected.exchangeBaseUrl, exchangeBaseUrl: exchange.exchangeBaseUrl,
partialContractTerms: { partialContractTerms: {
summary: subject, summary: subject,
}, },
@ -84,11 +91,9 @@ export function useComponentState(
value: subject, value: subject,
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },
doSelectExchange: { doSelectExchange: selectedExchange.doSelect,
//FIX
},
invalid: !subject || Amounts.isZero(amount), invalid: !subject || Amounts.isZero(amount),
exchangeUrl: selected.exchangeBaseUrl, exchangeUrl: exchange.exchangeBaseUrl,
create: { create: {
onClick: accept, onClick: accept,
}, },
@ -101,3 +106,8 @@ export function useComponentState(
operationError, operationError,
}; };
} }
}

View File

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

View File

@ -36,8 +36,6 @@ export function useComponentStateFromParams(
return { amount: Amounts.parseOrThrow(amount), exchanges }; return { amount: Amounts.parseOrThrow(amount), exchanges };
}); });
console.log("uri info", uriInfoHook)
if (!uriInfoHook) return { status: "loading", error: undefined }; if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) { if (uriInfoHook.hasError) {
@ -80,7 +78,6 @@ export function useComponentStateFromURI(
return { talerWithdrawUri, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, exchanges }; return { talerWithdrawUri, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, exchanges };
}); });
console.log("uri info", uriInfoHook)
if (!uriInfoHook) return { status: "loading", error: undefined }; if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) { 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> { 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 }) const selectedExchange = useSelectedExchange({ currency: chosenAmount.currency, defaultExchange, list: exchangeList })
if (selectedExchange.status === 'no-exchange') { if (selectedExchange.status !== 'ready') {
return {
status: "no-exchange",
error: undefined,
}
}
if (selectedExchange.status === 'selecting-exchange') {
return selectedExchange return selectedExchange
} }
console.log("exchange selected", selectedExchange.selected)
return () => { return () => {
@ -142,7 +130,7 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can
return { state }; return { state };
}, []); }, []);
console.log("terms", terms)
/** /**
* With the exchange and amount, ask the wallet the information * With the exchange and amount, ask the wallet the information
* about the withdrawal * 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 { export function LoadingInfoView({ error }: State.LoadingInfoError): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();

View File

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

View File

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

View File

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

View File

@ -31,6 +31,9 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import arrowDown from "../../svg/chevron-down.svg"; import arrowDown from "../../svg/chevron-down.svg";
import { State } from "./index.js"; import { State } from "./index.js";
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
const ButtonGroup = styled.div` const ButtonGroup = styled.div`
& > button { & > 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(); const { i18n } = useTranslationContext();
if (!currency) {
return ( return (
<div> <div>
<i18n.Translate>no exchanges</i18n.Translate> <i18n.Translate>could not find any exchange</i18n.Translate>
</div>
);
}
return (
<div>
<i18n.Translate>could not find any exchange for the currency {currency}</i18n.Translate>
</div> </div>
); );
} }