diff options
author | Sebastian <sebasjm@gmail.com> | 2022-10-28 13:39:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-10-28 13:39:26 -0300 |
commit | fe6e9be70225cf2953822ff64b9e90066cab97ea (patch) | |
tree | d3c2f2829a963efd6596dbc7fd04d2a325bd2ae0 /packages | |
parent | 7c33040ae377548e5589b11ad218c8b95b82c9c5 (diff) |
manage account instead of add account
Diffstat (limited to 'packages')
15 files changed, 867 insertions, 362 deletions
diff --git a/packages/taler-wallet-webextension/src/svg/check_24px.svg b/packages/taler-wallet-webextension/src/svg/check_24px.svg new file mode 100644 index 000000000..522695ef3 --- /dev/null +++ b/packages/taler-wallet-webextension/src/svg/check_24px.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/svg/warning_24px.svg b/packages/taler-wallet-webextension/src/svg/warning_24px.svg new file mode 100644 index 000000000..d27c4c6ec --- /dev/null +++ b/packages/taler-wallet-webextension/src/svg/warning_24px.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/AddAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddAccount/stories.tsx deleted file mode 100644 index 696e424c4..000000000 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - 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 { createExample } from "../../test-utils.js"; -import { ReadyView } from "./views.js"; - -export default { - title: "example", -}; - -export const Ready = createExample(ReadyView, {}); diff --git a/packages/taler-wallet-webextension/src/wallet/AddAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddAccount/views.tsx deleted file mode 100644 index d6ab7e967..000000000 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/views.tsx +++ /dev/null @@ -1,249 +0,0 @@ -/* - 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 { parsePaytoUri } from "@gnu-taler/taler-util"; -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 } 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 { State } from "./index.js"; - -export function LoadingUriView({ error }: State.LoadingUriError): VNode { - const { i18n } = useTranslationContext(); - - return ( - <LoadingError - title={<i18n.Translate>Could not load</i18n.Translate>} - error={error} - /> - ); -} - -export function ReadyView({ - currency, - error, - accountType, - alias, - onAccountAdded, - onCancel, - uri, -}: State.Ready): VNode { - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - <section> - <SubTitle> - <i18n.Translate>Add bank account for {currency}</i18n.Translate> - </SubTitle> - <LightText> - <i18n.Translate> - Enter the URL of an exchange you trust. - </i18n.Translate> - </LightText> - - {error && ( - <ErrorMessage - title={<i18n.Translate>Unable add this account</i18n.Translate>} - description={error} - /> - )} - <p> - <Input> - <SelectList - label={<i18n.Translate>Select account type</i18n.Translate>} - list={accountType.list} - name="accountType" - value={accountType.value} - onChange={accountType.onChange} - /> - </Input> - </p> - {accountType.value === "" ? undefined : ( - <Fragment> - <p> - <CustomFieldByAccountType type={accountType.value} field={uri} /> - </p> - <p> - <TextField - label="Account alias" - variant="standard" - required - fullWidth - disabled={accountType.value === ""} - value={alias.value} - onChange={alias.onInput} - /> - </p> - </Fragment> - )} - </section> - <footer> - <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> - </footer> - </Fragment> - ); -} - -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> - <TextField - label="Bitcoin address" - variant="standard" - fullWidth - value={value} - error={value !== undefined && !!errors?.value} - onChange={(v) => { - setValue(v); - if (!errors) { - field.onInput(`payto://bitcoin/${value}`); - } - }} - /> - {value !== undefined && errors?.value && ( - <ErrorMessage title={<span>{errors?.value}</span>} /> - )} - </Fragment> - ); -} - -function undefinedIfEmpty<T extends object>(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<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> - <TextField - label="Bank host" - variant="standard" - fullWidth - value={host} - error={host !== undefined && !!errors?.host} - onChange={(v) => { - setHost(v); - if (!errors) { - field.onInput(`payto://x-taler-bank/${host}/${account}`); - } - }} - />{" "} - {host !== undefined && errors?.host && ( - <ErrorMessage title={<span>{errors?.host}</span>} /> - )} - <TextField - label="Bank account" - variant="standard" - fullWidth - value={account} - error={account !== undefined && !!errors?.account} - onChange={(v) => { - setAccount(v || ""); - if (!errors) { - field.onInput(`payto://x-taler-bank/${host}/${account}`); - } - }} - />{" "} - {account !== undefined && errors?.account && ( - <ErrorMessage title={<span>{errors?.account}</span>} /> - )} - </Fragment> - ); -} - -function IbanAddressAccount({ 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> - <TextField - label="IBAN number" - variant="standard" - fullWidth - value={value} - error={value !== undefined && !!errors?.value} - onChange={(v) => { - setValue(v); - if (!errors) { - field.onInput(`payto://iba/${value}`); - } - }} - /> - {value !== undefined && errors?.value && ( - <ErrorMessage title={<span>{errors?.value}</span>} /> - )} - </Fragment> - ); -} - -function CustomFieldByAccountType({ - type, - field, -}: { - type: string; - field: TextFieldHandler; -}): VNode { - if (type === "bitcoin") { - return <BitcoinAddressAccount field={field} />; - } - if (type === "x-taler-bank") { - return <TalerBankAddressAccount field={field} />; - } - if (type === "iban") { - return <IbanAddressAccount field={field} />; - } - return <Fragment />; -} diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts index 77661fe15..85896da26 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -24,7 +24,7 @@ import { } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; import { wxApi } from "../../wxApi.js"; -import { AddAccountPage } from "../AddAccount/index.js"; +import { ManageAccountPage } from "../ManageAccount/index.js"; import { useComponentState } from "./state.js"; import { AmountOrCurrencyErrorView, @@ -62,7 +62,7 @@ export namespace State { } export interface AddingAccount { - status: "adding-account"; + status: "manage-account"; error: undefined; currency: string; onAccountAdded: (p: string) => void; @@ -94,7 +94,7 @@ export namespace State { error: undefined; currency: string; - selectedAccount: PaytoUri | undefined; + currentAccount: PaytoUri; totalFee: AmountJson; totalToDeposit: AmountJson; @@ -112,7 +112,7 @@ const viewMapping: StateViewMap<State> = { "amount-or-currency-error": AmountOrCurrencyErrorView, "no-enough-balance": NoEnoughBalanceView, "no-accounts": NoAccountToDepositView, - "adding-account": AddAccountPage, + "manage-account": ManageAccountPage, ready: ReadyView, }; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index b3a377040..fe692e80d 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -50,9 +50,7 @@ export function useComponentState( // const [accountIdx, setAccountIdx] = useState<number>(0); const [amount, setAmount] = useState(initialValue); - const [selectedAccount, setSelectedAccount] = useState< - PaytoUri | undefined - >(); + const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined); const [addingAccount, setAddingAccount] = useState(false); @@ -82,7 +80,7 @@ export function useComponentState( if (addingAccount) { return { - status: "adding-account", + status: "manage-account", error: undefined, currency, onAccountAdded: (p: string) => { @@ -92,6 +90,7 @@ export function useComponentState( }, onCancel: () => { setAddingAccount(false); + hook.retry(); }, }; } @@ -122,13 +121,12 @@ export function useComponentState( }, }; } + const firstAccount = accounts[0].uri + const currentAccount = !selectedAccount ? firstAccount : selectedAccount; const accountMap = createLabelsForBankAccount(accounts); - accountMap[""] = "Select one account..."; async function updateAccountFromList(accountStr: string): Promise<void> { - // const newSelected = !accountMap[accountStr] ? undefined : accountMap[accountStr]; - // if (!newSelected) return; const uri = !accountStr ? undefined : parsePaytoUri(accountStr); if (uri && parsedAmount) { try { @@ -136,7 +134,6 @@ export function useComponentState( setSelectedAccount(uri); setFee(result); } catch (e) { - console.error(e) setSelectedAccount(uri); setFee(undefined); } @@ -145,13 +142,12 @@ export function useComponentState( async function updateAmount(numStr: string): Promise<void> { const parsed = Amounts.parse(`${currency}:${numStr}`); - if (parsed && selectedAccount) { + if (parsed) { try { - const result = await getFeeForAmount(selectedAccount, parsed, api); + const result = await getFeeForAmount(currentAccount, parsed, api); setAmount(numStr); setFee(result); } catch (e) { - console.error(e) setAmount(numStr); setFee(undefined); } @@ -179,15 +175,14 @@ export function useComponentState( const unableToDeposit = !parsedAmount || //no amount specified - selectedAccount === undefined || //no account selected Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee fee === undefined || //no fee calculated yet amountError !== undefined; //amount field may be invalid async function doSend(): Promise<void> { - if (!selectedAccount || !parsedAmount || !currency) return; + if (!parsedAmount || !currency) return; - const depositPaytoUri = `payto://${selectedAccount.targetType}/${selectedAccount.targetPath}`; + const depositPaytoUri = `payto://${currentAccount.targetType}/${currentAccount.targetPath}`; const amount = Amounts.stringify(parsedAmount); await api.wallet.call(WalletApiOperation.CreateDepositGroup, { amount, depositPaytoUri @@ -211,10 +206,10 @@ export function useComponentState( }, account: { list: accountMap, - value: !selectedAccount ? "" : stringifyPaytoUri(selectedAccount), + value: stringifyPaytoUri(currentAccount), onChange: updateAccountFromList, }, - selectedAccount, + currentAccount, cancelHandler: { onClick: async () => { onCancel(currency); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx index ed5945c06..64b2c91a7 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx @@ -55,6 +55,13 @@ export const WithNoAccountForIBAN = createExample(ReadyView, { null; }, }, + currentAccount: { + isKnown: true, + targetType: "iban", + iban: "ABCD1234", + params: {}, + targetPath: "/ABCD1234", + }, currency: "USD", amount: { onInput: async () => { @@ -83,6 +90,13 @@ export const WithIBANAccountTypeSelected = createExample(ReadyView, { null; }, }, + currentAccount: { + isKnown: true, + targetType: "iban", + iban: "ABCD1234", + params: {}, + targetPath: "/ABCD1234", + }, currency: "USD", amount: { onInput: async () => { @@ -111,6 +125,13 @@ export const NewBitcoinAccountTypeSelected = createExample(ReadyView, { null; }, }, + currentAccount: { + isKnown: true, + targetType: "iban", + iban: "ABCD1234", + params: {}, + targetPath: "/ABCD1234", + }, onAddAccount: {}, currency: "USD", amount: { diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 62097c3e4..4a648312e 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -172,7 +172,7 @@ describe("DepositPage states", () => { if (r.status !== "ready") expect.fail(); expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); - expect(r.account.value).eq(""); + expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); expect(r.amount.value).eq("0"); expect(r.depositHandler.onClick).undefined; } @@ -195,7 +195,7 @@ describe("DepositPage states", () => { }], }) handler.addWalletCallResponse(WalletApiOperation.ListKnownBankAccounts, undefined, { - accounts: [ibanPayto] + accounts: [talerBankPayto, ibanPayto] }); handler.addWalletCallResponse(WalletApiOperation.GetFeeForDeposit, undefined, withoutFee()) handler.addWalletCallResponse(WalletApiOperation.GetFeeForDeposit, undefined, withoutFee()) @@ -221,7 +221,7 @@ describe("DepositPage states", () => { if (r.status !== "ready") expect.fail(); expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); - expect(r.account.value).eq(""); + expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); expect(r.amount.value).eq("0"); expect(r.depositHandler.onClick).undefined; expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); @@ -328,7 +328,7 @@ describe("DepositPage states", () => { }], }) handler.addWalletCallResponse(WalletApiOperation.ListKnownBankAccounts, undefined, { - accounts: [ibanPayto] + accounts: [talerBankPayto, ibanPayto] }); handler.addWalletCallResponse(WalletApiOperation.GetFeeForDeposit, undefined, withSomeFee()) handler.addWalletCallResponse(WalletApiOperation.GetFeeForDeposit, undefined, withSomeFee()) @@ -353,7 +353,7 @@ describe("DepositPage states", () => { if (r.status !== "ready") expect.fail(); expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); - expect(r.account.value).eq(""); + expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); expect(r.amount.value).eq("0"); expect(r.depositHandler.onClick).undefined; expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx index ddb23c9bb..e864c8413 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx @@ -160,61 +160,55 @@ export function ReadyView(state: State.Ready): VNode { variant="text" style={{ marginLeft: "auto" }} > - <i18n.Translate>Add another account</i18n.Translate> + <i18n.Translate>Manage accounts</i18n.Translate> </Button> </div> - {state.selectedAccount && ( - <Fragment> - <p> - <AccountDetails account={state.selectedAccount} /> - </p> - <InputWithLabel invalid={!!state.amount.error}> - <label> - <i18n.Translate>Amount</i18n.Translate> - </label> - <div> - <span>{state.currency}</span> - <input - type="number" - value={state.amount.value} - onInput={(e) => state.amount.onInput(e.currentTarget.value)} - /> - </div> - {state.amount.error && ( - <ErrorText>{state.amount.error}</ErrorText> - )} - </InputWithLabel> - - <InputWithLabel> - <label> - <i18n.Translate>Deposit fee</i18n.Translate> - </label> - <div> - <span>{state.currency}</span> - <input - type="number" - disabled - value={Amounts.stringifyValue(state.totalFee)} - /> - </div> - </InputWithLabel> - - <InputWithLabel> - <label> - <i18n.Translate>Total deposit</i18n.Translate> - </label> - <div> - <span>{state.currency}</span> - <input - type="number" - disabled - value={Amounts.stringifyValue(state.totalToDeposit)} - /> - </div> - </InputWithLabel> - </Fragment> - )} + <p> + <AccountDetails account={state.currentAccount} /> + </p> + <InputWithLabel invalid={!!state.amount.error}> + <label> + <i18n.Translate>Amount</i18n.Translate> + </label> + <div> + <span>{state.currency}</span> + <input + type="number" + value={state.amount.value} + onInput={(e) => state.amount.onInput(e.currentTarget.value)} + /> + </div> + {state.amount.error && <ErrorText>{state.amount.error}</ErrorText>} + </InputWithLabel> + + <InputWithLabel> + <label> + <i18n.Translate>Deposit fee</i18n.Translate> + </label> + <div> + <span>{state.currency}</span> + <input + type="number" + disabled + value={Amounts.stringifyValue(state.totalFee)} + /> + </div> + </InputWithLabel> + + <InputWithLabel> + <label> + <i18n.Translate>Total deposit</i18n.Translate> + </label> + <div> + <span>{state.currency}</span> + <input + type="number" + disabled + value={Amounts.stringifyValue(state.totalToDeposit)} + /> + </div> + </InputWithLabel> </section> <footer> <Button diff --git a/packages/taler-wallet-webextension/src/wallet/AddAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts index 09609a8a1..cd591be74 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { KnownBankAccountsInfo } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; import { @@ -57,17 +58,23 @@ export namespace State { alias: TextFieldHandler; onAccountAdded: ButtonHandler; onCancel: ButtonHandler; + accountByType: AccountByType, + deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>, } } +export type AccountByType = { + [key: string]: KnownBankAccountsInfo[] +}; + const viewMapping: StateViewMap<State> = { loading: Loading, "loading-error": LoadingUriView, ready: ReadyView, }; -export const AddAccountPage = compose( - "AddAccount", +export const ManageAccountPage = compose( + "ManageAccountPage", (p: Props) => useComponentState(p, wxApi), viewMapping, ); diff --git a/packages/taler-wallet-webextension/src/wallet/AddAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts index 6c113d732..ad8643133 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts @@ -14,12 +14,12 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +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 { Props, State } from "./index.js"; +import { AccountByType, Props, State } from "./index.js"; export function useComponentState( { currency, onAccountAdded, onCancel }: Props, @@ -45,10 +45,10 @@ export function useComponentState( } const accountType: Record<string, string> = { - "": "Choose one account", + "": "Choose one account type", iban: "IBAN", - bitcoin: "Bitcoin", - "x-taler-bank": "Taler Bank", + // bitcoin: "Bitcoin", + // "x-taler-bank": "Taler Bank", }; const uri = parsePaytoUri(payto); const found = @@ -73,6 +73,24 @@ export function useComponentState( 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<void> { + const payto = stringifyPaytoUri(account.uri); + await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, { + payto + }) + hook?.retry() + } + return { status: "ready", error: undefined, @@ -97,6 +115,8 @@ export function useComponentState( setPayto(v); }, }, + accountByType, + deleteAccount, onAccountAdded: { onClick: unableToAdd ? undefined : addAccount, }, 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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @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/AddAccount/test.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts index eae4d4ca2..eae4d4ca2 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddAccount/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts 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 <http://www.gnu.org/licenses/> + */ + +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<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 LoadingUriView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + <LoadingError + title={<i18n.Translate>Could not load</i18n.Translate>} + error={error} + /> + ); +} + +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.Translate>Unable add this account</i18n.Translate>} + description={error} + /> + )} + <p> + <Input> + <SelectList + label={<i18n.Translate>Select account type</i18n.Translate>} + list={accountType.list} + name="accountType" + value={accountType.value} + onChange={accountType.onChange} + /> + </Input> + </p> + {accountType.value === "" ? undefined : ( + <Fragment> + <p> + <CustomFieldByAccountType + type={accountType.value as AccountType} + field={uri} + /> + </p> + <p> + <TextField + label="Account alias" + variant="standard" + required + fullWidth + disabled={accountType.value === ""} + value={alias.value} + onChange={alias.onInput} + /> + </p> + </Fragment> + )} + </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>Int. Account Number</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.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 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> + <TextField + label="Bitcoin address" + variant="standard" + fullWidth + value={value} + error={value !== undefined && !!errors?.value} + onChange={(v) => { + setValue(v); + if (!errors) { + field.onInput(`payto://bitcoin/${v}`); + } + }} + /> + {value !== undefined && errors?.value && ( + <ErrorMessage title={<span>{errors?.value}</span>} /> + )} + </Fragment> + ); +} + +function undefinedIfEmpty<T extends object>(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<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> + <TextField + label="Bank host" + variant="standard" + fullWidth + value={host} + error={host !== undefined && !!errors?.host} + onChange={(v) => { + setHost(v); + if (!errors) { + field.onInput(`payto://x-taler-bank/${v}/${account}`); + } + }} + />{" "} + {host !== undefined && errors?.host && ( + <ErrorMessage title={<span>{errors?.host}</span>} /> + )} + <TextField + label="Bank account" + variant="standard" + fullWidth + value={account} + error={account !== undefined && !!errors?.account} + onChange={(v) => { + setAccount(v || ""); + if (!errors) { + field.onInput(`payto://x-taler-bank/${host}/${v}`); + } + }} + />{" "} + {account !== undefined && errors?.account && ( + <ErrorMessage title={<span>{errors?.account}</span>} /> + )} + </Fragment> + ); +} + +function IbanAddressAccount({ 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> + <TextField + label="IBAN number" + variant="standard" + fullWidth + value={value} + error={value !== undefined && !!errors?.value} + onChange={(v) => { + setValue(v); + if (!errors) { + field.onInput(`payto://iban/${v}`); + } + }} + /> + {value !== undefined && errors?.value && ( + <ErrorMessage title={<span>{errors?.value}</span>} /> + )} + </Fragment> + ); +} + +function CustomFieldByAccountType({ + type, + field, +}: { + type: AccountType; + field: TextFieldHandler; +}): VNode { + const { i18n } = useTranslationContext(); + + const AccountForm = formComponentByAccountType[type]; + + return ( + <div> + <WarningText> + <i18n.Translate> + We can not validate the account so make sure the value is correct. + </i18n.Translate> + </WarningText> + <AccountForm field={field} /> + </div> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx index c2f0c6481..d63f25ead 100644 --- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx @@ -37,6 +37,7 @@ 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"; +import * as a20 from "./ManageAccount/stories.js"; export default [ a1, @@ -57,4 +58,5 @@ export default [ a17, a18, a19, + a20, ]; |