fix: take into account dd48 when adding exchanges
This commit is contained in:
parent
148846e68f
commit
b173b3ac0f
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@gnu-taler/taler-wallet-webextension",
|
||||
"version": "0.9.3-dev.27",
|
||||
"version": "0.9.3-dev.31",
|
||||
"description": "GNU Taler Wallet browser extension",
|
||||
"main": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
|
@ -34,8 +34,10 @@ export type SafeHandler<T> = {
|
||||
[__safe_handler]: true;
|
||||
};
|
||||
|
||||
type UnsafeHandler<T> = ((p: T) => Promise<void>) | ((p: T) => void);
|
||||
|
||||
export function withSafe<T>(
|
||||
handler: (p: T) => Promise<void>,
|
||||
handler: UnsafeHandler<T>,
|
||||
onError: (e: Error) => void,
|
||||
): SafeHandler<T> {
|
||||
const sh = async function (p: T): Promise<void> {
|
||||
|
@ -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,
|
||||
);
|
@ -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 ,
|
||||
};
|
||||
}
|
||||
|
@ -20,26 +20,10 @@
|
||||
*/
|
||||
|
||||
import * as tests from "@gnu-taler/web-util/testing";
|
||||
import { ExchangeAddConfirmPage as TestedComponent } from "./ExchangeAddConfirm.js";
|
||||
import { ConfirmView, VerifyView } from "./views.js";
|
||||
|
||||
export default {
|
||||
title: "exchange add confirm",
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onRetry: { action: "onRetry" },
|
||||
onDelete: { action: "onDelete" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
title: "example",
|
||||
};
|
||||
|
||||
export const TermsNotFound = tests.createExample(TestedComponent, {
|
||||
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/",
|
||||
});
|
||||
// export const Ready = tests.createExample(ReadyView, {});
|
@ -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");
|
||||
});
|
||||
});
|
@ -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'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>
|
||||
);
|
||||
}
|
@ -65,15 +65,15 @@ import {
|
||||
WithdrawPageFromParams,
|
||||
WithdrawPageFromURI,
|
||||
} from "../cta/Withdraw/index.js";
|
||||
import { useIsOnline } from "../hooks/useIsOnline.js";
|
||||
import { strings } from "../i18n/strings.js";
|
||||
import { platform } from "../platform/foreground.js";
|
||||
import CloseIcon from "../svg/close_24px.inline.svg";
|
||||
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
|
||||
import { AddExchange } from "./AddExchange/index.js";
|
||||
import { BackupPage } from "./BackupPage.js";
|
||||
import { DepositPage } from "./DepositPage/index.js";
|
||||
import { DestinationSelectionPage } from "./DestinationSelection/index.js";
|
||||
import { DeveloperPage } from "./DeveloperPage.js";
|
||||
import { ExchangeAddPage } from "./ExchangeAddPage.js";
|
||||
import { HistoryPage } from "./History.js";
|
||||
import { NotificationsPage } from "./Notifications/index.js";
|
||||
import { ProviderDetailPage } from "./ProviderDetailPage.js";
|
||||
@ -81,7 +81,6 @@ import { QrReaderPage } from "./QrReader.js";
|
||||
import { SettingsPage } from "./Settings.js";
|
||||
import { TransactionPage } from "./Transaction.js";
|
||||
import { WelcomePage } from "./Welcome.js";
|
||||
import { useIsOnline } from "../hooks/useIsOnline.js";
|
||||
|
||||
export function Application(): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
@ -143,7 +142,7 @@ export function Application(): VNode {
|
||||
path={Pages.settingsExchangeAdd.pattern}
|
||||
component={() => (
|
||||
<WalletTemplate>
|
||||
<ExchangeAddPage onBack={() => redirectTo(Pages.balance)} />
|
||||
<AddExchange onBack={() => redirectTo(Pages.balance)} />
|
||||
</WalletTemplate>
|
||||
)}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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);
|
||||
},
|
||||
});
|
@ -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'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>
|
||||
);
|
||||
}
|
@ -21,8 +21,6 @@
|
||||
|
||||
export * as a1 from "./Backup.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 a8 from "./AddBackupProvider/stories.js";
|
||||
export * as a10 from "./ProviderDetail.stories.js";
|
||||
|
@ -76,7 +76,7 @@ export async function defaultRequestHandler<T>(
|
||||
type: ErrorType.UNEXPECTED,
|
||||
exception: undefined,
|
||||
loading: false,
|
||||
message: `invalid URL: "${validURL}"`,
|
||||
message: `invalid URL: "${baseUrl}${endpoint}"`,
|
||||
};
|
||||
throw new RequestError(error)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user