Merge branch 'master' into age-withdraw
This commit is contained in:
commit
b5f5001249
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "@gnu-taler/demobank-ui",
|
"name": "@gnu-taler/demobank-ui",
|
||||||
"version": "0.9.3-dev.27",
|
"version": "0.9.3-dev.29",
|
||||||
"license": "AGPL-3.0-OR-LATER",
|
"license": "AGPL-3.0-OR-LATER",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1652,9 +1652,6 @@ export class MerchantService implements MerchantServiceInterface {
|
|||||||
|
|
||||||
const body: MerchantInstanceConfig = {
|
const body: MerchantInstanceConfig = {
|
||||||
auth,
|
auth,
|
||||||
accounts: instanceConfig.paytoUris.map((x) => ({
|
|
||||||
payto_uri: x,
|
|
||||||
})),
|
|
||||||
id: instanceConfig.id,
|
id: instanceConfig.id,
|
||||||
name: instanceConfig.name,
|
name: instanceConfig.name,
|
||||||
address: instanceConfig.address ?? {},
|
address: instanceConfig.address ?? {},
|
||||||
|
@ -28,9 +28,17 @@ import {
|
|||||||
MerchantApiClient,
|
MerchantApiClient,
|
||||||
rsaBlind,
|
rsaBlind,
|
||||||
setGlobalLogLevelFromString,
|
setGlobalLogLevelFromString,
|
||||||
|
RegisterAccountRequest,
|
||||||
|
HttpStatusCode,
|
||||||
|
MerchantInstanceConfig,
|
||||||
|
Duration,
|
||||||
|
generateIban,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { clk } from "@gnu-taler/taler-util/clk";
|
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 {
|
import {
|
||||||
CryptoDispatcher,
|
CryptoDispatcher,
|
||||||
downloadExchangeInfo,
|
downloadExchangeInfo,
|
||||||
@ -46,7 +54,11 @@ import { runBench2 } from "./bench2.js";
|
|||||||
import { runBench3 } from "./bench3.js";
|
import { runBench3 } from "./bench3.js";
|
||||||
import { runEnvFull } from "./env-full.js";
|
import { runEnvFull } from "./env-full.js";
|
||||||
import { runEnv1 } from "./env1.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 { getTestInfo, runTests } from "./integrationtests/testrunner.js";
|
||||||
import { lintExchangeDeployment } from "./lint.js";
|
import { lintExchangeDeployment } from "./lint.js";
|
||||||
|
|
||||||
@ -312,8 +324,7 @@ deploymentCli
|
|||||||
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
|
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
|
||||||
await topupReserveWithDemobank({
|
await topupReserveWithDemobank({
|
||||||
amount: "KUDOS:10",
|
amount: "KUDOS:10",
|
||||||
corebankApiBaseUrl:
|
corebankApiBaseUrl: "https://bank.demo.taler.net/",
|
||||||
"https://bank.demo.taler.net/",
|
|
||||||
exchangeInfo,
|
exchangeInfo,
|
||||||
http,
|
http,
|
||||||
reservePub: reserveKeyPair.pub,
|
reservePub: reserveKeyPair.pub,
|
||||||
@ -341,8 +352,7 @@ deploymentCli
|
|||||||
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
|
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
|
||||||
await topupReserveWithDemobank({
|
await topupReserveWithDemobank({
|
||||||
amount: "TESTKUDOS:10",
|
amount: "TESTKUDOS:10",
|
||||||
corebankApiBaseUrl:
|
corebankApiBaseUrl: "https://bank.test.taler.net/",
|
||||||
"https://bank.test.taler.net/",
|
|
||||||
exchangeInfo,
|
exchangeInfo,
|
||||||
http,
|
http,
|
||||||
reservePub: reserveKeyPair.pub,
|
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
|
deploymentCli
|
||||||
.subcommand("coincfg", "gen-coin-config", {
|
.subcommand("coincfg", "gen-coin-config", {
|
||||||
help: "Generate a coin/denomination configuration for the exchange.",
|
help: "Generate a coin/denomination configuration for the exchange.",
|
||||||
|
@ -72,11 +72,6 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
|
|||||||
),
|
),
|
||||||
jurisdiction: {},
|
jurisdiction: {},
|
||||||
name: "My Default Instance",
|
name: "My Default Instance",
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
payto_uri: generateRandomPayto("bar"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
auth: {
|
auth: {
|
||||||
method: "token",
|
method: "token",
|
||||||
token: "secret-token:i-am-default",
|
token: "secret-token:i-am-default",
|
||||||
@ -95,11 +90,6 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
|
|||||||
),
|
),
|
||||||
jurisdiction: {},
|
jurisdiction: {},
|
||||||
name: "My Second Instance",
|
name: "My Second Instance",
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
payto_uri: generateRandomPayto("bar"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
auth: {
|
auth: {
|
||||||
method: "token",
|
method: "token",
|
||||||
token: "secret-token:i-am-myinst",
|
token: "secret-token:i-am-myinst",
|
||||||
|
@ -74,18 +74,6 @@ export interface DeleteTippingReserveArgs {
|
|||||||
purge?: boolean;
|
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 {
|
interface MerchantBankAccount {
|
||||||
// The payto:// URI where the wallet will send coins.
|
// The payto:// URI where the wallet will send coins.
|
||||||
payto_uri: string;
|
payto_uri: string;
|
||||||
@ -102,7 +90,6 @@ interface MerchantBankAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantInstanceConfig {
|
export interface MerchantInstanceConfig {
|
||||||
accounts: MerchantBankAccount[];
|
|
||||||
auth: MerchantAuthConfiguration;
|
auth: MerchantAuthConfiguration;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -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",
|
||||||
|
@ -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> {
|
||||||
|
@ -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 * 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/",
|
|
||||||
});
|
|
@ -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,
|
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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -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 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";
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user