diff options
34 files changed, 1220 insertions, 1115 deletions
diff --git a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx index 133c06339..79712c2f4 100644 --- a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx +++ b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx @@ -18,8 +18,8 @@ import { Outlined, StyledCheckboxLabel } from "./styled/index.js"; import { h, VNode } from "preact"; interface Props { - enabled: boolean; - onToggle: () => Promise<void>; + enabled?: boolean; + onToggle?: () => Promise<void>; label: VNode; name: string; } diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts index 61f286d1f..ff04a8247 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts @@ -17,9 +17,7 @@ import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; -import { - State as SelectExchangeState -} from "../../hooks/useSelectedExchange.js"; +import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; @@ -34,12 +32,12 @@ export interface Props { onSuccess: (tx: string) => Promise<void>; } -export type State = State.Loading +export type State = + | State.Loading | State.LoadingUriError | State.Ready | SelectExchangeState.Selecting - | SelectExchangeState.NoExchange - ; + | SelectExchangeState.NoExchange; export namespace State { export interface Loading { diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts index 4f75e982d..205a664e0 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts @@ -23,7 +23,7 @@ import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; import * as wxApi from "../../wxApi.js"; import { Props, State } from "./index.js"; -type RecursiveState<S extends object> = S | (() => RecursiveState<S>) +type RecursiveState<S extends object> = S | (() => RecursiveState<S>); export function useComponentState( { amount: amountStr, onClose, onSuccess }: Props, @@ -46,7 +46,7 @@ export function useComponentState( }; } - const exchangeList = hook.response.exchanges + const exchangeList = hook.response.exchanges; return () => { const [subject, setSubject] = useState(""); @@ -55,14 +55,17 @@ export function useComponentState( TalerErrorDetail | undefined >(undefined); + const selectedExchange = useSelectedExchange({ + currency: amount.currency, + defaultExchange: undefined, + list: exchangeList, + }); - const selectedExchange = useSelectedExchange({ currency: amount.currency, defaultExchange: undefined, list: exchangeList }) - - if (selectedExchange.status !== 'ready') { - return selectedExchange + if (selectedExchange.status !== "ready") { + return selectedExchange; } - const exchange = selectedExchange.selected + const exchange = selectedExchange.selected; async function accept(): Promise<void> { try { @@ -105,9 +108,5 @@ export function useComponentState( error: undefined, operationError, }; - } - - - - + }; } diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx index 306d1b199..77885b0c1 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx @@ -38,9 +38,7 @@ export const Ready = createExample(ReadyView, { value: 1, fraction: 0, }, - doSelectExchange: { - - }, + doSelectExchange: {}, exchangeUrl: "https://exchange.taler.ar", subject: { value: "some subject", diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/cta/TermsOfService/index.ts new file mode 100644 index 000000000..9485f9d0a --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TermsOfService/index.ts @@ -0,0 +1,96 @@ +/* + 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 { Loading } from "../../components/Loading.js"; +import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { ToggleHandler } from "../../mui/handlers.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import * as wxApi from "../../wxApi.js"; +import { useComponentState } from "./state.js"; +import { TermsState } from "./utils.js"; +import { + ErrorAcceptingView, + LoadingUriView, + ShowButtonsAcceptedTosView, + ShowButtonsNonAcceptedTosView, + ShowTosContentView, +} from "./views.js"; + +export interface Props { + exchangeUrl: string; + onChange: (v: boolean) => void; + readOnly?: boolean; +} + +export type State = + | State.Loading + | State.LoadingUriError + | State.ErrorAccepting + | State.ShowContent + | State.ShowButtonsAccepted + | State.ShowButtonsNotAccepted + | State.ShowContent; + +export namespace State { + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "loading-error"; + error: HookError; + } + + export interface ErrorAccepting { + status: "error-accepting"; + error: HookError; + } + + export interface BaseInfo { + error: undefined; + termsAccepted: ToggleHandler; + showingTermsOfService: ToggleHandler; + terms: TermsState; + } + export interface ShowContent extends BaseInfo { + status: "show-content"; + error: undefined; + } + export interface ShowButtonsAccepted extends BaseInfo { + status: "show-buttons-accepted"; + error: undefined; + } + export interface ShowButtonsNotAccepted extends BaseInfo { + status: "show-buttons-not-accepted"; + error: undefined; + } +} + +const viewMapping: StateViewMap<State> = { + loading: Loading, + "loading-error": LoadingUriView, + "show-content": ShowTosContentView, + "show-buttons-accepted": ShowButtonsAcceptedTosView, + "show-buttons-not-accepted": ShowButtonsNonAcceptedTosView, + "error-accepting": ErrorAcceptingView, +}; + +export const TermsOfService = compose( + "TermsOfService", + (p: Props) => useComponentState(p, wxApi), + viewMapping, +); diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/state.ts b/packages/taler-wallet-webextension/src/cta/TermsOfService/state.ts new file mode 100644 index 000000000..4e89bc243 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TermsOfService/state.ts @@ -0,0 +1,136 @@ +/* + 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 } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import * as wxApi from "../../wxApi.js"; +import { Props, State } from "./index.js"; +import { buildTermsOfServiceState } from "./utils.js"; + +export function useComponentState( + { exchangeUrl, readOnly, onChange }: Props, + api: typeof wxApi, +): State { + const [showContent, setShowContent] = useState<boolean>(false); + // const [accepted, setAccepted] = useState<boolean>(false); + const [errorAccepting, setErrorAccepting] = useState<Error | undefined>( + undefined, + ); + + /** + * For the exchange selected, bring the status of the terms of service + */ + const terms = useAsyncAsHook(async () => { + const exchangeTos = await api.getExchangeTos(exchangeUrl, ["text/xml"]); + + const state = buildTermsOfServiceState(exchangeTos); + + return { state }; + }, []); + + if (!terms) { + return { + status: "loading", + error: undefined, + }; + } + if (terms.hasError) { + return { + status: "loading-error", + error: terms, + }; + } + + if (errorAccepting) { + return { + status: "error-accepting", + error: { + hasError: true, + operational: false, + message: errorAccepting.message, + }, + }; + } + + const { state } = terms.response; + + async function onUpdate(accepted: boolean): Promise<void> { + if (!state) return; + + try { + if (accepted) { + await api.setExchangeTosAccepted(exchangeUrl, state.version); + } else { + // mark as not accepted + await api.setExchangeTosAccepted(exchangeUrl, undefined); + } + // setAccepted(accepted); + onChange(accepted); //external update + } catch (e) { + if (e instanceof Error) { + //FIXME: uncomment this and display error + // setErrorAccepting(e.message); + setErrorAccepting(e); + } + } + } + + const accepted = state.status === "accepted"; + + const base: State.BaseInfo = { + error: undefined, + showingTermsOfService: { + value: showContent, + button: { + onClick: readOnly + ? undefined + : async () => { + setShowContent(!showContent); + }, + }, + }, + terms: state, + termsAccepted: { + value: accepted, + button: { + onClick: async () => { + const newValue = !accepted; //toggle + onUpdate(newValue); + setShowContent(false); + }, + }, + }, + }; + + if (showContent) { + return { + status: "show-content", + ...base, + }; + } + //showing buttons + if (accepted) { + return { + status: "show-buttons-accepted", + ...base, + }; + } else { + return { + status: "show-buttons-not-accepted", + ...base, + }; + } +} diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/stories.tsx b/packages/taler-wallet-webextension/src/cta/TermsOfService/stories.tsx new file mode 100644 index 000000000..2479274cb --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TermsOfService/stories.tsx @@ -0,0 +1,29 @@ +/* + 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 { createExample } from "../../test-utils.js"; +// import { ReadyView } from "./views.js"; + +export default { + title: "TermsOfService", +}; + +// export const Ready = createExample(ReadyView, {}); diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/test.ts b/packages/taler-wallet-webextension/src/cta/TermsOfService/test.ts new file mode 100644 index 000000000..eae4d4ca2 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TermsOfService/test.ts @@ -0,0 +1,28 @@ +/* + 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"; + +describe("test description", () => { + it("should assert", () => { + expect([]).deep.equals([]); + }); +}); diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/utils.ts b/packages/taler-wallet-webextension/src/cta/TermsOfService/utils.ts new file mode 100644 index 000000000..cee6557f7 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TermsOfService/utils.ts @@ -0,0 +1,130 @@ +/* + 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 { GetExchangeTosResult } from "@gnu-taler/taler-util"; + +export function buildTermsOfServiceState( + tos: GetExchangeTosResult, +): TermsState { + const content: TermsDocument | undefined = parseTermsOfServiceContent( + tos.contentType, + tos.content, + ); + + const status: TermsStatus = buildTermsOfServiceStatus( + tos.content, + tos.acceptedEtag, + tos.currentEtag, + ); + + return { content, status, version: tos.currentEtag }; +} +export function buildTermsOfServiceStatus( + content: string | undefined, + acceptedVersion: string | undefined, + currentVersion: string | undefined, +): TermsStatus { + return !content + ? "notfound" + : !acceptedVersion + ? "new" + : acceptedVersion !== currentVersion + ? "changed" + : "accepted"; +} + +function parseTermsOfServiceContent( + type: string, + text: string, +): TermsDocument | undefined { + if (type === "text/xml") { + try { + const document = new DOMParser().parseFromString(text, "text/xml"); + return { type: "xml", document }; + } catch (e) { + console.log(e); + } + } else if (type === "text/html") { + try { + const href = new URL(text); + return { type: "html", href }; + } catch (e) { + console.log(e); + } + } else if (type === "text/json") { + try { + const data = JSON.parse(text); + return { type: "json", data }; + } catch (e) { + console.log(e); + } + } else if (type === "text/pdf") { + try { + const location = new URL(text); + return { type: "pdf", location }; + } catch (e) { + console.log(e); + } + } else if (type === "text/plain") { + try { + const content = text; + return { type: "plain", content }; + } catch (e) { + console.log(e); + } + } + return undefined; +} + +export type TermsState = { + content: TermsDocument | undefined; + status: TermsStatus; + version: string; +}; + +type TermsStatus = "new" | "accepted" | "changed" | "notfound"; + +type TermsDocument = + | TermsDocumentXml + | TermsDocumentHtml + | TermsDocumentPlain + | TermsDocumentJson + | TermsDocumentPdf; + +export interface TermsDocumentXml { + type: "xml"; + document: Document; +} + +export interface TermsDocumentHtml { + type: "html"; + href: URL; +} + +export interface TermsDocumentPlain { + type: "plain"; + content: string; +} + +export interface TermsDocumentJson { + type: "json"; + data: any; +} + +export interface TermsDocumentPdf { + type: "pdf"; + location: URL; +} diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/cta/TermsOfService/views.tsx new file mode 100644 index 000000000..ed6d7dee4 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TermsOfService/views.tsx @@ -0,0 +1,224 @@ +/* + 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 { LoadingError } from "../../components/LoadingError.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { TermsState } from "./utils.js"; +import { State } from "./index.js"; +import { CheckboxOutlined } from "../../components/CheckboxOutlined.js"; +import { + LinkSuccess, + TermsOfService, + WarningBox, + WarningText, +} from "../../components/styled/index.js"; +import { ExchangeXmlTos } from "../../components/ExchangeToS.js"; +import { ToggleHandler } from "../../mui/handlers.js"; +import { Button } from "../../mui/Button.js"; + +export function LoadingUriView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + <LoadingError + title={<i18n.Translate>Could not load</i18n.Translate>} + error={error} + /> + ); +} + +export function ErrorAcceptingView({ error }: State.ErrorAccepting): VNode { + const { i18n } = useTranslationContext(); + + return ( + <LoadingError + title={<i18n.Translate>Could not load</i18n.Translate>} + error={error} + /> + ); +} + +export function ShowButtonsAcceptedTosView({ + termsAccepted, + showingTermsOfService, + terms, +}: State.ShowButtonsAccepted): VNode { + const { i18n } = useTranslationContext(); + const ableToReviewTermsOfService = + showingTermsOfService.button.onClick !== undefined; + + return ( + <Fragment> + {ableToReviewTermsOfService && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <LinkSuccess + upperCased + onClick={showingTermsOfService.button.onClick} + > + <i18n.Translate>Show terms of service</i18n.Translate> + </LinkSuccess> + </section> + )} + <section style={{ justifyContent: "space-around", display: "flex" }}> + <CheckboxOutlined + name="terms" + enabled={termsAccepted.value} + label={ + <i18n.Translate> + I accept the exchange terms of service + </i18n.Translate> + } + onToggle={termsAccepted.button.onClick} + /> + </section> + </Fragment> + ); +} + +export function ShowButtonsNonAcceptedTosView({ + termsAccepted, + showingTermsOfService, + terms, +}: State.ShowButtonsNotAccepted): VNode { + const { i18n } = useTranslationContext(); + const ableToReviewTermsOfService = + showingTermsOfService.button.onClick !== undefined; + + if (!ableToReviewTermsOfService) { + return ( + <Fragment> + {terms.status === "notfound" && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <WarningText> + <i18n.Translate> + Exchange doesn't have terms of service + </i18n.Translate> + </WarningText> + </section> + )} + </Fragment> + ); + } + + return ( + <Fragment> + {terms.status === "notfound" && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <WarningText> + <i18n.Translate> + Exchange doesn't have terms of service + </i18n.Translate> + </WarningText> + </section> + )} + {terms.status === "new" && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <Button + variant="contained" + color="success" + onClick={showingTermsOfService.button.onClick} + > + <i18n.Translate>Review exchange terms of service</i18n.Translate> + </Button> + </section> + )} + {terms.status === "changed" && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <Button + variant="contained" + color="success" + onClick={showingTermsOfService.button.onClick} + > + <i18n.Translate> + Review new version of terms of service + </i18n.Translate> + </Button> + </section> + )} + </Fragment> + ); +} + +export function ShowTosContentView({ + termsAccepted, + showingTermsOfService, + terms, +}: State.ShowContent): VNode { + const { i18n } = useTranslationContext(); + const ableToReviewTermsOfService = + showingTermsOfService.button.onClick !== undefined; + + return ( + <Fragment> + {terms.status !== "notfound" && !terms.content && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <WarningBox> + <i18n.Translate> + The exchange reply with a empty terms of service + </i18n.Translate> + </WarningBox> + </section> + )} + {terms.content && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + {terms.content.type === "xml" && ( + <TermsOfService> + <ExchangeXmlTos doc={terms.content.document} /> + </TermsOfService> + )} + {terms.content.type === "plain" && ( + <div style={{ textAlign: "left" }}> + <pre>{terms.content.content}</pre> + </div> + )} + {terms.content.type === "html" && ( + <iframe src={terms.content.href.toString()} /> + )} + {terms.content.type === "pdf" && ( + <a href={terms.content.location.toString()} download="tos.pdf"> + <i18n.Translate>Download Terms of Service</i18n.Translate> + </a> + )} + </section> + )} + {termsAccepted && ableToReviewTermsOfService && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <LinkSuccess + upperCased + onClick={showingTermsOfService.button.onClick} + > + <i18n.Translate>Hide terms of service</i18n.Translate> + </LinkSuccess> + </section> + )} + {terms.status !== "notfound" && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <CheckboxOutlined + name="terms" + enabled={termsAccepted.value} + label={ + <i18n.Translate> + I accept the exchange terms of service + </i18n.Translate> + } + onToggle={termsAccepted.button.onClick} + /> + </section> + )} + </Fragment> + ); +} diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx deleted file mode 100644 index 383daac59..000000000 --- a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx +++ /dev/null @@ -1,187 +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 { createExample } from "../test-utils.js"; -import { termsHtml, termsPdf, termsPlain, termsXml } from "./termsExample.js"; -import { TermsOfServiceSection as TestedComponent } from "./TermsOfServiceSection.js"; - -function parseFromString(s: string): Document { - if (typeof window === "undefined") { - return {} as Document; - } - return new window.DOMParser().parseFromString(s, "text/xml"); -} - -export default { - title: "cta/terms of service", - component: TestedComponent, -}; - -export const ReviewingPLAIN = createExample(TestedComponent, { - terms: { - content: { - type: "plain", - content: termsPlain, - }, - status: "new", - version: "", - }, - reviewing: true, -}); - -export const ReviewingHTML = createExample(TestedComponent, { - terms: { - content: { - type: "html", - href: new URL(`data:text/html;base64,${toBase64(termsHtml)}`), - }, - version: "", - status: "new", - }, - reviewing: true, -}); - -function toBase64(str: string): string { - const encoded = encodeURIComponent(str).replace( - /%([0-9A-F]{2})/g, - function (match, p1) { - return String.fromCharCode(parseInt(p1, 16)); - }, - ); - if (typeof btoa === "undefined") { - //nodejs - return Buffer.from(encoded).toString("base64"); - } else { - //browser - return btoa(encoded); - } -} - -export const ReviewingPDF = createExample(TestedComponent, { - terms: { - content: { - type: "pdf", - location: new URL(`data:text/html;base64,${toBase64(termsPdf)}`), - }, - status: "new", - version: "", - }, - reviewing: true, -}); - -export const ReviewingXML = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - status: "new", - version: "", - }, - reviewing: true, -}); - -export const NewAccepted = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - status: "new", - version: "", - }, - reviewed: true, -}); - -export const ShowAgainXML = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - version: "", - status: "new", - }, - reviewed: true, - reviewing: true, -}); - -export const ChangedButNotReviewable = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - version: "", - status: "changed", - }, -}); - -export const ChangedAndAllowReview = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - version: "", - status: "changed", - }, - onReview: () => null, -}); - -export const NewButNotReviewable = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - version: "", - status: "new", - }, -}); - -export const NewAndAllowReview = createExample(TestedComponent, { - terms: { - content: { - type: "xml", - document: parseFromString(termsXml), - }, - version: "", - status: "new", - }, - onReview: () => null, -}); - -export const NotFound = createExample(TestedComponent, { - terms: { - content: undefined, - status: "notfound", - version: "", - }, -}); - -export const AlreadyAccepted = createExample(TestedComponent, { - terms: { - status: "accepted", - content: undefined, - version: "", - }, -}); diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx deleted file mode 100644 index b60c86021..000000000 --- a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx +++ /dev/null @@ -1,196 +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 { CheckboxOutlined } from "../components/CheckboxOutlined.js"; -import { ExchangeXmlTos } from "../components/ExchangeToS.js"; -import { - LinkSuccess, - TermsOfService, - WarningBox, - WarningText, -} from "../components/styled/index.js"; -import { useTranslationContext } from "../context/translation.js"; -import { Button } from "../mui/Button.js"; -import { TermsState } from "../utils/index.js"; - -export interface Props { - reviewing: boolean; - reviewed: boolean; - terms: TermsState; - onReview?: (b: boolean) => void; - onAccept: (b: boolean) => void; -} -export function TermsOfServiceSection({ - reviewed, - reviewing, - terms, - onAccept, - onReview, -}: Props): VNode { - const { i18n } = useTranslationContext(); - const ableToReviewTermsOfService = onReview !== undefined; - if (!reviewing) { - if (!reviewed) { - if (!ableToReviewTermsOfService) { - return ( - <Fragment> - {terms.status === "notfound" && ( - <section - style={{ justifyContent: "space-around", display: "flex" }} - > - <WarningText> - <i18n.Translate> - Exchange doesn't have terms of service - </i18n.Translate> - </WarningText> - </section> - )} - </Fragment> - ); - } - return ( - <Fragment> - {terms.status === "notfound" && ( - <section - style={{ justifyContent: "space-around", display: "flex" }} - > - <WarningText> - <i18n.Translate> - Exchange doesn't have terms of service - </i18n.Translate> - </WarningText> - </section> - )} - {terms.status === "new" && ( - <section - style={{ justifyContent: "space-around", display: "flex" }} - > - <Button - variant="contained" - color="success" - onClick={async () => onReview(true)} - > - <i18n.Translate> - Review exchange terms of service - </i18n.Translate> - </Button> - </section> - )} - {terms.status === "changed" && ( - <section - style={{ justifyContent: "space-around", display: "flex" }} - > - <Button - variant="contained" - color="success" - onClick={async () => onReview(true)} - > - <i18n.Translate> - Review new version of terms of service - </i18n.Translate> - </Button> - </section> - )} - </Fragment> - ); - } - return ( - <Fragment> - {ableToReviewTermsOfService && ( - <section style={{ justifyContent: "space-around", display: "flex" }}> - <LinkSuccess upperCased onClick={() => onReview(true)}> - <i18n.Translate>Show terms of service</i18n.Translate> - </LinkSuccess> - </section> - )} - <section style={{ justifyContent: "space-around", display: "flex" }}> - <CheckboxOutlined - name="terms" - enabled={reviewed} - label={ - <i18n.Translate> - I accept the exchange terms of service - </i18n.Translate> - } - onToggle={async () => { - onAccept(!reviewed); - if (ableToReviewTermsOfService) onReview(false); - }} - /> - </section> - </Fragment> - ); - } - return ( - <Fragment> - {terms.status !== "notfound" && !terms.content && ( - <section style={{ justifyContent: "space-around", display: "flex" }}> - <WarningBox> - <i18n.Translate> - The exchange reply with a empty terms of service - </i18n.Translate> - </WarningBox> - </section> - )} - {terms.status !== "accepted" && terms.content && ( - <section style={{ justifyContent: "space-around", display: "flex" }}> - {terms.content.type === "xml" && ( - <TermsOfService> - <ExchangeXmlTos doc={terms.content.document} /> - </TermsOfService> - )} - {terms.content.type === "plain" && ( - <div style={{ textAlign: "left" }}> - <pre>{terms.content.content}</pre> - </div> - )} - {terms.content.type === "html" && ( - <iframe src={terms.content.href.toString()} /> - )} - {terms.content.type === "pdf" && ( - <a href={terms.content.location.toString()} download="tos.pdf"> - <i18n.Translate>Download Terms of Service</i18n.Translate> - </a> - )} - </section> - )} - {reviewed && ableToReviewTermsOfService && ( - <section style={{ justifyContent: "space-around", display: "flex" }}> - <LinkSuccess upperCased onClick={() => onReview(false)}> - <i18n.Translate>Hide terms of service</i18n.Translate> - </LinkSuccess> - </section> - )} - {terms.status !== "notfound" && ( - <section style={{ justifyContent: "space-around", display: "flex" }}> - <CheckboxOutlined - name="terms" - enabled={reviewed} - label={ - <i18n.Translate> - I accept the exchange terms of service - </i18n.Translate> - } - onToggle={async () => { - onAccept(!reviewed); - if (ableToReviewTermsOfService) onReview(false); - }} - /> - </section> - )} - </Fragment> - ); -} diff --git a/packages/taler-wallet-webextension/src/cta/Tip/index.ts b/packages/taler-wallet-webextension/src/cta/Tip/index.ts index 157cf7d4e..03cbd2196 100644 --- a/packages/taler-wallet-webextension/src/cta/Tip/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Tip/index.ts @@ -17,10 +17,9 @@ import { AmountJson } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; -import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; +import { ButtonHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; import * as wxApi from "../../wxApi.js"; -import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js"; import { useComponentState } from "./state.js"; import { AcceptedView, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts index 9de9c693a..075b21dc3 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts @@ -14,27 +14,20 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountJson } from "@gnu-taler/taler-util"; +import { AmountJson, ExchangeListItem } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; -import { - State as SelectExchangeState -} from "../../hooks/useSelectedExchange.js"; +import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; import * as wxApi from "../../wxApi.js"; -import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js"; import { useComponentStateFromParams, - useComponentStateFromURI + useComponentStateFromURI, } from "./state.js"; import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js"; -import { - LoadingInfoView, - LoadingUriView, - SuccessView -} from "./views.js"; +import { LoadingInfoView, LoadingUriView, SuccessView } from "./views.js"; import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js"; export interface PropsFromURI { @@ -75,7 +68,7 @@ export namespace State { status: "success"; error: undefined; - exchangeUrl: string; + currentExchange: ExchangeListItem; chosenAmount: AmountJson; withdrawalFee: AmountJson; @@ -83,13 +76,12 @@ export namespace State { doWithdrawal: ButtonHandler; doSelectExchange: ButtonHandler; - tosProps?: TermsOfServiceSectionProps; - mustAcceptFirst: boolean; ageRestriction?: SelectFieldHandler; talerWithdrawUri?: string; cancel: () => Promise<void>; + onTosUpdate: () => void; }; } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index 5b5c11182..c2b9e375f 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -15,17 +15,15 @@ */ /* eslint-disable react-hooks/rules-of-hooks */ -import { AmountJson, Amounts, ExchangeListItem, parsePaytoUri } from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, ExchangeListItem } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-wallet-core"; import { useState } from "preact/hooks"; -import { Amount } from "../../components/Amount.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; -import { buildTermsOfServiceState } from "../../utils/index.js"; import * as wxApi from "../../wxApi.js"; -import { PropsFromURI, PropsFromParams, State } from "./index.js"; +import { PropsFromParams, PropsFromURI, State } from "./index.js"; -type RecursiveState<S extends object> = S | (() => RecursiveState<S>) +type RecursiveState<S extends object> = S | (() => RecursiveState<S>); export function useComponentStateFromParams( { amount, cancel, onSuccess }: PropsFromParams, @@ -46,18 +44,38 @@ export function useComponentStateFromParams( } const chosenAmount = uriInfoHook.response.amount; - const exchangeList = uriInfoHook.response.exchanges.exchanges - - async function doManualWithdraw(exchange: string, ageRestricted: number | undefined): Promise<{ transactionId: string, confirmTransferUrl: string | undefined }> { - const res = await api.acceptManualWithdrawal(exchange, Amounts.stringify(chosenAmount), ageRestricted); + const exchangeList = uriInfoHook.response.exchanges.exchanges; + + async function doManualWithdraw( + exchange: string, + ageRestricted: number | undefined, + ): Promise<{ + transactionId: string; + confirmTransferUrl: string | undefined; + }> { + const res = await api.acceptManualWithdrawal( + exchange, + Amounts.stringify(chosenAmount), + ageRestricted, + ); return { confirmTransferUrl: undefined, - transactionId: res.transactionId + transactionId: res.transactionId, }; } - return () => exchangeSelectionState(doManualWithdraw, cancel, onSuccess, undefined, chosenAmount, exchangeList, undefined, api) - + return () => + exchangeSelectionState( + uriInfoHook.retry, + doManualWithdraw, + cancel, + onSuccess, + undefined, + chosenAmount, + exchangeList, + undefined, + api, + ); } export function useComponentStateFromURI( @@ -75,7 +93,12 @@ export function useComponentStateFromURI( }); const exchanges = await api.listExchanges(); const { amount, defaultExchangeBaseUrl } = uriInfo; - return { talerWithdrawUri, amount: Amounts.parseOrThrow(amount), thisExchange: defaultExchangeBaseUrl, exchanges }; + return { + talerWithdrawUri, + amount: Amounts.parseOrThrow(amount), + thisExchange: defaultExchangeBaseUrl, + exchanges, + }; }); if (!uriInfoHook) return { status: "loading", error: undefined }; @@ -90,53 +113,75 @@ export function useComponentStateFromURI( const uri = uriInfoHook.response.talerWithdrawUri; const chosenAmount = uriInfoHook.response.amount; const defaultExchange = uriInfoHook.response.thisExchange; - const exchangeList = uriInfoHook.response.exchanges.exchanges - - async function doManagedWithdraw(exchange: string, ageRestricted: number | undefined): Promise<{ transactionId: string, confirmTransferUrl: string | undefined }> { - const res = await api.acceptWithdrawal(uri, exchange, ageRestricted,); + const exchangeList = uriInfoHook.response.exchanges.exchanges; + + async function doManagedWithdraw( + exchange: string, + ageRestricted: number | undefined, + ): Promise<{ + transactionId: string; + confirmTransferUrl: string | undefined; + }> { + const res = await api.acceptWithdrawal(uri, exchange, ageRestricted); return { confirmTransferUrl: res.confirmTransferUrl, - transactionId: res.transactionId + transactionId: res.transactionId, }; } - return () => exchangeSelectionState(doManagedWithdraw, cancel, onSuccess, uri, chosenAmount, exchangeList, defaultExchange, api) - + return () => + exchangeSelectionState( + uriInfoHook.retry, + doManagedWithdraw, + cancel, + onSuccess, + uri, + chosenAmount, + exchangeList, + defaultExchange, + api, + ); } -type ManualOrManagedWithdrawFunction = (exchange: string, ageRestricted: number | undefined) => Promise<{ transactionId: string, confirmTransferUrl: string | undefined }> - -function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, cancel: () => Promise<void>, onSuccess: (txid: string) => Promise<void>, talerWithdrawUri: string | undefined, chosenAmount: AmountJson, exchangeList: ExchangeListItem[], defaultExchange: string | undefined, api: typeof wxApi,): RecursiveState<State> { - - const selectedExchange = useSelectedExchange({ currency: chosenAmount.currency, defaultExchange, list: exchangeList }) +type ManualOrManagedWithdrawFunction = ( + exchange: string, + ageRestricted: number | undefined, +) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>; + +function exchangeSelectionState( + onTosUpdate: () => void, + doWithdraw: ManualOrManagedWithdrawFunction, + cancel: () => Promise<void>, + onSuccess: (txid: string) => Promise<void>, + talerWithdrawUri: string | undefined, + chosenAmount: AmountJson, + exchangeList: ExchangeListItem[], + defaultExchange: string | undefined, + api: typeof wxApi, +): RecursiveState<State> { + const selectedExchange = useSelectedExchange({ + currency: chosenAmount.currency, + defaultExchange, + list: exchangeList, + }); - if (selectedExchange.status !== 'ready') { - return selectedExchange + if (selectedExchange.status !== "ready") { + return selectedExchange; } return () => { - const [ageRestricted, setAgeRestricted] = useState(0); - const currentExchange = selectedExchange.selected - /** - * For the exchange selected, bring the status of the terms of service - */ - const terms = useAsyncAsHook(async () => { - const exchangeTos = await api.getExchangeTos(currentExchange.exchangeBaseUrl, [ - "text/xml", - ]); - - const state = buildTermsOfServiceState(exchangeTos); - - return { state }; - }, []); + const currentExchange = selectedExchange.selected; + const tosNeedToBeAccepted = + !currentExchange.tos.acceptedVersion || + currentExchange.tos.currentVersion !== + currentExchange.tos.acceptedVersion; /** * With the exchange and amount, ask the wallet the information * about the withdrawal */ const amountHook = useAsyncAsHook(async () => { - const info = await api.getExchangeWithdrawalInfo({ exchangeBaseUrl: currentExchange.exchangeBaseUrl, amount: chosenAmount, @@ -155,20 +200,18 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can }; }, []); - const [reviewing, setReviewing] = useState<boolean>(false); - const [reviewed, setReviewed] = useState<boolean>(false); - const [withdrawError, setWithdrawError] = useState<TalerError | undefined>( undefined, ); const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false); - async function doWithdrawAndCheckError(): Promise<void> { - try { setDoingWithdraw(true); - const res = await doWithdraw(currentExchange.exchangeBaseUrl, !ageRestricted ? undefined : ageRestricted) + const res = await doWithdraw( + currentExchange.exchangeBaseUrl, + !ageRestricted ? undefined : ageRestricted, + ); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; } else { @@ -201,33 +244,6 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can ).amount; const toBeReceived = amountHook.response.amount.effective; - const { state: termsState } = (!terms - ? undefined - : terms.hasError - ? undefined - : terms.response) || { state: undefined }; - - async function onAccept(accepted: boolean): Promise<void> { - if (!termsState) return; - - try { - await api.setExchangeTosAccepted( - currentExchange.exchangeBaseUrl, - accepted ? termsState.version : undefined, - ); - setReviewed(accepted); - } catch (e) { - if (e instanceof Error) { - //FIXME: uncomment this and display error - // setErrorAccepting(e.message); - } - } - } - - const mustAcceptFirst = - termsState !== undefined && - (termsState.status === "changed" || termsState.status === "new"); - const ageRestrictionOptions = amountHook.response.ageRestrictionOptions?.reduce( (p, c) => ({ ...p, [c]: `under ${c}` }), @@ -242,17 +258,17 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can //TODO: calculate based on exchange info const ageRestriction = ageRestrictionEnabled ? { - list: ageRestrictionOptions, - value: String(ageRestricted), - onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), - } + list: ageRestrictionOptions, + value: String(ageRestricted), + onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), + } : undefined; return { status: "success", error: undefined, doSelectExchange: selectedExchange.doSelect, - exchangeUrl: currentExchange.exchangeBaseUrl, + currentExchange, toBeReceived, withdrawalFee, chosenAmount, @@ -260,22 +276,13 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can ageRestriction, doWithdrawal: { onClick: - doingWithdraw || (mustAcceptFirst && !reviewed) + doingWithdraw || tosNeedToBeAccepted ? undefined : doWithdrawAndCheckError, error: withdrawError, }, - tosProps: !termsState - ? undefined - : { - onAccept, - onReview: setReviewing, - reviewed: reviewed, - reviewing: reviewing, - terms: termsState, - }, - mustAcceptFirst, + onTosUpdate, cancel, }; - } + }; } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx index a3daeb5e9..1c3eaaf34 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx @@ -19,8 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { ExchangeListItem } from "@gnu-taler/taler-util"; import { createExample } from "../../test-utils.js"; -import { TermsState } from "../../utils/index.js"; +// import { TermsState } from "../../utils/index.js"; import { SuccessView } from "./views.js"; export default { @@ -38,16 +39,16 @@ const nullHandler = { }, }; -const normalTosState = { - terms: { - status: "accepted", - version: "", - } as TermsState, - onAccept: () => null, - onReview: () => null, - reviewed: false, - reviewing: false, -}; +// const normalTosState = { +// terms: { +// status: "accepted", +// version: "", +// } as TermsState, +// onAccept: () => null, +// onReview: () => null, +// reviewed: false, +// reviewing: false, +// }; const ageRestrictionOptions: Record<string, string> = "6:12:18" .split(":") @@ -69,15 +70,16 @@ export const TermsOfServiceNotYetLoaded = createExample(SuccessView, { fraction: 10000000, }, doWithdrawal: nullHandler, - exchangeUrl: "https://exchange.demo.taler.net", - mustAcceptFirst: false, + currentExchange: { + exchangeBaseUrl: "https://exchange.demo.taler.net", + tos: {}, + } as Partial<ExchangeListItem> as any, withdrawalFee: { currency: "USD", fraction: 10000000, value: 1, }, - doSelectExchange: { - }, + doSelectExchange: {}, toBeReceived: { currency: "USD", fraction: 0, @@ -94,8 +96,10 @@ export const WithSomeFee = createExample(SuccessView, { fraction: 10000000, }, doWithdrawal: nullHandler, - exchangeUrl: "https://exchange.demo.taler.net", - mustAcceptFirst: false, + currentExchange: { + exchangeBaseUrl: "https://exchange.demo.taler.net", + tos: {}, + } as Partial<ExchangeListItem> as any, withdrawalFee: { currency: "USD", fraction: 10000000, @@ -106,9 +110,7 @@ export const WithSomeFee = createExample(SuccessView, { fraction: 0, value: 1, }, - doSelectExchange: { - }, - tosProps: normalTosState, + doSelectExchange: {}, }); export const WithoutFee = createExample(SuccessView, { @@ -120,21 +122,21 @@ export const WithoutFee = createExample(SuccessView, { fraction: 0, }, doWithdrawal: nullHandler, - exchangeUrl: "https://exchange.demo.taler.net", - mustAcceptFirst: false, + currentExchange: { + exchangeBaseUrl: "https://exchange.demo.taler.net", + tos: {}, + } as Partial<ExchangeListItem> as any, withdrawalFee: { currency: "USD", fraction: 0, value: 0, }, - doSelectExchange: { - }, + doSelectExchange: {}, toBeReceived: { currency: "USD", fraction: 0, value: 2, }, - tosProps: normalTosState, }); export const EditExchangeUntouched = createExample(SuccessView, { @@ -146,21 +148,21 @@ export const EditExchangeUntouched = createExample(SuccessView, { fraction: 10000000, }, doWithdrawal: nullHandler, - exchangeUrl: "https://exchange.demo.taler.net", - mustAcceptFirst: false, + currentExchange: { + exchangeBaseUrl: "https://exchange.demo.taler.net", + tos: {}, + } as Partial<ExchangeListItem> as any, withdrawalFee: { currency: "USD", fraction: 0, value: 0, }, - doSelectExchange: { - }, + doSelectExchange: {}, toBeReceived: { currency: "USD", fraction: 0, value: 2, }, - tosProps: normalTosState, }); export const EditExchangeModified = createExample(SuccessView, { @@ -172,21 +174,21 @@ export const EditExchangeModified = createExample(SuccessView, { fraction: 10000000, }, doWithdrawal: nullHandler, - exchangeUrl: "https://exchange.demo.taler.net", - mustAcceptFirst: false, + currentExchange: { + exchangeBaseUrl: "https://exchange.demo.taler.net", + tos: {}, + } as Partial<ExchangeListItem> as any, withdrawalFee: { currency: "USD", fraction: 0, value: 0, }, - doSelectExchange: { - }, + doSelectExchange: {}, toBeReceived: { currency: "USD", fraction: 0, value: 2, }, - tosProps: normalTosState, }); export const WithAgeRestriction = createExample(SuccessView, { @@ -198,11 +200,12 @@ export const WithAgeRestriction = createExample(SuccessView, { value: 2, fraction: 10000000, }, - doSelectExchange: { - }, + doSelectExchange: {}, doWithdrawal: nullHandler, - exchangeUrl: "https://exchange.demo.taler.net", - mustAcceptFirst: false, + currentExchange: { + exchangeBaseUrl: "https://exchange.demo.taler.net", + tos: {}, + } as Partial<ExchangeListItem> as any, withdrawalFee: { currency: "USD", fraction: 0, @@ -213,5 +216,4 @@ export const WithAgeRestriction = createExample(SuccessView, { fraction: 0, value: 2, }, - tosProps: normalTosState, }); diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index 7ccf7f606..f3598b557 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -37,7 +37,8 @@ const exchanges: ExchangeFullDetails[] = [ exchangeBaseUrl: "http://exchange.demo.taler.net", paytoUris: [], tos: { - acceptedVersion: "", + acceptedVersion: "v1", + currentVersion: "v1", }, auditors: [ { @@ -58,7 +59,7 @@ const exchanges: ExchangeFullDetails[] = [ accounts: [], feesForType: {}, }, - }, + } as Partial<ExchangeFullDetails> as ExchangeFullDetails, ]; describe("Withdraw CTA states", () => { @@ -161,17 +162,20 @@ describe("Withdraw CTA states", () => { }, { listExchanges: async () => ({ exchanges }), - getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ - amount: "ARS:2", - possibleExchanges: exchanges, - defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, - }), + getWithdrawalDetailsForUri: async ({ + talerWithdrawUri, + }: any): Promise<ExchangeWithdrawDetails> => + ({ + amount: "ARS:2", + possibleExchanges: exchanges, + defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + } as Partial<ExchangeWithdrawDetails> as ExchangeWithdrawDetails), getExchangeWithdrawalInfo: async (): Promise<ExchangeWithdrawDetails> => - ({ - withdrawalAmountRaw: "ARS:2", - withdrawalAmountEffective: "ARS:2", - } as any), + ({ + withdrawalAmountRaw: "ARS:2", + withdrawalAmountEffective: "ARS:2", + } as any), getExchangeTos: async (): Promise<GetExchangeTosResult> => ({ contentType: "text", content: "just accept", @@ -205,25 +209,39 @@ describe("Withdraw CTA states", () => { expect(state.status).equals("success"); if (state.status !== "success") return; - // expect(state.exchange.isDirty).false; - // expect(state.exchange.value).equal("http://exchange.demo.taler.net"); - // expect(state.exchange.list).deep.equal({ - // "http://exchange.demo.taler.net": "http://exchange.demo.taler.net", - // }); - // expect(state.showExchangeSelection).false; - expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).not.undefined; - expect(state.mustAcceptFirst).false; } await assertNoPendingUpdate(); }); it("should be accept the tos before withdraw", async () => { + const listExchangesResponse = { + exchanges: exchanges.map((e) => ({ + ...e, + tos: { + ...e.tos, + acceptedVersion: undefined, + }, + })) as ExchangeFullDetails[], + }; + + function updateAcceptedVersionToCurrentVersion(): void { + listExchangesResponse.exchanges = listExchangesResponse.exchanges.map( + (e) => ({ + ...e, + tos: { + ...e.tos, + acceptedVersion: e.tos.currentVersion, + }, + }), + ); + } + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => useComponentStateFromURI( @@ -237,18 +255,19 @@ describe("Withdraw CTA states", () => { }, }, { - listExchanges: async () => ({ exchanges }), - getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ - amount: "ARS:2", - possibleExchanges: exchanges, - defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, - }), + listExchanges: async () => listExchangesResponse, + getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => + ({ + amount: "ARS:2", + possibleExchanges: exchanges, + defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + } as Partial<ExchangeWithdrawDetails> as ExchangeWithdrawDetails), getExchangeWithdrawalInfo: async (): Promise<ExchangeWithdrawDetails> => - ({ - withdrawalAmountRaw: "ARS:2", - withdrawalAmountEffective: "ARS:2", - } as any), + ({ + withdrawalAmountRaw: "ARS:2", + withdrawalAmountEffective: "ARS:2", + } as any), getExchangeTos: async (): Promise<GetExchangeTosResult> => ({ contentType: "text", content: "just accept", @@ -283,22 +302,14 @@ describe("Withdraw CTA states", () => { expect(state.status).equals("success"); if (state.status !== "success") return; - // expect(state.exchange.isDirty).false; - // expect(state.exchange.value).equal("http://exchange.demo.taler.net"); - // expect(state.exchange.list).deep.equal({ - // "http://exchange.demo.taler.net": "http://exchange.demo.taler.net", - // }); - // expect(state.showExchangeSelection).false; - expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).undefined; - expect(state.mustAcceptFirst).true; - // accept TOS - state.tosProps?.onAccept(true); + updateAcceptedVersionToCurrentVersion(); + state.onTosUpdate(); } await waitNextUpdate(); @@ -308,19 +319,11 @@ describe("Withdraw CTA states", () => { expect(state.status).equals("success"); if (state.status !== "success") return; - // expect(state.exchange.isDirty).false; - // expect(state.exchange.value).equal("http://exchange.demo.taler.net"); - // expect(state.exchange.list).deep.equal({ - // "http://exchange.demo.taler.net": "http://exchange.demo.taler.net", - // }); - // expect(state.showExchangeSelection).false; - expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).not.undefined; - expect(state.mustAcceptFirst).true; } await assertNoPendingUpdate(); diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx index 1e8284739..44c7db83f 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -15,30 +15,28 @@ */ import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Amount } from "../../components/Amount.js"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; import { LogoHeader } from "../../components/LogoHeader.js"; import { Part } from "../../components/Part.js"; +import { QR } from "../../components/QR.js"; import { SelectList } from "../../components/SelectList.js"; import { Input, Link, LinkSuccess, SubTitle, - SuccessBox, SvgIcon, WalletAction, } from "../../components/styled/index.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; +import editIcon from "../../svg/edit_24px.svg"; import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js"; -import { TermsOfServiceSection } from "../TermsOfServiceSection.js"; +import { TermsOfService } from "../TermsOfService/index.js"; import { State } from "./index.js"; -import editIcon from "../../svg/edit_24px.svg"; -import { Amount } from "../../components/Amount.js"; -import { QR } from "../../components/QR.js"; -import { useState } from "preact/hooks"; -import { ErrorMessage } from "../../components/ErrorMessage.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -66,6 +64,9 @@ export function LoadingInfoView({ error }: State.LoadingInfoError): VNode { export function SuccessView(state: State.Success): VNode { const { i18n } = useTranslationContext(); + const currentTosVersionIsAccepted = + state.currentExchange.tos.acceptedVersion === + state.currentExchange.tos.currentVersion; return ( <WalletAction> <LogoHeader /> @@ -103,7 +104,9 @@ export function SuccessView(state: State.Success): VNode { </Button> </div> } - text={<ExchangeDetails exchange={state.exchangeUrl} />} + text={ + <ExchangeDetails exchange={state.currentExchange.exchangeBaseUrl} /> + } kind="neutral" big /> @@ -130,43 +133,29 @@ export function SuccessView(state: State.Success): VNode { </Input> )} </section> - {state.tosProps && <TermsOfServiceSection {...state.tosProps} />} - {state.tosProps ? ( - <Fragment> - <section> - {(state.tosProps.terms.status === "accepted" || - (state.mustAcceptFirst && state.tosProps.reviewed)) && ( - <Button - variant="contained" - color="success" - disabled={!state.doWithdrawal.onClick} - onClick={state.doWithdrawal.onClick} - > - <i18n.Translate> - Withdraw <Amount value={state.toBeReceived} /> - </i18n.Translate> - </Button> - )} - {state.tosProps.terms.status === "notfound" && ( - <Button - variant="contained" - color="warning" - disabled={!state.doWithdrawal.onClick} - onClick={state.doWithdrawal.onClick} - > - <i18n.Translate>Withdraw anyway</i18n.Translate> - </Button> - )} - </section> - {state.talerWithdrawUri ? ( - <WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} /> - ) : undefined} - </Fragment> - ) : ( - <section> - <i18n.Translate>Loading terms of service...</i18n.Translate> - </section> - )} + + <section> + {currentTosVersionIsAccepted ? ( + <Button + variant="contained" + color="success" + disabled={!state.doWithdrawal.onClick} + onClick={state.doWithdrawal.onClick} + > + <i18n.Translate> + Withdraw <Amount value={state.toBeReceived} /> + </i18n.Translate> + </Button> + ) : ( + <TermsOfService + exchangeUrl={state.currentExchange.exchangeBaseUrl} + onChange={state.onTosUpdate} + /> + )} + </section> + {state.talerWithdrawUri ? ( + <WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} /> + ) : undefined} <section> <Link upperCased onClick={state.cancel}> <i18n.Translate>Cancel</i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts index 2f0ef33fb..c54defccf 100644 --- a/packages/taler-wallet-webextension/src/cta/index.stories.ts +++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts @@ -24,7 +24,7 @@ import * as a3 from "./Payment/stories.jsx"; import * as a4 from "./Refund/stories.jsx"; import * as a5 from "./Tip/stories.jsx"; import * as a6 from "./Withdraw/stories.jsx"; -import * as a7 from "./TermsOfServiceSection.stories.js"; +import * as a7 from "./TermsOfService/stories.js"; import * as a8 from "./InvoiceCreate/stories.js"; import * as a9 from "./InvoicePay/stories.js"; import * as a10 from "./TransferCreate/stories.js"; diff --git a/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts index 7219c30d2..c04dcce84 100644 --- a/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts +++ b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts @@ -22,21 +22,21 @@ type State = State.Ready | State.NoExchange | State.Selecting; export namespace State { export interface NoExchange { - status: "no-exchange" + status: "no-exchange"; error: undefined; currency: string | undefined; } export interface Ready { - status: "ready", - doSelect: ButtonHandler, + status: "ready"; + doSelect: ButtonHandler; selected: ExchangeListItem; } export interface Selecting { - status: "selecting-exchange", - error: undefined, + status: "selecting-exchange"; + error: undefined; onSelection: (url: string) => Promise<void>; onCancel: () => Promise<void>; - list: ExchangeListItem[], + list: ExchangeListItem[]; currency: string; currentExchange: string; } @@ -45,38 +45,42 @@ export namespace State { interface Props { currency: string; //there is a preference for the default at the initial state - defaultExchange?: string, + defaultExchange?: string; //list of exchanges - list: ExchangeListItem[], + list: ExchangeListItem[]; } - - -export function useSelectedExchange({ currency, defaultExchange, list }: Props): State { +export function useSelectedExchange({ + currency, + defaultExchange, + list, +}: Props): State { const [isSelecting, setIsSelecting] = useState(false); - const [selectedExchange, setSelectedExchange] = useState<string | undefined>(undefined); + const [selectedExchange, setSelectedExchange] = useState<string | undefined>( + undefined, + ); if (!list.length) { return { status: "no-exchange", error: undefined, currency: undefined, - } + }; } - const listCurrency = list.filter((e) => e.currency === currency) + const listCurrency = list.filter((e) => e.currency === currency); if (!listCurrency.length) { // there should be at least one exchange for this currency return { status: "no-exchange", error: undefined, currency, - } + }; } - if (isSelecting) { - const currentExchange = selectedExchange ?? defaultExchange ?? listCurrency[0].exchangeBaseUrl; + const currentExchange = + selectedExchange ?? defaultExchange ?? listCurrency[0].exchangeBaseUrl; return { status: "selecting-exchange", error: undefined, @@ -85,44 +89,46 @@ export function useSelectedExchange({ currency, defaultExchange, list }: Props): currentExchange: currentExchange, onSelection: async (exchangeBaseUrl: string) => { setIsSelecting(false); - setSelectedExchange(exchangeBaseUrl) + setSelectedExchange(exchangeBaseUrl); }, onCancel: async () => { setIsSelecting(false); - } - } + }, + }; } { - const found = !selectedExchange ? undefined : list.find( - (e) => e.exchangeBaseUrl === selectedExchange, - ) - if (found) return { - status: "ready", - doSelect: { - onClick: async () => setIsSelecting(true) - }, - selected: found - }; + const found = !selectedExchange + ? undefined + : list.find((e) => e.exchangeBaseUrl === selectedExchange); + if (found) + return { + status: "ready", + doSelect: { + onClick: async () => setIsSelecting(true), + }, + selected: found, + }; } { - const found = !defaultExchange ? undefined : list.find( - (e) => e.exchangeBaseUrl === defaultExchange, - ) - if (found) return { - status: "ready", - doSelect: { - onClick: async () => setIsSelecting(true) - }, - selected: found - }; + const found = !defaultExchange + ? undefined + : list.find((e) => e.exchangeBaseUrl === defaultExchange); + if (found) + return { + status: "ready", + doSelect: { + onClick: async () => setIsSelecting(true), + }, + selected: found, + }; } return { status: "ready", doSelect: { - onClick: async () => setIsSelecting(true) + onClick: async () => setIsSelecting(true), }, - selected: listCurrency[0] - } + selected: listCurrency[0], + }; } diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts index 7e9c5670e..e2339bff3 100644 --- a/packages/taler-wallet-webextension/src/test-utils.ts +++ b/packages/taler-wallet-webextension/src/test-utils.ts @@ -82,7 +82,7 @@ export function renderNodeOrBrowser(Component: any, args: any): void { document.body.removeChild(div); } } -type RecursiveState<S> = S | (() => RecursiveState<S>) +type RecursiveState<S> = S | (() => RecursiveState<S>); interface Mounted<T> { unmount: () => void; @@ -107,12 +107,12 @@ export function mountHook<T extends object>( // component that's going to hold the hook function Component(): VNode { try { - let componentOrResult = callback() + let componentOrResult = callback(); while (typeof componentOrResult === "function") { componentOrResult = componentOrResult(); } //typecheck fails here - const l: Exclude<T, () => void> = componentOrResult as any + const l: Exclude<T, () => void> = componentOrResult as any; lastResult = l; } catch (e) { if (e instanceof Error) { diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts index 3535910cf..2323c7b21 100644 --- a/packages/taler-wallet-webextension/src/utils/index.ts +++ b/packages/taler-wallet-webextension/src/utils/index.ts @@ -14,12 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - AmountJson, - Amounts, - GetExchangeTosResult, -} from "@gnu-taler/taler-util"; -import { VNode, createElement } from "preact"; +import { createElement, VNode } from "preact"; function getJsonIfOk(r: Response): Promise<any> { if (r.ok) { @@ -31,7 +26,8 @@ function getJsonIfOk(r: Response): Promise<any> { } throw new Error( - `Try another server: (${r.status}) ${r.statusText || "internal server error" + `Try another server: (${r.status}) ${ + r.statusText || "internal server error" }`, ); } @@ -78,140 +74,25 @@ export async function queryToSlashKeys<T>(url: string): Promise<T> { return timeout(3000, query); } -export function buildTermsOfServiceState( - tos: GetExchangeTosResult, -): TermsState { - const content: TermsDocument | undefined = parseTermsOfServiceContent( - tos.contentType, - tos.content, - ); - - const status: TermsStatus = buildTermsOfServiceStatus( - tos.content, - tos.acceptedEtag, - tos.currentEtag, - ); - - return { content, status, version: tos.currentEtag }; -} -export function buildTermsOfServiceStatus( - content: string | undefined, - acceptedVersion: string | undefined, - currentVersion: string | undefined, -): TermsStatus { - return !content - ? "notfound" - : !acceptedVersion - ? "new" - : acceptedVersion !== currentVersion - ? "changed" - : "accepted"; -} - -function parseTermsOfServiceContent( - type: string, - text: string, -): TermsDocument | undefined { - if (type === "text/xml") { - try { - const document = new DOMParser().parseFromString(text, "text/xml"); - return { type: "xml", document }; - } catch (e) { - console.log(e); - } - } else if (type === "text/html") { - try { - const href = new URL(text); - return { type: "html", href }; - } catch (e) { - console.log(e); - } - } else if (type === "text/json") { - try { - const data = JSON.parse(text); - return { type: "json", data }; - } catch (e) { - console.log(e); - } - } else if (type === "text/pdf") { - try { - const location = new URL(text); - return { type: "pdf", location }; - } catch (e) { - console.log(e); - } - } else if (type === "text/plain") { - try { - const content = text; - return { type: "plain", content }; - } catch (e) { - console.log(e); - } - } - return undefined; -} - -export type TermsState = { - content: TermsDocument | undefined; - status: TermsStatus; - version: string; -}; - -type TermsStatus = "new" | "accepted" | "changed" | "notfound"; - -type TermsDocument = - | TermsDocumentXml - | TermsDocumentHtml - | TermsDocumentPlain - | TermsDocumentJson - | TermsDocumentPdf; - -export interface TermsDocumentXml { - type: "xml"; - document: Document; -} - -export interface TermsDocumentHtml { - type: "html"; - href: URL; -} - -export interface TermsDocumentPlain { - type: "plain"; - content: string; -} - -export interface TermsDocumentJson { - type: "json"; - data: any; -} - -export interface TermsDocumentPdf { - type: "pdf"; - location: URL; -} - export type StateFunc<S> = (p: S) => VNode; export type StateViewMap<StateType extends { status: string }> = { [S in StateType as S["status"]]: StateFunc<S>; }; -type RecursiveState<S extends object> = S | (() => RecursiveState<S>) +type RecursiveState<S extends object> = S | (() => RecursiveState<S>); export function compose<SType extends { status: string }, PType>( name: string, hook: (p: PType) => RecursiveState<SType>, viewMap: StateViewMap<SType>, ): (p: PType) => VNode { - function withHook(stateHook: () => RecursiveState<SType>): () => VNode { - function TheComponent(): VNode { const state = stateHook(); if (typeof state === "function") { - const subComponent = withHook(state) + const subComponent = withHook(state); return createElement(subComponent, {}); } @@ -225,7 +106,7 @@ export function compose<SType extends { status: string }, PType>( } return (p: PType) => { - const h = withHook(() => hook(p)) - return h() + const h = withHook(() => hook(p)); + return h(); }; } diff --git a/packages/taler-wallet-webextension/src/wallet/AddAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/AddAccount/index.ts index 527c9c8e2..0b50d9d85 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddAccount/index.ts @@ -20,7 +20,11 @@ import { compose, StateViewMap } from "../../utils/index.js"; import { LoadingUriView, ReadyView } from "./views.js"; import * as wxApi from "../../wxApi.js"; import { useComponentState } from "./state.js"; -import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; +import { + ButtonHandler, + SelectFieldHandler, + TextFieldHandler, +} from "../../mui/handlers.js"; export interface Props { currency: string; diff --git a/packages/taler-wallet-webextension/src/wallet/AddAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/AddAccount/state.ts index 8f7920d35..f14c4c1bb 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddAccount/state.ts @@ -20,16 +20,18 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import * as wxApi from "../../wxApi.js"; import { Props, State } from "./index.js"; -export function useComponentState({ currency, onAccountAdded, onCancel }: Props, api: typeof wxApi): State { +export function useComponentState( + { currency, onAccountAdded, onCancel }: Props, + api: typeof wxApi, +): State { const hook = useAsyncAsHook(async () => { const { accounts } = await api.listKnownBankAccounts(currency); return { accounts }; }); - const [payto, setPayto] = useState("") - const [alias, setAlias] = useState("") - const [type, setType] = useState("") - + const [payto, setPayto] = useState(""); + const [alias, setAlias] = useState(""); + const [type, setType] = useState(""); if (!hook) { return { @@ -41,31 +43,38 @@ export function useComponentState({ currency, onAccountAdded, onCancel }: Props, return { status: "loading-error", error: hook, - } + }; } const accountType: Record<string, string> = { "": "Choose one account", - "iban": "IBAN", - "bitcoin": "Bitcoin", - "x-taler-bank": "Taler Bank" - } - const uri = parsePaytoUri(payto) - const found = hook.response.accounts.findIndex(a => stringifyPaytoUri(a.uri) === payto) !== -1 + iban: "IBAN", + bitcoin: "Bitcoin", + "x-taler-bank": "Taler Bank", + }; + const uri = parsePaytoUri(payto); + const found = + hook.response.accounts.findIndex( + (a) => stringifyPaytoUri(a.uri) === payto, + ) !== -1; async function addAccount(): Promise<void> { if (!uri || found) return; - await api.addKnownBankAccounts(uri, currency, alias) - onAccountAdded(payto) + await api.addKnownBankAccounts(uri, currency, alias); + onAccountAdded(payto); } - const paytoUriError = payto === "" ? undefined - : !uri ? "the uri is not ok" - : found ? "that account is already present" - : undefined + const paytoUriError = + payto === "" + ? undefined + : !uri + ? "the uri is not ok" + : found + ? "that account is already present" + : undefined; - const unableToAdd = !type || !alias || paytoUriError + const unableToAdd = !type || !alias || paytoUriError; return { status: "ready", @@ -75,27 +84,27 @@ export function useComponentState({ currency, onAccountAdded, onCancel }: Props, list: accountType, value: type, onChange: async (v) => { - setType(v) - } + setType(v); + }, }, alias: { value: alias, onInput: async (v) => { - setAlias(v) + setAlias(v); }, }, uri: { value: payto, error: paytoUriError, onInput: async (v) => { - setPayto(v) - } + setPayto(v); + }, }, onAccountAdded: { - onClick: unableToAdd ? undefined : addAccount + onClick: unableToAdd ? undefined : addAccount, }, onCancel: { - onClick: async () => onCancel() - } + onClick: async () => onCancel(), + }, }; } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts index eb97ccf7f..81d401a70 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -17,11 +17,22 @@ import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; import { compose, StateViewMap } from "../../utils/index.js"; -import { AmountOrCurrencyErrorView, LoadingErrorView, NoAccountToDepositView, NoEnoughBalanceView, ReadyView } from "./views.js"; +import { + AmountOrCurrencyErrorView, + LoadingErrorView, + NoAccountToDepositView, + NoEnoughBalanceView, + ReadyView, +} from "./views.js"; import * as wxApi from "../../wxApi.js"; import { useComponentState } from "./state.js"; import { AmountJson, PaytoUri } from "@gnu-taler/taler-util"; -import { ButtonHandler, SelectFieldHandler, TextFieldHandler, ToggleHandler } from "../../mui/handlers.js"; +import { + ButtonHandler, + SelectFieldHandler, + TextFieldHandler, + ToggleHandler, +} from "../../mui/handlers.js"; import { AddAccountPage } from "../AddAccount/index.js"; export interface Props { @@ -31,7 +42,8 @@ export interface Props { onSuccess: (currency: string) => void; } -export type State = State.Loading +export type State = + | State.Loading | State.LoadingUriError | State.AmountOrCurrencyError | State.NoEnoughBalance diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index 87705507c..57380a632 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -14,13 +14,24 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountJson, Amounts, DepositGroupFees, KnownBankAccountsInfo, parsePaytoUri, PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { + AmountJson, + Amounts, + DepositGroupFees, + KnownBankAccountsInfo, + parsePaytoUri, + PaytoUri, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; import { useState } from "preact/hooks"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import * as wxApi from "../../wxApi.js"; import { Props, State } from "./index.js"; -export function useComponentState({ amount: amountStr, currency: currencyStr, onCancel, onSuccess }: Props, api: typeof wxApi): State { +export function useComponentState( + { amount: amountStr, currency: currencyStr, onCancel, onSuccess }: Props, + api: typeof wxApi, +): State { const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr); const currency = parsed !== undefined ? parsed.currency : currencyStr; @@ -46,8 +57,8 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on if (!currency) { return { status: "amount-or-currency-error", - error: undefined - } + error: undefined, + }; } if (!hook) { @@ -60,7 +71,7 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on return { status: "loading-error", error: hook, - } + }; } const { accounts, balances } = hook.response; @@ -74,13 +85,12 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on onAccountAdded: (p: string) => { updateAccountFromList(p); setAddingAccount(false); - hook.retry() + hook.retry(); }, onCancel: () => { setAddingAccount(false); - } - , - } + }, + }; } const bs = balances.filter((b) => b.available.startsWith(currency)); @@ -103,13 +113,15 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on error: undefined, currency, onAddAccount: { - onClick: async () => { setAddingAccount(true) } + onClick: async () => { + setAddingAccount(true); + }, }, - } + }; } const accountMap = createLabelsForBankAccount(accounts); - accountMap[""] = "Select one account..." + accountMap[""] = "Select one account..."; async function updateAccountFromList(accountStr: string): Promise<void> { // const newSelected = !accountMap[accountStr] ? undefined : accountMap[accountStr]; @@ -144,18 +156,19 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount : Amounts.getZero(currency); - const totalToDeposit = parsedAmount && fee !== undefined - ? Amounts.sub(parsedAmount, totalFee).amount - : Amounts.getZero(currency); + const totalToDeposit = + parsedAmount && fee !== undefined + ? Amounts.sub(parsedAmount, totalFee).amount + : Amounts.getZero(currency); const isDirty = amount !== initialValue; const amountError = !isDirty ? undefined : !parsedAmount - ? "Invalid amount" - : Amounts.cmp(balance, parsedAmount) === -1 - ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` - : undefined; + ? "Invalid amount" + : Amounts.cmp(balance, parsedAmount) === -1 + ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` + : undefined; const unableToDeposit = !parsedAmount || @@ -181,10 +194,11 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on value: String(amount), onInput: updateAmount, error: amountError, - }, onAddAccount: { - onClick: async () => { setAddingAccount(true) } + onClick: async () => { + setAddingAccount(true); + }, }, account: { list: accountMap, @@ -219,22 +233,26 @@ async function getFeeForAmount( export function labelForAccountType(id: string) { switch (id) { - case "": return "Choose one"; - case "x-taler-bank": return "Taler Bank"; - case "bitcoin": return "Bitcoin"; - case "iban": return "IBAN"; - default: return id; + case "": + return "Choose one"; + case "x-taler-bank": + return "Taler Bank"; + case "bitcoin": + return "Bitcoin"; + case "iban": + return "IBAN"; + default: + return id; } } export function createLabelsForBankAccount( knownBankAccounts: Array<KnownBankAccountsInfo>, ): { [value: string]: string } { - const initialList: Record<string, string> = { - } + const initialList: Record<string, string> = {}; if (!knownBankAccounts.length) return initialList; return knownBankAccounts.reduce((prev, cur, i) => { - prev[stringifyPaytoUri(cur.uri)] = cur.alias + prev[stringifyPaytoUri(cur.uri)] = cur.alias; return prev; }, initialList); } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index a1d4ca85a..68df5e402 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -19,7 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts, Balance, BalancesResponse, DepositGroupFees, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { + Amounts, + Balance, + BalancesResponse, + DepositGroupFees, + parsePaytoUri, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; import { expect } from "chai"; import { mountHook } from "../../test-utils.js"; @@ -52,17 +59,19 @@ const nullFunction: any = () => null; type VoidFunction = () => void; describe("DepositPage states", () => { - it("should have status 'no-enough-balance' when balance is empty", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { - getBalance: async () => - ({ - balances: [{ available: `${currency}:0` }], - } as Partial<BalancesResponse>), - listKnownBankAccounts: async () => ({ accounts: {} }), - } as Partial<typeof wxApi> as any), + useComponentState( + { currency, onCancel: nullFunction, onSuccess: nullFunction }, + { + getBalance: async () => + ({ + balances: [{ available: `${currency}:0` }], + } as Partial<BalancesResponse>), + listKnownBankAccounts: async () => ({ accounts: {} }), + } as Partial<typeof wxApi> as any, + ), ); { @@ -111,25 +120,28 @@ describe("DepositPage states", () => { uri: parsePaytoUri("payto://iban/ES8877998399652238")!, kyc_completed: false, currency: "EUR", - alias: "my iban account" + alias: "my iban account", }; const talerBankPayto = { uri: parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!, kyc_completed: false, currency: "EUR", - alias: "my taler account" + alias: "my taler account", }; it("should have status 'ready' but unable to deposit ", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { - getBalance: async () => - ({ - balances: [{ available: `${currency}:1` }], - } as Partial<BalancesResponse>), - listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), - } as Partial<typeof wxApi> as any), + useComponentState( + { currency, onCancel: nullFunction, onSuccess: nullFunction }, + { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial<BalancesResponse>), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + } as Partial<typeof wxApi> as any, + ), ); { @@ -155,14 +167,17 @@ describe("DepositPage states", () => { it.skip("should not be able to deposit more than the balance ", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { - getBalance: async () => - ({ - balances: [{ available: `${currency}:1` }], - } as Partial<BalancesResponse>), - listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), - getFeeForDeposit: withoutFee, - } as Partial<typeof wxApi> as any), + useComponentState( + { currency, onCancel: nullFunction, onSuccess: nullFunction }, + { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial<BalancesResponse>), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withoutFee, + } as Partial<typeof wxApi> as any, + ), ); { @@ -217,14 +232,17 @@ describe("DepositPage states", () => { it.skip("should calculate the fee upon entering amount ", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { - getBalance: async () => - ({ - balances: [{ available: `${currency}:1` }], - } as Partial<BalancesResponse>), - listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), - getFeeForDeposit: withSomeFee, - } as Partial<typeof wxApi> as any), + useComponentState( + { currency, onCancel: nullFunction, onSuccess: nullFunction }, + { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial<BalancesResponse>), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withSomeFee, + } as Partial<typeof wxApi> as any, + ), ); { @@ -281,16 +299,19 @@ describe("DepositPage states", () => { it("should calculate the fee upon selecting account ", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { - getBalance: async () => - ({ - balances: [{ available: `${currency}:1` }], - } as Partial<BalancesResponse>), - listKnownBankAccounts: async () => ({ - accounts: [ibanPayto, talerBankPayto], - }), - getFeeForDeposit: freeJustForIBAN, - } as Partial<typeof wxApi> as any), + useComponentState( + { currency, onCancel: nullFunction, onSuccess: nullFunction }, + { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial<BalancesResponse>), + listKnownBankAccounts: async () => ({ + accounts: [ibanPayto, talerBankPayto], + }), + getFeeForDeposit: freeJustForIBAN, + } as Partial<typeof wxApi> as any, + ), ); { @@ -327,7 +348,6 @@ describe("DepositPage states", () => { expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.depositHandler.onClick).undefined; - } await waitNextUpdate(""); @@ -358,7 +378,6 @@ describe("DepositPage states", () => { expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); expect(r.depositHandler.onClick).undefined; - } await waitNextUpdate(""); @@ -374,7 +393,6 @@ describe("DepositPage states", () => { expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); expect(r.depositHandler.onClick).undefined; - if (r.account.onChange === undefined) expect.fail(); r.account.onChange(stringifyPaytoUri(talerBankPayto.uri)); } @@ -391,7 +409,6 @@ describe("DepositPage states", () => { expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); expect(r.depositHandler.onClick).undefined; - } await waitNextUpdate(""); @@ -414,14 +431,17 @@ describe("DepositPage states", () => { it.skip("should be able to deposit if has the enough balance ", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { - getBalance: async () => - ({ - balances: [{ available: `${currency}:15` }], - } as Partial<BalancesResponse>), - listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), - getFeeForDeposit: withSomeFee, - } as Partial<typeof wxApi> as any), + useComponentState( + { currency, onCancel: nullFunction, onSuccess: nullFunction }, + { + getBalance: async () => + ({ + balances: [{ available: `${currency}:15` }], + } as Partial<BalancesResponse>), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withSomeFee, + } as Partial<typeof wxApi> as any, + ), ); { @@ -456,7 +476,6 @@ describe("DepositPage states", () => { expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)); expect(r.depositHandler.onClick).undefined; - } await waitNextUpdate(); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx index c6bff219f..b58fce8e6 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx @@ -19,20 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { termsXml } from "../cta/termsExample.js"; import { createExample } from "../test-utils.js"; -import { View as TestedComponent } from "./ExchangeAddConfirm.js"; - -function parseFromString(s: string): Document { - if (typeof window === "undefined") { - return { - querySelector: () => ({ - children: [], - }), - } as any; - } - return new window.DOMParser().parseFromString(s, "text/xml"); -} +import { ExchangeAddConfirmPage as TestedComponent } from "./ExchangeAddConfirm.js"; export default { title: "wallet/exchange add/confirm", @@ -46,33 +34,12 @@ export default { 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: parseFromString(termsXml), - }, - }, - onAccept: async () => undefined, }); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx index a92ece066..b0602d1e6 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx @@ -17,10 +17,9 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Title } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; -import { TermsOfServiceSection } from "../cta/TermsOfServiceSection.js"; +import { TermsOfService } from "../cta/TermsOfService/index.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { Button } from "../mui/Button.js"; -import { buildTermsOfServiceState, TermsState } from "../utils/index.js"; import * as wxApi from "../wxApi.js"; export interface Props { @@ -34,69 +33,9 @@ export function ExchangeAddConfirmPage({ 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<string | undefined>( - // undefined, - // ); - - const onAccept = async (): Promise<void> => { - if (!terms) return; - try { - await wxApi.setExchangeTosAccepted(url, terms.version); - } catch (e) { - if (e instanceof Error) { - // setErrorAccepting(e.message); - } - } - }; - return ( - <View - url={url} - onAccept={onAccept} - onCancel={onCancel} - onConfirm={onConfirm} - terms={terms} - /> - ); -} - -export interface ViewProps { - url: string; - terms: TermsState | undefined; - onAccept: (b: boolean) => Promise<void>; - onCancel: () => Promise<void>; - onConfirm: () => Promise<void>; -} - -export function View({ - url, - terms, - onAccept: doAccept, - onConfirm, - onCancel, -}: ViewProps): VNode { const { i18n } = useTranslationContext(); - const needsReview = - !terms || terms.status === "changed" || terms.status === "new"; - const [reviewed, setReviewed] = useState<boolean>(false); + + const [accepted, setAccepted] = useState(false); return ( <Fragment> @@ -111,52 +50,27 @@ export function View({ </a> </div> </section> - {terms && ( - <TermsOfServiceSection - reviewed={reviewed} - reviewing={true} - terms={terms} - onAccept={(value) => - doAccept(value).then(() => { - setReviewed(value); - }) - } - /> - )} + + <TermsOfService key="terms" exchangeUrl={url} onChange={setAccepted} /> <footer> - <Button variant="contained" color="secondary" onClick={onCancel}> + <Button + key="cancel" + variant="contained" + color="secondary" + onClick={onCancel} + > <i18n.Translate>Cancel</i18n.Translate> </Button> - {!terms && ( - <Button variant="contained" disabled> - <i18n.Translate>Loading terms..</i18n.Translate> - </Button> - )} - {terms && ( - <Fragment> - {needsReview && !reviewed && ( - <Button - variant="contained" - color="success" - disabled - onClick={onConfirm} - > - <i18n.Translate>Add exchange</i18n.Translate> - </Button> - )} - {(terms.status === "accepted" || (needsReview && reviewed)) && ( - <Button variant="contained" color="success" onClick={onConfirm}> - <i18n.Translate>Add exchange</i18n.Translate> - </Button> - )} - {terms.status === "notfound" && ( - <Button variant="contained" color="warning" onClick={onConfirm}> - <i18n.Translate>Add exchange anyway</i18n.Translate> - </Button> - )} - </Fragment> - )} + <Button + key="add" + variant="contained" + color="success" + disabled={!accepted} + onClick={onConfirm} + > + <i18n.Translate>Add exchange</i18n.Translate> + </Button> </footer> </Fragment> ); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts index 9603b3d2c..06d519268 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts @@ -17,13 +17,12 @@ import { DenomOperationMap, ExchangeFullDetails, - ExchangeListItem, FeeDescriptionPair + ExchangeListItem, + FeeDescriptionPair, } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; -import { - State as SelectExchangeState -} from "../../hooks/useSelectedExchange.js"; +import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; import * as wxApi from "../../wxApi.js"; @@ -32,12 +31,12 @@ import { ComparingView, ErrorLoadingView, NoExchangesView, - ReadyView + ReadyView, } from "./views.js"; export interface Props { - list: ExchangeListItem[], - currentExchange: string, + list: ExchangeListItem[]; + currentExchange: string; onCancel: () => Promise<void>; onSelection: (exchange: string) => Promise<void>; } diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts index 954e52239..e1b270a42 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts @@ -25,9 +25,13 @@ export function useComponentState( { onCancel, onSelection, list: exchanges, currentExchange }: Props, api: typeof wxApi, ): State { - const initialValue = exchanges.findIndex(e => e.exchangeBaseUrl === currentExchange); + const initialValue = exchanges.findIndex( + (e) => e.exchangeBaseUrl === currentExchange, + ); if (initialValue === -1) { - throw Error(`wrong usage of ExchangeSelection component, currentExchange '${currentExchange}' is not in the list of exchanges`) + throw Error( + `wrong usage of ExchangeSelection component, currentExchange '${currentExchange}' is not in the list of exchanges`, + ); } const [value, setValue] = useState(String(initialValue)); @@ -113,7 +117,7 @@ export function useComponentState( withdraw: createPairTimeline( selected.denomFees.withdraw, original.denomFees.withdraw, - ) + ), }; return { diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx index 6b753e215..d39aa3878 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx @@ -14,24 +14,20 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - Amounts, - FeeDescription, - FeeDescriptionPair, -} from "@gnu-taler/taler-util"; +import { FeeDescription, FeeDescriptionPair } from "@gnu-taler/taler-util"; import { styled } from "@linaria/react"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Amount } from "../../components/Amount.js"; import { LoadingError } from "../../components/LoadingError.js"; import { SelectList } from "../../components/SelectList.js"; -import { Input, LinkPrimary, SvgIcon } from "../../components/styled/index.js"; +import { Input, SvgIcon } from "../../components/styled/index.js"; import { Time } from "../../components/Time.js"; import { useTranslationContext } from "../../context/translation.js"; +import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { Button } from "../../mui/Button.js"; import arrowDown from "../../svg/chevron-down.svg"; import { State } from "./index.js"; -import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; const ButtonGroup = styled.div` & > button { @@ -39,6 +35,16 @@ const ButtonGroup = styled.div` margin-right: 8px; } `; +const ButtonGroupFooter = styled.div` + & { + display: flex; + justify-content: space-between; + } + & > button { + margin-left: 8px; + margin-right: 8px; + } +`; const FeeDescriptionTable = styled.table` & { @@ -343,10 +349,10 @@ export function ComparingView({ </table> </section> <section> - <ButtonGroup> - <LinkPrimary>Privacy policy</LinkPrimary> - <LinkPrimary>Terms of service</LinkPrimary> - </ButtonGroup> + <ButtonGroupFooter> + <Button variant="outlined">Privacy policy</Button> + <Button variant="outlined">Terms of service</Button> + </ButtonGroupFooter> </section> </Container> ); @@ -609,10 +615,10 @@ export function ReadyView({ </FeeDescriptionTable> </section> <section> - <ButtonGroup> - <LinkPrimary>Privacy policy</LinkPrimary> - <LinkPrimary>Terms of service</LinkPrimary> - </ButtonGroup> + <ButtonGroupFooter> + <Button variant="outlined">Privacy policy</Button> + <Button variant="outlined">Terms of service</Button> + </ButtonGroupFooter> </section> </Container> ); diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 28ee229eb..56e610e8a 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -36,7 +36,7 @@ import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js"; import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js"; import { ToggleHandler } from "../mui/handlers.js"; import { Pages } from "../NavigationBar.js"; -import { buildTermsOfServiceStatus } from "../utils/index.js"; +import { buildTermsOfServiceStatus } from "../cta/TermsOfService/utils.js"; import * as wxApi from "../wxApi.js"; import { platform } from "../platform/api.js"; import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js"; diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 1b0f67346..e0a1ee238 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -24,8 +24,16 @@ import { AcceptExchangeTosRequest, AcceptManualWithdrawalResult, - AcceptPeerPullPaymentRequest, AcceptPeerPullPaymentResponse, AcceptPeerPushPaymentRequest, AcceptPeerPushPaymentResponse, AcceptTipRequest, AcceptTipResponse, AcceptWithdrawalResponse, - AddExchangeRequest, AddKnownBankAccountsRequest, AmountString, + AcceptPeerPullPaymentRequest, + AcceptPeerPullPaymentResponse, + AcceptPeerPushPaymentRequest, + AcceptPeerPushPaymentResponse, + AcceptTipRequest, + AcceptTipResponse, + AcceptWithdrawalResponse, + AddExchangeRequest, + AddKnownBankAccountsRequest, + AmountString, ApplyRefundResponse, BalancesResponse, CheckPeerPullPaymentRequest, @@ -37,7 +45,12 @@ import { CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, - DeleteTransactionRequest, DepositGroupFees, ExchangeFullDetails, ExchangesListResponse, ForgetKnownBankAccountsRequest, GetExchangeTosResult, + DeleteTransactionRequest, + DepositGroupFees, + ExchangeFullDetails, + ExchangesListResponse, + ForgetKnownBankAccountsRequest, + GetExchangeTosResult, GetExchangeWithdrawalInfo, GetFeeForDepositRequest, GetWithdrawalDetailsForUriRequest, @@ -47,7 +60,9 @@ import { InitiatePeerPushPaymentResponse, KnownBankAccounts, Logger, - NotificationType, PaytoUri, PrepareDepositRequest, + NotificationType, + PaytoUri, + PrepareDepositRequest, PrepareDepositResponse, PreparePayResult, PrepareRefundRequest, @@ -55,9 +70,13 @@ import { PrepareTipRequest, PrepareTipResult, RetryTransactionRequest, - SetWalletDeviceIdRequest, stringifyPaytoUri, Transaction, - TransactionsResponse, WalletCoreVersion, - WalletDiagnostics, WithdrawUriInfoResponse + SetWalletDeviceIdRequest, + stringifyPaytoUri, + Transaction, + TransactionsResponse, + WalletCoreVersion, + WalletDiagnostics, + WithdrawUriInfoResponse, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, @@ -66,7 +85,7 @@ import { PendingOperationsResponse, RemoveBackupProviderRequest, TalerError, - WalletContractData + WalletContractData, } from "@gnu-taler/taler-wallet-core"; import { MessageFromBackend, platform } from "./platform/api.js"; @@ -268,13 +287,13 @@ export function addKnownBankAccounts( return callBackend("addKnownBankAccounts", { payto: stringifyPaytoUri(payto), currency, - alias + alias, } as AddKnownBankAccountsRequest); } -export function forgetKnownBankAccounts( - payto: string, -): Promise<void> { - return callBackend("forgetKnownBankAccounts", { payto } as ForgetKnownBankAccountsRequest); +export function forgetKnownBankAccounts(payto: string): Promise<void> { + return callBackend("forgetKnownBankAccounts", { + payto, + } as ForgetKnownBankAccountsRequest); } /** |