/*
This file is part of TALER
(C) 2015-2016 GNUnet e.V.
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.
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
TALER; see the file COPYING. If not, see
*/
/**
* Page shown to the user to confirm creation
* of a reserve, usually requested by the bank.
*
* @author Florian Dold
*/
import {
AmountJson,
Amounts,
ExchangeListItem,
GetExchangeTosResult,
i18n,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import { CheckboxOutlined } from "../components/CheckboxOutlined";
import { ExchangeXmlTos } from "../components/ExchangeToS";
import { LogoHeader } from "../components/LogoHeader";
import { Part } from "../components/Part";
import { SelectList } from "../components/SelectList";
import {
ButtonSuccess,
ButtonWarning,
LinkSuccess,
TermsOfService,
WalletAction,
WarningText,
} from "../components/styled";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import {
acceptWithdrawal,
getExchangeTos,
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
listExchanges,
setExchangeTosAccepted,
} from "../wxApi";
interface Props {
talerWithdrawUri?: string;
}
export interface ViewProps {
details: GetExchangeTosResult;
withdrawalFee: AmountJson;
exchangeBaseUrl: string;
amount: AmountJson;
onSwitchExchange: (ex: string) => void;
onWithdraw: () => Promise;
onReview: (b: boolean) => void;
onAccept: (b: boolean) => void;
reviewing: boolean;
reviewed: boolean;
confirmed: boolean;
terms: {
value?: TermsDocument;
status: TermsStatus;
};
knownExchanges: ExchangeListItem[];
}
type TermsStatus = "new" | "accepted" | "changed" | "notfound";
type TermsDocument =
| TermsDocumentXml
| TermsDocumentHtml
| TermsDocumentPlain
| TermsDocumentJson
| TermsDocumentPdf;
interface TermsDocumentXml {
type: "xml";
document: Document;
}
interface TermsDocumentHtml {
type: "html";
href: URL;
}
interface TermsDocumentPlain {
type: "plain";
content: string;
}
interface TermsDocumentJson {
type: "json";
data: any;
}
interface TermsDocumentPdf {
type: "pdf";
location: URL;
}
function amountToString(text: AmountJson) {
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj);
return `${amount} ${aj.currency}`;
}
export function View({
details,
withdrawalFee,
exchangeBaseUrl,
knownExchanges,
amount,
onWithdraw,
onSwitchExchange,
terms,
reviewing,
onReview,
onAccept,
reviewed,
confirmed,
}: ViewProps) {
const needsReview = terms.status === "changed" || terms.status === "new";
const [switchingExchange, setSwitchingExchange] = useState<
string | undefined
>(undefined);
const exchanges = knownExchanges.reduce(
(prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }),
{},
);
return (
{i18n.str`Digital cash withdrawal`}
{Amounts.isNonZero(withdrawalFee) && (
)}
{!reviewing && (
{switchingExchange !== undefined ? (
onSwitchExchange(switchingExchange)}
>
{i18n.str`Confirm exchange selection`}
) : (
setSwitchingExchange("")}>
{i18n.str`Switch exchange`}
)}
)}
{!reviewing && reviewed && (
onReview(true)}>
{i18n.str`Show terms of service`}
)}
{terms.status === "notfound" && (
{i18n.str`Exchange doesn't have terms of service`}
)}
{reviewing && (
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "xml" && (
)}
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "plain" && (
)}
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "html" && (
)}
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "pdf" && (
Download Terms of Service
)}
)}
{reviewing && reviewed && (
onReview(false)}>
{i18n.str`Hide terms of service`}
)}
{(reviewing || reviewed) && (
{
onAccept(!reviewed);
onReview(false);
}}
/>
)}
{/**
* Main action section
*/}
{terms.status === "new" && !reviewed && !reviewing && (
onReview(true)}
>
{i18n.str`Review exchange terms of service`}
)}
{terms.status === "changed" && !reviewed && !reviewing && (
onReview(true)}
>
{i18n.str`Review new version of terms of service`}
)}
{(terms.status === "accepted" || (needsReview && reviewed)) && (
{i18n.str`Confirm withdrawal`}
)}
{terms.status === "notfound" && (
{i18n.str`Withdraw anyway`}
)}
);
}
export function WithdrawPageWithParsedURI({
uri,
uriInfo,
}: {
uri: string;
uriInfo: WithdrawUriInfoResponse;
}) {
const [customExchange, setCustomExchange] = useState(
undefined,
);
const [errorAccepting, setErrorAccepting] = useState(
undefined,
);
const [reviewing, setReviewing] = useState(false);
const [reviewed, setReviewed] = useState(false);
const [confirmed, setConfirmed] = useState(false);
const knownExchangesHook = useAsyncAsHook(() => listExchanges());
const knownExchanges =
!knownExchangesHook || knownExchangesHook.hasError
? []
: knownExchangesHook.response.exchanges;
const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount);
const thisCurrencyExchanges = knownExchanges.filter(
(ex) => ex.currency === withdrawAmount.currency,
);
const exchange =
customExchange ||
uriInfo.defaultExchangeBaseUrl ||
thisCurrencyExchanges[0]?.exchangeBaseUrl;
const detailsHook = useAsyncAsHook(async () => {
if (!exchange) throw Error("no default exchange");
const tos = await getExchangeTos(exchange, ["text/xml"]);
const info = await getExchangeWithdrawalInfo({
exchangeBaseUrl: exchange,
amount: withdrawAmount,
tosAcceptedFormat: ["text/xml"],
});
return { tos, info };
});
if (!detailsHook) {
return (
Getting withdrawal details.
);
}
if (detailsHook.hasError) {
return (
Problems getting details: {detailsHook.message}
);
}
const details = detailsHook.response;
const onAccept = async (): Promise => {
try {
await setExchangeTosAccepted(exchange, details.tos.currentEtag);
setReviewed(true);
} catch (e) {
if (e instanceof Error) {
setErrorAccepting(e.message);
}
}
};
const onWithdraw = async (): Promise => {
setConfirmed(true);
console.log("accepting exchange", exchange);
try {
const res = await acceptWithdrawal(uri, exchange);
console.log("accept withdrawal response", res);
if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl;
}
} catch (e) {
setConfirmed(false);
}
};
const termsContent: TermsDocument | undefined = parseTermsOfServiceContent(
details.tos.contentType,
details.tos.content,
);
const status: TermsStatus = !termsContent
? "notfound"
: !details.tos.acceptedEtag
? "new"
: details.tos.acceptedEtag !== details.tos.currentEtag
? "changed"
: "accepted";
return (
);
}
export function WithdrawPage({ talerWithdrawUri }: Props): VNode {
const uriInfoHook = useAsyncAsHook(() =>
!talerWithdrawUri
? Promise.reject(undefined)
: getWithdrawalDetailsForUri({ talerWithdrawUri }),
);
if (!talerWithdrawUri) {
return (
missing withdraw uri
);
}
if (!uriInfoHook) {
return (
Loading...
);
}
if (uriInfoHook.hasError) {
return (
This URI is not valid anymore: {uriInfoHook.message}
);
}
return (
);
}
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;
}