From 829a59e1a24d6a99ce7554d28acfd05f21baeaf8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 22 Nov 2021 17:34:27 -0300 Subject: add exchange feature --- .../src/wallet/BalancePage.tsx | 17 ++- .../src/wallet/CreateManualWithdraw.tsx | 28 ++-- .../src/wallet/ExchangeAddConfirm.stories.tsx | 67 +++++++++ .../src/wallet/ExchangeAddConfirm.tsx | 152 +++++++++++++++++++++ .../src/wallet/ExchangeAddPage.tsx | 75 ++++++++++ .../src/wallet/ExchangeAddSetUrl.stories.tsx | 62 +++++++++ .../src/wallet/ExchangeSetUrl.tsx | 130 ++++++++++++++++++ .../src/wallet/ProviderAddPage.tsx | 31 +---- .../src/wallet/ReserveCreated.tsx | 5 +- .../src/wallet/Settings.tsx | 5 +- 10 files changed, 522 insertions(+), 50 deletions(-) create mode 100644 packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx (limited to 'packages/taler-wallet-webextension/src/wallet') diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx index 04d79a5ea..0a8910646 100644 --- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx @@ -17,7 +17,7 @@ import { BalancesResponse, i18n } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { BalanceTable } from "../components/BalanceTable"; -import { ButtonPrimary, ErrorBox } from "../components/styled/index"; +import { ButtonPrimary, Centered, ErrorBox } from "../components/styled/index"; import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { PageLink } from "../renderHtml"; import * as wxApi from "../wxApi"; @@ -66,10 +66,17 @@ export function BalanceView({ if (balance.response.balances.length === 0) { return (

- - You have no balance to show. Need some{" "} - help getting started? - + + + You have no balance to show. Need some{" "} + help getting started? + +

+ + Withdraw + +
+

); } diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 1bceabd20..554952795 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -21,6 +21,7 @@ import { AmountJson, Amounts, i18n } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; +import { route } from "preact-router"; import { useState } from "preact/hooks"; import { ErrorMessage } from "../components/ErrorMessage"; import { SelectList } from "../components/SelectList"; @@ -32,7 +33,9 @@ import { Input, InputWithLabel, LightText, + LinkPrimary, } from "../components/styled"; +import { Pages } from "../NavigationBar"; export interface Props { error: string | undefined; @@ -87,12 +90,7 @@ export function CreateManualWithdraw({ return ( No exchange configured - { - null; - }} - > + route(Pages.exchange_add)}> Add exchange @@ -108,8 +106,9 @@ export function CreateManualWithdraw({ />

Manual Withdrawal

- Choose a exchange to create a reserve and then fill the reserve to - withdraw the coins + Choose a exchange from where the coins will be withdrawn. The exchange + will send the coins to this wallet after receiving a wire transfer + with the correct subject.

@@ -130,11 +129,14 @@ export function CreateManualWithdraw({ onChange={changeExchange} /> - {/*

- - Add new exchange - -

*/} +
+ route(Pages.exchange_add)} + style={{ marginLeft: "auto" }} + > + Add exchange + +
{currency && ( diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx new file mode 100644 index 000000000..2e034458a --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { termsXml } from "../cta/termsExample"; +import { createExample } from "../test-utils"; +import { View as TestedComponent } from "./ExchangeAddConfirm"; + +export default { + title: "wallet/exchange add/confirm", + component: TestedComponent, + argTypes: { + onRetry: { action: "onRetry" }, + onDelete: { action: "onDelete" }, + onBack: { action: "onBack" }, + }, +}; + +export const TermsNotFound = createExample(TestedComponent, { + url: "https://exchange.demo.taler.net/", + terms: { + status: "notfound", + version: "1", + content: undefined, + }, + onAccept: async () => undefined, +}); + +export const NewTerms = createExample(TestedComponent, { + url: "https://exchange.demo.taler.net/", + terms: { + status: "new", + version: "1", + content: undefined, + }, + onAccept: async () => undefined, +}); + +export const TermsChanged = createExample(TestedComponent, { + url: "https://exchange.demo.taler.net/", + terms: { + status: "changed", + version: "1", + content: { + type: "xml", + document: new DOMParser().parseFromString(termsXml, "text/xml"), + }, + }, + onAccept: async () => undefined, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx new file mode 100644 index 000000000..5c7f94ecd --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx @@ -0,0 +1,152 @@ +import { i18n } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { + Button, + ButtonSuccess, + ButtonWarning, + WarningBox, +} from "../components/styled/index"; +import { TermsOfServiceSection } from "../cta/TermsOfServiceSection"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import { buildTermsOfServiceState, TermsState } from "../utils"; +import * as wxApi from "../wxApi"; + +export interface Props { + url: string; + onCancel: () => void; + onConfirm: () => void; +} + +export function ExchangeAddConfirmPage({ + url, + onCancel, + onConfirm, +}: Props): VNode { + const detailsHook = useAsyncAsHook(async () => { + const tos = await wxApi.getExchangeTos(url, ["text/xml"]); + + const tosState = buildTermsOfServiceState(tos); + + return { tos: tosState }; + }); + + const termsNotFound: TermsState = { + status: "notfound", + version: "", + content: undefined, + }; + const terms = !detailsHook + ? undefined + : detailsHook.hasError + ? termsNotFound + : detailsHook.response.tos; + + // const [errorAccepting, setErrorAccepting] = useState( + // undefined, + // ); + + const onAccept = async (): Promise => { + if (!terms) return; + try { + await wxApi.setExchangeTosAccepted(url, terms.version); + } catch (e) { + if (e instanceof Error) { + // setErrorAccepting(e.message); + } + } + }; + return ( + + ); +} + +export interface ViewProps { + url: string; + terms: TermsState | undefined; + onAccept: (b: boolean) => Promise; + onCancel: () => void; + onConfirm: () => void; +} + +export function View({ + url, + terms, + onAccept: doAccept, + onConfirm, + onCancel, +}: ViewProps): VNode { + const needsReview = + !terms || terms.status === "changed" || terms.status === "new"; + const [reviewed, setReviewed] = useState(false); + + return ( + +
+

Review terms of service

+
+ Exchange URL: + + {url} + +
+
+ {terms && terms.status === "notfound" && ( +
+ + {i18n.str`Exchange doesn't have terms of service`} + +
+ )} + + {terms && ( + + doAccept(value).then(() => { + setReviewed(value); + }) + } + /> + )} + +
+ + {!terms && ( + + )} + {terms && ( + + {needsReview && !reviewed && ( + + {i18n.str`Add exchange`} + + )} + {(terms.status === "accepted" || (needsReview && reviewed)) && ( + + {i18n.str`Add exchange`} + + )} + {terms.status === "notfound" && ( + + {i18n.str`Add exchange anyway`} + + )} + + )} +
+
+ ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx new file mode 100644 index 000000000..10449c101 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx @@ -0,0 +1,75 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +import { + canonicalizeBaseUrl, + TalerConfigResponse, +} from "@gnu-taler/taler-util"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import { queryToSlashKeys } from "../utils"; +import * as wxApi from "../wxApi"; +import { ExchangeAddConfirmPage } from "./ExchangeAddConfirm"; +import { ExchangeSetUrlPage } from "./ExchangeSetUrl"; + +interface Props { + currency: string; + onBack: () => void; +} + +export function ExchangeAddPage({ onBack }: Props): VNode { + const [verifying, setVerifying] = useState< + { url: string; config: TalerConfigResponse } | undefined + >(undefined); + + const knownExchangesResponse = useAsyncAsHook(wxApi.listExchanges); + const knownExchanges = !knownExchangesResponse + ? [] + : knownExchangesResponse.hasError + ? [] + : knownExchangesResponse.response.exchanges; + + if (!verifying) { + return ( + queryToSlashKeys(url)} + onConfirm={(url) => + queryToSlashKeys(url) + .then((config) => { + setVerifying({ url, config }); + }) + .catch((e) => e.message) + } + /> + ); + } + return ( + { + await wxApi.addExchange({ + exchangeBaseUrl: canonicalizeBaseUrl(verifying.url), + forceUpdate: true, + }); + onBack(); + }} + /> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx new file mode 100644 index 000000000..bc182cb70 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx @@ -0,0 +1,62 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createExample } from "../test-utils"; +import { queryToSlashKeys } from "../utils"; +import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl"; + +export default { + title: "wallet/exchange add/set url", + component: TestedComponent, + argTypes: { + onRetry: { action: "onRetry" }, + onDelete: { action: "onDelete" }, + onBack: { action: "onBack" }, + }, +}; + +export const ExpectedUSD = createExample(TestedComponent, { + expectedCurrency: "USD", + onVerify: queryToSlashKeys, + knownExchanges: [], +}); + +export const ExpectedKUDOS = createExample(TestedComponent, { + expectedCurrency: "KUDOS", + onVerify: queryToSlashKeys, + knownExchanges: [], +}); + +export const InitialState = createExample(TestedComponent, { + onVerify: queryToSlashKeys, + knownExchanges: [], +}); + +export const WithDemoAsKnownExchange = createExample(TestedComponent, { + knownExchanges: [ + { + currency: "TESTKUDOS", + exchangeBaseUrl: "https://exchange.demo.taler.net/", + paytoUris: [], + }, + ], + onVerify: queryToSlashKeys, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx new file mode 100644 index 000000000..e87a8894f --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx @@ -0,0 +1,130 @@ +import { + canonicalizeBaseUrl, + ExchangeListItem, + i18n, + TalerConfigResponse, +} from "@gnu-taler/taler-util"; +import { Fragment, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { ErrorMessage } from "../components/ErrorMessage"; +import { + Button, + ButtonPrimary, + Input, + WarningBox, +} from "../components/styled/index"; + +export interface Props { + initialValue?: string; + expectedCurrency?: string; + knownExchanges: ExchangeListItem[]; + onCancel: () => void; + onVerify: (s: string) => Promise; + onConfirm: (url: string) => Promise; + withError?: string; +} + +export function ExchangeSetUrlPage({ + initialValue, + knownExchanges, + expectedCurrency, + onCancel, + onVerify, + onConfirm, + withError, +}: Props) { + const [value, setValue] = useState(initialValue || ""); + const [dirty, setDirty] = useState(false); + const [result, setResult] = useState( + undefined, + ); + const [error, setError] = useState(withError); + + useEffect(() => { + try { + const url = canonicalizeBaseUrl(value); + + const found = + knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1; + + if (found) { + setError("This exchange is already known"); + return; + } + onVerify(url) + .then((r) => { + setResult(r); + }) + .catch(() => { + setResult(undefined); + }); + setDirty(true); + } catch { + setResult(undefined); + } + }, [value]); + + return ( + +
+ {!expectedCurrency ? ( +

Add new exchange

+ ) : ( +

Add exchange for {expectedCurrency}

+ )} + +

+ + + setValue(e.currentTarget.value)} + /> + + {result && ( + + + + + + + + + + + )} +

+
+ {result && expectedCurrency && expectedCurrency !== result.currency && ( + + This exchange doesn't match the expected currency{" "} + {expectedCurrency} + + )} +
+ + { + const url = canonicalizeBaseUrl(value); + return onConfirm(url).then((r) => (r ? setError(r) : undefined)); + }} + > + Next + +
+
+ ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx index 41852e38c..16f239674 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx @@ -31,6 +31,7 @@ import { LightText, SmallLightText, } from "../components/styled/index"; +import { queryToSlashConfig } from "../utils"; import * as wxApi from "../wxApi"; interface Props { @@ -38,45 +39,19 @@ interface Props { onBack: () => void; } -function getJsonIfOk(r: Response) { - if (r.ok) { - return r.json(); - } else { - if (r.status >= 400 && r.status < 500) { - throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`); - } else { - throw new Error( - `Try another server: (${r.status}) ${ - r.statusText || "internal server error" - }`, - ); - } - } -} - export function ProviderAddPage({ onBack }: Props): VNode { const [verifying, setVerifying] = useState< | { url: string; name: string; provider: BackupBackupProviderTerms } | undefined >(undefined); - async function getProviderInfo( - url: string, - ): Promise { - return fetch(new URL("config", url).href) - .catch((e) => { - throw new Error(`Network error`); - }) - .then(getJsonIfOk); - } - if (!verifying) { return ( getProviderInfo(url)} + onVerify={(url) => queryToSlashConfig(url)} onConfirm={(url, name) => - getProviderInfo(url) + queryToSlashConfig(url) .then((provider) => { setVerifying({ url, name, provider }); }) diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx index 075126dc8..f009c5ad0 100644 --- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx @@ -3,6 +3,7 @@ import { Fragment, h, VNode } from "preact"; import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType"; import { QR } from "../components/QR"; import { ButtonDestructive, WarningBox } from "../components/styled"; +import { amountToString } from "../utils"; export interface Props { reservePub: string; payto: string; @@ -29,10 +30,10 @@ export function ReserveCreated({

Exchange is ready for withdrawal!

To complete the process you need to wire{" "} - {Amounts.stringify(amount)} to the exchange bank account + {amountToString(amount)} to the exchange bank account

- Manage exchange + Add an exchange

-- cgit v1.2.3