fix: take into account dd48 when adding exchanges

This commit is contained in:
Sebastian 2023-10-09 15:42:20 -03:00
parent 148846e68f
commit b173b3ac0f
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
14 changed files with 624 additions and 483 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@gnu-taler/taler-wallet-webextension", "name": "@gnu-taler/taler-wallet-webextension",
"version": "0.9.3-dev.27", "version": "0.9.3-dev.31",
"description": "GNU Taler Wallet browser extension", "description": "GNU Taler Wallet browser extension",
"main": "./build/index.js", "main": "./build/index.js",
"types": "./build/index.d.ts", "types": "./build/index.d.ts",
@ -75,4 +75,4 @@
"pogen": { "pogen": {
"domain": "taler-wallet-webex" "domain": "taler-wallet-webex"
} }
} }

View File

@ -34,8 +34,10 @@ export type SafeHandler<T> = {
[__safe_handler]: true; [__safe_handler]: true;
}; };
type UnsafeHandler<T> = ((p: T) => Promise<void>) | ((p: T) => void);
export function withSafe<T>( export function withSafe<T>(
handler: (p: T) => Promise<void>, handler: UnsafeHandler<T>,
onError: (e: Error) => void, onError: (e: Error) => void,
): SafeHandler<T> { ): SafeHandler<T> {
const sh = async function (p: T): Promise<void> { const sh = async function (p: T): Promise<void> {

View File

@ -0,0 +1,84 @@
/*
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 { TalerConfigResponse } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ConfirmView, VerifyView } from "./views.js";
import { HttpResponse, InputFieldHandler } from "@gnu-taler/web-util/browser";
import { TextFieldHandler } from "../../mui/handlers.js";
export interface Props {
currency?: string;
onBack: () => Promise<void>;
noDebounce?: boolean;
}
export type State = State.Loading
| State.LoadingUriError
| State.Confirm
| State.Verify;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
export interface LoadingUriError {
status: "error";
error: ErrorAlert;
}
export interface BaseInfo {
error: undefined;
}
export interface Confirm extends BaseInfo {
status: "confirm";
url: string;
onCancel: () => Promise<void>;
onConfirm: () => Promise<void>;
error: undefined;
}
export interface Verify extends BaseInfo {
status: "verify";
error: undefined;
onCancel: () => Promise<void>;
onAccept: () => Promise<void>;
url: TextFieldHandler,
knownExchanges: URL[],
result: HttpResponse<TalerConfigResponse, unknown> | undefined,
expectedCurrency: string | undefined,
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
confirm: ConfirmView,
verify: VerifyView,
};
export const AddExchange = compose(
"AddExchange",
(p: Props) => useComponentState(p),
viewMapping,
);

View File

@ -0,0 +1,149 @@
/*
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 { useState, useEffect, useCallback } from "preact/hooks";
import { Props, State } from "./index.js";
import { ExchangeEntryStatus, TalerConfigResponse, TranslatedString, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { RecursiveState } from "../../utils/index.js";
import { HttpResponse, useApiContext } from "@gnu-taler/web-util/browser";
import { alertFromError } from "../../context/alert.js";
import { withSafe } from "../../mui/handlers.js";
export function useComponentState({ onBack, currency, noDebounce }: Props): RecursiveState<State> {
const [verified, setVerified] = useState<
{ url: string; config: TalerConfigResponse } | undefined
>(undefined);
const api = useBackendContext();
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
);
const walletExchanges = !hook ? [] : hook.hasError ? [] : hook.response.exchanges
const used = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Used);
const preset = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Preset);
if (!verified) {
return (): State => {
const { request } = useApiContext();
const ccc = useCallback(async (str: string) => {
const c = canonicalizeBaseUrl(str)
const found = used.findIndex((e) => e.exchangeBaseUrl === c);
if (found !== -1) {
throw Error("This exchange is already active")
}
const result = await request<TalerConfigResponse>(c, "/keys")
return result
}, [used])
const { result, value: url, update, error: requestError } = useDebounce<HttpResponse<TalerConfigResponse, unknown>>(ccc, noDebounce ?? false)
const [inputError, setInputError] = useState<string>()
return {
status: "verify",
error: undefined,
onCancel: onBack,
expectedCurrency: currency,
onAccept: async () => {
if (!url || !result || !result.ok) return;
setVerified({ url, config: result.data })
},
result,
knownExchanges: preset.map(e => new URL(e.exchangeBaseUrl)),
url: {
value: url ?? "",
error: inputError ?? requestError,
onInput: withSafe(update, (e) => {
setInputError(e.message)
})
},
};
}
}
async function onConfirm() {
if (!verified) return;
await api.wallet.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: canonicalizeBaseUrl(verified.url),
forceUpdate: true,
});
onBack();
}
return {
status: "confirm",
error: undefined,
onCancel: onBack,
onConfirm,
url: verified.url
};
}
function useDebounce<T>(
onTrigger: (v: string) => Promise<T>,
disabled: boolean,
): {
loading: boolean;
error?: string;
value: string | undefined;
result: T | undefined;
update: (s: string) => void;
} {
const [value, setValue] = useState<string>();
const [dirty, setDirty] = useState(false);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<T | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const [handler, setHandler] = useState<any | undefined>(undefined);
if (!disabled) {
useEffect(() => {
if (!value) return;
clearTimeout(handler);
const h = setTimeout(async () => {
setDirty(true);
setLoading(true);
try {
const result = await onTrigger(value);
setResult(result);
setError(undefined);
setLoading(false);
} catch (e) {
const errorMessage =
e instanceof Error ? e.message : `unknown error: ${e}`;
setError(errorMessage);
setLoading(false);
setResult(undefined);
}
}, 500);
setHandler(h);
}, [value, setHandler, onTrigger]);
}
return {
error: dirty ? error : undefined,
loading: loading,
result: result,
value: value,
update: disabled ? onTrigger : setValue ,
};
}

View File

@ -20,26 +20,10 @@
*/ */
import * as tests from "@gnu-taler/web-util/testing"; import * as tests from "@gnu-taler/web-util/testing";
import { ExchangeAddConfirmPage as TestedComponent } from "./ExchangeAddConfirm.js"; import { ConfirmView, VerifyView } from "./views.js";
export default { export default {
title: "exchange add confirm", title: "example",
component: TestedComponent,
argTypes: {
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
}; };
export const TermsNotFound = tests.createExample(TestedComponent, { // export const Ready = tests.createExample(ReadyView, {});
url: "https://exchange.demo.taler.net/",
});
export const NewTerms = tests.createExample(TestedComponent, {
url: "https://exchange.demo.taler.net/",
});
export const TermsChanged = tests.createExample(TestedComponent, {
url: "https://exchange.demo.taler.net/",
});

View File

@ -0,0 +1,178 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { expect } from "chai";
import { createWalletApiMock } from "../../test-utils.js";
import * as tests from "@gnu-taler/web-util/testing";
import { Props } from "./index.js";
import { useComponentState } from "./state.js";
import { nullFunction } from "../../mui/handlers.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { ExchangeEntryStatus, ExchangeTosStatus, ExchangeUpdateStatus } from "@gnu-taler/taler-util";
const props: Props = {
onBack: nullFunction,
noDebounce: true,
};
describe("AddExchange states", () => {
it("should start in 'verify' state", async () => {
const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.ListExchanges, {}, {
exchanges:[{
exchangeBaseUrl: "http://exchange.local/",
ageRestrictionOptions: [],
currency: "ARS",
exchangeEntryStatus: ExchangeEntryStatus.Ephemeral,
tosStatus: ExchangeTosStatus.Pending,
exchangeUpdateStatus: ExchangeUpdateStatus.Failed,
paytoUris: [],
}]
})
const hookBehavior = await tests.hookBehaveLikeThis(
useComponentState,
props,
[
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
},
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should not be able to add a known exchange", async () => {
const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.ListExchanges, {}, {
exchanges:[{
exchangeBaseUrl: "http://exchange.local/",
ageRestrictionOptions: [],
currency: "ARS",
exchangeEntryStatus: ExchangeEntryStatus.Used,
tosStatus: ExchangeTosStatus.Pending,
exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
paytoUris: [],
}]
})
const hookBehavior = await tests.hookBehaveLikeThis(
useComponentState,
props,
[
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
},
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
expect(state.error).is.undefined;
expect(state.url.onInput).is.not.undefined;
if (!state.url.onInput) return;
state.url.onInput("http://exchange.local/")
},
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
expect(state.url.error).eq("This exchange is already active");
expect(state.url.onInput).is.not.undefined;
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be able to add a preset exchange", async () => {
const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.ListExchanges, {}, {
exchanges:[{
exchangeBaseUrl: "http://exchange.local/",
ageRestrictionOptions: [],
currency: "ARS",
exchangeEntryStatus: ExchangeEntryStatus.Preset,
tosStatus: ExchangeTosStatus.Pending,
exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
paytoUris: [],
}]
})
const hookBehavior = await tests.hookBehaveLikeThis(
useComponentState,
props,
[
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
},
(state) => {
expect(state.status).equal("verify");
if (state.status !== "verify") return;
expect(state.url.value).eq("");
expect(state.expectedCurrency).is.undefined;
expect(state.result).is.undefined;
expect(state.error).is.undefined;
expect(state.url.onInput).is.not.undefined;
if (!state.url.onInput) return;
state.url.onInput("http://exchange.local/")
},
],
TestingContext,
);
expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});

View File

@ -0,0 +1,201 @@
/*
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 { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorMessage } from "../../components/ErrorMessage.js";
import { Input, LightText, SubTitle, Title, WarningBox } from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
export function VerifyView({
expectedCurrency,
onCancel,
onAccept,
result,
knownExchanges,
url,
}: State.Verify): VNode {
const { i18n } = useTranslationContext();
return (
<Fragment>
<section>
{!expectedCurrency ? (
<Title>
<i18n.Translate>Add new exchange</i18n.Translate>
</Title>
) : (
<SubTitle>
<i18n.Translate>Add exchange for {expectedCurrency}</i18n.Translate>
</SubTitle>
)}
{!result && (
<LightText>
<i18n.Translate>
Enter the URL of an exchange you trust.
</i18n.Translate>
</LightText>
)}
{result && (
<LightText>
<i18n.Translate>
An exchange has been found! Review the information and click next
</i18n.Translate>
</LightText>
)}
{result && result.ok && expectedCurrency && expectedCurrency !== result.data.currency && (
<WarningBox>
<i18n.Translate>
This exchange doesn&apos;t match the expected currency
<b>{expectedCurrency}</b>
</i18n.Translate>
</WarningBox>
)}
{result && !result.ok && !result.loading && (
<ErrorMessage
title={i18n.str`Unable to verify this exchange`}
description={result.message}
/>
)}
<p>
<Input invalid={result && !result.ok} >
<label>URL</label>
<input
type="text"
placeholder="https://"
value={url.value}
onInput={(e) => {
if (url.onInput) {
url.onInput(e.currentTarget.value)
}
}}
/>
</Input>
{result && result.loading && (
<div>
<i18n.Translate>loading</i18n.Translate>...
</div>
)}
{result && result.ok && !result.loading && (
<Fragment>
<Input>
<label>
<i18n.Translate>Version</i18n.Translate>
</label>
<input type="text" disabled value={result.data.version} />
</Input>
<Input>
<label>
<i18n.Translate>Currency</i18n.Translate>
</label>
<input type="text" disabled value={result.data.currency} />
</Input>
</Fragment>
)}
</p>
{url.error && (
<ErrorMessage
title={i18n.str`Can't use this URL`}
description={url.error}
/>
)}
</section>
<footer>
<Button variant="contained" color="secondary" onClick={onCancel}>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button
variant="contained"
disabled={
!result ||
result.loading ||
!result.ok ||
(!!expectedCurrency && expectedCurrency !== result.data.currency)
}
onClick={onAccept}
>
<i18n.Translate>Next</i18n.Translate>
</Button>
</footer>
<section>
<ul>
{knownExchanges.map(ex => {
return <li><a href="#" onClick={(e) => {
if (url.onInput) {
url.onInput(ex.href)
}
e.preventDefault()
}}>
{ex.href}</a></li>
})}
</ul>
</section>
</Fragment>
);
}
export function ConfirmView({
url,
onCancel,
onConfirm,
}: State.Confirm): VNode {
const { i18n } = useTranslationContext();
const [accepted, setAccepted] = useState(false);
return (
<Fragment>
<section>
<Title>
<i18n.Translate>Review terms of service</i18n.Translate>
</Title>
<div>
<i18n.Translate>Exchange URL</i18n.Translate>:
<a href={url} target="_blank" rel="noreferrer">
{url}
</a>
</div>
</section>
<TermsOfService key="terms" exchangeUrl={url} onChange={setAccepted} />
<footer>
<Button
key="cancel"
variant="contained"
color="secondary"
onClick={onCancel}
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button
key="add"
variant="contained"
color="success"
disabled={!accepted}
onClick={onConfirm}
>
<i18n.Translate>Add exchange</i18n.Translate>
</Button>
</footer>
</Fragment>
);
}

View File

@ -65,15 +65,15 @@ import {
WithdrawPageFromParams, WithdrawPageFromParams,
WithdrawPageFromURI, WithdrawPageFromURI,
} from "../cta/Withdraw/index.js"; } from "../cta/Withdraw/index.js";
import { useIsOnline } from "../hooks/useIsOnline.js";
import { strings } from "../i18n/strings.js"; import { strings } from "../i18n/strings.js";
import { platform } from "../platform/foreground.js";
import CloseIcon from "../svg/close_24px.inline.svg"; import CloseIcon from "../svg/close_24px.inline.svg";
import { AddBackupProviderPage } from "./AddBackupProvider/index.js"; import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
import { AddExchange } from "./AddExchange/index.js";
import { BackupPage } from "./BackupPage.js"; import { BackupPage } from "./BackupPage.js";
import { DepositPage } from "./DepositPage/index.js"; import { DepositPage } from "./DepositPage/index.js";
import { DestinationSelectionPage } from "./DestinationSelection/index.js"; import { DestinationSelectionPage } from "./DestinationSelection/index.js";
import { DeveloperPage } from "./DeveloperPage.js"; import { DeveloperPage } from "./DeveloperPage.js";
import { ExchangeAddPage } from "./ExchangeAddPage.js";
import { HistoryPage } from "./History.js"; import { HistoryPage } from "./History.js";
import { NotificationsPage } from "./Notifications/index.js"; import { NotificationsPage } from "./Notifications/index.js";
import { ProviderDetailPage } from "./ProviderDetailPage.js"; import { ProviderDetailPage } from "./ProviderDetailPage.js";
@ -81,7 +81,6 @@ import { QrReaderPage } from "./QrReader.js";
import { SettingsPage } from "./Settings.js"; import { SettingsPage } from "./Settings.js";
import { TransactionPage } from "./Transaction.js"; import { TransactionPage } from "./Transaction.js";
import { WelcomePage } from "./Welcome.js"; import { WelcomePage } from "./Welcome.js";
import { useIsOnline } from "../hooks/useIsOnline.js";
export function Application(): VNode { export function Application(): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -143,7 +142,7 @@ export function Application(): VNode {
path={Pages.settingsExchangeAdd.pattern} path={Pages.settingsExchangeAdd.pattern}
component={() => ( component={() => (
<WalletTemplate> <WalletTemplate>
<ExchangeAddPage onBack={() => redirectTo(Pages.balance)} /> <AddExchange onBack={() => redirectTo(Pages.balance)} />
</WalletTemplate> </WalletTemplate>
)} )}
/> />

View File

@ -1,75 +0,0 @@
/*
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 { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Title } from "../components/styled/index.js";
import { TermsOfService } from "../components/TermsOfService/index.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../mui/Button.js";
export interface Props {
url: string;
onCancel: () => Promise<void>;
onConfirm: () => Promise<void>;
}
export function ExchangeAddConfirmPage({
url,
onCancel,
onConfirm,
}: Props): VNode {
const { i18n } = useTranslationContext();
const [accepted, setAccepted] = useState(false);
return (
<Fragment>
<section>
<Title>
<i18n.Translate>Review terms of service</i18n.Translate>
</Title>
<div>
<i18n.Translate>Exchange URL</i18n.Translate>:
<a href={url} target="_blank" rel="noreferrer">
{url}
</a>
</div>
</section>
<TermsOfService key="terms" exchangeUrl={url} onChange={setAccepted} />
<footer>
<Button
key="cancel"
variant="contained"
color="secondary"
onClick={onCancel}
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button
key="add"
variant="contained"
color="success"
disabled={!accepted}
onClick={onConfirm}
>
<i18n.Translate>Add exchange</i18n.Translate>
</Button>
</footer>
</Fragment>
);
}

View File

@ -1,102 +0,0 @@
/*
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 {
canonicalizeBaseUrl,
TalerConfigResponse,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { queryToSlashKeys } from "../utils/index.js";
import { ExchangeAddConfirmPage } from "./ExchangeAddConfirm.js";
import { ExchangeSetUrlPage } from "./ExchangeSetUrl.js";
interface Props {
currency?: string;
onBack: () => Promise<void>;
}
export function ExchangeAddPage({ currency, onBack }: Props): VNode {
const [verifying, setVerifying] = useState<
{ url: string; config: TalerConfigResponse } | undefined
>(undefined);
const api = useBackendContext();
const knownExchangesResponse = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
);
const knownExchanges = !knownExchangesResponse
? []
: knownExchangesResponse.hasError
? []
: knownExchangesResponse.response.exchanges;
if (!verifying) {
return (
<ExchangeSetUrlPage
onCancel={onBack}
expectedCurrency={currency}
onVerify={async (url) => {
const found =
knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
if (found) {
throw Error("This exchange is already known");
}
return {
name: "1",
version: "15:0:0",
currency: "ARS",
};
}}
onConfirm={
async (url) => {
setVerifying({
url,
config: {
name: "1",
version: "15:0:0",
currency: "ARS",
},
});
return undefined;
}
// queryToSlashKeys<TalerConfigResponse>(url)
// .then((config) => {
// setVerifying({ url, config });
// })
// .catch((e) => e.message)
}
/>
);
}
return (
<ExchangeAddConfirmPage
url={verifying.url}
onCancel={onBack}
onConfirm={async () => {
await api.wallet.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: canonicalizeBaseUrl(verifying.url),
forceUpdate: true,
});
onBack();
}}
/>
);
}

View File

@ -1,68 +0,0 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import * as tests from "@gnu-taler/web-util/testing";
import { queryToSlashKeys } from "../utils/index.js";
import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl.js";
export default {
title: "exchange add set url",
};
export const ExpectedUSD = tests.createExample(TestedComponent, {
expectedCurrency: "USD",
onVerify: queryToSlashKeys,
});
export const ExpectedKUDOS = tests.createExample(TestedComponent, {
expectedCurrency: "KUDOS",
onVerify: queryToSlashKeys,
});
export const InitialState = tests.createExample(TestedComponent, {
onVerify: queryToSlashKeys,
});
const knownExchanges = [
{
currency: "TESTKUDOS",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
tos: {
currentVersion: "1",
acceptedVersion: "1",
content: "content of tos",
contentType: "text/plain",
},
paytoUris: [],
},
];
export const WithDemoAsKnownExchange = tests.createExample(TestedComponent, {
onVerify: async (url) => {
const found =
knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
if (found) {
throw Error("This exchange is already known");
}
return queryToSlashKeys(url);
},
});

View File

@ -1,209 +0,0 @@
/*
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 {
canonicalizeBaseUrl,
TalerConfigResponse,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage.js";
import {
Input,
LightText,
SubTitle,
Title,
WarningBox,
} from "../components/styled/index.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../mui/Button.js";
export interface Props {
initialValue?: string;
expectedCurrency?: string;
onCancel: () => Promise<void>;
onVerify: (s: string) => Promise<TalerConfigResponse | undefined>;
onConfirm: (url: string) => Promise<string | undefined>;
withError?: string;
}
function useEndpointStatus<T>(
endpoint: string,
onVerify: (e: string) => Promise<T>,
): {
loading: boolean;
error?: string;
endpoint: string;
result: T | undefined;
updateEndpoint: (s: string) => void;
} {
const [value, setValue] = useState<string>(endpoint);
const [dirty, setDirty] = useState(false);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<T | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const [handler, setHandler] = useState<number | undefined>(undefined);
useEffect(() => {
if (!value) return;
window.clearTimeout(handler);
const h = window.setTimeout(async () => {
setDirty(true);
setLoading(true);
try {
const url = canonicalizeBaseUrl(value);
const result = await onVerify(url);
setResult(result);
setError(undefined);
setLoading(false);
} catch (e) {
const errorMessage =
e instanceof Error ? e.message : `unknown error: ${e}`;
setError(errorMessage);
setLoading(false);
setResult(undefined);
}
}, 500);
setHandler(h);
}, [value, setHandler, onVerify]);
return {
error: dirty ? error : undefined,
loading: loading,
result: result,
endpoint: value,
updateEndpoint: setValue,
};
}
export function ExchangeSetUrlPage({
initialValue,
expectedCurrency,
onCancel,
onVerify,
onConfirm,
}: Props): VNode {
const { i18n } = useTranslationContext();
const { loading, result, endpoint, updateEndpoint, error } =
useEndpointStatus(initialValue ?? "", onVerify);
const [confirmationError, setConfirmationError] = useState<
string | undefined
>(undefined);
return (
<Fragment>
<section>
{!expectedCurrency ? (
<Title>
<i18n.Translate>Add new exchange</i18n.Translate>
</Title>
) : (
<SubTitle>
<i18n.Translate>Add exchange for {expectedCurrency}</i18n.Translate>
</SubTitle>
)}
{!result && (
<LightText>
<i18n.Translate>
Enter the URL of an exchange you trust.
</i18n.Translate>
</LightText>
)}
{result && (
<LightText>
<i18n.Translate>
An exchange has been found! Review the information and click next
</i18n.Translate>
</LightText>
)}
{result && expectedCurrency && expectedCurrency !== result.currency && (
<WarningBox>
<i18n.Translate>
This exchange doesn&apos;t match the expected currency
<b>{expectedCurrency}</b>
</i18n.Translate>
</WarningBox>
)}
{error && (
<ErrorMessage
title={i18n.str`Unable to verify this exchange`}
description={error}
/>
)}
{confirmationError && (
<ErrorMessage
title={i18n.str`Unable to add this exchange`}
description={confirmationError}
/>
)}
<p>
<Input invalid={!!error}>
<label>URL</label>
<input
type="text"
placeholder="https://"
value={endpoint}
onInput={(e) => updateEndpoint(e.currentTarget.value)}
/>
</Input>
{loading && (
<div>
<i18n.Translate>loading</i18n.Translate>...
</div>
)}
{result && !loading && (
<Fragment>
<Input>
<label>
<i18n.Translate>Version</i18n.Translate>
</label>
<input type="text" disabled value={result.version} />
</Input>
<Input>
<label>
<i18n.Translate>Currency</i18n.Translate>
</label>
<input type="text" disabled value={result.currency} />
</Input>
</Fragment>
)}
</p>
</section>
<footer>
<Button variant="contained" color="secondary" onClick={onCancel}>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button
variant="contained"
disabled={
!result ||
!!error ||
(!!expectedCurrency && expectedCurrency !== result.currency)
}
onClick={() => {
const url = canonicalizeBaseUrl(endpoint);
return onConfirm(url).then((r) =>
r ? setConfirmationError(r) : undefined,
);
}}
>
<i18n.Translate>Next</i18n.Translate>
</Button>
</footer>
</Fragment>
);
}

View File

@ -21,8 +21,6 @@
export * as a1 from "./Backup.stories.js"; export * as a1 from "./Backup.stories.js";
export * as a4 from "./DepositPage/stories.js"; export * as a4 from "./DepositPage/stories.js";
export * as a5 from "./ExchangeAddConfirm.stories.js";
export * as a6 from "./ExchangeAddSetUrl.stories.js";
export * as a7 from "./History.stories.js"; export * as a7 from "./History.stories.js";
export * as a8 from "./AddBackupProvider/stories.js"; export * as a8 from "./AddBackupProvider/stories.js";
export * as a10 from "./ProviderDetail.stories.js"; export * as a10 from "./ProviderDetail.stories.js";

View File

@ -76,7 +76,7 @@ export async function defaultRequestHandler<T>(
type: ErrorType.UNEXPECTED, type: ErrorType.UNEXPECTED,
exception: undefined, exception: undefined,
loading: false, loading: false,
message: `invalid URL: "${validURL}"`, message: `invalid URL: "${baseUrl}${endpoint}"`,
}; };
throw new RequestError(error) throw new RequestError(error)
} }