diff options
7 files changed, 386 insertions, 2 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index 70fb7bdcb..42a365f8c 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -98,6 +98,7 @@ export const Pages = { receiveCash: pageDefinition<{ amount?: string }>("/destination/get/:amount?"), dev: "/dev", + exchanges: "/exchanges", backup: "/backup", backupProviderDetail: pageDefinition<{ pid: string }>( "/backup/provider/:pid", diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index 5917be092..dd3f6c9c7 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -37,6 +37,18 @@ const exchanges: ExchangeListItem[] = [ tos: { acceptedVersion: "", }, + auditors: [ + { + auditor_pub: "pubpubpubpubpub", + auditor_url: "https://audotor.taler.net", + denomination_keys: [], + }, + ], + denominations: [{} as any], + wireInfo: { + accounts: [], + feesForType: {}, + }, }, ]; diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 6c08ecb71..1f375a82e 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -58,6 +58,7 @@ import { DestinationSelectionSendCash, } from "./DestinationSelection.js"; import { Amounts } from "@gnu-taler/taler-util"; +import { ExchangeSelection } from "./ExchangeSelection.js"; export function Application(): VNode { const [globalNotification, setGlobalNotification] = useState< @@ -141,6 +142,7 @@ export function Application(): VNode { ) } /> + <Route path={Pages.exchanges} component={ExchangeSelection} /> <Route path={Pages.sendCash.pattern} component={DestinationSelectionSendCash} diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection.stories.tsx new file mode 100644 index 000000000..b99c6c014 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection.stories.tsx @@ -0,0 +1,85 @@ +/* + 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 { TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { createExample } from "../test-utils.js"; +import { ExchangeSelectionView } from "./ExchangeSelection.js"; + +export default { + title: "wallet/select exchange", +}; + +const exchangeList = [ + { + currency: "KUDOS", + exchangeBaseUrl: "https://exchange.demo.taler.net", + paytoUris: [], + tos: {}, + auditors: [ + { + auditor_pub: "pubpubpubpubpub", + auditor_url: "https://audotor.taler.net", + denomination_keys: [], + }, + ], + denominations: [ + { + stampStart: TalerProtocolTimestamp.never(), + stampExpireWithdraw: TalerProtocolTimestamp.never(), + stampExpireLegal: TalerProtocolTimestamp.never(), + stampExpireDeposit: TalerProtocolTimestamp.never(), + }, + ], + wireInfo: { + accounts: [], + feesForType: {}, + }, + }, + { + currency: "ARS", + exchangeBaseUrl: "https://exchange.taler.ar", + paytoUris: [], + tos: {}, + auditors: [ + { + auditor_pub: "pubpubpubpubpub", + auditor_url: "https://audotor.taler.net", + denomination_keys: [], + }, + ], + denominations: [ + { + stampStart: TalerProtocolTimestamp.never(), + stampExpireWithdraw: TalerProtocolTimestamp.never(), + stampExpireLegal: TalerProtocolTimestamp.never(), + stampExpireDeposit: TalerProtocolTimestamp.never(), + } as any, + ], + wireInfo: { + accounts: [], + feesForType: {}, + }, + }, +]; + +export const Listing = createExample(ExchangeSelectionView, { + exchanges: exchangeList, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection.tsx new file mode 100644 index 000000000..1fa921429 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection.tsx @@ -0,0 +1,282 @@ +/* + 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 { + AbsoluteTime, + ExchangeListItem, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; +import { styled } from "@linaria/react"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../components/Loading.js"; +import { LoadingError } from "../components/LoadingError.js"; +import { SelectList } from "../components/SelectList.js"; +import { Input, LinkPrimary } from "../components/styled/index.js"; +import { Time } from "../components/Time.js"; +import { useTranslationContext } from "../context/translation.js"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; +import { Button } from "../mui/Button.js"; +import * as wxApi from "../wxApi.js"; + +const Container = styled.div` + display: flex; + flex-direction: column; + & > * { + margin-bottom: 20px; + } +`; + +interface Props { + initialValue?: number; + exchanges: ExchangeListItem[]; + onSelected: (exchange: string) => void; +} + +const ButtonGroup = styled.div` + & > button { + margin-left: 8px; + margin-right: 8px; + } +`; + +export function ExchangeSelection(): VNode { + const hook = useAsyncAsHook(wxApi.listExchanges); + const { i18n } = useTranslationContext(); + if (!hook) { + return <Loading />; + } + if (hook.hasError) { + return ( + <LoadingError + error={hook} + title={<i18n.Translate>Could not load list of exchange</i18n.Translate>} + /> + ); + } + return ( + <ExchangeSelectionView + exchanges={hook.response.exchanges} + onSelected={(exchange) => alert(`ok, selected: ${exchange}`)} + /> + ); +} + +export function ExchangeSelectionView({ + initialValue, + exchanges, + onSelected, +}: Props): VNode { + const list: Record<string, string> = {}; + exchanges.forEach((e, i) => (list[String(i)] = e.exchangeBaseUrl)); + + const [value, setValue] = useState(String(initialValue || 0)); + const { i18n } = useTranslationContext(); + + if (!exchanges.length) { + return <div>no exchanges for listing, please add one</div>; + } + + const current = exchanges[Number(value)]; + + const hasChange = value !== current.exchangeBaseUrl; + + function nearestTimestamp( + first: TalerProtocolTimestamp, + second: TalerProtocolTimestamp, + ): TalerProtocolTimestamp { + const f = AbsoluteTime.fromTimestamp(first); + const s = AbsoluteTime.fromTimestamp(second); + const a = AbsoluteTime.min(f, s); + return AbsoluteTime.toTimestamp(a); + } + + let nextFeeUpdate = TalerProtocolTimestamp.never(); + + nextFeeUpdate = Object.values(current.wireInfo.feesForType).reduce( + (prev, cur) => { + return cur.reduce((p, c) => nearestTimestamp(p, c.endStamp), prev); + }, + nextFeeUpdate, + ); + + nextFeeUpdate = current.denominations.reduce((prev, cur) => { + return [ + cur.stampExpireWithdraw, + cur.stampExpireLegal, + cur.stampExpireDeposit, + ].reduce(nearestTimestamp, prev); + }, nextFeeUpdate); + + return ( + <Container> + <h2> + <i18n.Translate>Service fee description</i18n.Translate> + </h2> + + <section> + <div + style={{ + display: "flex", + flexWrap: "wrap", + alignItems: "center", + justifyContent: "space-between", + }} + > + <p> + <Input> + <SelectList + label={<i18n.Translate>Known exchanges</i18n.Translate>} + list={list} + name="lang" + value={value} + onChange={(v) => setValue(v)} + /> + </Input> + </p> + {hasChange ? ( + <ButtonGroup> + <Button + variant="outlined" + onClick={async () => { + setValue(current.exchangeBaseUrl); + }} + > + Reset + </Button> + <Button + variant="contained" + onClick={async () => { + onSelected(value); + }} + > + Use this exchange + </Button> + </ButtonGroup> + ) : ( + <Button + variant="outlined" + onClick={async () => { + null; + }} + > + Close + </Button> + )} + </div> + </section> + <section> + <dl> + <dt>Auditors</dt> + {current.auditors.map((a) => { + <dd>{a.auditor_url}</dd>; + })} + </dl> + <table> + <tr> + <td>currency</td> + <td>{current.currency}</td> + </tr> + <tr> + <td>next fee update</td> + <td> + { + <Time + timestamp={AbsoluteTime.fromTimestamp(nextFeeUpdate)} + format="dd MMMM yyyy, HH:mm" + /> + } + </td> + </tr> + </table> + </section> + <section> + <table> + <thead> + <tr> + <td>Denomination operations</td> + <td>Current fee</td> + </tr> + </thead> + <tbody> + <tr> + <td colSpan={2}>deposit (i)</td> + </tr> + + <tr> + <td>* 10</td> + <td>0.1</td> + </tr> + <tr> + <td>* 5</td> + <td>0.05</td> + </tr> + <tr> + <td>* 1</td> + <td>0.01</td> + </tr> + </tbody> + </table> + </section> + <section> + <table> + <thead> + <tr> + <td>Wallet operations</td> + <td>Current fee</td> + </tr> + </thead> + <tbody> + <tr> + <td>history(i) </td> + <td>0.1</td> + </tr> + <tr> + <td>kyc (i) </td> + <td>0.1</td> + </tr> + <tr> + <td>account (i) </td> + <td>0.1</td> + </tr> + <tr> + <td>purse (i) </td> + <td>0.1</td> + </tr> + <tr> + <td>wire SEPA (i) </td> + <td>0.1</td> + </tr> + <tr> + <td>closing SEPA(i) </td> + <td>0.1</td> + </tr> + <tr> + <td>wad SEPA (i) </td> + <td>0.1</td> + </tr> + </tbody> + </table> + </section> + <section> + <ButtonGroup> + <LinkPrimary>Privacy policy</LinkPrimary> + <LinkPrimary>Terms of service</LinkPrimary> + </ButtonGroup> + </section> + </Container> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx index 6a500a48e..5c01b1132 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx @@ -57,7 +57,7 @@ export const WithOneExchange = createExample(TestedComponent, { contentType: "text/plain", }, paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"], - }, + } as any, //TODO: complete with auditors, wireInfo and denominations ], }); @@ -87,7 +87,7 @@ export const WithExchangeInDifferentState = createExample(TestedComponent, { contentType: "text/plain", }, paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"], - }, + } as any, //TODO: complete with auditors, wireInfo and denominations { currency: "USD", exchangeBaseUrl: "http://exchange3.taler", diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx index 25537691d..11f1fb422 100644 --- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx @@ -36,6 +36,7 @@ import * as a15 from "./AddNewActionView.stories.js"; import * as a16 from "./DeveloperPage.stories.js"; import * as a17 from "./QrReader.stories.js"; import * as a18 from "./DestinationSelection.stories.js"; +import * as a19 from "./ExchangeSelection.stories.js"; export default [ a1, @@ -55,4 +56,5 @@ export default [ a16, a17, a18, + a19, ]; |