terms and privacy on exchange selection

This commit is contained in:
Sebastian 2022-10-14 16:12:24 -03:00
parent 6acddd6d70
commit b011c8a32e
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
17 changed files with 179 additions and 91 deletions

View File

@ -31,15 +31,13 @@ import {
export interface Props { export interface Props {
exchangeUrl: string; exchangeUrl: string;
onChange: (v: boolean) => void; onChange?: (v: boolean) => void;
readOnly?: boolean;
} }
export type State = export type State =
| State.Loading | State.Loading
| State.LoadingUriError | State.LoadingUriError
| State.ErrorAccepting | State.ErrorAccepting
| State.ShowContent
| State.ShowButtonsAccepted | State.ShowButtonsAccepted
| State.ShowButtonsNotAccepted | State.ShowButtonsNotAccepted
| State.ShowContent; | State.ShowContent;
@ -62,21 +60,21 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
error: undefined; error: undefined;
termsAccepted: ToggleHandler;
showingTermsOfService: ToggleHandler;
terms: TermsState; terms: TermsState;
} }
export interface ShowContent extends BaseInfo { export interface ShowContent extends BaseInfo {
status: "show-content"; status: "show-content";
error: undefined; termsAccepted?: ToggleHandler;
showingTermsOfService?: ToggleHandler;
} }
export interface ShowButtonsAccepted extends BaseInfo { export interface ShowButtonsAccepted extends BaseInfo {
status: "show-buttons-accepted"; status: "show-buttons-accepted";
error: undefined; termsAccepted: ToggleHandler;
showingTermsOfService: ToggleHandler;
} }
export interface ShowButtonsNotAccepted extends BaseInfo { export interface ShowButtonsNotAccepted extends BaseInfo {
status: "show-buttons-not-accepted"; status: "show-buttons-not-accepted";
error: undefined; showingTermsOfService: ToggleHandler;
} }
} }

View File

@ -21,11 +21,11 @@ import { Props, State } from "./index.js";
import { buildTermsOfServiceState } from "./utils.js"; import { buildTermsOfServiceState } from "./utils.js";
export function useComponentState( export function useComponentState(
{ exchangeUrl, readOnly, onChange }: Props, { exchangeUrl, onChange }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const [showContent, setShowContent] = useState<boolean>(false); const readOnly = !onChange;
// const [accepted, setAccepted] = useState<boolean>(false); const [showContent, setShowContent] = useState<boolean>(readOnly);
const [errorAccepting, setErrorAccepting] = useState<Error | undefined>( const [errorAccepting, setErrorAccepting] = useState<Error | undefined>(
undefined, undefined,
); );
@ -78,7 +78,7 @@ export function useComponentState(
await api.setExchangeTosAccepted(exchangeUrl, undefined); await api.setExchangeTosAccepted(exchangeUrl, undefined);
} }
// setAccepted(accepted); // setAccepted(accepted);
onChange(accepted); //external update if (!readOnly) onChange(accepted); //external update
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
//FIXME: uncomment this and display error //FIXME: uncomment this and display error
@ -90,16 +90,14 @@ export function useComponentState(
const accepted = state.status === "accepted"; const accepted = state.status === "accepted";
const base: State.BaseInfo = { const base = {
error: undefined, error: undefined,
showingTermsOfService: { showingTermsOfService: {
value: showContent, value: showContent,
button: { button: {
onClick: readOnly onClick: async () => {
? undefined setShowContent(!showContent);
: async () => { },
setShowContent(!showContent);
},
}, },
}, },
terms: state, terms: state,
@ -118,7 +116,10 @@ export function useComponentState(
if (showContent) { if (showContent) {
return { return {
status: "show-content", status: "show-content",
...base, error: undefined,
terms: state,
showingTermsOfService: readOnly ? undefined : base.showingTermsOfService,
termsAccepted: readOnly ? undefined : base.termsAccepted,
}; };
} }
//showing buttons //showing buttons

View File

@ -97,7 +97,7 @@ export type TermsState = {
type TermsStatus = "new" | "accepted" | "changed" | "notfound"; type TermsStatus = "new" | "accepted" | "changed" | "notfound";
type TermsDocument = export type TermsDocument =
| TermsDocumentXml | TermsDocumentXml
| TermsDocumentHtml | TermsDocumentHtml
| TermsDocumentPlain | TermsDocumentPlain

View File

@ -17,7 +17,7 @@
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { LoadingError } from "../../components/LoadingError.js"; import { LoadingError } from "../../components/LoadingError.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { TermsState } from "./utils.js"; import { TermsDocument, TermsState } from "./utils.js";
import { State } from "./index.js"; import { State } from "./index.js";
import { CheckboxOutlined } from "../../components/CheckboxOutlined.js"; import { CheckboxOutlined } from "../../components/CheckboxOutlined.js";
import { import {
@ -90,7 +90,6 @@ export function ShowButtonsAcceptedTosView({
} }
export function ShowButtonsNonAcceptedTosView({ export function ShowButtonsNonAcceptedTosView({
termsAccepted,
showingTermsOfService, showingTermsOfService,
terms, terms,
}: State.ShowButtonsNotAccepted): VNode { }: State.ShowButtonsNotAccepted): VNode {
@ -160,7 +159,7 @@ export function ShowTosContentView({
}: State.ShowContent): VNode { }: State.ShowContent): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const ableToReviewTermsOfService = const ableToReviewTermsOfService =
showingTermsOfService.button.onClick !== undefined; showingTermsOfService?.button.onClick !== undefined;
return ( return (
<Fragment> <Fragment>
@ -195,7 +194,7 @@ export function ShowTosContentView({
)} )}
</section> </section>
)} )}
{termsAccepted && ableToReviewTermsOfService && ( {showingTermsOfService && ableToReviewTermsOfService && (
<section style={{ justifyContent: "space-around", display: "flex" }}> <section style={{ justifyContent: "space-around", display: "flex" }}>
<LinkSuccess <LinkSuccess
upperCased upperCased
@ -205,7 +204,7 @@ export function ShowTosContentView({
</LinkSuccess> </LinkSuccess>
</section> </section>
)} )}
{terms.status !== "notfound" && ( {termsAccepted && terms.status !== "notfound" && (
<section style={{ justifyContent: "space-around", display: "flex" }}> <section style={{ justifyContent: "space-around", display: "flex" }}>
<CheckboxOutlined <CheckboxOutlined
name="terms" name="terms"

View File

@ -23,5 +23,6 @@ import * as a1 from "./Banner.stories.js";
import * as a2 from "./PendingTransactions.stories.js"; import * as a2 from "./PendingTransactions.stories.js";
import * as a3 from "./Amount.stories.js"; import * as a3 from "./Amount.stories.js";
import * as a4 from "./ShowFullContractTermPopup.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];

View File

@ -35,7 +35,7 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import editIcon from "../../svg/edit_24px.svg"; import editIcon from "../../svg/edit_24px.svg";
import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js"; 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"; import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function LoadingUriView({ error }: State.LoadingUriError): VNode {

View File

@ -24,10 +24,9 @@ import * as a3 from "./Payment/stories.jsx";
import * as a4 from "./Refund/stories.jsx"; import * as a4 from "./Refund/stories.jsx";
import * as a5 from "./Tip/stories.jsx"; import * as a5 from "./Tip/stories.jsx";
import * as a6 from "./Withdraw/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 a8 from "./InvoiceCreate/stories.js";
import * as a9 from "./InvoicePay/stories.js"; import * as a9 from "./InvoicePay/stories.js";
import * as a10 from "./TransferCreate/stories.js"; import * as a10 from "./TransferCreate/stories.js";
import * as a11 from "./TransferPickup/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];

View File

@ -300,20 +300,29 @@ function openWalletPageFromPopup(page: string): void {
}); });
} }
let i = 0;
async function sendMessageToWalletBackground( async function sendMessageToWalletBackground(
operation: string, operation: string,
payload: any, payload: any,
): Promise<any> { ): Promise<any> {
return new Promise<any>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
logger.trace("send operation to the wallet background", operation); logger.trace("send operation to the wallet background", operation);
chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { chrome.runtime.sendMessage(
if (chrome.runtime.lastError) { { operation, payload, id: `id_${i++ % 1000}` },
reject(chrome.runtime.lastError.message); (backgroundResponse) => {
} console.log("BUG: got response from background", backgroundResponse);
resolve(resp);
// return true to keep the channel open if (chrome.runtime.lastError) {
return true; 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,
): void { ): void {
chrome.runtime.onMessage.addListener((m, s, c) => { 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 // keep the connection open
return true; return true;

View File

@ -16,11 +16,9 @@
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Title } from "../components/styled/index.js"; import { Title } from "../components/styled/index.js";
import { TermsOfService } from "../components/TermsOfService/index.js";
import { useTranslationContext } from "../context/translation.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 { Button } from "../mui/Button.js";
import * as wxApi from "../wxApi.js";
export interface Props { export interface Props {
url: string; url: string;

View File

@ -21,6 +21,7 @@ import {
FeeDescriptionPair, FeeDescriptionPair,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { TermsState } from "../../components/TermsOfService/utils.js";
import { HookError } from "../../hooks/useAsyncAsHook.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 { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
@ -31,7 +32,9 @@ import {
ComparingView, ComparingView,
ErrorLoadingView, ErrorLoadingView,
NoExchangesView, NoExchangesView,
PrivacyContentView,
ReadyView, ReadyView,
TosContentView,
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -46,6 +49,8 @@ export type State =
| State.LoadingUriError | State.LoadingUriError
| State.Ready | State.Ready
| State.Comparing | State.Comparing
| State.ShowingTos
| State.ShowingPrivacy
| SelectExchangeState.NoExchange; | SelectExchangeState.NoExchange;
export namespace State { export namespace State {
@ -63,6 +68,8 @@ export namespace State {
exchanges: SelectFieldHandler; exchanges: SelectFieldHandler;
selected: ExchangeFullDetails; selected: ExchangeFullDetails;
error: undefined; error: undefined;
onShowTerms: ButtonHandler;
onShowPrivacy: ButtonHandler;
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
@ -76,6 +83,16 @@ export namespace State {
onReset: ButtonHandler; onReset: ButtonHandler;
onSelect: 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> = { const viewMapping: StateViewMap<State> = {
@ -83,6 +100,8 @@ const viewMapping: StateViewMap<State> = {
"error-loading": ErrorLoadingView, "error-loading": ErrorLoadingView,
comparing: ComparingView, comparing: ComparingView,
"no-exchange": NoExchangesView, "no-exchange": NoExchangesView,
"showing-tos": TosContentView,
"showing-privacy": PrivacyContentView,
ready: ReadyView, ready: ReadyView,
}; };

View File

@ -50,9 +50,15 @@ export function useComponentState(
const original = !initialExchange const original = !initialExchange
? undefined ? undefined
: await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl); : await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl);
return { exchanges, selected, original }; return { exchanges, selected, original };
}, [value]); }, [value]);
const [showingTos, setShowingTos] = useState<string | undefined>(undefined);
const [showingPrivacy, setShowingPrivacy] = useState<string | undefined>(
undefined,
);
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
@ -82,6 +88,27 @@ export function useComponentState(
{} as Record<string, string>, {} 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) { if (!original) {
// !original <=> selected == original // !original <=> selected == original
return { return {
@ -98,6 +125,16 @@ export function useComponentState(
onClick: onCancel, onClick: onCancel,
}, },
selected, selected,
onShowPrivacy: {
onClick: async () => {
setShowingPrivacy(selected.exchangeBaseUrl);
},
},
onShowTerms: {
onClick: async () => {
setShowingTos(selected.exchangeBaseUrl);
},
},
}; };
} }
@ -140,6 +177,16 @@ export function useComponentState(
onSelection(selected.exchangeBaseUrl); onSelection(selected.exchangeBaseUrl);
}, },
}, },
onShowPrivacy: {
onClick: async () => {
setShowingPrivacy(selected.exchangeBaseUrl);
},
},
onShowTerms: {
onClick: async () => {
setShowingTos(selected.exchangeBaseUrl);
},
},
selected, selected,
pairTimeline, pairTimeline,
}; };

View File

@ -39,6 +39,8 @@ export const Bitcoin1 = createExample(ReadyView, {
transferFees: {}, transferFees: {},
globalFees: [], globalFees: [],
} as any, } as any,
onShowPrivacy: {},
onShowTerms: {},
onClose: {}, onClose: {},
}); });
export const Bitcoin2 = createExample(ReadyView, { export const Bitcoin2 = createExample(ReadyView, {
@ -57,6 +59,8 @@ export const Bitcoin2 = createExample(ReadyView, {
transferFees: {}, transferFees: {},
globalFees: [], globalFees: [],
} as any, } as any,
onShowPrivacy: {},
onShowTerms: {},
onClose: {}, onClose: {},
}); });
@ -75,6 +79,8 @@ export const Kudos1 = createExample(ReadyView, {
transferFees: {}, transferFees: {},
globalFees: [], globalFees: [],
} as any, } as any,
onShowPrivacy: {},
onShowTerms: {},
onClose: {}, onClose: {},
}); });
export const Kudos2 = createExample(ReadyView, { export const Kudos2 = createExample(ReadyView, {
@ -93,6 +99,8 @@ export const Kudos2 = createExample(ReadyView, {
transferFees: {}, transferFees: {},
globalFees: [], globalFees: [],
} as any, } as any,
onShowPrivacy: {},
onShowTerms: {},
onClose: {}, onClose: {},
}); });
export const ComparingBitcoin = createExample(ComparingView, { export const ComparingBitcoin = createExample(ComparingView, {
@ -108,6 +116,8 @@ export const ComparingBitcoin = createExample(ComparingView, {
globalFees: [], globalFees: [],
} as any, } as any,
onReset: {}, onReset: {},
onShowPrivacy: {},
onShowTerms: {},
onSelect: {}, onSelect: {},
error: undefined, error: undefined,
pairTimeline: { pairTimeline: {
@ -130,6 +140,8 @@ export const ComparingKudos = createExample(ComparingView, {
globalFees: [], globalFees: [],
} as any, } as any,
onReset: {}, onReset: {},
onShowPrivacy: {},
onShowTerms: {},
onSelect: {}, onSelect: {},
error: undefined, error: undefined,
pairTimeline: { pairTimeline: {

View File

@ -22,6 +22,7 @@ import { Amount } from "../../components/Amount.js";
import { LoadingError } from "../../components/LoadingError.js"; import { LoadingError } from "../../components/LoadingError.js";
import { SelectList } from "../../components/SelectList.js"; import { SelectList } from "../../components/SelectList.js";
import { Input, SvgIcon } from "../../components/styled/index.js"; import { Input, SvgIcon } from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Time } from "../../components/Time.js"; import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.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({ export function NoExchangesView({
currency, currency,
}: SelectExchangeState.NoExchange): VNode { }: SelectExchangeState.NoExchange): VNode {
@ -145,6 +176,8 @@ export function ComparingView({
onReset, onReset,
onSelect, onSelect,
pairTimeline, pairTimeline,
onShowPrivacy,
onShowTerms,
}: State.Comparing): VNode { }: State.Comparing): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
@ -304,54 +337,14 @@ export function ComparingView({
</tbody> </tbody>
</FeeDescriptionTable>{" "} </FeeDescriptionTable>{" "}
</section> </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> <section>
<ButtonGroupFooter> <ButtonGroupFooter>
<Button variant="outlined">Privacy policy</Button> <Button onClick={onShowPrivacy.onClick} variant="outlined">
<Button variant="outlined">Terms of service</Button> Privacy policy
</Button>
<Button onClick={onShowTerms.onClick} variant="outlined">
Terms of service
</Button>
</ButtonGroupFooter> </ButtonGroupFooter>
</section> </section>
</Container> </Container>
@ -362,6 +355,8 @@ export function ReadyView({
exchanges, exchanges,
selected, selected,
onClose, onClose,
onShowPrivacy,
onShowTerms,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -616,8 +611,12 @@ export function ReadyView({
</section> </section>
<section> <section>
<ButtonGroupFooter> <ButtonGroupFooter>
<Button variant="outlined">Privacy policy</Button> <Button onClick={onShowPrivacy.onClick} variant="outlined">
<Button variant="outlined">Terms of service</Button> Privacy policy
</Button>
<Button onClick={onShowTerms.onClick} variant="outlined">
Terms of service
</Button>
</ButtonGroupFooter> </ButtonGroupFooter>
</section> </section>
</Container> </Container>

View File

@ -36,7 +36,7 @@ import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js";
import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js"; import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.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 * as wxApi from "../wxApi.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js"; import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";

View File

@ -182,6 +182,7 @@ async function dispatch(
break; break;
} }
r = await w.handleCoreApiRequest(req.operation, req.id, req.payload); r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
console.log("response received from wallet", r);
break; break;
} }
} }
@ -330,7 +331,9 @@ export async function wxMain(): Promise<void> {
// script on the page // script on the page
platform.listenToAllChannels((message, sender, callback) => { platform.listenToAllChannels((message, sender, callback) => {
afterWalletIsInitialized.then(() => { afterWalletIsInitialized.then(() => {
dispatch(message, sender, callback); dispatch(message, sender, (response: CoreApiResponse) => {
callback(response);
});
}); });
}); });