From fe6e9be70225cf2953822ff64b9e90066cab97ea Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 28 Oct 2022 13:39:06 -0300 Subject: manage account instead of add account --- .../src/wallet/ManageAccount/index.ts | 80 +++ .../src/wallet/ManageAccount/state.ts | 127 +++++ .../src/wallet/ManageAccount/stories.tsx | 208 ++++++++ .../src/wallet/ManageAccount/test.ts | 28 ++ .../src/wallet/ManageAccount/views.tsx | 534 +++++++++++++++++++++ 5 files changed, 977 insertions(+) create mode 100644 packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx (limited to 'packages/taler-wallet-webextension/src/wallet/ManageAccount') diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts new file mode 100644 index 000000000..cd591be74 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts @@ -0,0 +1,80 @@ +/* + 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 + */ + +import { KnownBankAccountsInfo } from "@gnu-taler/taler-util"; +import { Loading } from "../../components/Loading.js"; +import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { + ButtonHandler, + SelectFieldHandler, + TextFieldHandler +} from "../../mui/handlers.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import { wxApi } from "../../wxApi.js"; +import { useComponentState } from "./state.js"; +import { LoadingUriView, ReadyView } from "./views.js"; + +export interface Props { + currency: string; + onAccountAdded: (uri: string) => void; + onCancel: () => void; +} + +export type State = State.Loading | State.LoadingUriError | State.Ready; + +export namespace State { + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "loading-error"; + error: HookError; + } + + export interface BaseInfo { + error: undefined; + } + export interface Ready extends BaseInfo { + status: "ready"; + error: undefined; + currency: string; + accountType: SelectFieldHandler; + uri: TextFieldHandler; + alias: TextFieldHandler; + onAccountAdded: ButtonHandler; + onCancel: ButtonHandler; + accountByType: AccountByType, + deleteAccount: (a: KnownBankAccountsInfo) => Promise, + } +} + +export type AccountByType = { + [key: string]: KnownBankAccountsInfo[] +}; + +const viewMapping: StateViewMap = { + loading: Loading, + "loading-error": LoadingUriView, + ready: ReadyView, +}; + +export const ManageAccountPage = compose( + "ManageAccountPage", + (p: Props) => useComponentState(p, wxApi), + viewMapping, +); diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts new file mode 100644 index 000000000..ad8643133 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts @@ -0,0 +1,127 @@ +/* + 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 + */ + +import { KnownBankAccountsInfo, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import { wxApi } from "../../wxApi.js"; +import { AccountByType, Props, State } from "./index.js"; + +export function useComponentState( + { currency, onAccountAdded, onCancel }: Props, + api: typeof wxApi, +): State { + const hook = useAsyncAsHook(() => api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency })); + + const [payto, setPayto] = useState(""); + const [alias, setAlias] = useState(""); + const [type, setType] = useState(""); + + if (!hook) { + return { + status: "loading", + error: undefined, + }; + } + if (hook.hasError) { + return { + status: "loading-error", + error: hook, + }; + } + + const accountType: Record = { + "": "Choose one account type", + iban: "IBAN", + // bitcoin: "Bitcoin", + // "x-taler-bank": "Taler Bank", + }; + const uri = parsePaytoUri(payto); + const found = + hook.response.accounts.findIndex( + (a) => stringifyPaytoUri(a.uri) === payto, + ) !== -1; + + async function addAccount(): Promise { + if (!uri || found) return; + + const normalizedPayto = stringifyPaytoUri(uri); + await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, { + alias, currency, payto: normalizedPayto + }); + onAccountAdded(payto); + } + + const paytoUriError = + found + ? "that account is already present" + : undefined; + + const unableToAdd = !type || !alias || !!paytoUriError || !uri; + + const accountByType: AccountByType = { + iban: [], + bitcoin: [], + "x-taler-bank": [], + } + + hook.response.accounts.forEach(acc => { + accountByType[acc.uri.targetType].push(acc) + }); + + async function deleteAccount(account: KnownBankAccountsInfo): Promise { + const payto = stringifyPaytoUri(account.uri); + await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, { + payto + }) + hook?.retry() + } + + return { + status: "ready", + error: undefined, + currency, + accountType: { + list: accountType, + value: type, + onChange: async (v) => { + setType(v); + }, + }, + alias: { + value: alias, + onInput: async (v) => { + setAlias(v); + }, + }, + uri: { + value: payto, + error: paytoUriError, + onInput: async (v) => { + setPayto(v); + }, + }, + accountByType, + deleteAccount, + onAccountAdded: { + onClick: unableToAdd ? undefined : addAccount, + }, + onCancel: { + onClick: async () => onCancel(), + }, + }; +} diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx new file mode 100644 index 000000000..c0d3a38b0 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx @@ -0,0 +1,208 @@ +/* + 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createExample } from "../../test-utils.js"; +import { ReadyView } from "./views.js"; + +export default { + title: "wallet/manage account", +}; + +const nullFunction = async () => { + null; +}; + +export const JustTwoBitcoinAccounts = createExample(ReadyView, { + status: "ready", + currency: "ARS", + accountType: { + list: { + "": "Choose one account type", + iban: "IBAN", + // bitcoin: "Bitcoin", + // "x-taler-bank": "Taler Bank", + }, + value: "", + }, + alias: { + value: "", + onInput: nullFunction, + }, + uri: { + value: "", + onInput: nullFunction, + }, + accountByType: { + iban: [], + "x-taler-bank": [], + bitcoin: [ + { + alias: "my bitcoin addr", + currency: "BTC", + kyc_completed: false, + uri: { + targetType: "bitcoin", + segwitAddrs: [], + isKnown: true, + targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + params: {}, + }, + }, + { + alias: "my other addr", + currency: "BTC", + kyc_completed: true, + uri: { + targetType: "bitcoin", + segwitAddrs: [], + isKnown: true, + targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + params: {}, + }, + }, + ], + }, + onAccountAdded: {}, + onCancel: {}, +}); + +export const WithAllTypeOfAccounts = createExample(ReadyView, { + status: "ready", + currency: "ARS", + accountType: { + list: { + "": "Choose one account type", + iban: "IBAN", + // bitcoin: "Bitcoin", + // "x-taler-bank": "Taler Bank", + }, + value: "", + }, + alias: { + value: "", + onInput: nullFunction, + }, + uri: { + value: "", + onInput: nullFunction, + }, + accountByType: { + iban: [ + { + alias: "my bank", + currency: "ARS", + kyc_completed: true, + uri: { + targetType: "iban", + iban: "ASDQWEQWE", + isKnown: true, + targetPath: "/ASDQWEQWE", + params: {}, + }, + }, + ], + "x-taler-bank": [ + { + alias: "my xtaler bank", + currency: "ARS", + kyc_completed: true, + uri: { + targetType: "x-taler-bank", + host: "localhost", + account: "123", + isKnown: true, + targetPath: "localhost/123", + params: {}, + }, + }, + ], + bitcoin: [ + { + alias: "my bitcoin addr", + currency: "BTC", + kyc_completed: false, + uri: { + targetType: "bitcoin", + segwitAddrs: [], + isKnown: true, + targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + params: {}, + }, + }, + { + alias: "my other addr", + currency: "BTC", + kyc_completed: true, + uri: { + targetType: "bitcoin", + segwitAddrs: [], + isKnown: true, + targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + params: {}, + }, + }, + ], + }, + onAccountAdded: {}, + onCancel: {}, +}); + +export const AddingIbanAccount = createExample(ReadyView, { + status: "ready", + currency: "ARS", + accountType: { + list: { + "": "Choose one account type", + iban: "IBAN", + // bitcoin: "Bitcoin", + // "x-taler-bank": "Taler Bank", + }, + value: "iban", + }, + alias: { + value: "", + onInput: nullFunction, + }, + uri: { + value: "", + onInput: nullFunction, + }, + accountByType: { + iban: [ + { + alias: "my bank", + currency: "ARS", + kyc_completed: true, + uri: { + targetType: "iban", + iban: "ASDQWEQWE", + isKnown: true, + targetPath: "/ASDQWEQWE", + params: {}, + }, + }, + ], + "x-taler-bank": [], + bitcoin: [], + }, + onAccountAdded: {}, + onCancel: {}, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts new file mode 100644 index 000000000..eae4d4ca2 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts @@ -0,0 +1,28 @@ +/* + 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { expect } from "chai"; + +describe("test description", () => { + it("should assert", () => { + expect([]).deep.equals([]); + }); +}); 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..9bb9e5814 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -0,0 +1,534 @@ +/* + 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 + */ + +import { + KnownBankAccountsInfo, + PaytoUriBitcoin, + PaytoUriIBAN, + PaytoUriTalerBank, +} from "@gnu-taler/taler-util"; +import { styled } from "@linaria/react"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorMessage } from "../../components/ErrorMessage.js"; +import { LoadingError } from "../../components/LoadingError.js"; +import { SelectList } from "../../components/SelectList.js"; +import { + Input, + LightText, + SubTitle, + SvgIcon, + WarningText, +} from "../../components/styled/index.js"; +import { useTranslationContext } from "../../context/translation.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.svg"; +import warningIcon from "../../svg/warning_24px.svg"; +import deleteIcon from "../../svg/delete_24px.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; + }) => 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 LoadingUriView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + Could not load} + error={error} + /> + ); +} + +export function ReadyView({ + currency, + error, + accountType, + accountByType, + alias, + onAccountAdded, + deleteAccount, + onCancel, + uri, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + +
+ + Known accounts for {currency} + +

+ + To add a new account first select the account type. + +

+ + {error && ( + Unable add this account} + description={error} + /> + )} +

+ + Select account type} + list={accountType.list} + name="accountType" + value={accountType.value} + onChange={accountType.onChange} + /> + +

+ {accountType.value === "" ? undefined : ( + +

+ +

+

+ +

+
+ )} +
+
+ + +
+
+ {Object.entries(accountByType).map(([type, list]) => { + const Table = tableComponentByAccountType[type as AccountType]; + return ; + })} + + + ); +} + +function IbanTable({ + list, + onDelete, +}: { + list: KnownBankAccountsInfo[]; + onDelete: (ac: KnownBankAccountsInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (list.length === 0) return ; + return ( +
+

+ IBAN accounts +

+ +
+ + + + + + + + + {list.map((account) => { + const p = account.uri as PaytoUriIBAN; + return ( + + + + + + + ); + })} + + + + ); +} + +function TalerBankTable({ + list, + onDelete, +}: { + list: KnownBankAccountsInfo[]; + onDelete: (ac: KnownBankAccountsInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (list.length === 0) return ; + return ( +
+

+ Taler accounts +

+ +
+ + + + + + + + + + {list.map((account) => { + const p = account.uri as PaytoUriTalerBank; + return ( + + + + + + + + ); + })} + + + + ); +} + +function BitcoinTable({ + list, + onDelete, +}: { + list: KnownBankAccountsInfo[]; + onDelete: (ac: KnownBankAccountsInfo) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (list.length === 0) return ; + return ( +
+

+ Bitcoin accounts +

+ +
+ + + + + + + + + {list.map((account) => { + const p = account.uri as PaytoUriBitcoin; + return ( + + + + + + + ); + })} + + + + ); +} + +function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode { + const { i18n } = useTranslationContext(); + const [value, setValue] = useState(undefined); + const errors = undefinedIfEmpty({ + value: !value ? i18n.str`Can't be empty` : undefined, + }); + return ( + + { + setValue(v); + if (!errors) { + field.onInput(`payto://bitcoin/${v}`); + } + }} + /> + {value !== undefined && errors?.value && ( + {errors?.value}} /> + )} + + ); +} + +function undefinedIfEmpty(obj: T): T | undefined { + return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) + ? obj + : undefined; +} + +function TalerBankAddressAccount({ + field, +}: { + field: TextFieldHandler; +}): VNode { + const { i18n } = useTranslationContext(); + const [host, setHost] = useState(undefined); + const [account, setAccount] = useState(undefined); + const errors = undefinedIfEmpty({ + host: !host ? i18n.str`Can't be empty` : undefined, + account: !account ? i18n.str`Can't be empty` : undefined, + }); + return ( + + { + setHost(v); + if (!errors) { + field.onInput(`payto://x-taler-bank/${v}/${account}`); + } + }} + />{" "} + {host !== undefined && errors?.host && ( + {errors?.host}} /> + )} + { + setAccount(v || ""); + if (!errors) { + field.onInput(`payto://x-taler-bank/${host}/${v}`); + } + }} + />{" "} + {account !== undefined && errors?.account && ( + {errors?.account}} /> + )} + + ); +} + +function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode { + const { i18n } = useTranslationContext(); + const [value, setValue] = useState(undefined); + const errors = undefinedIfEmpty({ + value: !value ? i18n.str`Can't be empty` : undefined, + }); + return ( + + { + setValue(v); + if (!errors) { + field.onInput(`payto://iban/${v}`); + } + }} + /> + {value !== undefined && errors?.value && ( + {errors?.value}} /> + )} + + ); +} + +function CustomFieldByAccountType({ + type, + field, +}: { + type: AccountType; + field: TextFieldHandler; +}): VNode { + const { i18n } = useTranslationContext(); + + const AccountForm = formComponentByAccountType[type]; + + return ( +
+ + + We can not validate the account so make sure the value is correct. + + + +
+ ); +} -- cgit v1.2.3
+ Alias + + Int. Account Number + + KYC +
{account.alias}{p.targetPath} + {account.kyc_completed ? ( + + ) : ( + + )} + + +
+ Alias + + Host + + Account + + KYC +
{account.alias}{p.host}{p.account} + {account.kyc_completed ? ( + + ) : ( + + )} + + +
+ Alias + + Address + + KYC +
{account.alias}{p.targetPath} + {account.kyc_completed ? ( + + ) : ( + + )} + + +