exchange selection for invoices and some fixes
This commit is contained in:
parent
7adaeff0a5
commit
859991a40c
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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>,
|
||||
);
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user