diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/DestinationSelection')
5 files changed, 944 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts new file mode 100644 index 000000000..b56fe5523 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts @@ -0,0 +1,98 @@ +/* + 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 { ErrorAlertView } from "../../components/CurrentAlerts.js"; +import { Loading } from "../../components/Loading.js"; +import { ErrorAlert } from "../../context/alert.js"; +import { + AmountFieldHandler, + ButtonHandler, + ToggleHandler, +} from "../../mui/handlers.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import { useComponentState } from "./state.js"; +import { ReadyView, SelectCurrencyView } from "./views.js"; + +export type Props = PropsGet | PropsSend; + +interface PropsGet { + type: "get"; + amount?: string; + goToWalletManualWithdraw: (amount: string) => void; + goToWalletWalletInvoice: (amount: string) => void; +} +interface PropsSend { + type: "send"; + amount?: string; + goToWalletBankDeposit: (amount: string) => void; + goToWalletWalletSend: (amount: string) => void; +} + +export type State = + | State.Loading + | State.LoadingUriError + | State.Ready + | State.SelectCurrency; + +export namespace State { + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "error"; + error: ErrorAlert; + } + + export interface SelectCurrency { + status: "select-currency"; + error: undefined; + currencies: Record<string, string>; + onCurrencySelected: (currency: string) => void; + } + + export interface Ready { + status: "ready"; + error: undefined; + type: Props["type"]; + selectCurrency: ButtonHandler; + selectMax: ButtonHandler; + previous: Contact[]; + goToBank: ButtonHandler; + goToWallet: ButtonHandler; + amountHandler: AmountFieldHandler; + } +} + +export type Contact = { + icon_type: string; + name: string; + description: string; +}; + +const viewMapping: StateViewMap<State> = { + loading: Loading, + error: ErrorAlertView, + "select-currency": SelectCurrencyView, + ready: ReadyView, +}; + +export const DestinationSelectionPage = compose( + "DestinationSelectionPage", + (p: Props) => useComponentState(p), + viewMapping, +); diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts new file mode 100644 index 000000000..d4e270a6c --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts @@ -0,0 +1,198 @@ +/* + 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 { Amounts } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useState } from "preact/hooks"; +import { alertFromError, useAlertContext } from "../../context/alert.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import { RecursiveState, assertUnreachable } from "../../utils/index.js"; +import { Contact, Props, State } from "./index.js"; + +export function useComponentState(props: Props): RecursiveState<State> { + const api = useBackendContext(); + const { pushAlertOnError } = useAlertContext(); + + const parsedInitialAmount = !props.amount + ? undefined + : Amounts.parse(props.amount); + + const hook = useAsyncAsHook(async () => { + if (!parsedInitialAmount) return undefined; + const balance = await api.wallet.call(WalletApiOperation.GetBalanceDetail, { + currency: parsedInitialAmount.currency, + }); + return { balance }; + }); + + const info = hook && !hook.hasError ? hook.response : undefined; + + // const initialCurrency = parsedInitialAmount?.currency; + + const [amount, setAmount] = useState( + !parsedInitialAmount ? undefined : parsedInitialAmount, + ); + //FIXME: get this information from wallet + // eslint-disable-next-line no-constant-condition + const previous: Contact[] = true + ? [] + : [ + { + name: "International Bank", + icon_type: "bank", + description: "account ending with 3454", + }, + { + name: "Max", + icon_type: "bank", + description: "account ending with 3454", + }, + { + name: "Alex", + icon_type: "bank", + description: "account ending with 3454", + }, + ]; + + if (!amount) { + return () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { i18n } = useTranslationContext(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const hook = useAsyncAsHook(() => + api.wallet.call(WalletApiOperation.ListExchanges, {}), + ); + + if (!hook) { + return { + status: "loading", + error: undefined, + }; + } + if (hook.hasError) { + return { + status: "error", + error: alertFromError(i18n, + i18n.str`Could not load exchanges`, hook), + }; + } + const currencies: Record<string, string> = {}; + hook.response.exchanges.forEach((e) => { + if (e.currency) { + currencies[e.currency] = e.currency; + } + }); + currencies[""] = "Select a currency"; + + return { + status: "select-currency", + error: undefined, + onCurrencySelected: (c: string) => { + setAmount(Amounts.zeroOfCurrency(c)); + }, + currencies, + }; + }; + } + + const currencyAndAmount = Amounts.stringify(amount); + const invalid = Amounts.isZero(amount); + + switch (props.type) { + case "send": + return { + status: "ready", + error: undefined, + previous, + selectCurrency: { + onClick: pushAlertOnError(async () => { + setAmount(undefined); + }), + }, + goToBank: { + onClick: invalid + ? undefined + : pushAlertOnError(async () => { + props.goToWalletBankDeposit(currencyAndAmount); + }), + }, + selectMax: { + onClick: pushAlertOnError(async () => { + const resp = await api.wallet.call( + WalletApiOperation.GetMaxDepositAmount, + { + currency: amount.currency, + }, + ); + setAmount(Amounts.parseOrThrow(resp.effectiveAmount)); + }), + }, + goToWallet: { + onClick: invalid + ? undefined + : pushAlertOnError(async () => { + props.goToWalletWalletSend(currencyAndAmount); + }), + }, + amountHandler: { + onInput: pushAlertOnError(async (s) => setAmount(s)), + value: amount, + }, + type: props.type, + }; + case "get": + return { + status: "ready", + error: undefined, + previous, + selectCurrency: { + onClick: pushAlertOnError(async () => { + setAmount(undefined); + }), + }, + selectMax: { + onClick: invalid + ? undefined + : pushAlertOnError(async () => { + props.goToWalletManualWithdraw(currencyAndAmount); + }), + }, + goToBank: { + onClick: invalid + ? undefined + : pushAlertOnError(async () => { + props.goToWalletManualWithdraw(currencyAndAmount); + }), + }, + goToWallet: { + onClick: invalid + ? undefined + : pushAlertOnError(async () => { + props.goToWalletWalletInvoice(currencyAndAmount); + }), + }, + amountHandler: { + onInput: pushAlertOnError(async (s) => setAmount(s)), + value: amount, + }, + type: props.type, + }; + default: + assertUnreachable(props); + } +} diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx new file mode 100644 index 000000000..e1ac958f7 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx @@ -0,0 +1,65 @@ +/* + 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 * as tests from "@gnu-taler/web-util/testing"; +import { ReadyView, SelectCurrencyView } from "./views.js"; + +export default { + title: "destination", +}; + +export const GetCash = tests.createExample(ReadyView, { + amountHandler: { + value: { + currency: "EUR", + fraction: 0, + value: 2, + }, + }, + goToBank: {}, + selectMax: {}, + goToWallet: {}, + previous: [], + selectCurrency: {}, + type: "get", +}); +export const SendCash = tests.createExample(ReadyView, { + amountHandler: { + value: { + currency: "EUR", + fraction: 0, + value: 1, + }, + }, + selectMax: {}, + goToBank: {}, + goToWallet: {}, + previous: [], + selectCurrency: {}, + type: "send", +}); + +export const SelectCurrency = tests.createExample(SelectCurrencyView, { + currencies: { + "": "Select a currency", + USD: "USD", + }, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts new file mode 100644 index 000000000..683378613 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts @@ -0,0 +1,153 @@ +/* + 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 { + Amounts, + ExchangeEntryStatus, + ExchangeListItem, + ExchangeTosStatus, + ExchangeUpdateStatus, + ScopeType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import * as tests from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; +import { nullFunction } from "../../mui/handlers.js"; +import { createWalletApiMock } from "../../test-utils.js"; +import { useComponentState } from "./state.js"; + +const exchangeArs: ExchangeListItem = { + currency: "ARS", + exchangeBaseUrl: "http://", + masterPub: "123qwe123", + scopeInfo: { + currency: "ARS", + type: ScopeType.Exchange, + url: "http://", + }, + tosStatus: ExchangeTosStatus.Accepted, + exchangeEntryStatus: ExchangeEntryStatus.Used, + exchangeUpdateStatus: ExchangeUpdateStatus.Initial, + paytoUris: [], + ageRestrictionOptions: [], + lastUpdateTimestamp: undefined, + noFees: false, + peerPaymentsDisabled: false, +}; + +describe("Destination selection states", () => { + it("should select currency if no amount specified", async () => { + const { handler, TestingContext } = createWalletApiMock(); + + handler.addWalletCallResponse( + WalletApiOperation.ListExchanges, + {}, + { + exchanges: [exchangeArs], + }, + ); + + const props = { + type: "get" as const, + goToWalletManualWithdraw: nullFunction, + goToWalletWalletInvoice: nullFunction, + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + ({ status }) => { + expect(status).equal("loading"); + }, + (state) => { + if (state.status !== "select-currency") expect.fail(); + if (state.error) expect.fail(); + expect(state.currencies).deep.eq({ + ARS: "ARS", + "": "Select a currency", + }); + + state.onCurrencySelected(exchangeArs.currency!); + }, + (state) => { + if (state.status !== "ready") expect.fail(); + if (state.error) expect.fail(); + expect(state.goToBank.onClick).eq(undefined); + expect(state.goToWallet.onClick).eq(undefined); + + expect(state.amountHandler.value).deep.eq( + Amounts.parseOrThrow("ARS:0"), + ); + }, + ], + TestingContext, + ); + + expect(hookBehavior).deep.equal({ result: "ok" }); + expect(handler.getCallingQueueState()).eq("empty"); + }); + + it("should be possible to start with an amount specified in request params", async () => { + const { handler, TestingContext } = createWalletApiMock(); + + const props = { + type: "get" as const, + goToWalletManualWithdraw: nullFunction, + goToWalletWalletInvoice: nullFunction, + amount: "ARS:2", + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + // ({ status }) => { + // expect(status).equal("loading"); + // }, + (state) => { + if (state.status !== "ready") expect.fail(); + if (state.error) expect.fail(); + expect(state.goToBank.onClick).not.eq(undefined); + expect(state.goToWallet.onClick).not.eq(undefined); + + expect(state.amountHandler.value).deep.eq( + Amounts.parseOrThrow("ARS:2"), + ); + }, + (state) => { + if (state.status !== "ready") expect.fail(); + if (state.error) expect.fail(); + expect(state.goToBank.onClick).not.eq(undefined); + expect(state.goToWallet.onClick).not.eq(undefined); + + expect(state.amountHandler.value).deep.eq( + Amounts.parseOrThrow("ARS:2"), + ); + }, + ], + TestingContext, + ); + + expect(hookBehavior).deep.equal({ result: "ok" }); + expect(handler.getCallingQueueState()).eq("empty"); + }); +}); diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx new file mode 100644 index 000000000..8a74a20f1 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx @@ -0,0 +1,430 @@ +/* + 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 { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { styled } from "@linaria/react"; +import { Fragment, h, VNode } from "preact"; +import { AmountField } from "../../components/AmountField.js"; +import { EnabledBySettings } from "../../components/EnabledBySettings.js"; +import { SelectList } from "../../components/SelectList.js"; +import { + Input, + LightText, + LinkPrimary, + SvgIcon, +} from "../../components/styled/index.js"; +import { Button } from "../../mui/Button.js"; +import { Grid } from "../../mui/Grid.js"; +import { Paper } from "../../mui/Paper.js"; +import { Pages } from "../../NavigationBar.js"; +import arrowIcon from "../../svg/chevron-down.inline.svg"; +import bankIcon from "../../svg/ri-bank-line.inline.svg"; +import { assertUnreachable } from "../../utils/index.js"; +import { Contact, State } from "./index.js"; + +export function SelectCurrencyView({ + currencies, + onCurrencySelected, +}: State.SelectCurrency): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <h2> + <i18n.Translate> + Choose a currency to proceed or add another exchange + </i18n.Translate> + </h2> + + <p> + <Input> + <SelectList + label={i18n.str`Known currencies`} + list={currencies} + name="lang" + value={""} + onChange={(v) => onCurrencySelected(v)} + /> + </Input> + </p> + <div style={{ display: "flex", justifyContent: "space-between" }}> + <div /> + <LinkPrimary href={Pages.settingsExchangeAdd({})}> + <i18n.Translate>Add an exchange</i18n.Translate> + </LinkPrimary> + </div> + </Fragment> + ); +} + +const Container = styled.div` + display: flex; + flex-direction: column; + & > * { + margin: 8px; + } +`; + +const ContactTable = styled.table` + width: 100%; + & > tr > td { + padding: 8px; + & > div:not([data-disabled]):hover { + background-color: lightblue; + } + color: black; + div[data-disabled] > * { + color: gray; + } + } + + & > tr:nth-child(2n) { + background: #ebebeb; + } +`; + +const MediaExample = styled.div` + text-size-adjust: 100%; + color: inherit; + font-family: inherit; + font-size: inherit; + line-height: inherit; + text-transform: none; + text-align: left; + box-sizing: border-box; + align-items: center; + display: flex; + padding: 8px 8px; + + &[data-disabled]:hover { + cursor: inherit; + } + cursor: pointer; +`; + +const MediaLeft = styled.div` + text-size-adjust: 100%; + + color: inherit; + font-family: inherit; + font-size: inherit; + line-height: inherit; + text-transform: none; + text-align: left; + box-sizing: border-box; + padding-right: 8px; + display: block; +`; + +const MediaBody = styled.div` + text-size-adjust: 100%; + + font-family: inherit; + text-transform: none; + text-align: left; + box-sizing: border-box; + flex: 1 1; + font-size: 14px; + font-weight: 500; + line-height: 1.42857; +`; +const MediaRight = styled.div` + text-size-adjust: 100%; + + color: inherit; + font-family: inherit; + font-size: inherit; + line-height: inherit; + text-transform: none; + text-align: left; + box-sizing: border-box; + padding-left: 8px; +`; + +const CircleDiv = styled.div` + box-sizing: border-box; + align-items: center; + background-position: 50%; + background-repeat: no-repeat; + background-size: cover; + border-radius: 50%; + display: flex; + justify-content: center; + margin-left: auto; + margin-right: auto; + overflow: hidden; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + color 0.15s ease; + font-size: 16px; + background-color: #86a7bd1a; + height: 40px; + line-height: 40px; + width: 40px; + border: none; +`; + +export function ReadyView(props: State.Ready): VNode { + switch (props.type) { + case "get": + return ReadyGetView(props); + case "send": + return ReadySendView(props); + default: + assertUnreachable(props.type); + } +} +export function ReadyGetView({ + amountHandler, + goToBank, + goToWallet, + selectCurrency, + previous, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Container> + <h1> + <i18n.Translate>Specify the amount and the origin</i18n.Translate> + </h1> + <Grid container columns={2} justifyContent="space-between"> + <AmountField + label={i18n.str`Amount`} + required + handler={amountHandler} + /> + + <Button onClick={selectCurrency.onClick}> + <i18n.Translate>Change currency</i18n.Translate> + </Button> + </Grid> + + <Grid container spacing={1} columns={1}> + {previous.length > 0 ? ( + <Fragment> + <p> + <i18n.Translate>Use previous origins:</i18n.Translate> + </p> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <ContactTable> + {previous.map((info, i) => ( + <tr key={i}> + <td> + <RowExample + info={info} + disabled={!amountHandler.onInput} + /> + </td> + </tr> + ))} + </ContactTable> + </Paper> + </Grid> + </Fragment> + ) : undefined} + {previous.length > 0 ? ( + <Grid item> + <p> + <i18n.Translate> + Or specify the origin of the money + </i18n.Translate> + </p> + </Grid> + ) : ( + <Grid item> + <p> + <i18n.Translate>Specify the origin of the money</i18n.Translate> + </p> + </Grid> + )} + <Grid item container columns={2} spacing={1}> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <p> + <i18n.Translate>From my bank account</i18n.Translate> + </p> + <Button onClick={goToBank.onClick}> + <i18n.Translate>Withdraw</i18n.Translate> + </Button> + </Paper> + </Grid> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <p> + <i18n.Translate>From another wallet</i18n.Translate> + </p> + <Button onClick={goToWallet.onClick}> + <i18n.Translate>Invoice</i18n.Translate> + </Button> + </Paper> + </Grid> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <p> + <i18n.Translate>From a <pre style={{display:"inline"}}>taler://peer-push-credit</pre> URI</i18n.Translate> + </p> + <a href={Pages.qr}> + <i18n.Translate>Enter URI here</i18n.Translate> + </a> + </Paper> + </Grid> + </Grid> + </Grid> + </Container> + ); +} +export function ReadySendView({ + amountHandler, + goToBank, + goToWallet, + previous, + selectMax, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Container> + <h1> + <i18n.Translate>Specify the amount and the destination</i18n.Translate> + </h1> + + <Grid container columns={2} justifyContent="space-between"> + <AmountField + label={i18n.str`Amount`} + required + handler={amountHandler} + /> + <EnabledBySettings name="advancedMode"> + <Button onClick={selectMax.onClick}> + <i18n.Translate>Send all</i18n.Translate> + </Button> + </EnabledBySettings> + </Grid> + + <Grid container spacing={1} columns={1}> + {previous.length > 0 ? ( + <Fragment> + <p> + <i18n.Translate>Use previous destinations:</i18n.Translate> + </p> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <ContactTable> + {previous.map((info, i) => ( + <tr key={i}> + <td> + <RowExample + info={info} + disabled={!amountHandler.onInput} + /> + </td> + </tr> + ))} + </ContactTable> + </Paper> + </Grid> + </Fragment> + ) : undefined} + {previous.length > 0 ? ( + <Grid item> + <p> + <i18n.Translate> + Or specify the destination of the money + </i18n.Translate> + </p> + </Grid> + ) : ( + <Grid item> + <p> + <i18n.Translate> + Specify the destination of the money + </i18n.Translate> + </p> + </Grid> + )} + <Grid item container columns={2} spacing={1}> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <p> + <i18n.Translate>To my bank account</i18n.Translate> + </p> + <Button onClick={goToBank.onClick}> + <i18n.Translate>Deposit</i18n.Translate> + </Button> + </Paper> + </Grid> + <Grid item xs={1}> + <Paper style={{ padding: 8 }}> + <p> + <i18n.Translate>To another wallet</i18n.Translate> + </p> + <Button onClick={goToWallet.onClick}> + <i18n.Translate>Send</i18n.Translate> + </Button> + </Paper> + </Grid> + </Grid> + </Grid> + </Container> + ); +} + +function RowExample({ + info, + disabled, +}: { + info: Contact; + disabled?: boolean; +}): VNode { + const icon = info.icon_type === "bank" ? bankIcon : undefined; + return ( + <MediaExample data-disabled={disabled}> + <MediaLeft> + <CircleDiv> + {icon !== undefined ? ( + <SvgIcon + title={info.name} + dangerouslySetInnerHTML={{ + __html: icon, + }} + color="currentColor" + /> + ) : ( + <span>A</span> + )} + </CircleDiv> + </MediaLeft> + <MediaBody> + <span>{info.name}</span> + <LightText>{info.description}</LightText> + </MediaBody> + <MediaRight> + <SvgIcon + title="Select this contact" + dangerouslySetInnerHTML={{ __html: arrowIcon }} + color="currentColor" + transform="rotate(-90deg)" + /> + </MediaRight> + </MediaExample> + ); +} |