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";
|
import { h, VNode } from "preact";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
onToggle: () => Promise<void>;
|
onToggle?: () => Promise<void>;
|
||||||
label: VNode;
|
label: VNode;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,7 @@
|
|||||||
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
|
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
|
||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||||
import {
|
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
|
||||||
State as SelectExchangeState
|
|
||||||
} from "../../hooks/useSelectedExchange.js";
|
|
||||||
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
|
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
|
||||||
import { compose, StateViewMap } from "../../utils/index.js";
|
import { compose, StateViewMap } from "../../utils/index.js";
|
||||||
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
|
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
|
||||||
@ -34,12 +32,12 @@ export interface Props {
|
|||||||
onSuccess: (tx: string) => Promise<void>;
|
onSuccess: (tx: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = State.Loading
|
export type State =
|
||||||
|
| State.Loading
|
||||||
| State.LoadingUriError
|
| State.LoadingUriError
|
||||||
| State.Ready
|
| State.Ready
|
||||||
| SelectExchangeState.Selecting
|
| SelectExchangeState.Selecting
|
||||||
| SelectExchangeState.NoExchange
|
| SelectExchangeState.NoExchange;
|
||||||
;
|
|
||||||
|
|
||||||
export namespace State {
|
export namespace State {
|
||||||
export interface Loading {
|
export interface Loading {
|
||||||
|
@ -23,7 +23,7 @@ import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
|
|||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
import { Props, State } from "./index.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(
|
export function useComponentState(
|
||||||
{ amount: amountStr, onClose, onSuccess }: Props,
|
{ amount: amountStr, onClose, onSuccess }: Props,
|
||||||
@ -46,7 +46,7 @@ export function useComponentState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchangeList = hook.response.exchanges
|
const exchangeList = hook.response.exchanges;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const [subject, setSubject] = useState("");
|
const [subject, setSubject] = useState("");
|
||||||
@ -55,14 +55,17 @@ export function useComponentState(
|
|||||||
TalerErrorDetail | undefined
|
TalerErrorDetail | undefined
|
||||||
>(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> {
|
async function accept(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -105,9 +108,5 @@ export function useComponentState(
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
operationError,
|
operationError,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,7 @@ export const Ready = createExample(ReadyView, {
|
|||||||
value: 1,
|
value: 1,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
|
|
||||||
},
|
|
||||||
exchangeUrl: "https://exchange.taler.ar",
|
exchangeUrl: "https://exchange.taler.ar",
|
||||||
subject: {
|
subject: {
|
||||||
value: "some 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 { AmountJson } from "@gnu-taler/taler-util";
|
||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
import { HookError } from "../../hooks/useAsyncAsHook.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 { compose, StateViewMap } from "../../utils/index.js";
|
||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js";
|
|
||||||
import { useComponentState } from "./state.js";
|
import { useComponentState } from "./state.js";
|
||||||
import {
|
import {
|
||||||
AcceptedView,
|
AcceptedView,
|
||||||
|
@ -14,27 +14,20 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
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 { Loading } from "../../components/Loading.js";
|
||||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||||
import {
|
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
|
||||||
State as SelectExchangeState
|
|
||||||
} from "../../hooks/useSelectedExchange.js";
|
|
||||||
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
|
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
|
||||||
import { compose, StateViewMap } from "../../utils/index.js";
|
import { compose, StateViewMap } from "../../utils/index.js";
|
||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js";
|
|
||||||
import {
|
import {
|
||||||
useComponentStateFromParams,
|
useComponentStateFromParams,
|
||||||
useComponentStateFromURI
|
useComponentStateFromURI,
|
||||||
} from "./state.js";
|
} from "./state.js";
|
||||||
|
|
||||||
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
|
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
|
||||||
import {
|
import { LoadingInfoView, LoadingUriView, SuccessView } from "./views.js";
|
||||||
LoadingInfoView,
|
|
||||||
LoadingUriView,
|
|
||||||
SuccessView
|
|
||||||
} from "./views.js";
|
|
||||||
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
|
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
|
||||||
|
|
||||||
export interface PropsFromURI {
|
export interface PropsFromURI {
|
||||||
@ -75,7 +68,7 @@ export namespace State {
|
|||||||
status: "success";
|
status: "success";
|
||||||
error: undefined;
|
error: undefined;
|
||||||
|
|
||||||
exchangeUrl: string;
|
currentExchange: ExchangeListItem;
|
||||||
|
|
||||||
chosenAmount: AmountJson;
|
chosenAmount: AmountJson;
|
||||||
withdrawalFee: AmountJson;
|
withdrawalFee: AmountJson;
|
||||||
@ -83,13 +76,12 @@ export namespace State {
|
|||||||
|
|
||||||
doWithdrawal: ButtonHandler;
|
doWithdrawal: ButtonHandler;
|
||||||
doSelectExchange: ButtonHandler;
|
doSelectExchange: ButtonHandler;
|
||||||
tosProps?: TermsOfServiceSectionProps;
|
|
||||||
mustAcceptFirst: boolean;
|
|
||||||
|
|
||||||
ageRestriction?: SelectFieldHandler;
|
ageRestriction?: SelectFieldHandler;
|
||||||
|
|
||||||
talerWithdrawUri?: string;
|
talerWithdrawUri?: string;
|
||||||
cancel: () => Promise<void>;
|
cancel: () => Promise<void>;
|
||||||
|
onTosUpdate: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,17 +15,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* 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 { TalerError } from "@gnu-taler/taler-wallet-core";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Amount } from "../../components/Amount.js";
|
|
||||||
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
||||||
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
|
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
|
||||||
import { buildTermsOfServiceState } from "../../utils/index.js";
|
|
||||||
import * as wxApi from "../../wxApi.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(
|
export function useComponentStateFromParams(
|
||||||
{ amount, cancel, onSuccess }: PropsFromParams,
|
{ amount, cancel, onSuccess }: PropsFromParams,
|
||||||
@ -46,18 +44,38 @@ export function useComponentStateFromParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chosenAmount = uriInfoHook.response.amount;
|
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 }> {
|
async function doManualWithdraw(
|
||||||
const res = await api.acceptManualWithdrawal(exchange, Amounts.stringify(chosenAmount), ageRestricted);
|
exchange: string,
|
||||||
|
ageRestricted: number | undefined,
|
||||||
|
): Promise<{
|
||||||
|
transactionId: string;
|
||||||
|
confirmTransferUrl: string | undefined;
|
||||||
|
}> {
|
||||||
|
const res = await api.acceptManualWithdrawal(
|
||||||
|
exchange,
|
||||||
|
Amounts.stringify(chosenAmount),
|
||||||
|
ageRestricted,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
confirmTransferUrl: undefined,
|
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(
|
export function useComponentStateFromURI(
|
||||||
@ -75,7 +93,12 @@ export function useComponentStateFromURI(
|
|||||||
});
|
});
|
||||||
const exchanges = await api.listExchanges();
|
const exchanges = await api.listExchanges();
|
||||||
const { amount, defaultExchangeBaseUrl } = uriInfo;
|
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 };
|
if (!uriInfoHook) return { status: "loading", error: undefined };
|
||||||
@ -90,53 +113,75 @@ export function useComponentStateFromURI(
|
|||||||
const uri = uriInfoHook.response.talerWithdrawUri;
|
const uri = uriInfoHook.response.talerWithdrawUri;
|
||||||
const chosenAmount = uriInfoHook.response.amount;
|
const chosenAmount = uriInfoHook.response.amount;
|
||||||
const defaultExchange = uriInfoHook.response.thisExchange;
|
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 }> {
|
async function doManagedWithdraw(
|
||||||
const res = await api.acceptWithdrawal(uri, exchange, ageRestricted,);
|
exchange: string,
|
||||||
|
ageRestricted: number | undefined,
|
||||||
|
): Promise<{
|
||||||
|
transactionId: string;
|
||||||
|
confirmTransferUrl: string | undefined;
|
||||||
|
}> {
|
||||||
|
const res = await api.acceptWithdrawal(uri, exchange, ageRestricted);
|
||||||
return {
|
return {
|
||||||
confirmTransferUrl: res.confirmTransferUrl,
|
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 () => {
|
return () => {
|
||||||
|
|
||||||
const [ageRestricted, setAgeRestricted] = useState(0);
|
const [ageRestricted, setAgeRestricted] = useState(0);
|
||||||
const currentExchange = selectedExchange.selected
|
const currentExchange = selectedExchange.selected;
|
||||||
/**
|
const tosNeedToBeAccepted =
|
||||||
* For the exchange selected, bring the status of the terms of service
|
!currentExchange.tos.acceptedVersion ||
|
||||||
*/
|
currentExchange.tos.currentVersion !==
|
||||||
const terms = useAsyncAsHook(async () => {
|
currentExchange.tos.acceptedVersion;
|
||||||
const exchangeTos = await api.getExchangeTos(currentExchange.exchangeBaseUrl, [
|
|
||||||
"text/xml",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const state = buildTermsOfServiceState(exchangeTos);
|
|
||||||
|
|
||||||
return { state };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* With the exchange and amount, ask the wallet the information
|
* With the exchange and amount, ask the wallet the information
|
||||||
* about the withdrawal
|
* about the withdrawal
|
||||||
*/
|
*/
|
||||||
const amountHook = useAsyncAsHook(async () => {
|
const amountHook = useAsyncAsHook(async () => {
|
||||||
|
|
||||||
const info = await api.getExchangeWithdrawalInfo({
|
const info = await api.getExchangeWithdrawalInfo({
|
||||||
exchangeBaseUrl: currentExchange.exchangeBaseUrl,
|
exchangeBaseUrl: currentExchange.exchangeBaseUrl,
|
||||||
amount: chosenAmount,
|
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>(
|
const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
|
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
async function doWithdrawAndCheckError(): Promise<void> {
|
async function doWithdrawAndCheckError(): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setDoingWithdraw(true);
|
setDoingWithdraw(true);
|
||||||
const res = await doWithdraw(currentExchange.exchangeBaseUrl, !ageRestricted ? undefined : ageRestricted)
|
const res = await doWithdraw(
|
||||||
|
currentExchange.exchangeBaseUrl,
|
||||||
|
!ageRestricted ? undefined : ageRestricted,
|
||||||
|
);
|
||||||
if (res.confirmTransferUrl) {
|
if (res.confirmTransferUrl) {
|
||||||
document.location.href = res.confirmTransferUrl;
|
document.location.href = res.confirmTransferUrl;
|
||||||
} else {
|
} else {
|
||||||
@ -201,33 +244,6 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can
|
|||||||
).amount;
|
).amount;
|
||||||
const toBeReceived = amountHook.response.amount.effective;
|
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 =
|
const ageRestrictionOptions =
|
||||||
amountHook.response.ageRestrictionOptions?.reduce(
|
amountHook.response.ageRestrictionOptions?.reduce(
|
||||||
(p, c) => ({ ...p, [c]: `under ${c}` }),
|
(p, c) => ({ ...p, [c]: `under ${c}` }),
|
||||||
@ -242,17 +258,17 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can
|
|||||||
//TODO: calculate based on exchange info
|
//TODO: calculate based on exchange info
|
||||||
const ageRestriction = ageRestrictionEnabled
|
const ageRestriction = ageRestrictionEnabled
|
||||||
? {
|
? {
|
||||||
list: ageRestrictionOptions,
|
list: ageRestrictionOptions,
|
||||||
value: String(ageRestricted),
|
value: String(ageRestricted),
|
||||||
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
|
onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
doSelectExchange: selectedExchange.doSelect,
|
doSelectExchange: selectedExchange.doSelect,
|
||||||
exchangeUrl: currentExchange.exchangeBaseUrl,
|
currentExchange,
|
||||||
toBeReceived,
|
toBeReceived,
|
||||||
withdrawalFee,
|
withdrawalFee,
|
||||||
chosenAmount,
|
chosenAmount,
|
||||||
@ -260,22 +276,13 @@ function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, can
|
|||||||
ageRestriction,
|
ageRestriction,
|
||||||
doWithdrawal: {
|
doWithdrawal: {
|
||||||
onClick:
|
onClick:
|
||||||
doingWithdraw || (mustAcceptFirst && !reviewed)
|
doingWithdraw || tosNeedToBeAccepted
|
||||||
? undefined
|
? undefined
|
||||||
: doWithdrawAndCheckError,
|
: doWithdrawAndCheckError,
|
||||||
error: withdrawError,
|
error: withdrawError,
|
||||||
},
|
},
|
||||||
tosProps: !termsState
|
onTosUpdate,
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
onAccept,
|
|
||||||
onReview: setReviewing,
|
|
||||||
reviewed: reviewed,
|
|
||||||
reviewing: reviewing,
|
|
||||||
terms: termsState,
|
|
||||||
},
|
|
||||||
mustAcceptFirst,
|
|
||||||
cancel,
|
cancel,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,9 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ExchangeListItem } from "@gnu-taler/taler-util";
|
||||||
import { createExample } from "../../test-utils.js";
|
import { createExample } from "../../test-utils.js";
|
||||||
import { TermsState } from "../../utils/index.js";
|
// import { TermsState } from "../../utils/index.js";
|
||||||
import { SuccessView } from "./views.js";
|
import { SuccessView } from "./views.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -38,16 +39,16 @@ const nullHandler = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalTosState = {
|
// const normalTosState = {
|
||||||
terms: {
|
// terms: {
|
||||||
status: "accepted",
|
// status: "accepted",
|
||||||
version: "",
|
// version: "",
|
||||||
} as TermsState,
|
// } as TermsState,
|
||||||
onAccept: () => null,
|
// onAccept: () => null,
|
||||||
onReview: () => null,
|
// onReview: () => null,
|
||||||
reviewed: false,
|
// reviewed: false,
|
||||||
reviewing: false,
|
// reviewing: false,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const ageRestrictionOptions: Record<string, string> = "6:12:18"
|
const ageRestrictionOptions: Record<string, string> = "6:12:18"
|
||||||
.split(":")
|
.split(":")
|
||||||
@ -69,15 +70,16 @@ export const TermsOfServiceNotYetLoaded = createExample(SuccessView, {
|
|||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
},
|
},
|
||||||
doWithdrawal: nullHandler,
|
doWithdrawal: nullHandler,
|
||||||
exchangeUrl: "https://exchange.demo.taler.net",
|
currentExchange: {
|
||||||
mustAcceptFirst: false,
|
exchangeBaseUrl: "https://exchange.demo.taler.net",
|
||||||
|
tos: {},
|
||||||
|
} as Partial<ExchangeListItem> as any,
|
||||||
withdrawalFee: {
|
withdrawalFee: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
},
|
|
||||||
toBeReceived: {
|
toBeReceived: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
@ -94,8 +96,10 @@ export const WithSomeFee = createExample(SuccessView, {
|
|||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
},
|
},
|
||||||
doWithdrawal: nullHandler,
|
doWithdrawal: nullHandler,
|
||||||
exchangeUrl: "https://exchange.demo.taler.net",
|
currentExchange: {
|
||||||
mustAcceptFirst: false,
|
exchangeBaseUrl: "https://exchange.demo.taler.net",
|
||||||
|
tos: {},
|
||||||
|
} as Partial<ExchangeListItem> as any,
|
||||||
withdrawalFee: {
|
withdrawalFee: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
@ -106,9 +110,7 @@ export const WithSomeFee = createExample(SuccessView, {
|
|||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
},
|
|
||||||
tosProps: normalTosState,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WithoutFee = createExample(SuccessView, {
|
export const WithoutFee = createExample(SuccessView, {
|
||||||
@ -120,21 +122,21 @@ export const WithoutFee = createExample(SuccessView, {
|
|||||||
fraction: 0,
|
fraction: 0,
|
||||||
},
|
},
|
||||||
doWithdrawal: nullHandler,
|
doWithdrawal: nullHandler,
|
||||||
exchangeUrl: "https://exchange.demo.taler.net",
|
currentExchange: {
|
||||||
mustAcceptFirst: false,
|
exchangeBaseUrl: "https://exchange.demo.taler.net",
|
||||||
|
tos: {},
|
||||||
|
} as Partial<ExchangeListItem> as any,
|
||||||
withdrawalFee: {
|
withdrawalFee: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
},
|
|
||||||
toBeReceived: {
|
toBeReceived: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 2,
|
value: 2,
|
||||||
},
|
},
|
||||||
tosProps: normalTosState,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EditExchangeUntouched = createExample(SuccessView, {
|
export const EditExchangeUntouched = createExample(SuccessView, {
|
||||||
@ -146,21 +148,21 @@ export const EditExchangeUntouched = createExample(SuccessView, {
|
|||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
},
|
},
|
||||||
doWithdrawal: nullHandler,
|
doWithdrawal: nullHandler,
|
||||||
exchangeUrl: "https://exchange.demo.taler.net",
|
currentExchange: {
|
||||||
mustAcceptFirst: false,
|
exchangeBaseUrl: "https://exchange.demo.taler.net",
|
||||||
|
tos: {},
|
||||||
|
} as Partial<ExchangeListItem> as any,
|
||||||
withdrawalFee: {
|
withdrawalFee: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
},
|
|
||||||
toBeReceived: {
|
toBeReceived: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 2,
|
value: 2,
|
||||||
},
|
},
|
||||||
tosProps: normalTosState,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EditExchangeModified = createExample(SuccessView, {
|
export const EditExchangeModified = createExample(SuccessView, {
|
||||||
@ -172,21 +174,21 @@ export const EditExchangeModified = createExample(SuccessView, {
|
|||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
},
|
},
|
||||||
doWithdrawal: nullHandler,
|
doWithdrawal: nullHandler,
|
||||||
exchangeUrl: "https://exchange.demo.taler.net",
|
currentExchange: {
|
||||||
mustAcceptFirst: false,
|
exchangeBaseUrl: "https://exchange.demo.taler.net",
|
||||||
|
tos: {},
|
||||||
|
} as Partial<ExchangeListItem> as any,
|
||||||
withdrawalFee: {
|
withdrawalFee: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
},
|
|
||||||
toBeReceived: {
|
toBeReceived: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 2,
|
value: 2,
|
||||||
},
|
},
|
||||||
tosProps: normalTosState,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WithAgeRestriction = createExample(SuccessView, {
|
export const WithAgeRestriction = createExample(SuccessView, {
|
||||||
@ -198,11 +200,12 @@ export const WithAgeRestriction = createExample(SuccessView, {
|
|||||||
value: 2,
|
value: 2,
|
||||||
fraction: 10000000,
|
fraction: 10000000,
|
||||||
},
|
},
|
||||||
doSelectExchange: {
|
doSelectExchange: {},
|
||||||
},
|
|
||||||
doWithdrawal: nullHandler,
|
doWithdrawal: nullHandler,
|
||||||
exchangeUrl: "https://exchange.demo.taler.net",
|
currentExchange: {
|
||||||
mustAcceptFirst: false,
|
exchangeBaseUrl: "https://exchange.demo.taler.net",
|
||||||
|
tos: {},
|
||||||
|
} as Partial<ExchangeListItem> as any,
|
||||||
withdrawalFee: {
|
withdrawalFee: {
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
@ -213,5 +216,4 @@ export const WithAgeRestriction = createExample(SuccessView, {
|
|||||||
fraction: 0,
|
fraction: 0,
|
||||||
value: 2,
|
value: 2,
|
||||||
},
|
},
|
||||||
tosProps: normalTosState,
|
|
||||||
});
|
});
|
||||||
|
@ -37,7 +37,8 @@ const exchanges: ExchangeFullDetails[] = [
|
|||||||
exchangeBaseUrl: "http://exchange.demo.taler.net",
|
exchangeBaseUrl: "http://exchange.demo.taler.net",
|
||||||
paytoUris: [],
|
paytoUris: [],
|
||||||
tos: {
|
tos: {
|
||||||
acceptedVersion: "",
|
acceptedVersion: "v1",
|
||||||
|
currentVersion: "v1",
|
||||||
},
|
},
|
||||||
auditors: [
|
auditors: [
|
||||||
{
|
{
|
||||||
@ -58,7 +59,7 @@ const exchanges: ExchangeFullDetails[] = [
|
|||||||
accounts: [],
|
accounts: [],
|
||||||
feesForType: {},
|
feesForType: {},
|
||||||
},
|
},
|
||||||
},
|
} as Partial<ExchangeFullDetails> as ExchangeFullDetails,
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("Withdraw CTA states", () => {
|
describe("Withdraw CTA states", () => {
|
||||||
@ -161,17 +162,20 @@ describe("Withdraw CTA states", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
listExchanges: async () => ({ exchanges }),
|
listExchanges: async () => ({ exchanges }),
|
||||||
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
|
getWithdrawalDetailsForUri: async ({
|
||||||
amount: "ARS:2",
|
talerWithdrawUri,
|
||||||
possibleExchanges: exchanges,
|
}: any): Promise<ExchangeWithdrawDetails> =>
|
||||||
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
|
({
|
||||||
}),
|
amount: "ARS:2",
|
||||||
|
possibleExchanges: exchanges,
|
||||||
|
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
|
||||||
|
} as Partial<ExchangeWithdrawDetails> as ExchangeWithdrawDetails),
|
||||||
getExchangeWithdrawalInfo:
|
getExchangeWithdrawalInfo:
|
||||||
async (): Promise<ExchangeWithdrawDetails> =>
|
async (): Promise<ExchangeWithdrawDetails> =>
|
||||||
({
|
({
|
||||||
withdrawalAmountRaw: "ARS:2",
|
withdrawalAmountRaw: "ARS:2",
|
||||||
withdrawalAmountEffective: "ARS:2",
|
withdrawalAmountEffective: "ARS:2",
|
||||||
} as any),
|
} as any),
|
||||||
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
|
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
|
||||||
contentType: "text",
|
contentType: "text",
|
||||||
content: "just accept",
|
content: "just accept",
|
||||||
@ -205,25 +209,39 @@ describe("Withdraw CTA states", () => {
|
|||||||
expect(state.status).equals("success");
|
expect(state.status).equals("success");
|
||||||
if (state.status !== "success") return;
|
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.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
||||||
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
|
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
|
||||||
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
||||||
|
|
||||||
expect(state.doWithdrawal.onClick).not.undefined;
|
expect(state.doWithdrawal.onClick).not.undefined;
|
||||||
expect(state.mustAcceptFirst).false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertNoPendingUpdate();
|
await assertNoPendingUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be accept the tos before withdraw", async () => {
|
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 } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentStateFromURI(
|
useComponentStateFromURI(
|
||||||
@ -237,18 +255,19 @@ describe("Withdraw CTA states", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
listExchanges: async () => ({ exchanges }),
|
listExchanges: async () => listExchangesResponse,
|
||||||
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
|
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) =>
|
||||||
amount: "ARS:2",
|
({
|
||||||
possibleExchanges: exchanges,
|
amount: "ARS:2",
|
||||||
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
|
possibleExchanges: exchanges,
|
||||||
}),
|
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
|
||||||
|
} as Partial<ExchangeWithdrawDetails> as ExchangeWithdrawDetails),
|
||||||
getExchangeWithdrawalInfo:
|
getExchangeWithdrawalInfo:
|
||||||
async (): Promise<ExchangeWithdrawDetails> =>
|
async (): Promise<ExchangeWithdrawDetails> =>
|
||||||
({
|
({
|
||||||
withdrawalAmountRaw: "ARS:2",
|
withdrawalAmountRaw: "ARS:2",
|
||||||
withdrawalAmountEffective: "ARS:2",
|
withdrawalAmountEffective: "ARS:2",
|
||||||
} as any),
|
} as any),
|
||||||
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
|
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
|
||||||
contentType: "text",
|
contentType: "text",
|
||||||
content: "just accept",
|
content: "just accept",
|
||||||
@ -283,22 +302,14 @@ describe("Withdraw CTA states", () => {
|
|||||||
expect(state.status).equals("success");
|
expect(state.status).equals("success");
|
||||||
if (state.status !== "success") return;
|
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.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
||||||
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
|
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
|
||||||
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
||||||
|
|
||||||
expect(state.doWithdrawal.onClick).undefined;
|
expect(state.doWithdrawal.onClick).undefined;
|
||||||
expect(state.mustAcceptFirst).true;
|
|
||||||
|
|
||||||
// accept TOS
|
updateAcceptedVersionToCurrentVersion();
|
||||||
state.tosProps?.onAccept(true);
|
state.onTosUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitNextUpdate();
|
await waitNextUpdate();
|
||||||
@ -308,19 +319,11 @@ describe("Withdraw CTA states", () => {
|
|||||||
expect(state.status).equals("success");
|
expect(state.status).equals("success");
|
||||||
if (state.status !== "success") return;
|
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.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
||||||
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
|
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
|
||||||
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
|
||||||
|
|
||||||
expect(state.doWithdrawal.onClick).not.undefined;
|
expect(state.doWithdrawal.onClick).not.undefined;
|
||||||
expect(state.mustAcceptFirst).true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertNoPendingUpdate();
|
await assertNoPendingUpdate();
|
||||||
|
@ -15,30 +15,28 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Amount } from "../../components/Amount.js";
|
||||||
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
|
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
|
||||||
import { LoadingError } from "../../components/LoadingError.js";
|
import { LoadingError } from "../../components/LoadingError.js";
|
||||||
import { LogoHeader } from "../../components/LogoHeader.js";
|
import { LogoHeader } from "../../components/LogoHeader.js";
|
||||||
import { Part } from "../../components/Part.js";
|
import { Part } from "../../components/Part.js";
|
||||||
|
import { QR } from "../../components/QR.js";
|
||||||
import { SelectList } from "../../components/SelectList.js";
|
import { SelectList } from "../../components/SelectList.js";
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Link,
|
Link,
|
||||||
LinkSuccess,
|
LinkSuccess,
|
||||||
SubTitle,
|
SubTitle,
|
||||||
SuccessBox,
|
|
||||||
SvgIcon,
|
SvgIcon,
|
||||||
WalletAction,
|
WalletAction,
|
||||||
} from "../../components/styled/index.js";
|
} from "../../components/styled/index.js";
|
||||||
import { useTranslationContext } from "../../context/translation.js";
|
import { useTranslationContext } from "../../context/translation.js";
|
||||||
import { Button } from "../../mui/Button.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 editIcon from "../../svg/edit_24px.svg";
|
||||||
import { Amount } from "../../components/Amount.js";
|
import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
|
||||||
import { QR } from "../../components/QR.js";
|
import { TermsOfService } from "../TermsOfService/index.js";
|
||||||
import { useState } from "preact/hooks";
|
import { State } from "./index.js";
|
||||||
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
||||||
|
|
||||||
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -66,6 +64,9 @@ export function LoadingInfoView({ error }: State.LoadingInfoError): VNode {
|
|||||||
|
|
||||||
export function SuccessView(state: State.Success): VNode {
|
export function SuccessView(state: State.Success): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const currentTosVersionIsAccepted =
|
||||||
|
state.currentExchange.tos.acceptedVersion ===
|
||||||
|
state.currentExchange.tos.currentVersion;
|
||||||
return (
|
return (
|
||||||
<WalletAction>
|
<WalletAction>
|
||||||
<LogoHeader />
|
<LogoHeader />
|
||||||
@ -103,7 +104,9 @@ export function SuccessView(state: State.Success): VNode {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
text={<ExchangeDetails exchange={state.exchangeUrl} />}
|
text={
|
||||||
|
<ExchangeDetails exchange={state.currentExchange.exchangeBaseUrl} />
|
||||||
|
}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
big
|
big
|
||||||
/>
|
/>
|
||||||
@ -130,43 +133,29 @@ export function SuccessView(state: State.Success): VNode {
|
|||||||
</Input>
|
</Input>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{state.tosProps && <TermsOfServiceSection {...state.tosProps} />}
|
|
||||||
{state.tosProps ? (
|
<section>
|
||||||
<Fragment>
|
{currentTosVersionIsAccepted ? (
|
||||||
<section>
|
<Button
|
||||||
{(state.tosProps.terms.status === "accepted" ||
|
variant="contained"
|
||||||
(state.mustAcceptFirst && state.tosProps.reviewed)) && (
|
color="success"
|
||||||
<Button
|
disabled={!state.doWithdrawal.onClick}
|
||||||
variant="contained"
|
onClick={state.doWithdrawal.onClick}
|
||||||
color="success"
|
>
|
||||||
disabled={!state.doWithdrawal.onClick}
|
<i18n.Translate>
|
||||||
onClick={state.doWithdrawal.onClick}
|
Withdraw <Amount value={state.toBeReceived} />
|
||||||
>
|
</i18n.Translate>
|
||||||
<i18n.Translate>
|
</Button>
|
||||||
Withdraw <Amount value={state.toBeReceived} />
|
) : (
|
||||||
</i18n.Translate>
|
<TermsOfService
|
||||||
</Button>
|
exchangeUrl={state.currentExchange.exchangeBaseUrl}
|
||||||
)}
|
onChange={state.onTosUpdate}
|
||||||
{state.tosProps.terms.status === "notfound" && (
|
/>
|
||||||
<Button
|
)}
|
||||||
variant="contained"
|
</section>
|
||||||
color="warning"
|
{state.talerWithdrawUri ? (
|
||||||
disabled={!state.doWithdrawal.onClick}
|
<WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} />
|
||||||
onClick={state.doWithdrawal.onClick}
|
) : undefined}
|
||||||
>
|
|
||||||
<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>
|
<section>
|
||||||
<Link upperCased onClick={state.cancel}>
|
<Link upperCased onClick={state.cancel}>
|
||||||
<i18n.Translate>Cancel</i18n.Translate>
|
<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 a4 from "./Refund/stories.jsx";
|
||||||
import * as a5 from "./Tip/stories.jsx";
|
import * as a5 from "./Tip/stories.jsx";
|
||||||
import * as a6 from "./Withdraw/stories.jsx";
|
import * as a6 from "./Withdraw/stories.jsx";
|
||||||
import * as a7 from "./TermsOfServiceSection.stories.js";
|
import * as a7 from "./TermsOfService/stories.js";
|
||||||
import * as a8 from "./InvoiceCreate/stories.js";
|
import * as a8 from "./InvoiceCreate/stories.js";
|
||||||
import * as a9 from "./InvoicePay/stories.js";
|
import * as a9 from "./InvoicePay/stories.js";
|
||||||
import * as a10 from "./TransferCreate/stories.js";
|
import * as a10 from "./TransferCreate/stories.js";
|
||||||
|
@ -22,21 +22,21 @@ type State = State.Ready | State.NoExchange | State.Selecting;
|
|||||||
|
|
||||||
export namespace State {
|
export namespace State {
|
||||||
export interface NoExchange {
|
export interface NoExchange {
|
||||||
status: "no-exchange"
|
status: "no-exchange";
|
||||||
error: undefined;
|
error: undefined;
|
||||||
currency: string | undefined;
|
currency: string | undefined;
|
||||||
}
|
}
|
||||||
export interface Ready {
|
export interface Ready {
|
||||||
status: "ready",
|
status: "ready";
|
||||||
doSelect: ButtonHandler,
|
doSelect: ButtonHandler;
|
||||||
selected: ExchangeListItem;
|
selected: ExchangeListItem;
|
||||||
}
|
}
|
||||||
export interface Selecting {
|
export interface Selecting {
|
||||||
status: "selecting-exchange",
|
status: "selecting-exchange";
|
||||||
error: undefined,
|
error: undefined;
|
||||||
onSelection: (url: string) => Promise<void>;
|
onSelection: (url: string) => Promise<void>;
|
||||||
onCancel: () => Promise<void>;
|
onCancel: () => Promise<void>;
|
||||||
list: ExchangeListItem[],
|
list: ExchangeListItem[];
|
||||||
currency: string;
|
currency: string;
|
||||||
currentExchange: string;
|
currentExchange: string;
|
||||||
}
|
}
|
||||||
@ -45,38 +45,42 @@ export namespace State {
|
|||||||
interface Props {
|
interface Props {
|
||||||
currency: string;
|
currency: string;
|
||||||
//there is a preference for the default at the initial state
|
//there is a preference for the default at the initial state
|
||||||
defaultExchange?: string,
|
defaultExchange?: string;
|
||||||
//list of exchanges
|
//list of exchanges
|
||||||
list: ExchangeListItem[],
|
list: ExchangeListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSelectedExchange({
|
||||||
|
currency,
|
||||||
export function useSelectedExchange({ currency, defaultExchange, list }: Props): State {
|
defaultExchange,
|
||||||
|
list,
|
||||||
|
}: Props): State {
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
const [selectedExchange, setSelectedExchange] = useState<string | undefined>(undefined);
|
const [selectedExchange, setSelectedExchange] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
return {
|
return {
|
||||||
status: "no-exchange",
|
status: "no-exchange",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
currency: undefined,
|
currency: undefined,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const listCurrency = list.filter((e) => e.currency === currency)
|
const listCurrency = list.filter((e) => e.currency === currency);
|
||||||
if (!listCurrency.length) {
|
if (!listCurrency.length) {
|
||||||
// there should be at least one exchange for this currency
|
// there should be at least one exchange for this currency
|
||||||
return {
|
return {
|
||||||
status: "no-exchange",
|
status: "no-exchange",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
currency,
|
currency,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isSelecting) {
|
if (isSelecting) {
|
||||||
const currentExchange = selectedExchange ?? defaultExchange ?? listCurrency[0].exchangeBaseUrl;
|
const currentExchange =
|
||||||
|
selectedExchange ?? defaultExchange ?? listCurrency[0].exchangeBaseUrl;
|
||||||
return {
|
return {
|
||||||
status: "selecting-exchange",
|
status: "selecting-exchange",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
@ -85,44 +89,46 @@ export function useSelectedExchange({ currency, defaultExchange, list }: Props):
|
|||||||
currentExchange: currentExchange,
|
currentExchange: currentExchange,
|
||||||
onSelection: async (exchangeBaseUrl: string) => {
|
onSelection: async (exchangeBaseUrl: string) => {
|
||||||
setIsSelecting(false);
|
setIsSelecting(false);
|
||||||
setSelectedExchange(exchangeBaseUrl)
|
setSelectedExchange(exchangeBaseUrl);
|
||||||
},
|
},
|
||||||
onCancel: async () => {
|
onCancel: async () => {
|
||||||
setIsSelecting(false);
|
setIsSelecting(false);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const found = !selectedExchange ? undefined : list.find(
|
const found = !selectedExchange
|
||||||
(e) => e.exchangeBaseUrl === selectedExchange,
|
? undefined
|
||||||
)
|
: list.find((e) => e.exchangeBaseUrl === selectedExchange);
|
||||||
if (found) return {
|
if (found)
|
||||||
status: "ready",
|
return {
|
||||||
doSelect: {
|
status: "ready",
|
||||||
onClick: async () => setIsSelecting(true)
|
doSelect: {
|
||||||
},
|
onClick: async () => setIsSelecting(true),
|
||||||
selected: found
|
},
|
||||||
};
|
selected: found,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const found = !defaultExchange ? undefined : list.find(
|
const found = !defaultExchange
|
||||||
(e) => e.exchangeBaseUrl === defaultExchange,
|
? undefined
|
||||||
)
|
: list.find((e) => e.exchangeBaseUrl === defaultExchange);
|
||||||
if (found) return {
|
if (found)
|
||||||
status: "ready",
|
return {
|
||||||
doSelect: {
|
status: "ready",
|
||||||
onClick: async () => setIsSelecting(true)
|
doSelect: {
|
||||||
},
|
onClick: async () => setIsSelecting(true),
|
||||||
selected: found
|
},
|
||||||
};
|
selected: found,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
doSelect: {
|
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);
|
document.body.removeChild(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type RecursiveState<S> = S | (() => RecursiveState<S>)
|
type RecursiveState<S> = S | (() => RecursiveState<S>);
|
||||||
|
|
||||||
interface Mounted<T> {
|
interface Mounted<T> {
|
||||||
unmount: () => void;
|
unmount: () => void;
|
||||||
@ -107,12 +107,12 @@ export function mountHook<T extends object>(
|
|||||||
// component that's going to hold the hook
|
// component that's going to hold the hook
|
||||||
function Component(): VNode {
|
function Component(): VNode {
|
||||||
try {
|
try {
|
||||||
let componentOrResult = callback()
|
let componentOrResult = callback();
|
||||||
while (typeof componentOrResult === "function") {
|
while (typeof componentOrResult === "function") {
|
||||||
componentOrResult = componentOrResult();
|
componentOrResult = componentOrResult();
|
||||||
}
|
}
|
||||||
//typecheck fails here
|
//typecheck fails here
|
||||||
const l: Exclude<T, () => void> = componentOrResult as any
|
const l: Exclude<T, () => void> = componentOrResult as any;
|
||||||
lastResult = l;
|
lastResult = l;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
|
@ -14,12 +14,7 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { createElement, VNode } from "preact";
|
||||||
AmountJson,
|
|
||||||
Amounts,
|
|
||||||
GetExchangeTosResult,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { VNode, createElement } from "preact";
|
|
||||||
|
|
||||||
function getJsonIfOk(r: Response): Promise<any> {
|
function getJsonIfOk(r: Response): Promise<any> {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
@ -31,7 +26,8 @@ function getJsonIfOk(r: Response): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
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);
|
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 StateFunc<S> = (p: S) => VNode;
|
||||||
|
|
||||||
export type StateViewMap<StateType extends { status: string }> = {
|
export type StateViewMap<StateType extends { status: string }> = {
|
||||||
[S in StateType as S["status"]]: StateFunc<S>;
|
[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>(
|
export function compose<SType extends { status: string }, PType>(
|
||||||
name: string,
|
name: string,
|
||||||
hook: (p: PType) => RecursiveState<SType>,
|
hook: (p: PType) => RecursiveState<SType>,
|
||||||
viewMap: StateViewMap<SType>,
|
viewMap: StateViewMap<SType>,
|
||||||
): (p: PType) => VNode {
|
): (p: PType) => VNode {
|
||||||
|
|
||||||
function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
|
function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
|
||||||
|
|
||||||
function TheComponent(): VNode {
|
function TheComponent(): VNode {
|
||||||
const state = stateHook();
|
const state = stateHook();
|
||||||
|
|
||||||
if (typeof state === "function") {
|
if (typeof state === "function") {
|
||||||
const subComponent = withHook(state)
|
const subComponent = withHook(state);
|
||||||
return createElement(subComponent, {});
|
return createElement(subComponent, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +106,7 @@ export function compose<SType extends { status: string }, PType>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (p: PType) => {
|
return (p: PType) => {
|
||||||
const h = withHook(() => hook(p))
|
const h = withHook(() => hook(p));
|
||||||
return h()
|
return h();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,11 @@ import { compose, StateViewMap } from "../../utils/index.js";
|
|||||||
import { LoadingUriView, ReadyView } from "./views.js";
|
import { LoadingUriView, ReadyView } from "./views.js";
|
||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
import { useComponentState } from "./state.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 {
|
export interface Props {
|
||||||
currency: string;
|
currency: string;
|
||||||
|
@ -20,16 +20,18 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
|||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
import { Props, State } from "./index.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 hook = useAsyncAsHook(async () => {
|
||||||
const { accounts } = await api.listKnownBankAccounts(currency);
|
const { accounts } = await api.listKnownBankAccounts(currency);
|
||||||
return { accounts };
|
return { accounts };
|
||||||
});
|
});
|
||||||
|
|
||||||
const [payto, setPayto] = useState("")
|
const [payto, setPayto] = useState("");
|
||||||
const [alias, setAlias] = useState("")
|
const [alias, setAlias] = useState("");
|
||||||
const [type, setType] = useState("")
|
const [type, setType] = useState("");
|
||||||
|
|
||||||
|
|
||||||
if (!hook) {
|
if (!hook) {
|
||||||
return {
|
return {
|
||||||
@ -41,31 +43,38 @@ export function useComponentState({ currency, onAccountAdded, onCancel }: Props,
|
|||||||
return {
|
return {
|
||||||
status: "loading-error",
|
status: "loading-error",
|
||||||
error: hook,
|
error: hook,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountType: Record<string, string> = {
|
const accountType: Record<string, string> = {
|
||||||
"": "Choose one account",
|
"": "Choose one account",
|
||||||
"iban": "IBAN",
|
iban: "IBAN",
|
||||||
"bitcoin": "Bitcoin",
|
bitcoin: "Bitcoin",
|
||||||
"x-taler-bank": "Taler Bank"
|
"x-taler-bank": "Taler Bank",
|
||||||
}
|
};
|
||||||
const uri = parsePaytoUri(payto)
|
const uri = parsePaytoUri(payto);
|
||||||
const found = hook.response.accounts.findIndex(a => stringifyPaytoUri(a.uri) === payto) !== -1
|
const found =
|
||||||
|
hook.response.accounts.findIndex(
|
||||||
|
(a) => stringifyPaytoUri(a.uri) === payto,
|
||||||
|
) !== -1;
|
||||||
|
|
||||||
async function addAccount(): Promise<void> {
|
async function addAccount(): Promise<void> {
|
||||||
if (!uri || found) return;
|
if (!uri || found) return;
|
||||||
|
|
||||||
await api.addKnownBankAccounts(uri, currency, alias)
|
await api.addKnownBankAccounts(uri, currency, alias);
|
||||||
onAccountAdded(payto)
|
onAccountAdded(payto);
|
||||||
}
|
}
|
||||||
|
|
||||||
const paytoUriError = payto === "" ? undefined
|
const paytoUriError =
|
||||||
: !uri ? "the uri is not ok"
|
payto === ""
|
||||||
: found ? "that account is already present"
|
? undefined
|
||||||
: 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 {
|
return {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
@ -75,27 +84,27 @@ export function useComponentState({ currency, onAccountAdded, onCancel }: Props,
|
|||||||
list: accountType,
|
list: accountType,
|
||||||
value: type,
|
value: type,
|
||||||
onChange: async (v) => {
|
onChange: async (v) => {
|
||||||
setType(v)
|
setType(v);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
value: alias,
|
value: alias,
|
||||||
onInput: async (v) => {
|
onInput: async (v) => {
|
||||||
setAlias(v)
|
setAlias(v);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
uri: {
|
uri: {
|
||||||
value: payto,
|
value: payto,
|
||||||
error: paytoUriError,
|
error: paytoUriError,
|
||||||
onInput: async (v) => {
|
onInput: async (v) => {
|
||||||
setPayto(v)
|
setPayto(v);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
onAccountAdded: {
|
onAccountAdded: {
|
||||||
onClick: unableToAdd ? undefined : addAccount
|
onClick: unableToAdd ? undefined : addAccount,
|
||||||
},
|
},
|
||||||
onCancel: {
|
onCancel: {
|
||||||
onClick: async () => onCancel()
|
onClick: async () => onCancel(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,22 @@
|
|||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||||
import { compose, StateViewMap } from "../../utils/index.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 * as wxApi from "../../wxApi.js";
|
||||||
import { useComponentState } from "./state.js";
|
import { useComponentState } from "./state.js";
|
||||||
import { AmountJson, PaytoUri } from "@gnu-taler/taler-util";
|
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";
|
import { AddAccountPage } from "../AddAccount/index.js";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -31,7 +42,8 @@ export interface Props {
|
|||||||
onSuccess: (currency: string) => void;
|
onSuccess: (currency: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = State.Loading
|
export type State =
|
||||||
|
| State.Loading
|
||||||
| State.LoadingUriError
|
| State.LoadingUriError
|
||||||
| State.AmountOrCurrencyError
|
| State.AmountOrCurrencyError
|
||||||
| State.NoEnoughBalance
|
| State.NoEnoughBalance
|
||||||
|
@ -14,13 +14,24 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
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 { useState } from "preact/hooks";
|
||||||
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
import { Props, State } from "./index.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 parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr);
|
||||||
const currency = parsed !== undefined ? parsed.currency : currencyStr;
|
const currency = parsed !== undefined ? parsed.currency : currencyStr;
|
||||||
|
|
||||||
@ -46,8 +57,8 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on
|
|||||||
if (!currency) {
|
if (!currency) {
|
||||||
return {
|
return {
|
||||||
status: "amount-or-currency-error",
|
status: "amount-or-currency-error",
|
||||||
error: undefined
|
error: undefined,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hook) {
|
if (!hook) {
|
||||||
@ -60,7 +71,7 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on
|
|||||||
return {
|
return {
|
||||||
status: "loading-error",
|
status: "loading-error",
|
||||||
error: hook,
|
error: hook,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
const { accounts, balances } = hook.response;
|
const { accounts, balances } = hook.response;
|
||||||
|
|
||||||
@ -74,13 +85,12 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on
|
|||||||
onAccountAdded: (p: string) => {
|
onAccountAdded: (p: string) => {
|
||||||
updateAccountFromList(p);
|
updateAccountFromList(p);
|
||||||
setAddingAccount(false);
|
setAddingAccount(false);
|
||||||
hook.retry()
|
hook.retry();
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
setAddingAccount(false);
|
setAddingAccount(false);
|
||||||
}
|
},
|
||||||
,
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bs = balances.filter((b) => b.available.startsWith(currency));
|
const bs = balances.filter((b) => b.available.startsWith(currency));
|
||||||
@ -103,13 +113,15 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
currency,
|
currency,
|
||||||
onAddAccount: {
|
onAddAccount: {
|
||||||
onClick: async () => { setAddingAccount(true) }
|
onClick: async () => {
|
||||||
|
setAddingAccount(true);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountMap = createLabelsForBankAccount(accounts);
|
const accountMap = createLabelsForBankAccount(accounts);
|
||||||
accountMap[""] = "Select one account..."
|
accountMap[""] = "Select one account...";
|
||||||
|
|
||||||
async function updateAccountFromList(accountStr: string): Promise<void> {
|
async function updateAccountFromList(accountStr: string): Promise<void> {
|
||||||
// const newSelected = !accountMap[accountStr] ? undefined : accountMap[accountStr];
|
// 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.sum([fee.wire, fee.coin, fee.refresh]).amount
|
||||||
: Amounts.getZero(currency);
|
: Amounts.getZero(currency);
|
||||||
|
|
||||||
const totalToDeposit = parsedAmount && fee !== undefined
|
const totalToDeposit =
|
||||||
? Amounts.sub(parsedAmount, totalFee).amount
|
parsedAmount && fee !== undefined
|
||||||
: Amounts.getZero(currency);
|
? Amounts.sub(parsedAmount, totalFee).amount
|
||||||
|
: Amounts.getZero(currency);
|
||||||
|
|
||||||
const isDirty = amount !== initialValue;
|
const isDirty = amount !== initialValue;
|
||||||
const amountError = !isDirty
|
const amountError = !isDirty
|
||||||
? undefined
|
? undefined
|
||||||
: !parsedAmount
|
: !parsedAmount
|
||||||
? "Invalid amount"
|
? "Invalid amount"
|
||||||
: Amounts.cmp(balance, parsedAmount) === -1
|
: Amounts.cmp(balance, parsedAmount) === -1
|
||||||
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
|
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const unableToDeposit =
|
const unableToDeposit =
|
||||||
!parsedAmount ||
|
!parsedAmount ||
|
||||||
@ -181,10 +194,11 @@ export function useComponentState({ amount: amountStr, currency: currencyStr, on
|
|||||||
value: String(amount),
|
value: String(amount),
|
||||||
onInput: updateAmount,
|
onInput: updateAmount,
|
||||||
error: amountError,
|
error: amountError,
|
||||||
|
|
||||||
},
|
},
|
||||||
onAddAccount: {
|
onAddAccount: {
|
||||||
onClick: async () => { setAddingAccount(true) }
|
onClick: async () => {
|
||||||
|
setAddingAccount(true);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
account: {
|
account: {
|
||||||
list: accountMap,
|
list: accountMap,
|
||||||
@ -219,22 +233,26 @@ async function getFeeForAmount(
|
|||||||
|
|
||||||
export function labelForAccountType(id: string) {
|
export function labelForAccountType(id: string) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case "": return "Choose one";
|
case "":
|
||||||
case "x-taler-bank": return "Taler Bank";
|
return "Choose one";
|
||||||
case "bitcoin": return "Bitcoin";
|
case "x-taler-bank":
|
||||||
case "iban": return "IBAN";
|
return "Taler Bank";
|
||||||
default: return id;
|
case "bitcoin":
|
||||||
|
return "Bitcoin";
|
||||||
|
case "iban":
|
||||||
|
return "IBAN";
|
||||||
|
default:
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLabelsForBankAccount(
|
export function createLabelsForBankAccount(
|
||||||
knownBankAccounts: Array<KnownBankAccountsInfo>,
|
knownBankAccounts: Array<KnownBankAccountsInfo>,
|
||||||
): { [value: string]: string } {
|
): { [value: string]: string } {
|
||||||
const initialList: Record<string, string> = {
|
const initialList: Record<string, string> = {};
|
||||||
}
|
|
||||||
if (!knownBankAccounts.length) return initialList;
|
if (!knownBankAccounts.length) return initialList;
|
||||||
return knownBankAccounts.reduce((prev, cur, i) => {
|
return knownBankAccounts.reduce((prev, cur, i) => {
|
||||||
prev[stringifyPaytoUri(cur.uri)] = cur.alias
|
prev[stringifyPaytoUri(cur.uri)] = cur.alias;
|
||||||
return prev;
|
return prev;
|
||||||
}, initialList);
|
}, initialList);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,14 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @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 { expect } from "chai";
|
||||||
import { mountHook } from "../../test-utils.js";
|
import { mountHook } from "../../test-utils.js";
|
||||||
|
|
||||||
@ -52,17 +59,19 @@ const nullFunction: any = () => null;
|
|||||||
type VoidFunction = () => void;
|
type VoidFunction = () => void;
|
||||||
|
|
||||||
describe("DepositPage states", () => {
|
describe("DepositPage states", () => {
|
||||||
|
|
||||||
it("should have status 'no-enough-balance' when balance is empty", async () => {
|
it("should have status 'no-enough-balance' when balance is empty", async () => {
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, {
|
useComponentState(
|
||||||
getBalance: async () =>
|
{ currency, onCancel: nullFunction, onSuccess: nullFunction },
|
||||||
({
|
{
|
||||||
balances: [{ available: `${currency}:0` }],
|
getBalance: async () =>
|
||||||
} as Partial<BalancesResponse>),
|
({
|
||||||
listKnownBankAccounts: async () => ({ accounts: {} }),
|
balances: [{ available: `${currency}:0` }],
|
||||||
} as Partial<typeof wxApi> as any),
|
} as Partial<BalancesResponse>),
|
||||||
|
listKnownBankAccounts: async () => ({ accounts: {} }),
|
||||||
|
} as Partial<typeof wxApi> as any,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -111,25 +120,28 @@ describe("DepositPage states", () => {
|
|||||||
uri: parsePaytoUri("payto://iban/ES8877998399652238")!,
|
uri: parsePaytoUri("payto://iban/ES8877998399652238")!,
|
||||||
kyc_completed: false,
|
kyc_completed: false,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
alias: "my iban account"
|
alias: "my iban account",
|
||||||
};
|
};
|
||||||
const talerBankPayto = {
|
const talerBankPayto = {
|
||||||
uri: parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!,
|
uri: parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!,
|
||||||
kyc_completed: false,
|
kyc_completed: false,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
alias: "my taler account"
|
alias: "my taler account",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should have status 'ready' but unable to deposit ", async () => {
|
it("should have status 'ready' but unable to deposit ", async () => {
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, {
|
useComponentState(
|
||||||
getBalance: async () =>
|
{ currency, onCancel: nullFunction, onSuccess: nullFunction },
|
||||||
({
|
{
|
||||||
balances: [{ available: `${currency}:1` }],
|
getBalance: async () =>
|
||||||
} as Partial<BalancesResponse>),
|
({
|
||||||
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
|
balances: [{ available: `${currency}:1` }],
|
||||||
} as Partial<typeof wxApi> as any),
|
} 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 () => {
|
it.skip("should not be able to deposit more than the balance ", async () => {
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, {
|
useComponentState(
|
||||||
getBalance: async () =>
|
{ currency, onCancel: nullFunction, onSuccess: nullFunction },
|
||||||
({
|
{
|
||||||
balances: [{ available: `${currency}:1` }],
|
getBalance: async () =>
|
||||||
} as Partial<BalancesResponse>),
|
({
|
||||||
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
|
balances: [{ available: `${currency}:1` }],
|
||||||
getFeeForDeposit: withoutFee,
|
} as Partial<BalancesResponse>),
|
||||||
} as Partial<typeof wxApi> as any),
|
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 () => {
|
it.skip("should calculate the fee upon entering amount ", async () => {
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, {
|
useComponentState(
|
||||||
getBalance: async () =>
|
{ currency, onCancel: nullFunction, onSuccess: nullFunction },
|
||||||
({
|
{
|
||||||
balances: [{ available: `${currency}:1` }],
|
getBalance: async () =>
|
||||||
} as Partial<BalancesResponse>),
|
({
|
||||||
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
|
balances: [{ available: `${currency}:1` }],
|
||||||
getFeeForDeposit: withSomeFee,
|
} as Partial<BalancesResponse>),
|
||||||
} as Partial<typeof wxApi> as any),
|
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 () => {
|
it("should calculate the fee upon selecting account ", async () => {
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, {
|
useComponentState(
|
||||||
getBalance: async () =>
|
{ currency, onCancel: nullFunction, onSuccess: nullFunction },
|
||||||
({
|
{
|
||||||
balances: [{ available: `${currency}:1` }],
|
getBalance: async () =>
|
||||||
} as Partial<BalancesResponse>),
|
({
|
||||||
listKnownBankAccounts: async () => ({
|
balances: [{ available: `${currency}:1` }],
|
||||||
accounts: [ibanPayto, talerBankPayto],
|
} as Partial<BalancesResponse>),
|
||||||
}),
|
listKnownBankAccounts: async () => ({
|
||||||
getFeeForDeposit: freeJustForIBAN,
|
accounts: [ibanPayto, talerBankPayto],
|
||||||
} as Partial<typeof wxApi> as any),
|
}),
|
||||||
|
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.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
|
||||||
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
|
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
|
||||||
expect(r.depositHandler.onClick).undefined;
|
expect(r.depositHandler.onClick).undefined;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitNextUpdate("");
|
await waitNextUpdate("");
|
||||||
@ -358,7 +378,6 @@ describe("DepositPage states", () => {
|
|||||||
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
|
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
|
||||||
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
|
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
|
||||||
expect(r.depositHandler.onClick).undefined;
|
expect(r.depositHandler.onClick).undefined;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitNextUpdate("");
|
await waitNextUpdate("");
|
||||||
@ -374,7 +393,6 @@ describe("DepositPage states", () => {
|
|||||||
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
|
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
|
||||||
expect(r.depositHandler.onClick).undefined;
|
expect(r.depositHandler.onClick).undefined;
|
||||||
|
|
||||||
|
|
||||||
if (r.account.onChange === undefined) expect.fail();
|
if (r.account.onChange === undefined) expect.fail();
|
||||||
r.account.onChange(stringifyPaytoUri(talerBankPayto.uri));
|
r.account.onChange(stringifyPaytoUri(talerBankPayto.uri));
|
||||||
}
|
}
|
||||||
@ -391,7 +409,6 @@ describe("DepositPage states", () => {
|
|||||||
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
|
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
|
||||||
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
|
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
|
||||||
expect(r.depositHandler.onClick).undefined;
|
expect(r.depositHandler.onClick).undefined;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitNextUpdate("");
|
await waitNextUpdate("");
|
||||||
@ -414,14 +431,17 @@ describe("DepositPage states", () => {
|
|||||||
it.skip("should be able to deposit if has the enough balance ", async () => {
|
it.skip("should be able to deposit if has the enough balance ", async () => {
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
|
||||||
mountHook(() =>
|
mountHook(() =>
|
||||||
useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, {
|
useComponentState(
|
||||||
getBalance: async () =>
|
{ currency, onCancel: nullFunction, onSuccess: nullFunction },
|
||||||
({
|
{
|
||||||
balances: [{ available: `${currency}:15` }],
|
getBalance: async () =>
|
||||||
} as Partial<BalancesResponse>),
|
({
|
||||||
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
|
balances: [{ available: `${currency}:15` }],
|
||||||
getFeeForDeposit: withSomeFee,
|
} as Partial<BalancesResponse>),
|
||||||
} as Partial<typeof wxApi> as any),
|
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.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
|
||||||
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`));
|
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`));
|
||||||
expect(r.depositHandler.onClick).undefined;
|
expect(r.depositHandler.onClick).undefined;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitNextUpdate();
|
await waitNextUpdate();
|
||||||
|
@ -19,20 +19,8 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { termsXml } from "../cta/termsExample.js";
|
|
||||||
import { createExample } from "../test-utils.js";
|
import { createExample } from "../test-utils.js";
|
||||||
import { View as TestedComponent } from "./ExchangeAddConfirm.js";
|
import { ExchangeAddConfirmPage 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "wallet/exchange add/confirm",
|
title: "wallet/exchange add/confirm",
|
||||||
@ -46,33 +34,12 @@ export default {
|
|||||||
|
|
||||||
export const TermsNotFound = createExample(TestedComponent, {
|
export const TermsNotFound = createExample(TestedComponent, {
|
||||||
url: "https://exchange.demo.taler.net/",
|
url: "https://exchange.demo.taler.net/",
|
||||||
terms: {
|
|
||||||
status: "notfound",
|
|
||||||
version: "1",
|
|
||||||
content: undefined,
|
|
||||||
},
|
|
||||||
onAccept: async () => undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NewTerms = createExample(TestedComponent, {
|
export const NewTerms = createExample(TestedComponent, {
|
||||||
url: "https://exchange.demo.taler.net/",
|
url: "https://exchange.demo.taler.net/",
|
||||||
terms: {
|
|
||||||
status: "new",
|
|
||||||
version: "1",
|
|
||||||
content: undefined,
|
|
||||||
},
|
|
||||||
onAccept: async () => undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TermsChanged = createExample(TestedComponent, {
|
export const TermsChanged = createExample(TestedComponent, {
|
||||||
url: "https://exchange.demo.taler.net/",
|
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 { useState } from "preact/hooks";
|
||||||
import { Title } from "../components/styled/index.js";
|
import { Title } from "../components/styled/index.js";
|
||||||
import { useTranslationContext } from "../context/translation.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 { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||||
import { Button } from "../mui/Button.js";
|
import { Button } from "../mui/Button.js";
|
||||||
import { buildTermsOfServiceState, TermsState } from "../utils/index.js";
|
|
||||||
import * as wxApi from "../wxApi.js";
|
import * as wxApi from "../wxApi.js";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -34,69 +33,9 @@ export function ExchangeAddConfirmPage({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: Props): VNode {
|
}: 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 { i18n } = useTranslationContext();
|
||||||
const needsReview =
|
|
||||||
!terms || terms.status === "changed" || terms.status === "new";
|
const [accepted, setAccepted] = useState(false);
|
||||||
const [reviewed, setReviewed] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -111,52 +50,27 @@ export function View({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{terms && (
|
|
||||||
<TermsOfServiceSection
|
<TermsOfService key="terms" exchangeUrl={url} onChange={setAccepted} />
|
||||||
reviewed={reviewed}
|
|
||||||
reviewing={true}
|
|
||||||
terms={terms}
|
|
||||||
onAccept={(value) =>
|
|
||||||
doAccept(value).then(() => {
|
|
||||||
setReviewed(value);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<Button variant="contained" color="secondary" onClick={onCancel}>
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
<i18n.Translate>Cancel</i18n.Translate>
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
</Button>
|
</Button>
|
||||||
{!terms && (
|
<Button
|
||||||
<Button variant="contained" disabled>
|
key="add"
|
||||||
<i18n.Translate>Loading terms..</i18n.Translate>
|
variant="contained"
|
||||||
</Button>
|
color="success"
|
||||||
)}
|
disabled={!accepted}
|
||||||
{terms && (
|
onClick={onConfirm}
|
||||||
<Fragment>
|
>
|
||||||
{needsReview && !reviewed && (
|
<i18n.Translate>Add exchange</i18n.Translate>
|
||||||
<Button
|
</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>
|
|
||||||
)}
|
|
||||||
</footer>
|
</footer>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@ -17,13 +17,12 @@
|
|||||||
import {
|
import {
|
||||||
DenomOperationMap,
|
DenomOperationMap,
|
||||||
ExchangeFullDetails,
|
ExchangeFullDetails,
|
||||||
ExchangeListItem, FeeDescriptionPair
|
ExchangeListItem,
|
||||||
|
FeeDescriptionPair,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||||
import {
|
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
|
||||||
State as SelectExchangeState
|
|
||||||
} from "../../hooks/useSelectedExchange.js";
|
|
||||||
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
|
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
|
||||||
import { compose, StateViewMap } from "../../utils/index.js";
|
import { compose, StateViewMap } from "../../utils/index.js";
|
||||||
import * as wxApi from "../../wxApi.js";
|
import * as wxApi from "../../wxApi.js";
|
||||||
@ -32,12 +31,12 @@ import {
|
|||||||
ComparingView,
|
ComparingView,
|
||||||
ErrorLoadingView,
|
ErrorLoadingView,
|
||||||
NoExchangesView,
|
NoExchangesView,
|
||||||
ReadyView
|
ReadyView,
|
||||||
} from "./views.js";
|
} from "./views.js";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
list: ExchangeListItem[],
|
list: ExchangeListItem[];
|
||||||
currentExchange: string,
|
currentExchange: string;
|
||||||
onCancel: () => Promise<void>;
|
onCancel: () => Promise<void>;
|
||||||
onSelection: (exchange: string) => Promise<void>;
|
onSelection: (exchange: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,13 @@ export function useComponentState(
|
|||||||
{ onCancel, onSelection, list: exchanges, currentExchange }: Props,
|
{ onCancel, onSelection, list: exchanges, currentExchange }: Props,
|
||||||
api: typeof wxApi,
|
api: typeof wxApi,
|
||||||
): State {
|
): State {
|
||||||
const initialValue = exchanges.findIndex(e => e.exchangeBaseUrl === currentExchange);
|
const initialValue = exchanges.findIndex(
|
||||||
|
(e) => e.exchangeBaseUrl === currentExchange,
|
||||||
|
);
|
||||||
if (initialValue === -1) {
|
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));
|
const [value, setValue] = useState(String(initialValue));
|
||||||
|
|
||||||
@ -113,7 +117,7 @@ export function useComponentState(
|
|||||||
withdraw: createPairTimeline(
|
withdraw: createPairTimeline(
|
||||||
selected.denomFees.withdraw,
|
selected.denomFees.withdraw,
|
||||||
original.denomFees.withdraw,
|
original.denomFees.withdraw,
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -14,24 +14,20 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { FeeDescription, FeeDescriptionPair } from "@gnu-taler/taler-util";
|
||||||
Amounts,
|
|
||||||
FeeDescription,
|
|
||||||
FeeDescriptionPair,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { styled } from "@linaria/react";
|
import { styled } from "@linaria/react";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Amount } from "../../components/Amount.js";
|
import { Amount } from "../../components/Amount.js";
|
||||||
import { LoadingError } from "../../components/LoadingError.js";
|
import { LoadingError } from "../../components/LoadingError.js";
|
||||||
import { SelectList } from "../../components/SelectList.js";
|
import { SelectList } from "../../components/SelectList.js";
|
||||||
import { Input, LinkPrimary, SvgIcon } from "../../components/styled/index.js";
|
import { Input, SvgIcon } from "../../components/styled/index.js";
|
||||||
import { Time } from "../../components/Time.js";
|
import { Time } from "../../components/Time.js";
|
||||||
import { useTranslationContext } from "../../context/translation.js";
|
import { useTranslationContext } from "../../context/translation.js";
|
||||||
|
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
|
||||||
import { Button } from "../../mui/Button.js";
|
import { Button } from "../../mui/Button.js";
|
||||||
import arrowDown from "../../svg/chevron-down.svg";
|
import arrowDown from "../../svg/chevron-down.svg";
|
||||||
import { State } from "./index.js";
|
import { State } from "./index.js";
|
||||||
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
|
|
||||||
|
|
||||||
const ButtonGroup = styled.div`
|
const ButtonGroup = styled.div`
|
||||||
& > button {
|
& > button {
|
||||||
@ -39,6 +35,16 @@ const ButtonGroup = styled.div`
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
const ButtonGroupFooter = styled.div`
|
||||||
|
& {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
& > button {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const FeeDescriptionTable = styled.table`
|
const FeeDescriptionTable = styled.table`
|
||||||
& {
|
& {
|
||||||
@ -343,10 +349,10 @@ export function ComparingView({
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<ButtonGroup>
|
<ButtonGroupFooter>
|
||||||
<LinkPrimary>Privacy policy</LinkPrimary>
|
<Button variant="outlined">Privacy policy</Button>
|
||||||
<LinkPrimary>Terms of service</LinkPrimary>
|
<Button variant="outlined">Terms of service</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroupFooter>
|
||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
@ -609,10 +615,10 @@ export function ReadyView({
|
|||||||
</FeeDescriptionTable>
|
</FeeDescriptionTable>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<ButtonGroup>
|
<ButtonGroupFooter>
|
||||||
<LinkPrimary>Privacy policy</LinkPrimary>
|
<Button variant="outlined">Privacy policy</Button>
|
||||||
<LinkPrimary>Terms of service</LinkPrimary>
|
<Button variant="outlined">Terms of service</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroupFooter>
|
||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js";
|
|||||||
import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js";
|
import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js";
|
||||||
import { ToggleHandler } from "../mui/handlers.js";
|
import { ToggleHandler } from "../mui/handlers.js";
|
||||||
import { Pages } from "../NavigationBar.js";
|
import { Pages } from "../NavigationBar.js";
|
||||||
import { buildTermsOfServiceStatus } from "../utils/index.js";
|
import { buildTermsOfServiceStatus } from "../cta/TermsOfService/utils.js";
|
||||||
import * as wxApi from "../wxApi.js";
|
import * as wxApi from "../wxApi.js";
|
||||||
import { platform } from "../platform/api.js";
|
import { platform } from "../platform/api.js";
|
||||||
import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
|
import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
|
||||||
|
@ -24,8 +24,16 @@
|
|||||||
import {
|
import {
|
||||||
AcceptExchangeTosRequest,
|
AcceptExchangeTosRequest,
|
||||||
AcceptManualWithdrawalResult,
|
AcceptManualWithdrawalResult,
|
||||||
AcceptPeerPullPaymentRequest, AcceptPeerPullPaymentResponse, AcceptPeerPushPaymentRequest, AcceptPeerPushPaymentResponse, AcceptTipRequest, AcceptTipResponse, AcceptWithdrawalResponse,
|
AcceptPeerPullPaymentRequest,
|
||||||
AddExchangeRequest, AddKnownBankAccountsRequest, AmountString,
|
AcceptPeerPullPaymentResponse,
|
||||||
|
AcceptPeerPushPaymentRequest,
|
||||||
|
AcceptPeerPushPaymentResponse,
|
||||||
|
AcceptTipRequest,
|
||||||
|
AcceptTipResponse,
|
||||||
|
AcceptWithdrawalResponse,
|
||||||
|
AddExchangeRequest,
|
||||||
|
AddKnownBankAccountsRequest,
|
||||||
|
AmountString,
|
||||||
ApplyRefundResponse,
|
ApplyRefundResponse,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
CheckPeerPullPaymentRequest,
|
CheckPeerPullPaymentRequest,
|
||||||
@ -37,7 +45,12 @@ import {
|
|||||||
CoreApiResponse,
|
CoreApiResponse,
|
||||||
CreateDepositGroupRequest,
|
CreateDepositGroupRequest,
|
||||||
CreateDepositGroupResponse,
|
CreateDepositGroupResponse,
|
||||||
DeleteTransactionRequest, DepositGroupFees, ExchangeFullDetails, ExchangesListResponse, ForgetKnownBankAccountsRequest, GetExchangeTosResult,
|
DeleteTransactionRequest,
|
||||||
|
DepositGroupFees,
|
||||||
|
ExchangeFullDetails,
|
||||||
|
ExchangesListResponse,
|
||||||
|
ForgetKnownBankAccountsRequest,
|
||||||
|
GetExchangeTosResult,
|
||||||
GetExchangeWithdrawalInfo,
|
GetExchangeWithdrawalInfo,
|
||||||
GetFeeForDepositRequest,
|
GetFeeForDepositRequest,
|
||||||
GetWithdrawalDetailsForUriRequest,
|
GetWithdrawalDetailsForUriRequest,
|
||||||
@ -47,7 +60,9 @@ import {
|
|||||||
InitiatePeerPushPaymentResponse,
|
InitiatePeerPushPaymentResponse,
|
||||||
KnownBankAccounts,
|
KnownBankAccounts,
|
||||||
Logger,
|
Logger,
|
||||||
NotificationType, PaytoUri, PrepareDepositRequest,
|
NotificationType,
|
||||||
|
PaytoUri,
|
||||||
|
PrepareDepositRequest,
|
||||||
PrepareDepositResponse,
|
PrepareDepositResponse,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
PrepareRefundRequest,
|
PrepareRefundRequest,
|
||||||
@ -55,9 +70,13 @@ import {
|
|||||||
PrepareTipRequest,
|
PrepareTipRequest,
|
||||||
PrepareTipResult,
|
PrepareTipResult,
|
||||||
RetryTransactionRequest,
|
RetryTransactionRequest,
|
||||||
SetWalletDeviceIdRequest, stringifyPaytoUri, Transaction,
|
SetWalletDeviceIdRequest,
|
||||||
TransactionsResponse, WalletCoreVersion,
|
stringifyPaytoUri,
|
||||||
WalletDiagnostics, WithdrawUriInfoResponse
|
Transaction,
|
||||||
|
TransactionsResponse,
|
||||||
|
WalletCoreVersion,
|
||||||
|
WalletDiagnostics,
|
||||||
|
WithdrawUriInfoResponse,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AddBackupProviderRequest,
|
AddBackupProviderRequest,
|
||||||
@ -66,7 +85,7 @@ import {
|
|||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
RemoveBackupProviderRequest,
|
RemoveBackupProviderRequest,
|
||||||
TalerError,
|
TalerError,
|
||||||
WalletContractData
|
WalletContractData,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import { MessageFromBackend, platform } from "./platform/api.js";
|
import { MessageFromBackend, platform } from "./platform/api.js";
|
||||||
|
|
||||||
@ -268,13 +287,13 @@ export function addKnownBankAccounts(
|
|||||||
return callBackend("addKnownBankAccounts", {
|
return callBackend("addKnownBankAccounts", {
|
||||||
payto: stringifyPaytoUri(payto),
|
payto: stringifyPaytoUri(payto),
|
||||||
currency,
|
currency,
|
||||||
alias
|
alias,
|
||||||
} as AddKnownBankAccountsRequest);
|
} as AddKnownBankAccountsRequest);
|
||||||
}
|
}
|
||||||
export function forgetKnownBankAccounts(
|
export function forgetKnownBankAccounts(payto: string): Promise<void> {
|
||||||
payto: string,
|
return callBackend("forgetKnownBankAccounts", {
|
||||||
): Promise<void> {
|
payto,
|
||||||
return callBackend("forgetKnownBankAccounts", { payto } as ForgetKnownBankAccountsRequest);
|
} as ForgetKnownBankAccountsRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user