diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/cta/Withdraw.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/cta/Withdraw.tsx | 500 |
1 files changed, 312 insertions, 188 deletions
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx index 6ef72cbe6..603dafcde 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx @@ -21,21 +21,39 @@ * @author Florian Dold */ -import { AmountJson, Amounts, ExchangeListItem, GetExchangeTosResult, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util'; -import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw'; +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, LinkWarning, TermsOfService, WalletAction, WarningText } from '../components/styled'; -import { useAsyncAsHook } from '../hooks/useAsyncAsHook'; +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, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, setExchangeTosAccepted, listExchanges, getExchangeTos + acceptWithdrawal, + getExchangeTos, + getExchangeWithdrawalInfo, + getWithdrawalDetailsForUri, + listExchanges, + setExchangeTosAccepted, } from "../wxApi"; -import { wxMain } from '../wxBackend.js'; interface Props { talerWithdrawUri?: string; @@ -58,145 +76,193 @@ export interface ViewProps { status: TermsStatus; }; knownExchanges: ExchangeListItem[]; +} -}; - -type TermsStatus = 'new' | 'accepted' | 'changed' | 'notfound'; +type TermsStatus = "new" | "accepted" | "changed" | "notfound"; -type TermsDocument = TermsDocumentXml | TermsDocumentHtml | TermsDocumentPlain | TermsDocumentJson | TermsDocumentPdf; +type TermsDocument = + | TermsDocumentXml + | TermsDocumentHtml + | TermsDocumentPlain + | TermsDocumentJson + | TermsDocumentPdf; interface TermsDocumentXml { - type: 'xml', - document: Document, + type: "xml"; + document: Document; } interface TermsDocumentHtml { - type: 'html', - href: URL, + type: "html"; + href: URL; } interface TermsDocumentPlain { - type: 'plain', - content: string, + type: "plain"; + content: string; } interface TermsDocumentJson { - type: 'json', - data: any, + type: "json"; + data: any; } interface TermsDocumentPdf { - type: 'pdf', - location: URL, + type: "pdf"; + location: URL; } function amountToString(text: AmountJson) { - const aj = Amounts.jsonifyAmount(text) - const amount = Amounts.stringifyValue(aj) - return `${amount} ${aj.currency}` + 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 }), {}) +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 ( <WalletAction> <LogoHeader /> - <h2> - {i18n.str`Digital cash withdrawal`} - </h2> + <h2>{i18n.str`Digital cash withdrawal`}</h2> <section> - <Part title="Total to withdraw" text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} kind='positive' /> - <Part title="Chosen amount" text={amountToString(amount)} kind='neutral' /> - {Amounts.isNonZero(withdrawalFee) && - <Part title="Exchange fee" text={amountToString(withdrawalFee)} kind='negative' /> - } - <Part title="Exchange" text={exchangeBaseUrl} kind='neutral' big /> + <Part + title="Total to withdraw" + text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} + kind="positive" + /> + <Part + title="Chosen amount" + text={amountToString(amount)} + kind="neutral" + /> + {Amounts.isNonZero(withdrawalFee) && ( + <Part + title="Exchange fee" + text={amountToString(withdrawalFee)} + kind="negative" + /> + )} + <Part title="Exchange" text={exchangeBaseUrl} kind="neutral" big /> </section> - {!reviewing && + {!reviewing && ( <section> - {switchingExchange !== undefined ? <Fragment> - <div> - <SelectList label="Known exchanges" list={exchanges} name="" onChange={onSwitchExchange} /> - </div> - <LinkSuccess upperCased onClick={() => onSwitchExchange(switchingExchange)}> - {i18n.str`Confirm exchange selection`} - </LinkSuccess> - </Fragment> - : <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}> + {switchingExchange !== undefined ? ( + <Fragment> + <div> + <SelectList + label="Known exchanges" + list={exchanges} + name="" + onChange={onSwitchExchange} + /> + </div> + <LinkSuccess + upperCased + onClick={() => onSwitchExchange(switchingExchange)} + > + {i18n.str`Confirm exchange selection`} + </LinkSuccess> + </Fragment> + ) : ( + <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}> {i18n.str`Switch exchange`} - </LinkSuccess>} - + </LinkSuccess> + )} </section> - } - {!reviewing && reviewed && + )} + {!reviewing && reviewed && ( <section> - <LinkSuccess - upperCased - onClick={() => onReview(true)} - > + <LinkSuccess upperCased onClick={() => onReview(true)}> {i18n.str`Show terms of service`} </LinkSuccess> </section> - } - {terms.status === 'notfound' && + )} + {terms.status === "notfound" && ( <section> <WarningText> {i18n.str`Exchange doesn't have terms of service`} </WarningText> </section> - } - {reviewing && + )} + {reviewing && ( <section> - {terms.status !== 'accepted' && terms.value && terms.value.type === 'xml' && - <TermsOfService> - <ExchangeXmlTos doc={terms.value.document} /> - </TermsOfService> - } - {terms.status !== 'accepted' && terms.value && terms.value.type === 'plain' && - <div style={{ textAlign: 'left' }}> - <pre>{terms.value.content}</pre> - </div> - } - {terms.status !== 'accepted' && terms.value && terms.value.type === 'html' && - <iframe src={terms.value.href.toString()} /> - } - {terms.status !== 'accepted' && terms.value && terms.value.type === 'pdf' && - <a href={terms.value.location.toString()} download="tos.pdf" >Download Terms of Service</a> - } - </section>} - {reviewing && reviewed && + {terms.status !== "accepted" && + terms.value && + terms.value.type === "xml" && ( + <TermsOfService> + <ExchangeXmlTos doc={terms.value.document} /> + </TermsOfService> + )} + {terms.status !== "accepted" && + terms.value && + terms.value.type === "plain" && ( + <div style={{ textAlign: "left" }}> + <pre>{terms.value.content}</pre> + </div> + )} + {terms.status !== "accepted" && + terms.value && + terms.value.type === "html" && ( + <iframe src={terms.value.href.toString()} /> + )} + {terms.status !== "accepted" && + terms.value && + terms.value.type === "pdf" && ( + <a href={terms.value.location.toString()} download="tos.pdf"> + Download Terms of Service + </a> + )} + </section> + )} + {reviewing && reviewed && ( <section> - <LinkSuccess - upperCased - onClick={() => onReview(false)} - > + <LinkSuccess upperCased onClick={() => onReview(false)}> {i18n.str`Hide terms of service`} </LinkSuccess> </section> - } - {(reviewing || reviewed) && + )} + {(reviewing || reviewed) && ( <section> <CheckboxOutlined name="terms" enabled={reviewed} label={i18n.str`I accept the exchange terms of service`} onToggle={() => { - onAccept(!reviewed) - onReview(false) + onAccept(!reviewed); + onReview(false); }} /> </section> - } + )} {/** * Main action section */} <section> - {terms.status === 'new' && !reviewed && !reviewing && + {terms.status === "new" && !reviewed && !reviewing && ( <ButtonSuccess upperCased disabled={!exchangeBaseUrl} @@ -204,8 +270,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, > {i18n.str`Review exchange terms of service`} </ButtonSuccess> - } - {terms.status === 'changed' && !reviewed && !reviewing && + )} + {terms.status === "changed" && !reviewed && !reviewing && ( <ButtonWarning upperCased disabled={!exchangeBaseUrl} @@ -213,8 +279,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, > {i18n.str`Review new version of terms of service`} </ButtonWarning> - } - {(terms.status === 'accepted' || (needsReview && reviewed)) && + )} + {(terms.status === "accepted" || (needsReview && reviewed)) && ( <ButtonSuccess upperCased disabled={!exchangeBaseUrl || confirmed} @@ -222,8 +288,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, > {i18n.str`Confirm withdrawal`} </ButtonSuccess> - } - {terms.status === 'notfound' && + )} + {terms.status === "notfound" && ( <ButtonWarning upperCased disabled={!exchangeBaseUrl} @@ -231,60 +297,88 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, > {i18n.str`Withdraw anyway`} </ButtonWarning> - } + )} </section> </WalletAction> - ) + ); } -export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriInfo: WithdrawUriInfoResponse }) { - const [customExchange, setCustomExchange] = useState<string | undefined>(undefined) - const [errorAccepting, setErrorAccepting] = useState<string | undefined>(undefined) - - const [reviewing, setReviewing] = useState<boolean>(false) - const [reviewed, setReviewed] = useState<boolean>(false) - const [confirmed, setConfirmed] = useState<boolean>(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 +export function WithdrawPageWithParsedURI({ + uri, + uriInfo, +}: { + uri: string; + uriInfo: WithdrawUriInfoResponse; +}) { + const [customExchange, setCustomExchange] = useState<string | undefined>( + undefined, + ); + const [errorAccepting, setErrorAccepting] = useState<string | undefined>( + undefined, + ); + + const [reviewing, setReviewing] = useState<boolean>(false); + const [reviewed, setReviewed] = useState<boolean>(false); + const [confirmed, setConfirmed] = useState<boolean>(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']) + 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 } - }) + tosAcceptedFormat: ["text/xml"], + }); + return { tos, info }; + }); if (!detailsHook) { - return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>; + return ( + <span> + <i18n.Translate>Getting withdrawal details.</i18n.Translate> + </span> + ); } if (detailsHook.hasError) { - return <span><i18n.Translate>Problems getting details: {detailsHook.message}</i18n.Translate></span>; + return ( + <span> + <i18n.Translate> + Problems getting details: {detailsHook.message} + </i18n.Translate> + </span> + ); } - const details = detailsHook.response + const details = detailsHook.response; const onAccept = async (): Promise<void> => { try { - await setExchangeTosAccepted(exchange, details.tos.currentEtag) - setReviewed(true) + await setExchangeTosAccepted(exchange, details.tos.currentEtag); + setReviewed(true); } catch (e) { if (e instanceof Error) { - setErrorAccepting(e.message) + setErrorAccepting(e.message); } } - } + }; const onWithdraw = async (): Promise<void> => { - setConfirmed(true) + setConfirmed(true); console.log("accepting exchange", exchange); try { const res = await acceptWithdrawal(uri, exchange); @@ -293,91 +387,121 @@ export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriIn document.location.href = res.confirmTransferUrl; } } catch (e) { - setConfirmed(false) + 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 <View onWithdraw={onWithdraw} - details={details.tos} amount={withdrawAmount} - exchangeBaseUrl={exchange} - withdrawalFee={details.info.withdrawFee} //FIXME - terms={{ - status, value: termsContent - }} - onSwitchExchange={setCustomExchange} - knownExchanges={knownExchanges} - confirmed={confirmed} - reviewed={reviewed} onAccept={onAccept} - reviewing={reviewing} onReview={setReviewing} - /> + 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 ( + <View + onWithdraw={onWithdraw} + details={details.tos} + amount={withdrawAmount} + exchangeBaseUrl={exchange} + withdrawalFee={details.info.withdrawFee} //FIXME + terms={{ + status, + value: termsContent, + }} + onSwitchExchange={setCustomExchange} + knownExchanges={knownExchanges} + confirmed={confirmed} + reviewed={reviewed} + onAccept={onAccept} + reviewing={reviewing} + onReview={setReviewing} + /> + ); } -export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { - const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) : - getWithdrawalDetailsForUri({ talerWithdrawUri }) - ) +export function WithdrawPage({ talerWithdrawUri }: Props): VNode { + const uriInfoHook = useAsyncAsHook(() => + !talerWithdrawUri + ? Promise.reject(undefined) + : getWithdrawalDetailsForUri({ talerWithdrawUri }), + ); if (!talerWithdrawUri) { - return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; + return ( + <span> + <i18n.Translate>missing withdraw uri</i18n.Translate> + </span> + ); } if (!uriInfoHook) { - return <span><i18n.Translate>Loading...</i18n.Translate></span>; + return ( + <span> + <i18n.Translate>Loading...</i18n.Translate> + </span> + ); } if (uriInfoHook.hasError) { - return <span><i18n.Translate>This URI is not valid anymore: {uriInfoHook.message}</i18n.Translate></span>; + return ( + <span> + <i18n.Translate> + This URI is not valid anymore: {uriInfoHook.message} + </i18n.Translate> + </span> + ); } - return <WithdrawPageWithParsedURI uri={talerWithdrawUri} uriInfo={uriInfoHook.response} /> + return ( + <WithdrawPageWithParsedURI + uri={talerWithdrawUri} + uriInfo={uriInfoHook.response} + /> + ); } -function parseTermsOfServiceContent(type: string, text: string): TermsDocument | undefined { - if (type === 'text/xml') { +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 } + const document = new DOMParser().parseFromString(text, "text/xml"); + return { type: "xml", document }; } catch (e) { - console.log(e) - debugger; + console.log(e); } - } else if (type === 'text/html') { + } else if (type === "text/html") { try { - const href = new URL(text) - return { type: 'html', href } + const href = new URL(text); + return { type: "html", href }; } catch (e) { - console.log(e) - debugger; + console.log(e); } - } else if (type === 'text/json') { + } else if (type === "text/json") { try { - const data = JSON.parse(text) - return { type: 'json', data } + const data = JSON.parse(text); + return { type: "json", data }; } catch (e) { - console.log(e) - debugger; + console.log(e); } - } else if (type === 'text/pdf') { + } else if (type === "text/pdf") { try { - const location = new URL(text) - return { type: 'pdf', location } + const location = new URL(text); + return { type: "pdf", location }; } catch (e) { - console.log(e) - debugger; + console.log(e); } - } else if (type === 'text/plain') { + } else if (type === "text/plain") { try { - const content = text - return { type: 'plain', content } + const content = text; + return { type: "plain", content }; } catch (e) { - console.log(e) - debugger; + console.log(e); } } - return undefined + return undefined; } - |