new compose feature: sub-states
implemented in withdraw page, WIP
This commit is contained in:
parent
a5525eab1e
commit
52ec740c82
@ -48,6 +48,7 @@ export namespace State {
|
||||
}
|
||||
export interface Ready extends BaseInfo {
|
||||
status: "ready";
|
||||
doSelectExchange: ButtonHandler;
|
||||
create: ButtonHandler;
|
||||
subject: TextFieldHandler;
|
||||
toBeReceived: AmountJson;
|
||||
|
@ -84,6 +84,9 @@ export function useComponentState(
|
||||
value: subject,
|
||||
onInput: async (e) => setSubject(e),
|
||||
},
|
||||
doSelectExchange: {
|
||||
//FIX
|
||||
},
|
||||
invalid: !subject || Amounts.isZero(amount),
|
||||
exchangeUrl: selected.exchangeBaseUrl,
|
||||
create: {
|
||||
|
@ -37,6 +37,9 @@ export const Ready = createExample(ReadyView, {
|
||||
currency: "ARS",
|
||||
value: 1,
|
||||
fraction: 0,
|
||||
},
|
||||
doSelectExchange: {
|
||||
|
||||
},
|
||||
exchangeUrl: "https://exchange.taler.ar",
|
||||
subject: {
|
||||
|
@ -54,6 +54,7 @@ export function ReadyView({
|
||||
create,
|
||||
toBeReceived,
|
||||
chosenAmount,
|
||||
doSelectExchange,
|
||||
}: State.Ready): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
@ -93,13 +94,13 @@ export function ReadyView({
|
||||
}}
|
||||
>
|
||||
<i18n.Translate>Exchange</i18n.Translate>
|
||||
{/* <Link>
|
||||
<Button onClick={doSelectExchange.onClick} variant="text">
|
||||
<SvgIcon
|
||||
title="Edit"
|
||||
dangerouslySetInnerHTML={{ __html: editIcon }}
|
||||
color="black"
|
||||
/>
|
||||
</Link> */}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
text={<ExchangeDetails exchange={exchangeUrl} />}
|
||||
|
@ -128,6 +128,7 @@ export function useComponentState(
|
||||
});
|
||||
}
|
||||
const res = await api.confirmPay(payStatus.proposalId, undefined);
|
||||
// handle confirm pay
|
||||
if (res.type !== ConfirmPayResultType.Done) {
|
||||
throw TalerError.fromUncheckedDetail({
|
||||
code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
|
||||
|
@ -25,12 +25,17 @@ import {
|
||||
useComponentStateFromParams,
|
||||
useComponentStateFromURI,
|
||||
} from "./state.js";
|
||||
import {
|
||||
State as SelectExchangeState
|
||||
} from "../../hooks/useSelectedExchange.js";
|
||||
|
||||
import {
|
||||
LoadingExchangeView,
|
||||
LoadingInfoView,
|
||||
LoadingUriView,
|
||||
SuccessView,
|
||||
} from "./views.js";
|
||||
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
|
||||
|
||||
export interface PropsFromURI {
|
||||
talerWithdrawUri: string | undefined;
|
||||
@ -49,6 +54,7 @@ export type State =
|
||||
| State.LoadingUriError
|
||||
| State.LoadingExchangeError
|
||||
| State.LoadingInfoError
|
||||
| SelectExchangeState.Selecting
|
||||
| State.Success;
|
||||
|
||||
export namespace State {
|
||||
@ -57,12 +63,12 @@ export namespace State {
|
||||
error: undefined;
|
||||
}
|
||||
export interface LoadingUriError {
|
||||
status: "loading-uri";
|
||||
status: "loading-error";
|
||||
error: HookError;
|
||||
}
|
||||
export interface LoadingExchangeError {
|
||||
status: "loading-exchange";
|
||||
error: HookError;
|
||||
status: "no-exchange";
|
||||
error: undefined,
|
||||
}
|
||||
export interface LoadingInfoError {
|
||||
status: "loading-info";
|
||||
@ -80,6 +86,7 @@ export namespace State {
|
||||
toBeReceived: AmountJson;
|
||||
|
||||
doWithdrawal: ButtonHandler;
|
||||
doSelectExchange: ButtonHandler;
|
||||
tosProps?: TermsOfServiceSectionProps;
|
||||
mustAcceptFirst: boolean;
|
||||
|
||||
@ -92,9 +99,10 @@ export namespace State {
|
||||
|
||||
const viewMapping: StateViewMap<State> = {
|
||||
loading: Loading,
|
||||
"loading-uri": LoadingUriView,
|
||||
"loading-exchange": LoadingExchangeView,
|
||||
"loading-error": LoadingUriView,
|
||||
"no-exchange": LoadingExchangeView,
|
||||
"loading-info": LoadingInfoView,
|
||||
"selecting-exchange": ExchangeSelectionPage,
|
||||
success: SuccessView,
|
||||
};
|
||||
|
||||
|
@ -14,223 +14,58 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { AmountJson, Amounts, ExchangeListItem, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||
import { TalerError } from "@gnu-taler/taler-wallet-core";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Amount } from "../../components/Amount.js";
|
||||
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
||||
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
|
||||
import { buildTermsOfServiceState } from "../../utils/index.js";
|
||||
import * as wxApi from "../../wxApi.js";
|
||||
import { PropsFromURI, PropsFromParams, State } from "./index.js";
|
||||
|
||||
type RecursiveState<S extends object> = S | (() => RecursiveState<S>)
|
||||
|
||||
export function useComponentStateFromParams(
|
||||
{ amount, cancel, onSuccess }: PropsFromParams,
|
||||
api: typeof wxApi,
|
||||
): State {
|
||||
const [ageRestricted, setAgeRestricted] = useState(0);
|
||||
): RecursiveState<State> {
|
||||
const uriInfoHook = useAsyncAsHook(async () => {
|
||||
const exchanges = await api.listExchanges();
|
||||
return { amount: Amounts.parseOrThrow(amount), exchanges };
|
||||
});
|
||||
|
||||
const exchangeHook = useAsyncAsHook(api.listExchanges);
|
||||
console.log("uri info", uriInfoHook)
|
||||
|
||||
const exchangeHookDep =
|
||||
!exchangeHook || exchangeHook.hasError || !exchangeHook.response
|
||||
? undefined
|
||||
: exchangeHook.response;
|
||||
|
||||
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;
|
||||
/**
|
||||
* For the exchange selected, bring the status of the terms of service
|
||||
*/
|
||||
const terms = useAsyncAsHook(async () => {
|
||||
if (!exchange) return undefined;
|
||||
|
||||
const exchangeTos = await api.getExchangeTos(exchange.exchangeBaseUrl, [
|
||||
"text/xml",
|
||||
]);
|
||||
|
||||
const state = buildTermsOfServiceState(exchangeTos);
|
||||
|
||||
return { state };
|
||||
}, [exchangeHookDep]);
|
||||
|
||||
/**
|
||||
* With the exchange and amount, ask the wallet the information
|
||||
* about the withdrawal
|
||||
*/
|
||||
const amountHook = useAsyncAsHook(async () => {
|
||||
if (!exchange) return undefined;
|
||||
|
||||
const info = await api.getExchangeWithdrawalInfo({
|
||||
exchangeBaseUrl: exchange.exchangeBaseUrl,
|
||||
amount: chosenAmount,
|
||||
tosAcceptedFormat: ["text/xml"],
|
||||
ageRestricted,
|
||||
});
|
||||
|
||||
const withdrawAmount = {
|
||||
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
|
||||
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
|
||||
};
|
||||
if (!uriInfoHook) return { status: "loading", error: undefined };
|
||||
|
||||
if (uriInfoHook.hasError) {
|
||||
return {
|
||||
amount: withdrawAmount,
|
||||
ageRestrictionOptions: info.ageRestrictionOptions,
|
||||
status: "loading-error",
|
||||
error: uriInfoHook,
|
||||
};
|
||||
}, [exchangeHookDep]);
|
||||
}
|
||||
|
||||
const [reviewing, setReviewing] = useState<boolean>(false);
|
||||
const [reviewed, setReviewed] = useState<boolean>(false);
|
||||
const chosenAmount = uriInfoHook.response.amount;
|
||||
const exchangeList = uriInfoHook.response.exchanges.exchanges
|
||||
|
||||
const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
|
||||
|
||||
if (!exchangeHook) return { status: "loading", error: undefined };
|
||||
if (exchangeHook.hasError) {
|
||||
async function doManualWithdraw(exchange: string, ageRestricted: number | undefined): Promise<{ transactionId: string, confirmTransferUrl: string | undefined }> {
|
||||
const res = await api.acceptManualWithdrawal(exchange, Amounts.stringify(chosenAmount), ageRestricted);
|
||||
return {
|
||||
status: "loading-uri",
|
||||
error: exchangeHook,
|
||||
confirmTransferUrl: undefined,
|
||||
transactionId: res.transactionId
|
||||
};
|
||||
}
|
||||
|
||||
if (!exchange) {
|
||||
return {
|
||||
status: "loading-exchange",
|
||||
error: {
|
||||
hasError: true,
|
||||
operational: false,
|
||||
message: "ERROR_NO-DEFAULT-EXCHANGE",
|
||||
},
|
||||
};
|
||||
}
|
||||
return () => exchangeSelectionState(doManualWithdraw, cancel, onSuccess, undefined, chosenAmount, exchangeList, undefined, api)
|
||||
|
||||
async function doWithdrawAndCheckError(): Promise<void> {
|
||||
if (!exchange) return;
|
||||
|
||||
try {
|
||||
setDoingWithdraw(true);
|
||||
|
||||
const response = await wxApi.acceptManualWithdrawal(
|
||||
exchange.exchangeBaseUrl,
|
||||
Amounts.stringify(amount),
|
||||
);
|
||||
|
||||
onSuccess(response.transactionId);
|
||||
} catch (e) {
|
||||
if (e instanceof TalerError) {
|
||||
setWithdrawError(e);
|
||||
}
|
||||
}
|
||||
setDoingWithdraw(false);
|
||||
}
|
||||
|
||||
if (!amountHook) {
|
||||
return { status: "loading", error: undefined };
|
||||
}
|
||||
if (amountHook.hasError) {
|
||||
return {
|
||||
status: "loading-info",
|
||||
error: amountHook,
|
||||
};
|
||||
}
|
||||
if (!amountHook.response) {
|
||||
return { status: "loading", error: undefined };
|
||||
}
|
||||
|
||||
const withdrawalFee = Amounts.sub(
|
||||
amountHook.response.amount.raw,
|
||||
amountHook.response.amount.effective,
|
||||
).amount;
|
||||
const toBeReceived = amountHook.response.amount.effective;
|
||||
|
||||
const { state: termsState } = (!terms
|
||||
? undefined
|
||||
: terms.hasError
|
||||
? undefined
|
||||
: terms.response) || { state: undefined };
|
||||
|
||||
async function onAccept(accepted: boolean): Promise<void> {
|
||||
if (!termsState || !exchange) return;
|
||||
|
||||
try {
|
||||
await api.setExchangeTosAccepted(
|
||||
exchange.exchangeBaseUrl,
|
||||
accepted ? termsState.version : undefined,
|
||||
);
|
||||
setReviewed(accepted);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
//FIXME: uncomment this and display error
|
||||
// setErrorAccepting(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mustAcceptFirst =
|
||||
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 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;
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
error: undefined,
|
||||
exchangeUrl: exchange.exchangeBaseUrl,
|
||||
toBeReceived,
|
||||
withdrawalFee,
|
||||
chosenAmount,
|
||||
ageRestriction,
|
||||
doWithdrawal: {
|
||||
onClick:
|
||||
doingWithdraw || (mustAcceptFirst && !reviewed)
|
||||
? undefined
|
||||
: doWithdrawAndCheckError,
|
||||
error: withdrawError,
|
||||
},
|
||||
tosProps: !termsState
|
||||
? undefined
|
||||
: {
|
||||
onAccept,
|
||||
onReview: setReviewing,
|
||||
reviewed: reviewed,
|
||||
reviewing: reviewing,
|
||||
terms: termsState,
|
||||
},
|
||||
mustAcceptFirst,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
|
||||
export function useComponentStateFromURI(
|
||||
{ talerWithdrawUri, cancel, onSuccess }: PropsFromURI,
|
||||
api: typeof wxApi,
|
||||
): State {
|
||||
const [ageRestricted, setAgeRestricted] = useState(0);
|
||||
|
||||
): RecursiveState<State> {
|
||||
/**
|
||||
* Ask the wallet about the withdraw URI
|
||||
*/
|
||||
@ -240,207 +75,219 @@ export function useComponentStateFromURI(
|
||||
const uriInfo = await api.getWithdrawalDetailsForUri({
|
||||
talerWithdrawUri,
|
||||
});
|
||||
const exchanges = await api.listExchanges();
|
||||
const { amount, defaultExchangeBaseUrl } = uriInfo;
|
||||
return { amount, thisExchange: defaultExchangeBaseUrl };
|
||||
return { talerWithdrawUri, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, exchanges };
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the amount and select one exchange
|
||||
*/
|
||||
const uriHookDep =
|
||||
!uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response
|
||||
? 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 state = buildTermsOfServiceState(exchangeTos);
|
||||
|
||||
return { state };
|
||||
}, [uriHookDep]);
|
||||
|
||||
/**
|
||||
* With the exchange and amount, ask the wallet the information
|
||||
* about the withdrawal
|
||||
*/
|
||||
const amountHook = useAsyncAsHook(async () => {
|
||||
if (!uriHookDep?.thisExchange) return false;
|
||||
|
||||
const info = await api.getExchangeWithdrawalInfo({
|
||||
exchangeBaseUrl: uriHookDep?.thisExchange,
|
||||
amount: Amounts.parseOrThrow(uriHookDep.amount),
|
||||
tosAcceptedFormat: ["text/xml"],
|
||||
ageRestricted,
|
||||
});
|
||||
|
||||
const withdrawAmount = {
|
||||
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
|
||||
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
|
||||
};
|
||||
|
||||
return {
|
||||
amount: withdrawAmount,
|
||||
ageRestrictionOptions: info.ageRestrictionOptions,
|
||||
};
|
||||
}, [uriHookDep]);
|
||||
|
||||
const [reviewing, setReviewing] = useState<boolean>(false);
|
||||
const [reviewed, setReviewed] = useState<boolean>(false);
|
||||
|
||||
const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
|
||||
|
||||
console.log("uri info", uriInfoHook)
|
||||
if (!uriInfoHook) return { status: "loading", error: undefined };
|
||||
|
||||
if (uriInfoHook.hasError) {
|
||||
return {
|
||||
status: "loading-uri",
|
||||
status: "loading-error",
|
||||
error: uriInfoHook,
|
||||
};
|
||||
}
|
||||
|
||||
const { amount, thisExchange } = uriInfoHook.response;
|
||||
const uri = uriInfoHook.response.talerWithdrawUri;
|
||||
const chosenAmount = uriInfoHook.response.amount;
|
||||
const defaultExchange = uriInfoHook.response.thisExchange;
|
||||
const exchangeList = uriInfoHook.response.exchanges.exchanges
|
||||
|
||||
const chosenAmount = Amounts.parseOrThrow(amount);
|
||||
|
||||
if (!thisExchange) {
|
||||
async function doManagedWithdraw(exchange: string, ageRestricted: number | undefined): Promise<{ transactionId: string, confirmTransferUrl: string | undefined }> {
|
||||
const res = await api.acceptWithdrawal(uri, exchange, ageRestricted,);
|
||||
return {
|
||||
status: "loading-exchange",
|
||||
error: {
|
||||
hasError: true,
|
||||
operational: false,
|
||||
message: "ERROR_NO-DEFAULT-EXCHANGE",
|
||||
},
|
||||
confirmTransferUrl: res.confirmTransferUrl,
|
||||
transactionId: res.transactionId
|
||||
};
|
||||
}
|
||||
|
||||
// const selectedExchange = thisExchange;
|
||||
return () => exchangeSelectionState(doManagedWithdraw, cancel, onSuccess, uri, chosenAmount, exchangeList, defaultExchange, api)
|
||||
|
||||
async function doWithdrawAndCheckError(): Promise<void> {
|
||||
if (!thisExchange) return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDoingWithdraw(true);
|
||||
if (!talerWithdrawUri) return;
|
||||
const res = await api.acceptWithdrawal(
|
||||
talerWithdrawUri,
|
||||
thisExchange,
|
||||
!ageRestricted ? undefined : ageRestricted,
|
||||
);
|
||||
if (res.confirmTransferUrl) {
|
||||
document.location.href = res.confirmTransferUrl;
|
||||
} else {
|
||||
onSuccess(res.transactionId);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof TalerError) {
|
||||
setWithdrawError(e);
|
||||
}
|
||||
}
|
||||
setDoingWithdraw(false);
|
||||
}
|
||||
type ManualOrManagedWithdrawFunction = (exchange: string, ageRestricted: number | undefined) => Promise<{ transactionId: string, confirmTransferUrl: string | undefined }>
|
||||
|
||||
if (!amountHook) {
|
||||
return { status: "loading", error: undefined };
|
||||
}
|
||||
if (amountHook.hasError) {
|
||||
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: "loading-info",
|
||||
error: amountHook,
|
||||
};
|
||||
}
|
||||
if (!amountHook.response) {
|
||||
return { status: "loading", error: undefined };
|
||||
}
|
||||
|
||||
const withdrawalFee = Amounts.sub(
|
||||
amountHook.response.amount.raw,
|
||||
amountHook.response.amount.effective,
|
||||
).amount;
|
||||
const toBeReceived = amountHook.response.amount.effective;
|
||||
|
||||
const { state: termsState } = (!terms
|
||||
? undefined
|
||||
: terms.hasError
|
||||
? undefined
|
||||
: terms.response) || { state: undefined };
|
||||
|
||||
async function onAccept(accepted: boolean): Promise<void> {
|
||||
if (!termsState || !thisExchange) return;
|
||||
|
||||
try {
|
||||
await api.setExchangeTosAccepted(
|
||||
thisExchange,
|
||||
accepted ? termsState.version : undefined,
|
||||
);
|
||||
setReviewed(accepted);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
//FIXME: uncomment this and display error
|
||||
// setErrorAccepting(e.message);
|
||||
}
|
||||
status: "no-exchange",
|
||||
error: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const mustAcceptFirst =
|
||||
termsState !== undefined &&
|
||||
(termsState.status === "changed" || termsState.status === "new");
|
||||
if (selectedExchange.status === 'selecting-exchange') {
|
||||
return selectedExchange
|
||||
}
|
||||
console.log("exchange selected", selectedExchange.selected)
|
||||
|
||||
const ageRestrictionOptions =
|
||||
amountHook.response.ageRestrictionOptions?.reduce(
|
||||
(p, c) => ({ ...p, [c]: `under ${c}` }),
|
||||
{} as Record<string, string>,
|
||||
return () => {
|
||||
|
||||
const [ageRestricted, setAgeRestricted] = useState(0);
|
||||
const currentExchange = selectedExchange.selected
|
||||
/**
|
||||
* For the exchange selected, bring the status of the terms of service
|
||||
*/
|
||||
const terms = useAsyncAsHook(async () => {
|
||||
const exchangeTos = await api.getExchangeTos(currentExchange.exchangeBaseUrl, [
|
||||
"text/xml",
|
||||
]);
|
||||
|
||||
const state = buildTermsOfServiceState(exchangeTos);
|
||||
|
||||
return { state };
|
||||
}, []);
|
||||
console.log("terms", terms)
|
||||
/**
|
||||
* With the exchange and amount, ask the wallet the information
|
||||
* about the withdrawal
|
||||
*/
|
||||
const amountHook = useAsyncAsHook(async () => {
|
||||
|
||||
const info = await api.getExchangeWithdrawalInfo({
|
||||
exchangeBaseUrl: currentExchange.exchangeBaseUrl,
|
||||
amount: chosenAmount,
|
||||
tosAcceptedFormat: ["text/xml"],
|
||||
ageRestricted,
|
||||
});
|
||||
|
||||
const withdrawAmount = {
|
||||
raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
|
||||
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
|
||||
};
|
||||
|
||||
return {
|
||||
amount: withdrawAmount,
|
||||
ageRestrictionOptions: info.ageRestrictionOptions,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [reviewing, setReviewing] = useState<boolean>(false);
|
||||
const [reviewed, setReviewed] = useState<boolean>(false);
|
||||
|
||||
const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
|
||||
|
||||
const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
|
||||
if (ageRestrictionEnabled) {
|
||||
ageRestrictionOptions["0"] = "Not restricted";
|
||||
}
|
||||
|
||||
//TODO: calculate based on exchange info
|
||||
const ageRestriction = ageRestrictionEnabled
|
||||
? {
|
||||
async function doWithdrawAndCheckError(): Promise<void> {
|
||||
|
||||
try {
|
||||
setDoingWithdraw(true);
|
||||
const res = await doWithdraw(currentExchange.exchangeBaseUrl, !ageRestricted ? undefined : ageRestricted)
|
||||
if (res.confirmTransferUrl) {
|
||||
document.location.href = res.confirmTransferUrl;
|
||||
} else {
|
||||
onSuccess(res.transactionId);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof TalerError) {
|
||||
setWithdrawError(e);
|
||||
}
|
||||
}
|
||||
setDoingWithdraw(false);
|
||||
}
|
||||
|
||||
if (!amountHook) {
|
||||
return { status: "loading", error: undefined };
|
||||
}
|
||||
if (amountHook.hasError) {
|
||||
return {
|
||||
status: "loading-info",
|
||||
error: amountHook,
|
||||
};
|
||||
}
|
||||
if (!amountHook.response) {
|
||||
return { status: "loading", error: undefined };
|
||||
}
|
||||
|
||||
const withdrawalFee = Amounts.sub(
|
||||
amountHook.response.amount.raw,
|
||||
amountHook.response.amount.effective,
|
||||
).amount;
|
||||
const toBeReceived = amountHook.response.amount.effective;
|
||||
|
||||
const { state: termsState } = (!terms
|
||||
? undefined
|
||||
: terms.hasError
|
||||
? undefined
|
||||
: terms.response) || { state: undefined };
|
||||
|
||||
async function onAccept(accepted: boolean): Promise<void> {
|
||||
if (!termsState) return;
|
||||
|
||||
try {
|
||||
await api.setExchangeTosAccepted(
|
||||
currentExchange.exchangeBaseUrl,
|
||||
accepted ? termsState.version : undefined,
|
||||
);
|
||||
setReviewed(accepted);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
//FIXME: uncomment this and display error
|
||||
// setErrorAccepting(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mustAcceptFirst =
|
||||
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 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;
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
error: undefined,
|
||||
exchangeUrl: thisExchange,
|
||||
toBeReceived,
|
||||
withdrawalFee,
|
||||
chosenAmount,
|
||||
talerWithdrawUri,
|
||||
ageRestriction,
|
||||
doWithdrawal: {
|
||||
onClick:
|
||||
doingWithdraw || (mustAcceptFirst && !reviewed)
|
||||
? undefined
|
||||
: doWithdrawAndCheckError,
|
||||
error: withdrawError,
|
||||
},
|
||||
tosProps: !termsState
|
||||
? undefined
|
||||
: {
|
||||
return {
|
||||
status: "success",
|
||||
error: undefined,
|
||||
doSelectExchange: selectedExchange.doSelect,
|
||||
exchangeUrl: currentExchange.exchangeBaseUrl,
|
||||
toBeReceived,
|
||||
withdrawalFee,
|
||||
chosenAmount,
|
||||
talerWithdrawUri,
|
||||
ageRestriction,
|
||||
doWithdrawal: {
|
||||
onClick:
|
||||
doingWithdraw || (mustAcceptFirst && !reviewed)
|
||||
? undefined
|
||||
: doWithdrawAndCheckError,
|
||||
error: withdrawError,
|
||||
},
|
||||
tosProps: !termsState
|
||||
? undefined
|
||||
: {
|
||||
onAccept,
|
||||
onReview: setReviewing,
|
||||
reviewed: reviewed,
|
||||
reviewing: reviewing,
|
||||
terms: termsState,
|
||||
},
|
||||
mustAcceptFirst,
|
||||
cancel,
|
||||
};
|
||||
mustAcceptFirst,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,8 @@ export const TermsOfServiceNotYetLoaded = createExample(SuccessView, {
|
||||
fraction: 10000000,
|
||||
value: 1,
|
||||
},
|
||||
doSelectExchange: {
|
||||
},
|
||||
toBeReceived: {
|
||||
currency: "USD",
|
||||
fraction: 0,
|
||||
@ -104,6 +106,8 @@ export const WithSomeFee = createExample(SuccessView, {
|
||||
fraction: 0,
|
||||
value: 1,
|
||||
},
|
||||
doSelectExchange: {
|
||||
},
|
||||
tosProps: normalTosState,
|
||||
});
|
||||
|
||||
@ -123,6 +127,8 @@ export const WithoutFee = createExample(SuccessView, {
|
||||
fraction: 0,
|
||||
value: 0,
|
||||
},
|
||||
doSelectExchange: {
|
||||
},
|
||||
toBeReceived: {
|
||||
currency: "USD",
|
||||
fraction: 0,
|
||||
@ -147,6 +153,8 @@ export const EditExchangeUntouched = createExample(SuccessView, {
|
||||
fraction: 0,
|
||||
value: 0,
|
||||
},
|
||||
doSelectExchange: {
|
||||
},
|
||||
toBeReceived: {
|
||||
currency: "USD",
|
||||
fraction: 0,
|
||||
@ -171,6 +179,8 @@ export const EditExchangeModified = createExample(SuccessView, {
|
||||
fraction: 0,
|
||||
value: 0,
|
||||
},
|
||||
doSelectExchange: {
|
||||
},
|
||||
toBeReceived: {
|
||||
currency: "USD",
|
||||
fraction: 0,
|
||||
@ -188,6 +198,8 @@ export const WithAgeRestriction = createExample(SuccessView, {
|
||||
value: 2,
|
||||
fraction: 10000000,
|
||||
},
|
||||
doSelectExchange: {
|
||||
},
|
||||
doWithdrawal: nullHandler,
|
||||
exchangeUrl: "https://exchange.demo.taler.net",
|
||||
mustAcceptFirst: false,
|
||||
|
@ -29,6 +29,7 @@ import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core";
|
||||
import { expect } from "chai";
|
||||
import { mountHook } from "../../test-utils.js";
|
||||
import { useComponentStateFromURI } from "./state.js";
|
||||
import * as wxApi from "../../wxApi.js";
|
||||
|
||||
const exchanges: ExchangeFullDetails[] = [
|
||||
{
|
||||
@ -92,7 +93,7 @@ describe("Withdraw CTA states", () => {
|
||||
{
|
||||
const { status, error } = getLastResultOrThrow();
|
||||
|
||||
if (status != "loading-uri") expect.fail();
|
||||
if (status != "loading-error") expect.fail();
|
||||
if (!error) expect.fail();
|
||||
if (!error.hasError) expect.fail();
|
||||
if (error.operational) expect.fail();
|
||||
@ -127,7 +128,7 @@ describe("Withdraw CTA states", () => {
|
||||
|
||||
{
|
||||
const { status } = getLastResultOrThrow();
|
||||
expect(status).equals("loading");
|
||||
expect(status).equals("loading", "1");
|
||||
}
|
||||
|
||||
await waitNextUpdate();
|
||||
@ -135,13 +136,9 @@ describe("Withdraw CTA states", () => {
|
||||
{
|
||||
const { status, error } = getLastResultOrThrow();
|
||||
|
||||
expect(status).equals("loading-exchange");
|
||||
expect(status).equals("no-exchange", "3");
|
||||
|
||||
expect(error).deep.equals({
|
||||
hasError: true,
|
||||
operational: false,
|
||||
message: "ERROR_NO-DEFAULT-EXCHANGE",
|
||||
});
|
||||
expect(error).undefined;
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate();
|
||||
@ -169,10 +166,10 @@ describe("Withdraw CTA states", () => {
|
||||
}),
|
||||
getExchangeWithdrawalInfo:
|
||||
async (): Promise<ExchangeWithdrawDetails> =>
|
||||
({
|
||||
withdrawalAmountRaw: "ARS:2",
|
||||
withdrawalAmountEffective: "ARS:2",
|
||||
} as any),
|
||||
({
|
||||
withdrawalAmountRaw: "ARS:2",
|
||||
withdrawalAmountEffective: "ARS:2",
|
||||
} as any),
|
||||
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
|
||||
contentType: "text",
|
||||
content: "just accept",
|
||||
@ -246,10 +243,10 @@ describe("Withdraw CTA states", () => {
|
||||
}),
|
||||
getExchangeWithdrawalInfo:
|
||||
async (): Promise<ExchangeWithdrawDetails> =>
|
||||
({
|
||||
withdrawalAmountRaw: "ARS:2",
|
||||
withdrawalAmountEffective: "ARS:2",
|
||||
} as any),
|
||||
({
|
||||
withdrawalAmountRaw: "ARS:2",
|
||||
withdrawalAmountEffective: "ARS:2",
|
||||
} as any),
|
||||
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
|
||||
contentType: "text",
|
||||
content: "just accept",
|
||||
|
@ -38,6 +38,7 @@ import editIcon from "../../svg/edit_24px.svg";
|
||||
import { Amount } from "../../components/Amount.js";
|
||||
import { QR } from "../../components/QR.js";
|
||||
import { useState } from "preact/hooks";
|
||||
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
||||
|
||||
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
@ -52,15 +53,12 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingExchangeView({
|
||||
error,
|
||||
}: State.LoadingExchangeError): VNode {
|
||||
export function LoadingExchangeView(p: State.LoadingExchangeError): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
return (
|
||||
<LoadingError
|
||||
title={<i18n.Translate>Could not get exchange</i18n.Translate>}
|
||||
error={error}
|
||||
<ErrorMessage
|
||||
title={<i18n.Translate>Could not get a default exchange, please check configuration</i18n.Translate>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -106,13 +104,13 @@ export function SuccessView(state: State.Success): VNode {
|
||||
}}
|
||||
>
|
||||
<i18n.Translate>Exchange</i18n.Translate>
|
||||
{/* <Link>
|
||||
<Button onClick={state.doSelectExchange.onClick} variant="text">
|
||||
<SvgIcon
|
||||
title="Edit"
|
||||
dangerouslySetInnerHTML={{ __html: editIcon }}
|
||||
color="black"
|
||||
/>
|
||||
</Link> */}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
text={<ExchangeDetails exchange={state.exchangeUrl} />}
|
||||
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2022 Taler Systems S.A.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { ExchangeListItem } from "@gnu-taler/taler-util";
|
||||
import { useState } from "preact/hooks";
|
||||
import { ButtonHandler } from "../mui/handlers.js";
|
||||
|
||||
type State = State.Ready | State.NoExchange | State.Selecting;
|
||||
|
||||
export namespace State {
|
||||
export interface NoExchange {
|
||||
status: "no-exchange"
|
||||
error: undefined;
|
||||
}
|
||||
export interface Ready {
|
||||
status: "ready",
|
||||
doSelect: ButtonHandler,
|
||||
selected: ExchangeListItem;
|
||||
}
|
||||
export interface Selecting {
|
||||
status: "selecting-exchange",
|
||||
error: undefined,
|
||||
onSelection: (url: string) => Promise<void>;
|
||||
onCancel: () => Promise<void>;
|
||||
list: ExchangeListItem[],
|
||||
currency: string;
|
||||
currentExchange: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currency: string;
|
||||
//there is a preference for the default at the initial state
|
||||
defaultExchange?: string,
|
||||
//list of exchanges
|
||||
list: ExchangeListItem[],
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function useSelectedExchange({ currency, defaultExchange, list }: Props): State {
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [selectedExchange, setSelectedExchange] = useState<string | undefined>(undefined);
|
||||
|
||||
if (!list.length) {
|
||||
return {
|
||||
status: "no-exchange",
|
||||
error: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const firstByCurrency = list.find((e) => e.currency === currency)
|
||||
if (!firstByCurrency) {
|
||||
// there should be at least one exchange for this currency
|
||||
return {
|
||||
status: "no-exchange",
|
||||
error: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isSelecting) {
|
||||
const currentExchange = selectedExchange ?? defaultExchange ?? firstByCurrency.exchangeBaseUrl;
|
||||
return {
|
||||
status: "selecting-exchange",
|
||||
error: undefined,
|
||||
list,
|
||||
currency,
|
||||
currentExchange: currentExchange,
|
||||
onSelection: async (exchangeBaseUrl: string) => {
|
||||
setIsSelecting(false);
|
||||
setSelectedExchange(exchangeBaseUrl)
|
||||
},
|
||||
onCancel: async () => {
|
||||
setIsSelecting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const found = !selectedExchange ? undefined : list.find(
|
||||
(e) => e.exchangeBaseUrl === selectedExchange,
|
||||
)
|
||||
if (found) return {
|
||||
status: "ready",
|
||||
doSelect: {
|
||||
onClick: async () => setIsSelecting(true)
|
||||
},
|
||||
selected: found
|
||||
};
|
||||
}
|
||||
{
|
||||
const found = !defaultExchange ? undefined : list.find(
|
||||
(e) => e.exchangeBaseUrl === defaultExchange,
|
||||
)
|
||||
if (found) return {
|
||||
status: "ready",
|
||||
doSelect: {
|
||||
onClick: async () => setIsSelecting(true)
|
||||
},
|
||||
selected: found
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ready",
|
||||
doSelect: {
|
||||
onClick: async () => setIsSelecting(true)
|
||||
},
|
||||
selected: firstByCurrency
|
||||
}
|
||||
}
|
@ -82,31 +82,38 @@ export function renderNodeOrBrowser(Component: any, args: any): void {
|
||||
document.body.removeChild(div);
|
||||
}
|
||||
}
|
||||
type RecursiveState<S> = S | (() => RecursiveState<S>)
|
||||
|
||||
interface Mounted<T> {
|
||||
unmount: () => void;
|
||||
getLastResultOrThrow: () => T;
|
||||
getLastResultOrThrow: () => Exclude<T, VoidFunction>;
|
||||
assertNoPendingUpdate: () => void;
|
||||
waitNextUpdate: (s?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const isNode = typeof window === "undefined";
|
||||
|
||||
export function mountHook<T>(
|
||||
callback: () => T,
|
||||
export function mountHook<T extends object>(
|
||||
callback: () => RecursiveState<T>,
|
||||
Context?: ({ children }: { children: any }) => VNode,
|
||||
): Mounted<T> {
|
||||
// const result: { current: T | null } = {
|
||||
// current: null
|
||||
// }
|
||||
let lastResult: T | Error | null = null;
|
||||
let lastResult: Exclude<T, VoidFunction> | Error | null = null;
|
||||
|
||||
const listener: Array<() => void> = [];
|
||||
|
||||
// component that's going to hold the hook
|
||||
function Component(): VNode {
|
||||
try {
|
||||
lastResult = callback();
|
||||
let componentOrResult = callback()
|
||||
while (typeof componentOrResult === "function") {
|
||||
componentOrResult = componentOrResult();
|
||||
}
|
||||
//typecheck fails here
|
||||
const l: Exclude<T, () => void> = componentOrResult as any
|
||||
lastResult = l;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
lastResult = e;
|
||||
@ -157,13 +164,13 @@ export function mountHook<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function getLastResult(): T | Error | null {
|
||||
const copy = lastResult;
|
||||
function getLastResult(): Exclude<T | Error | null, VoidFunction> {
|
||||
const copy: Exclude<T | Error | null, VoidFunction> = lastResult;
|
||||
lastResult = null;
|
||||
return copy;
|
||||
}
|
||||
|
||||
function getLastResultOrThrow(): T {
|
||||
function getLastResultOrThrow(): Exclude<T, VoidFunction> {
|
||||
const r = getLastResult();
|
||||
if (r instanceof Error) throw r;
|
||||
if (!r) throw Error("there was no last result");
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
Amounts,
|
||||
GetExchangeTosResult,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { VNode } from "preact";
|
||||
import { VNode, createElement } from "preact";
|
||||
|
||||
function getJsonIfOk(r: Response): Promise<any> {
|
||||
if (r.ok) {
|
||||
@ -31,8 +31,7 @@ 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"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
@ -103,10 +102,10 @@ export function buildTermsOfServiceStatus(
|
||||
return !content
|
||||
? "notfound"
|
||||
: !acceptedVersion
|
||||
? "new"
|
||||
: acceptedVersion !== currentVersion
|
||||
? "changed"
|
||||
: "accepted";
|
||||
? "new"
|
||||
: acceptedVersion !== currentVersion
|
||||
? "changed"
|
||||
: "accepted";
|
||||
}
|
||||
|
||||
function parseTermsOfServiceContent(
|
||||
@ -198,17 +197,35 @@ export type StateViewMap<StateType extends { status: string }> = {
|
||||
[S in StateType as S["status"]]: StateFunc<S>;
|
||||
};
|
||||
|
||||
type RecursiveState<S extends object> = S | (() => RecursiveState<S>)
|
||||
|
||||
export function compose<SType extends { status: string }, PType>(
|
||||
name: string,
|
||||
hook: (p: PType) => SType,
|
||||
vs: StateViewMap<SType>,
|
||||
hook: (p: PType) => RecursiveState<SType>,
|
||||
viewMap: StateViewMap<SType>,
|
||||
): (p: PType) => VNode {
|
||||
const Component = (p: PType): VNode => {
|
||||
const state = hook(p);
|
||||
const s = state.status as unknown as SType["status"];
|
||||
const c = vs[s] as unknown as StateFunc<SType>;
|
||||
return c(state);
|
||||
|
||||
function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
|
||||
|
||||
function TheComponent(): VNode {
|
||||
const state = stateHook();
|
||||
|
||||
if (typeof state === "function") {
|
||||
const subComponent = withHook(state)
|
||||
return createElement(subComponent, {});
|
||||
}
|
||||
|
||||
const statusName = state.status as unknown as SType["status"];
|
||||
const viewComponent = viewMap[statusName] as unknown as StateFunc<SType>;
|
||||
return createElement(viewComponent, state);
|
||||
}
|
||||
TheComponent.name = `${name}`;
|
||||
|
||||
return TheComponent;
|
||||
}
|
||||
|
||||
return (p: PType) => {
|
||||
const h = withHook(() => hook(p))
|
||||
return h()
|
||||
};
|
||||
Component.name = `${name}`;
|
||||
return Component;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
AbsoluteTime,
|
||||
ExchangeFullDetails,
|
||||
OperationMap,
|
||||
ExchangeListItem,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { Loading } from "../../components/Loading.js";
|
||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||
@ -29,13 +30,14 @@ import * as wxApi from "../../wxApi.js";
|
||||
import { useComponentState } from "./state.js";
|
||||
import {
|
||||
ComparingView,
|
||||
LoadingUriView,
|
||||
ErrorLoadingView,
|
||||
NoExchangesView,
|
||||
ReadyView,
|
||||
} from "./views.js";
|
||||
|
||||
export interface Props {
|
||||
currency?: string;
|
||||
list: ExchangeListItem[],
|
||||
currentExchange: string,
|
||||
onCancel: () => Promise<void>;
|
||||
onSelection: (exchange: string) => Promise<void>;
|
||||
}
|
||||
@ -54,7 +56,7 @@ export namespace State {
|
||||
}
|
||||
|
||||
export interface LoadingUriError {
|
||||
status: "loading-uri";
|
||||
status: "error-loading";
|
||||
error: HookError;
|
||||
}
|
||||
|
||||
@ -85,7 +87,7 @@ export namespace State {
|
||||
|
||||
const viewMapping: StateViewMap<State> = {
|
||||
loading: Loading,
|
||||
"loading-uri": LoadingUriView,
|
||||
"error-loading": ErrorLoadingView,
|
||||
comparing: ComparingView,
|
||||
"no-exchanges": NoExchangesView,
|
||||
ready: ReadyView,
|
||||
|
@ -22,14 +22,17 @@ import * as wxApi from "../../wxApi.js";
|
||||
import { Props, State } from "./index.js";
|
||||
|
||||
export function useComponentState(
|
||||
{ onCancel, onSelection, currency }: Props,
|
||||
{ onCancel, onSelection, list: exchanges, currentExchange }: Props,
|
||||
api: typeof wxApi,
|
||||
): State {
|
||||
const initialValue = 0;
|
||||
const initialValue = exchanges.findIndex(e => e.exchangeBaseUrl === currentExchange);
|
||||
if (initialValue === -1) {
|
||||
throw Error(`wrong usage of ExchangeSelection component, currentExchange '${currentExchange}' is not in the list of exchanges`)
|
||||
}
|
||||
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 =
|
||||
@ -54,12 +57,12 @@ export function useComponentState(
|
||||
}
|
||||
if (hook.hasError) {
|
||||
return {
|
||||
status: "loading-uri",
|
||||
status: "error-loading",
|
||||
error: hook,
|
||||
};
|
||||
}
|
||||
|
||||
const { exchanges, selected, original } = hook.response;
|
||||
const { selected, original } = hook.response;
|
||||
|
||||
if (!selected) {
|
||||
//!selected <=> exchanges.length === 0
|
||||
|
@ -101,7 +101,7 @@ const Container = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||
export function ErrorLoadingView({ error }: State.LoadingUriError): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
return (
|
||||
|
Loading…
Reference in New Issue
Block a user