Compare commits
9 Commits
ffcb40b464
...
b5f5001249
Author | SHA1 | Date | |
---|---|---|---|
b5f5001249 | |||
57f8cd3853 | |||
1e10586322 | |||
a45f45b61b | |||
2ecdd6816d | |||
|
b173b3ac0f | ||
|
148846e68f | ||
2b293be4fe | |||
42b4f8f915 |
@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@gnu-taler/demobank-ui",
|
||||
"version": "0.9.3-dev.27",
|
||||
"version": "0.9.3-dev.29",
|
||||
"license": "AGPL-3.0-OR-LATER",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -1652,9 +1652,6 @@ export class MerchantService implements MerchantServiceInterface {
|
||||
|
||||
const body: MerchantInstanceConfig = {
|
||||
auth,
|
||||
accounts: instanceConfig.paytoUris.map((x) => ({
|
||||
payto_uri: x,
|
||||
})),
|
||||
id: instanceConfig.id,
|
||||
name: instanceConfig.name,
|
||||
address: instanceConfig.address ?? {},
|
||||
|
@ -28,9 +28,17 @@ import {
|
||||
MerchantApiClient,
|
||||
rsaBlind,
|
||||
setGlobalLogLevelFromString,
|
||||
RegisterAccountRequest,
|
||||
HttpStatusCode,
|
||||
MerchantInstanceConfig,
|
||||
Duration,
|
||||
generateIban,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { clk } from "@gnu-taler/taler-util/clk";
|
||||
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
|
||||
import {
|
||||
HttpResponse,
|
||||
createPlatformHttpLib,
|
||||
} from "@gnu-taler/taler-util/http";
|
||||
import {
|
||||
CryptoDispatcher,
|
||||
downloadExchangeInfo,
|
||||
@ -46,7 +54,11 @@ import { runBench2 } from "./bench2.js";
|
||||
import { runBench3 } from "./bench3.js";
|
||||
import { runEnvFull } from "./env-full.js";
|
||||
import { runEnv1 } from "./env1.js";
|
||||
import { GlobalTestState, runTestWithState } from "./harness/harness.js";
|
||||
import {
|
||||
GlobalTestState,
|
||||
delayMs,
|
||||
runTestWithState,
|
||||
} from "./harness/harness.js";
|
||||
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
|
||||
import { lintExchangeDeployment } from "./lint.js";
|
||||
|
||||
@ -312,8 +324,7 @@ deploymentCli
|
||||
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
|
||||
await topupReserveWithDemobank({
|
||||
amount: "KUDOS:10",
|
||||
corebankApiBaseUrl:
|
||||
"https://bank.demo.taler.net/",
|
||||
corebankApiBaseUrl: "https://bank.demo.taler.net/",
|
||||
exchangeInfo,
|
||||
http,
|
||||
reservePub: reserveKeyPair.pub,
|
||||
@ -341,8 +352,7 @@ deploymentCli
|
||||
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
|
||||
await topupReserveWithDemobank({
|
||||
amount: "TESTKUDOS:10",
|
||||
corebankApiBaseUrl:
|
||||
"https://bank.test.taler.net/",
|
||||
corebankApiBaseUrl: "https://bank.test.taler.net/",
|
||||
exchangeInfo,
|
||||
http,
|
||||
reservePub: reserveKeyPair.pub,
|
||||
@ -422,6 +432,224 @@ deploymentCli
|
||||
);
|
||||
});
|
||||
|
||||
deploymentCli
|
||||
.subcommand("waitService", "wait-taler-service", {
|
||||
help: "Wait for the config endpoint of a Taler-style service to be available",
|
||||
})
|
||||
.requiredArgument("serviceName", clk.STRING)
|
||||
.requiredArgument("serviceConfigUrl", clk.STRING)
|
||||
.action(async (args) => {
|
||||
const serviceName = args.waitService.serviceName;
|
||||
const serviceUrl = args.waitService.serviceConfigUrl;
|
||||
console.log(
|
||||
`Waiting for service ${serviceName} to be ready at ${serviceUrl}`,
|
||||
);
|
||||
const httpLib = createPlatformHttpLib();
|
||||
while (1) {
|
||||
console.log(`Fetching ${serviceUrl}`);
|
||||
let resp: HttpResponse;
|
||||
try {
|
||||
resp = await httpLib.fetch(serviceUrl);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Got network error for service ${serviceName} at ${serviceUrl}`,
|
||||
);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
if (resp.status != 200) {
|
||||
console.log(
|
||||
`Got unexpected status ${resp.status} for service at ${serviceUrl}`,
|
||||
);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
let respJson: any;
|
||||
try {
|
||||
respJson = await resp.json();
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Got json error for service ${serviceName} at ${serviceUrl}`,
|
||||
);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
const recServiceName = respJson.name;
|
||||
console.log(`Got name ${recServiceName}`);
|
||||
if (recServiceName != serviceName) {
|
||||
console.log(`A different service is still running at ${serviceUrl}`);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
console.log(`service ${serviceName} at ${serviceUrl} is now available`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
deploymentCli
|
||||
.subcommand("waitEndpoint", "wait-endpoint", {
|
||||
help: "Wait for an endpoint to return an HTTP 200 Ok status with JSON body",
|
||||
})
|
||||
.requiredArgument("serviceEndpoint", clk.STRING)
|
||||
.action(async (args) => {
|
||||
const serviceUrl = args.waitEndpoint.serviceEndpoint;
|
||||
console.log(`Waiting for endpoint ${serviceUrl} to be ready`);
|
||||
const httpLib = createPlatformHttpLib();
|
||||
while (1) {
|
||||
console.log(`Fetching ${serviceUrl}`);
|
||||
let resp: HttpResponse;
|
||||
try {
|
||||
resp = await httpLib.fetch(serviceUrl);
|
||||
} catch (e) {
|
||||
console.log(`Got network error for service at ${serviceUrl}`);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
if (resp.status != 200) {
|
||||
console.log(
|
||||
`Got unexpected status ${resp.status} for service at ${serviceUrl}`,
|
||||
);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
let respJson: any;
|
||||
try {
|
||||
respJson = await resp.json();
|
||||
} catch (e) {
|
||||
console.log(`Got json error for service at ${serviceUrl}`);
|
||||
await delayMs(1000);
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
deploymentCli
|
||||
.subcommand("genIban", "gen-iban", {
|
||||
help: "Generate a random IBAN.",
|
||||
})
|
||||
.requiredArgument("countryCode", clk.STRING)
|
||||
.requiredArgument("length", clk.INT)
|
||||
.action(async (args) => {
|
||||
console.log(generateIban(args.genIban.countryCode, args.genIban.length));
|
||||
});
|
||||
|
||||
deploymentCli
|
||||
.subcommand("provisionMerchantInstance", "provision-merchant-instance", {
|
||||
help: "Provision a merchant backend instance.",
|
||||
})
|
||||
.requiredArgument("merchantApiBaseUrl", clk.STRING)
|
||||
.requiredOption("managementToken", ["--management-token"], clk.STRING)
|
||||
.requiredOption("instanceToken", ["--instance-token"], clk.STRING)
|
||||
.requiredOption("name", ["--name"], clk.STRING)
|
||||
.requiredOption("id", ["--id"], clk.STRING)
|
||||
.requiredOption("payto", ["--payto"], clk.STRING)
|
||||
.action(async (args) => {
|
||||
const httpLib = createPlatformHttpLib();
|
||||
const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl;
|
||||
const managementToken = args.provisionMerchantInstance.managementToken;
|
||||
const instanceToken = args.provisionMerchantInstance.instanceToken;
|
||||
const instanceId = args.provisionMerchantInstance.id;
|
||||
const body: MerchantInstanceConfig = {
|
||||
address: {},
|
||||
auth: {
|
||||
method: "token",
|
||||
token: args.provisionMerchantInstance.instanceToken,
|
||||
},
|
||||
default_pay_delay: Duration.toTalerProtocolDuration(
|
||||
Duration.fromSpec({ hours: 1 }),
|
||||
),
|
||||
default_wire_transfer_delay: { d_us: 1 },
|
||||
id: instanceId,
|
||||
jurisdiction: {},
|
||||
name: args.provisionMerchantInstance.name,
|
||||
use_stefan: true,
|
||||
};
|
||||
const url = new URL("management/instances", baseUrl);
|
||||
const createResp = await httpLib.fetch(url.href, {
|
||||
method: "POST",
|
||||
body,
|
||||
headers: {
|
||||
Authorization: `Bearer ${managementToken}`,
|
||||
},
|
||||
});
|
||||
if (createResp.status >= 200 && createResp.status <= 299) {
|
||||
logger.info(`instance ${instanceId} created successfully`);
|
||||
} else if (createResp.status === HttpStatusCode.Conflict) {
|
||||
logger.info(`instance ${instanceId} already exists`);
|
||||
} else {
|
||||
logger.error(
|
||||
`unable to create instance ${instanceId}, HTTP status ${createResp.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const accountsUrl = new URL(
|
||||
`instances/${instanceId}/private/accounts`,
|
||||
baseUrl,
|
||||
);
|
||||
const accountBody = {
|
||||
payto_uri: args.provisionMerchantInstance.payto,
|
||||
};
|
||||
const createAccountResp = await httpLib.fetch(accountsUrl.href, {
|
||||
method: "POST",
|
||||
body: accountBody,
|
||||
headers: {
|
||||
Authorization: `Bearer ${instanceToken}`,
|
||||
},
|
||||
});
|
||||
if (createAccountResp.status != 200) {
|
||||
console.error(
|
||||
`unable to configure bank account for instance ${instanceId}, status ${createAccountResp.status}`,
|
||||
);
|
||||
const resp = await createAccountResp.json();
|
||||
console.error(j2s(resp));
|
||||
process.exit(2);
|
||||
}
|
||||
logger.info(`successfully configured bank account for ${instanceId}`);
|
||||
});
|
||||
|
||||
deploymentCli
|
||||
.subcommand("provisionBankAccount", "provision-bank-account", {
|
||||
help: "Provision a corebank account.",
|
||||
})
|
||||
.requiredArgument("corebankApiBaseUrl", clk.STRING)
|
||||
.flag("exchange", ["--exchange"])
|
||||
.flag("public", ["--public"])
|
||||
.requiredOption("login", ["--login"], clk.STRING)
|
||||
.requiredOption("name", ["--name"], clk.STRING)
|
||||
.requiredOption("password", ["--password"], clk.STRING)
|
||||
.maybeOption("internalPayto", ["--payto"], clk.STRING)
|
||||
.action(async (args) => {
|
||||
const httpLib = createPlatformHttpLib();
|
||||
const corebankApiBaseUrl = args.provisionBankAccount.corebankApiBaseUrl;
|
||||
const url = new URL("accounts", corebankApiBaseUrl);
|
||||
const accountLogin = args.provisionBankAccount.login;
|
||||
const body: RegisterAccountRequest = {
|
||||
name: args.provisionBankAccount.name,
|
||||
password: args.provisionBankAccount.password,
|
||||
username: accountLogin,
|
||||
is_public: !!args.provisionBankAccount.public,
|
||||
is_taler_exchange: !!args.provisionBankAccount.exchange,
|
||||
internal_payto_uri: args.provisionBankAccount.internalPayto,
|
||||
};
|
||||
const resp = await httpLib.fetch(url.href, {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
if (resp.status >= 200 && resp.status <= 299) {
|
||||
logger.info(`account ${accountLogin} successfully provisioned`);
|
||||
return;
|
||||
}
|
||||
if (resp.status === HttpStatusCode.Conflict) {
|
||||
logger.info(`account ${accountLogin} already provisioned`);
|
||||
return;
|
||||
}
|
||||
logger.error(
|
||||
`unable to provision bank account, HTTP response status ${resp.status}`,
|
||||
);
|
||||
process.exit(2);
|
||||
});
|
||||
|
||||
deploymentCli
|
||||
.subcommand("coincfg", "gen-coin-config", {
|
||||
help: "Generate a coin/denomination configuration for the exchange.",
|
||||
|
@ -72,11 +72,6 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
|
||||
),
|
||||
jurisdiction: {},
|
||||
name: "My Default Instance",
|
||||
accounts: [
|
||||
{
|
||||
payto_uri: generateRandomPayto("bar"),
|
||||
},
|
||||
],
|
||||
auth: {
|
||||
method: "token",
|
||||
token: "secret-token:i-am-default",
|
||||
@ -95,11 +90,6 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
|
||||
),
|
||||
jurisdiction: {},
|
||||
name: "My Second Instance",
|
||||
accounts: [
|
||||
{
|
||||
payto_uri: generateRandomPayto("bar"),
|
||||
},
|
||||
],
|
||||
auth: {
|
||||
method: "token",
|
||||
token: "secret-token:i-am-myinst",
|
||||
|
@ -74,18 +74,6 @@ export interface DeleteTippingReserveArgs {
|
||||
purge?: boolean;
|
||||
}
|
||||
|
||||
export interface MerchantInstanceConfig {
|
||||
accounts: MerchantBankAccount[];
|
||||
auth: MerchantAuthConfiguration;
|
||||
id: string;
|
||||
name: string;
|
||||
address: unknown;
|
||||
jurisdiction: unknown;
|
||||
use_stefan: boolean;
|
||||
default_wire_transfer_delay: TalerProtocolDuration;
|
||||
default_pay_delay: TalerProtocolDuration;
|
||||
}
|
||||
|
||||
interface MerchantBankAccount {
|
||||
// The payto:// URI where the wallet will send coins.
|
||||
payto_uri: string;
|
||||
@ -102,7 +90,6 @@ interface MerchantBankAccount {
|
||||
}
|
||||
|
||||
export interface MerchantInstanceConfig {
|
||||
accounts: MerchantBankAccount[];
|
||||
auth: MerchantAuthConfiguration;
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -184,7 +184,7 @@ export function pathsub(
|
||||
defaultValue = undefined;
|
||||
}
|
||||
|
||||
const r = lookup(inner, depth + 1);
|
||||
const r = lookup(varname, depth + 1);
|
||||
if (r !== undefined) {
|
||||
s = s.substring(0, start) + r + s.substring(p + 1);
|
||||
l = start + r.length;
|
||||
|
@ -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",
|
||||
@ -75,4 +75,4 @@
|
||||
"pogen": {
|
||||
"domain": "taler-wallet-webex"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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