refactored terms of service to remove duplicated code
prettfied some sources
This commit is contained in:
parent
ca8da4ed38
commit
da9ec5eb16
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -38,9 +38,7 @@ export const Ready = createExample(ReadyView, {
|
||||
value: 1,
|
||||
fraction: 0,
|
||||
},
|
||||
doSelectExchange: {
|
||||
|
||||
},
|
||||
doSelectExchange: {},
|
||||
exchangeUrl: "https://exchange.taler.ar",
|
||||
subject: {
|
||||
value: "some subject",
|
||||
|
@ -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,
|
||||
);
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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, {});
|
@ -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([]);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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: "",
|
||||
},
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
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);
|
||||
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
|
||||
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,);
|
||||
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 }>
|
||||
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> {
|
||||
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,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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 { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
|
||||
import { TermsOfServiceSection } from "../TermsOfServiceSection.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";
|
||||
import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
|
||||
import { TermsOfService } from "../TermsOfService/index.js";
|
||||
import { State } from "./index.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>
|
||||
|
@ -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";
|
||||
|
@ -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],
|
||||
};
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user