commit b011c8a32ed478807737b96a9d7fc4e0ff085bdb parent 6acddd6d70abc568e4b3740f56662691278aa645 Author: Sebastian <sebasjm@gmail.com> Date: Fri, 14 Oct 2022 16:12:24 -0300 terms and privacy on exchange selection Diffstat:
21 files changed, 740 insertions(+), 652 deletions(-)
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts @@ -0,0 +1,94 @@ +/* + 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; +} + +export type State = + | State.Loading + | State.LoadingUriError + | State.ErrorAccepting + | 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; + terms: TermsState; + } + export interface ShowContent extends BaseInfo { + status: "show-content"; + termsAccepted?: ToggleHandler; + showingTermsOfService?: ToggleHandler; + } + export interface ShowButtonsAccepted extends BaseInfo { + status: "show-buttons-accepted"; + termsAccepted: ToggleHandler; + showingTermsOfService: ToggleHandler; + } + export interface ShowButtonsNotAccepted extends BaseInfo { + status: "show-buttons-not-accepted"; + showingTermsOfService: ToggleHandler; + } +} + +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/components/TermsOfService/state.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts @@ -0,0 +1,137 @@ +/* + 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, onChange }: Props, + api: typeof wxApi, +): State { + const readOnly = !onChange; + const [showContent, setShowContent] = useState<boolean>(readOnly); + 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); + if (!readOnly) 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 = { + error: undefined, + showingTermsOfService: { + value: showContent, + button: { + onClick: 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", + error: undefined, + terms: state, + showingTermsOfService: readOnly ? undefined : base.showingTermsOfService, + termsAccepted: readOnly ? undefined : base.termsAccepted, + }; + } + //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/components/TermsOfService/stories.tsx diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/test.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/test.ts diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts b/packages/taler-wallet-webextension/src/components/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"; + +export 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/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx @@ -0,0 +1,223 @@ +/* + 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 { TermsDocument, 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({ + 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> + )} + {showingTermsOfService && ableToReviewTermsOfService && ( + <section style={{ justifyContent: "space-around", display: "flex" }}> + <LinkSuccess + upperCased + onClick={showingTermsOfService.button.onClick} + > + <i18n.Translate>Hide terms of service</i18n.Translate> + </LinkSuccess> + </section> + )} + {termsAccepted && 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/components/index.stories.tsx b/packages/taler-wallet-webextension/src/components/index.stories.tsx @@ -23,5 +23,6 @@ import * as a1 from "./Banner.stories.js"; import * as a2 from "./PendingTransactions.stories.js"; import * as a3 from "./Amount.stories.js"; import * as a4 from "./ShowFullContractTermPopup.stories.js"; +import * as a5 from "./TermsOfService/stories.js"; -export default [a1, a2, a3, a4]; +export default [a1, a2, a3, a4, a5]; diff --git a/packages/taler-wallet-webextension/src/cta/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/cta/TermsOfService/index.ts @@ -1,96 +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 { 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 @@ -1,136 +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 { 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/utils.ts b/packages/taler-wallet-webextension/src/cta/TermsOfService/utils.ts @@ -1,130 +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 { 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 @@ -1,224 +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 { 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/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -35,7 +35,7 @@ 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 { TermsOfService } from "../TermsOfService/index.js"; +import { TermsOfService } from "../../components/TermsOfService/index.js"; import { State } from "./index.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts @@ -24,10 +24,9 @@ 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 "./TermsOfService/stories.js"; import * as a8 from "./InvoiceCreate/stories.js"; import * as a9 from "./InvoicePay/stories.js"; import * as a10 from "./TransferCreate/stories.js"; import * as a11 from "./TransferPickup/stories.js"; -export default [a1, a3, a4, a5, a6, a7, a8, a9, a10, a11]; +export default [a1, a3, a4, a5, a6, a8, a9, a10, a11]; diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -300,20 +300,29 @@ function openWalletPageFromPopup(page: string): void { }); } +let i = 0; + async function sendMessageToWalletBackground( operation: string, payload: any, ): Promise<any> { return new Promise<any>((resolve, reject) => { logger.trace("send operation to the wallet background", operation); - chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError.message); - } - resolve(resp); - // return true to keep the channel open - return true; - }); + chrome.runtime.sendMessage( + { operation, payload, id: `id_${i++ % 1000}` }, + (backgroundResponse) => { + console.log("BUG: got response from background", backgroundResponse); + + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message); + } + // const apiResponse = JSON.parse(resp) + resolve(backgroundResponse); + + // return true to keep the channel open + return true; + }, + ); }); } @@ -364,7 +373,10 @@ function listenToAllChannels( ) => void, ): void { chrome.runtime.onMessage.addListener((m, s, c) => { - cb(m, s, c); + cb(m, s, (apiResponse) => { + console.log("BUG: sending response to client", apiResponse); + c(apiResponse); + }); // keep the connection open return true; diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx @@ -16,11 +16,9 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Title } from "../components/styled/index.js"; +import { TermsOfService } from "../components/TermsOfService/index.js"; import { useTranslationContext } from "../context/translation.js"; -import { TermsOfService } from "../cta/TermsOfService/index.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { Button } from "../mui/Button.js"; -import * as wxApi from "../wxApi.js"; export interface Props { url: string; diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts @@ -21,6 +21,7 @@ import { FeeDescriptionPair, } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; +import { TermsState } from "../../components/TermsOfService/utils.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; @@ -31,7 +32,9 @@ import { ComparingView, ErrorLoadingView, NoExchangesView, + PrivacyContentView, ReadyView, + TosContentView, } from "./views.js"; export interface Props { @@ -46,6 +49,8 @@ export type State = | State.LoadingUriError | State.Ready | State.Comparing + | State.ShowingTos + | State.ShowingPrivacy | SelectExchangeState.NoExchange; export namespace State { @@ -63,6 +68,8 @@ export namespace State { exchanges: SelectFieldHandler; selected: ExchangeFullDetails; error: undefined; + onShowTerms: ButtonHandler; + onShowPrivacy: ButtonHandler; } export interface Ready extends BaseInfo { @@ -76,6 +83,16 @@ export namespace State { onReset: ButtonHandler; onSelect: ButtonHandler; } + export interface ShowingTos { + status: "showing-tos"; + exchangeUrl: string; + onClose: ButtonHandler; + } + export interface ShowingPrivacy { + status: "showing-privacy"; + exchangeUrl: string; + onClose: ButtonHandler; + } } const viewMapping: StateViewMap<State> = { @@ -83,6 +100,8 @@ const viewMapping: StateViewMap<State> = { "error-loading": ErrorLoadingView, comparing: ComparingView, "no-exchange": NoExchangesView, + "showing-tos": TosContentView, + "showing-privacy": PrivacyContentView, ready: ReadyView, }; diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts @@ -50,9 +50,15 @@ export function useComponentState( const original = !initialExchange ? undefined : await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl); + return { exchanges, selected, original }; }, [value]); + const [showingTos, setShowingTos] = useState<string | undefined>(undefined); + const [showingPrivacy, setShowingPrivacy] = useState<string | undefined>( + undefined, + ); + if (!hook) { return { status: "loading", @@ -82,6 +88,27 @@ export function useComponentState( {} as Record<string, string>, ); + if (showingPrivacy) { + return { + status: "showing-privacy", + error: undefined, + onClose: { + onClick: async () => setShowingPrivacy(undefined), + }, + exchangeUrl: showingPrivacy, + }; + } + if (showingTos) { + return { + status: "showing-tos", + error: undefined, + onClose: { + onClick: async () => setShowingTos(undefined), + }, + exchangeUrl: showingTos, + }; + } + if (!original) { // !original <=> selected == original return { @@ -98,6 +125,16 @@ export function useComponentState( onClick: onCancel, }, selected, + onShowPrivacy: { + onClick: async () => { + setShowingPrivacy(selected.exchangeBaseUrl); + }, + }, + onShowTerms: { + onClick: async () => { + setShowingTos(selected.exchangeBaseUrl); + }, + }, }; } @@ -140,6 +177,16 @@ export function useComponentState( onSelection(selected.exchangeBaseUrl); }, }, + onShowPrivacy: { + onClick: async () => { + setShowingPrivacy(selected.exchangeBaseUrl); + }, + }, + onShowTerms: { + onClick: async () => { + setShowingTos(selected.exchangeBaseUrl); + }, + }, selected, pairTimeline, }; diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx @@ -39,6 +39,8 @@ export const Bitcoin1 = createExample(ReadyView, { transferFees: {}, globalFees: [], } as any, + onShowPrivacy: {}, + onShowTerms: {}, onClose: {}, }); export const Bitcoin2 = createExample(ReadyView, { @@ -57,6 +59,8 @@ export const Bitcoin2 = createExample(ReadyView, { transferFees: {}, globalFees: [], } as any, + onShowPrivacy: {}, + onShowTerms: {}, onClose: {}, }); @@ -75,6 +79,8 @@ export const Kudos1 = createExample(ReadyView, { transferFees: {}, globalFees: [], } as any, + onShowPrivacy: {}, + onShowTerms: {}, onClose: {}, }); export const Kudos2 = createExample(ReadyView, { @@ -93,6 +99,8 @@ export const Kudos2 = createExample(ReadyView, { transferFees: {}, globalFees: [], } as any, + onShowPrivacy: {}, + onShowTerms: {}, onClose: {}, }); export const ComparingBitcoin = createExample(ComparingView, { @@ -108,6 +116,8 @@ export const ComparingBitcoin = createExample(ComparingView, { globalFees: [], } as any, onReset: {}, + onShowPrivacy: {}, + onShowTerms: {}, onSelect: {}, error: undefined, pairTimeline: { @@ -130,6 +140,8 @@ export const ComparingKudos = createExample(ComparingView, { globalFees: [], } as any, onReset: {}, + onShowPrivacy: {}, + onShowTerms: {}, onSelect: {}, error: undefined, pairTimeline: { diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx @@ -22,6 +22,7 @@ import { Amount } from "../../components/Amount.js"; import { LoadingError } from "../../components/LoadingError.js"; import { SelectList } from "../../components/SelectList.js"; import { Input, SvgIcon } from "../../components/styled/index.js"; +import { TermsOfService } from "../../components/TermsOfService/index.js"; import { Time } from "../../components/Time.js"; import { useTranslationContext } from "../../context/translation.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; @@ -119,6 +120,36 @@ export function ErrorLoadingView({ error }: State.LoadingUriError): VNode { ); } +export function PrivacyContentView({ + exchangeUrl, + onClose, +}: State.ShowingPrivacy): VNode { + const { i18n } = useTranslationContext(); + return ( + <div> + <Button variant="outlined" onClick={onClose.onClick}> + <i18n.Translate>Close</i18n.Translate> + </Button> + <div>show privacy terms for {exchangeUrl}</div> + </div> + ); +} + +export function TosContentView({ + exchangeUrl, + onClose, +}: State.ShowingTos): VNode { + const { i18n } = useTranslationContext(); + return ( + <div> + <Button variant="outlined" onClick={onClose.onClick}> + <i18n.Translate>Close</i18n.Translate> + </Button> + <TermsOfService exchangeUrl={exchangeUrl} /> + </div> + ); +} + export function NoExchangesView({ currency, }: SelectExchangeState.NoExchange): VNode { @@ -145,6 +176,8 @@ export function ComparingView({ onReset, onSelect, pairTimeline, + onShowPrivacy, + onShowTerms, }: State.Comparing): VNode { const { i18n } = useTranslationContext(); return ( @@ -305,53 +338,13 @@ export function ComparingView({ </FeeDescriptionTable>{" "} </section> <section> - <table> - <thead> - <tr> - <td> - <i18n.Translate>Wallet operations</i18n.Translate> - </td> - <td> - <i18n.Translate>Fee</i18n.Translate> - </td> - </tr> - </thead> - <tbody> - <tr> - <td>history(i) </td> - <td>0.1</td> - </tr> - <tr> - <td>kyc (i) </td> - <td>0.1</td> - </tr> - <tr> - <td>account (i) </td> - <td>0.1</td> - </tr> - <tr> - <td>purse (i) </td> - <td>0.1</td> - </tr> - <tr> - <td>wire SEPA (i) </td> - <td>0.1</td> - </tr> - <tr> - <td>closing SEPA(i) </td> - <td>0.1</td> - </tr> - <tr> - <td>wad SEPA (i) </td> - <td>0.1</td> - </tr> - </tbody> - </table> - </section> - <section> <ButtonGroupFooter> - <Button variant="outlined">Privacy policy</Button> - <Button variant="outlined">Terms of service</Button> + <Button onClick={onShowPrivacy.onClick} variant="outlined"> + Privacy policy + </Button> + <Button onClick={onShowTerms.onClick} variant="outlined"> + Terms of service + </Button> </ButtonGroupFooter> </section> </Container> @@ -362,6 +355,8 @@ export function ReadyView({ exchanges, selected, onClose, + onShowPrivacy, + onShowTerms, }: State.Ready): VNode { const { i18n } = useTranslationContext(); @@ -616,8 +611,12 @@ export function ReadyView({ </section> <section> <ButtonGroupFooter> - <Button variant="outlined">Privacy policy</Button> - <Button variant="outlined">Terms of service</Button> + <Button onClick={onShowPrivacy.onClick} variant="outlined"> + Privacy policy + </Button> + <Button onClick={onShowTerms.onClick} 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 @@ -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 "../cta/TermsOfService/utils.js"; +import { buildTermsOfServiceStatus } from "../components/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/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -182,6 +182,7 @@ async function dispatch( break; } r = await w.handleCoreApiRequest(req.operation, req.id, req.payload); + console.log("response received from wallet", r); break; } } @@ -330,7 +331,9 @@ export async function wxMain(): Promise<void> { // script on the page platform.listenToAllChannels((message, sender, callback) => { afterWalletIsInitialized.then(() => { - dispatch(message, sender, callback); + dispatch(message, sender, (response: CoreApiResponse) => { + callback(response); + }); }); });