summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx931
1 files changed, 931 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
new file mode 100644
index 000000000..6f67d84b7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
@@ -0,0 +1,931 @@
+/*
+ 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 { FeeDescription, FeeDescriptionPair } from "@gnu-taler/taler-util";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Amount } from "../../components/Amount.js";
+import { AlertView } from "../../components/CurrentAlerts.js";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import { SelectList } from "../../components/SelectList.js";
+import { Input, SvgIcon } from "../../components/styled/index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { Time } from "../../components/Time.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
+import { Button } from "../../mui/Button.js";
+import arrowDown from "../../svg/chevron-down.inline.svg";
+import { State } from "./index.js";
+
+const ButtonGroup = styled.div`
+ & > button {
+ margin-left: 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`
+ & {
+ margin-bottom: 20px;
+ width: 100%;
+ border-collapse: collapse;
+ }
+ td {
+ padding: 8px;
+ }
+ td.fee {
+ text-align: center;
+ }
+ th.fee {
+ text-align: center;
+ }
+ td.value {
+ text-align: right;
+ width: 15%;
+ white-space: nowrap;
+ }
+ td.icon {
+ width: 24px;
+ }
+ td.icon > div {
+ width: 24px;
+ height: 24px;
+ margin: 0px;
+ }
+ td.expiration {
+ text-align: center;
+ }
+
+ tr[data-main="true"] {
+ background-color: #add8e662;
+ }
+ tr[data-main="true"] > td.value,
+ tr[data-main="true"] > td.expiration,
+ tr[data-main="true"] > td.fee {
+ border-bottom: lightgray solid 1px;
+ }
+ tr[data-hidden="true"] {
+ display: none;
+ }
+ tbody > tr.value[data-hasMore="true"],
+ tbody > tr.value[data-hasMore="true"] > td {
+ cursor: pointer;
+ }
+ th {
+ position: sticky;
+ top: 0;
+ background-color: white;
+ }
+`;
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ & > * {
+ margin-bottom: 20px;
+ }
+`;
+
+export function PrivacyContentView({
+ exchangeUrl,
+ onClose,
+}: State.ShowingPrivacy): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div>
+ <Button variant="outlined" onClick={onClose.onClick}>
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ <div>show privacy terms for {exchangeUrl}</div>
+ </div>
+ );
+}
+
+export function TosContentView({
+ exchangeUrl,
+ onClose,
+}: State.ShowingTos): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div>
+ <Button variant="outlined" onClick={onClose.onClick}>
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ <TermsOfService exchangeUrl={exchangeUrl} readOnly >
+ s
+ </TermsOfService>
+ </div>
+ );
+}
+
+export function NoExchangesView({
+ defaultExchange,
+ currency,
+}: SelectExchangeState.NoExchangeFound): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <p>
+ <AlertView
+ alert={{
+ type: "error",
+ message: i18n.str`There is no exchange available for currency ${currency}`,
+ description: i18n.str`You can add more exchanges from the settings.`,
+ cause: undefined,
+ context: undefined,
+ }}
+ />
+ </p>
+ {defaultExchange && (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Exchange ${defaultExchange} is not available`,
+ description: i18n.str`Exchange status can view accessed from the settings.`,
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
+
+export function ComparingView({
+ exchanges,
+ selected,
+ onReset,
+ onSelect,
+ coinOperationTimeline,
+ globalFeeTimeline,
+ wireFeeTimeline,
+ missingWireTYpe,
+ newWireType,
+ onShowPrivacy,
+ onShowTerms,
+}: State.Comparing): VNode {
+ const { i18n } = useTranslationContext();
+ 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>
+ Select {selected.currency} exchange
+ </i18n.Translate>
+ }
+ list={exchanges.list}
+ name="lang"
+ value={exchanges.value}
+ onChange={exchanges.onChange}
+ />
+ </Input>
+ </p>
+ <ButtonGroup>
+ <Button variant="outlined" onClick={onReset.onClick}>
+ <i18n.Translate>Reset</i18n.Translate>
+ </Button>
+ <Button variant="contained" onClick={onSelect.onClick}>
+ <i18n.Translate>Use this exchange</i18n.Translate>
+ </Button>
+ </ButtonGroup>
+ </div>
+ </section>
+ <section>
+ <dl>
+ <dt>
+ <i18n.Translate>Auditors</i18n.Translate>
+ </dt>
+ {selected.auditors.length === 0 ? (
+ <dd style={{ color: "red" }}>
+ <i18n.Translate>Doesn&apos;t have auditors</i18n.Translate>
+ </dd>
+ ) : (
+ selected.auditors.map((a) => {
+ <dd>{a.auditor_url}</dd>;
+ })
+ )}
+ </dl>
+ <table>
+ <tr>
+ <td>
+ <i18n.Translate>currency</i18n.Translate>
+ </td>
+ <td>{selected.currency}</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Coin operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by denomination
+ value and is valid for a period of time. The exchange will charge
+ the indicated amount every time a coin is used in such operation.
+ </i18n.Translate>
+ </p>
+ <p>
+ <i18n.Translate>Deposits</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.deposit}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Withdrawals</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.withdraw}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Refunds</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.refund}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>{" "}
+ <p>
+ <i18n.Translate>Refresh</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.refresh}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>{" "}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Transfer operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ {missingWireTYpe.map((type) => {
+ return (
+ <p key={type}>
+ Wire <b>{type}</b> is not supported for this exchange.
+ </p>
+ );
+ })}
+ {newWireType.map((type) => {
+ return (
+ <Fragment key={type}>
+ <p>
+ Wire <b>{type}</b> is not supported for the previous exchange.
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.transferFees[type]}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ {Object.entries(wireFeeTimeline).map(([type, fees], idx) => {
+ return (
+ <Fragment key={idx}>
+ <p>{type}</p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={fees}
+ sorting={(a, b) => a.localeCompare(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Wallet operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Feature</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={globalFeeTimeline}
+ sorting={(a, b) => a.localeCompare(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ </Container>
+ );
+}
+
+export function ReadyView({
+ exchanges,
+ selected,
+ onClose,
+ onShowPrivacy,
+ onShowTerms,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Container>
+ <h2>
+ <i18n.Translate>Service fee description</i18n.Translate>
+ </h2>
+ <p>
+ All fee indicated below are in the same and only currency the exchange
+ works.
+ </p>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ }}
+ >
+ {Object.keys(exchanges.list).length === 1 ? (
+ <Fragment>
+ <p>Exchange: {selected.exchangeBaseUrl}</p>
+ </Fragment>
+ ) : (
+ <p>
+ <Input>
+ <SelectList
+ label={
+ <i18n.Translate>
+ Select {selected.currency} exchange
+ </i18n.Translate>
+ }
+ list={exchanges.list}
+ name="lang"
+ value={exchanges.value}
+ onChange={exchanges.onChange}
+ />
+ </Input>
+ </p>
+ )}
+ <Button variant="outlined" onClick={onClose.onClick}>
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ </div>
+ </section>
+ <section>
+ <dl>
+ <dt>Auditors</dt>
+ {selected.auditors.length === 0 ? (
+ <dd style={{ color: "red" }}>
+ <i18n.Translate>Doesn&apos;t have auditors</i18n.Translate>
+ </dd>
+ ) : (
+ selected.auditors.map((a) => {
+ <dd>{a.auditor_url}</dd>;
+ })
+ )}
+ </dl>
+ <table>
+ <tr>
+ <td>
+ <i18n.Translate>Currency</i18n.Translate>
+ </td>
+ <td>
+ <b>{selected.currency}</b>
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Coin operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by denomination
+ value and is valid for a period of time. The exchange will charge
+ the indicated amount every time a coin is used in such operation.
+ </i18n.Translate>
+ </p>
+ <p>
+ <i18n.Translate>Deposits</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.deposit}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Withdrawals</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.withdraw}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Refunds</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.refund}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>{" "}
+ <p>
+ <i18n.Translate>Refresh</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.refresh}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Transfer operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ {Object.entries(selected.transferFees).map(([type, fees], idx) => {
+ return (
+ <Fragment key={idx}>
+ <p>{type}</p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue list={fees} />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Wallet operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Feature</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue list={selected.globalFees} />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ </Container>
+ );
+}
+
+function FeeDescriptionRowsGroup({
+ infos,
+}: {
+ infos: FeeDescription[];
+}): VNode {
+ const [expanded, setExpand] = useState(false);
+ const hasMoreInfo = infos.length > 1;
+ return (
+ <Fragment>
+ {infos.map((info, idx) => {
+ const main = idx === 0;
+ return (
+ <tr
+ key={idx}
+ class="value"
+ data-hasMore={hasMoreInfo}
+ data-main={main}
+ data-hidden={!main && !expanded}
+ onClick={() => setExpand((p) => !p)}
+ >
+ <td class="icon">
+ {hasMoreInfo && main ? (
+ <SvgIcon
+ title="Select this contact"
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ color="currentColor"
+ transform={expanded ? "" : "rotate(-90deg)"}
+ />
+ ) : undefined}
+ </td>
+ <td class="value">{main ? info.group : ""}</td>
+ {info.fee ? (
+ <td class="fee">{<Amount value={info.fee} hideCurrency />}</td>
+ ) : undefined}
+ <td class="expiration">
+ <Time timestamp={info.until} format="dd-MMM-yyyy" />
+ </td>
+ </tr>
+ );
+ })}
+ </Fragment>
+ );
+}
+
+function FeePairRowsGroup({ infos }: { infos: FeeDescriptionPair[] }): VNode {
+ const [expanded, setExpand] = useState(false);
+ const hasMoreInfo = infos.length > 1;
+ return (
+ <Fragment>
+ {infos.map((info, idx) => {
+ const main = idx === 0;
+ return (
+ <tr
+ key={idx}
+ class="value"
+ data-hasMore={hasMoreInfo}
+ data-main={main}
+ data-hidden={!main && !expanded}
+ onClick={() => setExpand((p) => !p)}
+ >
+ <td class="icon">
+ {hasMoreInfo && main ? (
+ <SvgIcon
+ title="Expand"
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ color="currentColor"
+ transform={expanded ? "" : "rotate(-90deg)"}
+ />
+ ) : undefined}
+ </td>
+ <td class="value">{main ? info.group : ""}</td>
+ {info.left ? (
+ <td class="fee">{<Amount value={info.left} hideCurrency />}</td>
+ ) : (
+ <td class="fee"> --- </td>
+ )}
+ {info.right ? (
+ <td class="fee">{<Amount value={info.right} hideCurrency />}</td>
+ ) : (
+ <td class="fee"> --- </td>
+ )}
+ <td class="expiration">
+ <Time timestamp={info.until} format="dd-MMM-yyyy HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ })}
+ </Fragment>
+ );
+}
+
+/**
+ * Group by value and then render using FeePairRowsGroup
+ * @param param0
+ * @returns
+ */
+function RenderFeePairByValue({
+ list,
+ sorting,
+}: {
+ list: FeeDescriptionPair[];
+ sorting?: (a: string, b: string) => number;
+}): VNode {
+ const grouped = list.reduce((prev, cur) => {
+ if (!prev[cur.group]) {
+ prev[cur.group] = [];
+ }
+ prev[cur.group].push(cur);
+ return prev;
+ }, {} as Record<string, FeeDescriptionPair[]>);
+ const p = Object.keys(grouped)
+ .sort(sorting)
+ .map((i, idx) => <FeePairRowsGroup key={idx} infos={grouped[i]} />);
+ return <Fragment>{p}</Fragment>;
+}
+/**
+ *
+ * Group by value and then render using FeeDescriptionRowsGroup
+ * @param param0
+ * @returns
+ */
+function RenderFeeDescriptionByValue({
+ list,
+ sorting,
+}: {
+ list: FeeDescription[];
+ sorting?: (a: string, b: string) => number;
+}): VNode {
+ const grouped = list.reduce((prev, cur) => {
+ if (!prev[cur.group]) {
+ prev[cur.group] = [];
+ }
+ prev[cur.group].push(cur);
+ return prev;
+ }, {} as Record<string, FeeDescription[]>);
+ const p = Object.keys(grouped)
+ .sort(sorting)
+ .map((i, idx) => <FeeDescriptionRowsGroup key={idx} infos={grouped[i]} />);
+ return <Fragment>{p}</Fragment>;
+}