diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx | 602 |
1 files changed, 602 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx new file mode 100644 index 000000000..7b80977f3 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -0,0 +1,602 @@ +/* + 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 { + buildPayto, + KnownBankAccountsInfo, + PaytoUriBitcoin, + PaytoUriIBAN, + PaytoUriTalerBank, + stringifyPaytoUri, + validateIban, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { styled } from "@linaria/react"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorMessage } from "../../components/ErrorMessage.js"; +import { SubTitle, SvgIcon } from "../../components/styled/index.js"; +import { Button } from "../../mui/Button.js"; +import { TextFieldHandler } from "../../mui/handlers.js"; +import { TextField } from "../../mui/TextField.js"; +import checkIcon from "../../svg/check_24px.inline.svg"; +import deleteIcon from "../../svg/delete_24px.inline.svg"; +import warningIcon from "../../svg/warning_24px.inline.svg"; +import { State } from "./index.js"; + +type AccountType = "bitcoin" | "x-taler-bank" | "iban"; +type ComponentFormByAccountType = { + [type in AccountType]: (props: { field: TextFieldHandler }) => VNode; +}; + +type ComponentListByAccountType = { + [type in AccountType]: (props: { + list: KnownBankAccountsInfo[]; + onDelete: (a: KnownBankAccountsInfo) => Promise<void>; + }) => VNode; +}; + +const formComponentByAccountType: ComponentFormByAccountType = { + iban: IbanAddressAccount, + bitcoin: BitcoinAddressAccount, + "x-taler-bank": TalerBankAddressAccount, +}; +const tableComponentByAccountType: ComponentListByAccountType = { + iban: IbanTable, + bitcoin: BitcoinTable, + "x-taler-bank": TalerBankTable, +}; + +const AccountTable = styled.table` + width: 100%; + + border-collapse: separate; + border-spacing: 0px 10px; + tbody tr:nth-child(odd) > td:not(.actions, .kyc) { + background-color: lightgrey; + } + .actions, + .kyc { + width: 10px; + background-color: inherit; + } +`; + +export function ReadyView({ + currency, + error, + accountType, + accountByType, + alias, + onAccountAdded, + deleteAccount, + onCancel, + uri, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <section> + <SubTitle> + <i18n.Translate>Known accounts for {currency}</i18n.Translate> + </SubTitle> + <p> + <i18n.Translate> + To add a new account first select the account type. + </i18n.Translate> + </p> + + {error && ( + <ErrorMessage + title={i18n.str`Unable add this account`} + description={error} + /> + )} + <div style={{ width: "100%", display: "flex" }}> + {Object.entries(accountType.list).map(([key, name], idx) => ( + <div + key={idx} + style={{ + marginLeft: 8, + padding: 8, + borderTopLeftRadius: 5, + borderTopRightRadius: 5, + backgroundColor: + accountType.value === key ? "#0042b2" : "unset", + color: accountType.value === key ? "white" : "unset", + }} + onClick={() => { + if (accountType.onChange) { + accountType.onChange(key); + } + }} + > + {name} + </div> + ))} + </div> + <div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}> + --- {uri.value} --- + <p> + <CustomFieldByAccountType + type={accountType.value as AccountType} + field={uri} + /> + </p> + </div> + <p> + <TextField + label="Alias" + variant="filled" + placeholder="Easy to remember description" + fullWidth + disabled={accountType.value === ""} + value={alias.value} + onChange={alias.onInput} + /> + </p> + </section> + <section> + <Button + variant="contained" + color="secondary" + onClick={onCancel.onClick} + > + <i18n.Translate>Cancel</i18n.Translate> + </Button> + <Button + variant="contained" + onClick={onAccountAdded.onClick} + disabled={!onAccountAdded.onClick} + > + <i18n.Translate>Add</i18n.Translate> + </Button> + </section> + <section> + {Object.entries(accountByType).map(([type, list]) => { + const Table = tableComponentByAccountType[type as AccountType]; + return <Table key={type} list={list} onDelete={deleteAccount} />; + })} + </section> + </Fragment> + ); +} + +function IbanTable({ + list, + onDelete, +}: { + list: KnownBankAccountsInfo[]; + onDelete: (ac: KnownBankAccountsInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (list.length === 0) return <Fragment />; + return ( + <div> + <h1> + <i18n.Translate>IBAN accounts</i18n.Translate> + </h1> + <AccountTable> + <thead> + <tr> + <th> + <i18n.Translate>Alias</i18n.Translate> + </th> + <th> + <i18n.Translate>Bank Id</i18n.Translate> + </th> + <th> + <i18n.Translate>Int. Account Number</i18n.Translate> + </th> + <th> + <i18n.Translate>Account name</i18n.Translate> + </th> + <th class="kyc"> + <i18n.Translate>KYC</i18n.Translate> + </th> + <th class="actions"></th> + </tr> + </thead> + <tbody> + {list.map((account) => { + const p = account.uri as PaytoUriIBAN; + return ( + <tr key={account.alias}> + <td>{account.alias}</td> + <td>{p.bic}</td> + <td>{p.iban}</td> + <td>{p.params["receiver-name"]}</td> + <td class="kyc"> + {account.kyc_completed ? ( + <SvgIcon + title={i18n.str`KYC done`} + dangerouslySetInnerHTML={{ __html: checkIcon }} + color="green" + /> + ) : ( + <SvgIcon + title={i18n.str`KYC missing`} + dangerouslySetInnerHTML={{ __html: warningIcon }} + color="orange" + /> + )} + </td> + <td class="actions"> + <Button + variant="outlined" + startIcon={deleteIcon} + size="small" + onClick={async () => onDelete(account)} + color="error" + > + Forget + </Button> + </td> + </tr> + ); + })} + </tbody> + </AccountTable> + </div> + ); +} + +function TalerBankTable({ + list, + onDelete, +}: { + list: KnownBankAccountsInfo[]; + onDelete: (ac: KnownBankAccountsInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (list.length === 0) return <Fragment />; + return ( + <div> + <h1> + <i18n.Translate>Taler accounts</i18n.Translate> + </h1> + <AccountTable> + <thead> + <tr> + <th> + <i18n.Translate>Alias</i18n.Translate> + </th> + <th> + <i18n.Translate>Host</i18n.Translate> + </th> + <th> + <i18n.Translate>Account</i18n.Translate> + </th> + <th class="kyc"> + <i18n.Translate>KYC</i18n.Translate> + </th> + <th class="actions"></th> + </tr> + </thead> + <tbody> + {list.map((account) => { + const p = account.uri as PaytoUriTalerBank; + return ( + <tr key={account.alias}> + <td>{account.alias}</td> + <td>{p.host}</td> + <td>{p.account}</td> + <td class="kyc"> + {account.kyc_completed ? ( + <SvgIcon + title={i18n.str`KYC done`} + dangerouslySetInnerHTML={{ __html: checkIcon }} + color="green" + /> + ) : ( + <SvgIcon + title={i18n.str`KYC missing`} + dangerouslySetInnerHTML={{ __html: warningIcon }} + color="orange" + /> + )} + </td> + <td class="actions"> + <Button + variant="outlined" + startIcon={deleteIcon} + size="small" + onClick={async () => onDelete(account)} + color="error" + > + Forget + </Button> + </td> + </tr> + ); + })} + </tbody> + </AccountTable> + </div> + ); +} + +function BitcoinTable({ + list, + onDelete, +}: { + list: KnownBankAccountsInfo[]; + onDelete: (ac: KnownBankAccountsInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (list.length === 0) return <Fragment />; + return ( + <div> + <h2> + <i18n.Translate>Bitcoin accounts</i18n.Translate> + </h2> + <AccountTable> + <thead> + <tr> + <th> + <i18n.Translate>Alias</i18n.Translate> + </th> + <th> + <i18n.Translate>Address</i18n.Translate> + </th> + <th class="kyc"> + <i18n.Translate>KYC</i18n.Translate> + </th> + <th class="actions"></th> + </tr> + </thead> + <tbody> + {list.map((account) => { + const p = account.uri as PaytoUriBitcoin; + return ( + <tr key={account.alias}> + <td>{account.alias}</td> + <td>{p.targetPath}</td> + <td class="kyc"> + {account.kyc_completed ? ( + <SvgIcon + title={i18n.str`KYC done`} + dangerouslySetInnerHTML={{ __html: checkIcon }} + color="green" + /> + ) : ( + <SvgIcon + title={i18n.str`KYC missing`} + dangerouslySetInnerHTML={{ __html: warningIcon }} + color="orange" + /> + )} + </td> + <td class="actions"> + <Button + variant="outlined" + startIcon={deleteIcon} + size="small" + onClick={async () => onDelete(account)} + color="error" + > + Forget + </Button> + </td> + </tr> + ); + })} + </tbody> + </AccountTable> + </div> + ); +} + +function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode { + const { i18n } = useTranslationContext(); + const [value, setValue] = useState<string | undefined>(undefined); + const errors = undefinedIfEmpty({ + value: !value ? i18n.str`Can't be empty` : undefined, + }); + return ( + <Fragment> + <h3> + <i18n.Translate>Bitcoin Account</i18n.Translate> + </h3> + <TextField + label="Bitcoin address" + variant="standard" + fullWidth + value={value} + error={value !== undefined ? errors?.value : undefined} + disabled={!field.onInput} + onChange={(v) => { + setValue(v); + if (!errors && field.onInput) { + const p = buildPayto("bitcoin", v, undefined); + field.onInput(stringifyPaytoUri(p)); + } + }} + /> + </Fragment> + ); +} + +function undefinedIfEmpty<T extends object>(obj: T): T | undefined { + return Object.keys(obj).some((k) => (obj as Record<string,unknown>)[k] !== undefined) + ? obj + : undefined; +} + +function TalerBankAddressAccount({ + field, +}: { + field: TextFieldHandler; +}): VNode { + const { i18n } = useTranslationContext(); + const [host, setHost] = useState<string | undefined>(undefined); + const [account, setAccount] = useState<string | undefined>(undefined); + const errors = undefinedIfEmpty({ + host: !host ? i18n.str`Can't be empty` : undefined, + account: !account ? i18n.str`Can't be empty` : undefined, + }); + return ( + <Fragment> + <h3> + <i18n.Translate>Taler Bank</i18n.Translate> + </h3> + <TextField + label="Bank host" + variant="standard" + fullWidth + value={host} + error={host !== undefined ? errors?.host : undefined} + disabled={!field.onInput} + onChange={(v) => { + setHost(v); + if (!errors && field.onInput && account) { + const p = buildPayto("x-taler-bank", v, account); + field.onInput(stringifyPaytoUri(p)); + } + }} + /> + <TextField + label="Bank account" + variant="standard" + fullWidth + disabled={!field.onInput} + value={account} + error={account !== undefined ? errors?.account : undefined} + onChange={(v) => { + setAccount(v || ""); + if (!errors && field.onInput && host) { + const p = buildPayto("x-taler-bank", host, v); + field.onInput(stringifyPaytoUri(p)); + } + }} + /> + </Fragment> + ); +} + +//Taken from libeufin and libeufin took it from the ISO20022 XSD schema +// const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/; +// const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/; + +function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode { + const { i18n } = useTranslationContext(); + // const [bic, setBic] = useState<string | undefined>(undefined); + const [iban, setIban] = useState<string | undefined>(undefined); + const [name, setName] = useState<string | undefined>(undefined); + const bic = "" + const errorsFN = (iban:string | undefined, name: string | undefined) => undefinedIfEmpty({ + // bic: !bic + // ? undefined + // : !bicRegex.test(bic) + // ? i18n.str`Invalid bic` + // : undefined, + iban: !iban + ? i18n.str`Can't be empty` + : validateIban(iban).type === "invalid" + ? i18n.str`Invalid iban` + : undefined, + name: !name ? i18n.str`Can't be empty` : undefined, + }); + const errors = errorsFN(iban, name) + + function sendUpdateIfNoErrors( + bic: string | undefined, + iban: string, + name: string, + ): void { + if (!field.onInput) return; + if (!errorsFN(iban, name)) { + const p = buildPayto("iban", iban, bic); + p.params["receiver-name"] = name; + field.onInput(stringifyPaytoUri(p)); + } else { + field.onInput("") + } + } + return ( + <Fragment> + <h3> + <i18n.Translate>International Bank Account Number</i18n.Translate> + </h3> + {/* <p> + <TextField + label="BIC" + variant="filled" + placeholder="BANKID" + fullWidth + value={bic} + error={bic !== undefined ? errors?.bic : undefined} + disabled={!field.onInput} + onChange={(v) => { + setBic(v); + sendUpdateIfNoErrors(v, iban || "", name || ""); + }} + /> + </p> */} + <p> + <TextField + label="IBAN" + variant="filled" + placeholder="XX123456" + fullWidth + required + value={iban} + error={iban !== undefined ? errors?.iban : undefined} + disabled={!field.onInput} + onChange={(v) => { + setIban(v); + sendUpdateIfNoErrors(bic, v, name || ""); + }} + /> + </p> + <p> + <TextField + label="Account name" + variant="filled" + placeholder="Name of the target bank account owner" + fullWidth + required + value={name} + error={name !== undefined ? errors?.name : undefined} + disabled={!field.onInput} + onChange={(v) => { + setName(v); + sendUpdateIfNoErrors(bic, iban || "", v); + }} + /> + </p> + </Fragment> + ); +} + +function CustomFieldByAccountType({ + type, + field, +}: { + type: AccountType; + field: TextFieldHandler; +}): VNode { + // const { i18n } = useTranslationContext(); + + const AccountForm = formComponentByAccountType[type]; + + return ( + <div> + <AccountForm field={field} /> + </div> + ); +} |