diff --git a/public/locales/en.json b/public/locales/en.json index 0a5e8a1..a4c8e17 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -156,6 +156,8 @@ "messageSuccessAdd": "Added wallet successfully!", "messageSuccessCreate": "Created wallet successfully!", + "errorPasswordRequired": "Password is required.", + "errorPrivatekeyRequired": "Private key is required.", "errorUnexpectedTitle": "Unexpected error", "errorUnexpectedDescription": "There was an error while adding the wallet. See console for details.", "errorDuplicateWalletTitle": "Wallet already exists", diff --git a/src/App.tsx b/src/App.tsx index 7352243..626f380 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ import "./App.less"; import { AppLayout } from "./layout/AppLayout"; -import { SyncWallets } from "./krist/wallets/SyncWallets"; +import { SyncWallets } from "./components/wallets/SyncWallets"; import { ForcedAuth } from "./components/auth/ForcedAuth"; export const store = createStore( diff --git a/src/components/wallets/SelectWalletCategory.tsx b/src/components/wallets/SelectWalletCategory.tsx new file mode 100644 index 0000000..e659472 --- /dev/null +++ b/src/components/wallets/SelectWalletCategory.tsx @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +import { Select, Input, Button, Typography, Divider } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; + +import { useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; +import { useTranslation } from "react-i18next"; + +import { localeSort } from "../../utils"; + +const { Text } = Typography; + +interface Props { + onNewCategory?: (name: string) => void; +} + +export function getSelectWalletCategory({ onNewCategory }: Props): JSX.Element { + // Required to fetch existing categories + const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const existingCategories = [...new Set(Object.values(wallets) + .filter(w => w.category !== undefined && w.category !== "") + .map(w => w.category) as string[])]; + localeSort(existingCategories); + + const { t } = useTranslation(); + const [input, setInput] = useState(); + const [categories, setCategories] = useState(existingCategories); + + function addCategory() { + if (!input) return; + + const categoryName = input.trim(); + if (!categoryName || categoryName.length > 32 + || categories.includes(categoryName)) return; + + const newCategories = [...categories, categoryName]; + localeSort(newCategories); + + setCategories(newCategories); + setInput(undefined); + + // TODO: fix bug where hitting enter will _sometimes_ not set the right + // category name on the form + + if (onNewCategory) onNewCategory(categoryName); + } + + return setInput(e.target.value)} + onPressEnter={e => { e.preventDefault(); addCategory(); }} + + placeholder={t("addWallet.walletCategoryDropdownNewPlaceholder")} + + size="small" + style={{ flex: 1, height: 24 }} + /> + + + + + ) + }> + + {t("addWallet.walletCategoryDropdownNone")} + + + {categories.map(c => + {c} + )} + ; +} diff --git a/src/components/wallets/SelectWalletFormat.tsx b/src/components/wallets/SelectWalletFormat.tsx new file mode 100644 index 0000000..2ecf6b7 --- /dev/null +++ b/src/components/wallets/SelectWalletFormat.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Select } from "antd"; + +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; + +import { useTranslation } from "react-i18next"; + +import { WalletFormatName, ADVANCED_FORMATS } from "../../krist/wallets/formats/WalletFormat"; + +interface Props { + initialFormat: WalletFormatName; +} + +export function getSelectWalletFormat({ initialFormat }: Props): JSX.Element { + const advancedWalletFormats = useSelector((s: RootState) => s.settings.walletFormats); + const { t } = useTranslation(); + + return ; +} diff --git a/src/components/wallets/SyncWallets.tsx b/src/components/wallets/SyncWallets.tsx new file mode 100644 index 0000000..85430b3 --- /dev/null +++ b/src/components/wallets/SyncWallets.tsx @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; + +import { useDispatch, useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; + +import { syncWallets } from "../../krist/wallets/Wallet"; + +/** Sync the wallets with the Krist node on startup. */ +export function SyncWallets(): JSX.Element | null { + const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const dispatch = useDispatch(); + + const [synced, setSynced] = useState(false); + + useEffect(() => { + if (synced) return; + setSynced(true); + + // TODO: show errors to the user? + syncWallets(dispatch, wallets).catch(console.error); + }, [synced]); + + return null; +} diff --git a/src/components/wallets/WalletCategoryDropdown.tsx b/src/components/wallets/WalletCategoryDropdown.tsx deleted file mode 100644 index 9205b2c..0000000 --- a/src/components/wallets/WalletCategoryDropdown.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState } from "react"; -import { Select, Input, Button, Typography, Divider } from "antd"; -import { PlusOutlined } from "@ant-design/icons"; - -import { useTranslation } from "react-i18next"; - -const { Text } = Typography; - -interface Props { - onNewCategory?: (name: string) => void; -} - -export function getWalletCategoryDropdown({ onNewCategory }: Props): JSX.Element { - const { t } = useTranslation(); - const [input, setInput] = useState(); - const [categories, setCategories] = useState(["Test category"]); - - function addCategory() { - if (!input) return; - - const categoryName = input.trim(); - if (!categoryName || categoryName.length > 32 - || categories.includes(categoryName)) return; - - const newCategories = [...categories, categoryName]; - newCategories.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true })); - - setCategories(newCategories); - setInput(undefined); - - // TODO: fix bug where hitting enter will _sometimes_ not set the right - // category name on the form - - if (onNewCategory) onNewCategory(categoryName); - } - - return setInput(e.target.value)} - onPressEnter={e => { e.preventDefault(); addCategory(); }} - - placeholder={t("addWallet.walletCategoryDropdownNewPlaceholder")} - - size="small" - style={{ flex: 1, height: 24 }} - /> - - - - - ) - }> - - {t("addWallet.walletCategoryDropdownNone")} - - - {categories.map(c => - {c} - )} - ; -} diff --git a/src/krist/wallets/SyncWallets.tsx b/src/krist/wallets/SyncWallets.tsx deleted file mode 100644 index 8ce8d30..0000000 --- a/src/krist/wallets/SyncWallets.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useState, useEffect } from "react"; - -import { useDispatch, useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; - -import { syncWallets } from "./Wallet"; - -/** Sync the wallets with the Krist node on startup. */ -export function SyncWallets(): JSX.Element | null { - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); - const dispatch = useDispatch(); - - const [synced, setSynced] = useState(false); - - useEffect(() => { - if (synced) return; - setSynced(true); - - // TODO: show errors to the user? - syncWallets(dispatch, wallets).catch(console.error); - }, [synced]); - - return null; -} diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index 9133e0e..002ab6f 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -189,8 +189,8 @@ const newWallet = { id, address, - label: wallet.label, - category: wallet.category, + label: wallet.label?.trim() || undefined, // clean up empty strings + category: wallet.category?.trim() || undefined, username: wallet.username, encPassword, diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index 8666956..aceae3a 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -10,10 +10,10 @@ import { FakeUsernameInput } from "../../components/auth/FakeUsernameInput"; import { CopyInputButton } from "../../components/CopyInputButton"; -import { getWalletCategoryDropdown } from "../../components/wallets/WalletCategoryDropdown"; +import { getSelectWalletCategory } from "../../components/wallets/SelectWalletCategory"; import { WalletFormatName, applyWalletFormat, formatNeedsUsername } from "../../krist/wallets/formats/WalletFormat"; -import { getSelectWalletFormat } from "./SelectWalletFormat"; +import { getSelectWalletFormat } from "../../components/wallets/SelectWalletFormat"; import { makeV2Address } from "../../krist/AddressAlgo"; import { addWallet } from "../../krist/wallets/Wallet"; @@ -55,7 +55,9 @@ async function onSubmit() { if (!masterPassword) throw new Error(t("masterPassword.errorNoPassword")); + const values = await form.validateFields(); + if (!values.password) return; // Check if the wallet already exists if (Object.values(wallets).find(w => w.address === calculatedAddress)) { @@ -149,7 +151,7 @@ {/* Wallet category */} - {getWalletCategoryDropdown({ onNewCategory: category => form.setFieldsValue({ category })})} + {getSelectWalletCategory({ onNewCategory: category => form.setFieldsValue({ category })})} @@ -172,7 +174,18 @@ style={{ marginBottom: 0 }} > - + s.settings.walletFormats); - const { t } = useTranslation(); - - return ; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 4480562..716ff6f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -37,3 +37,11 @@ .map(x => charset[x % charset.length]) .join(""); } + +/** Sort an array in-place in a human-friendly manner. */ +export function localeSort(arr: any[]): void { + arr.sort((a, b) => a.localeCompare(b, undefined, { + sensitivity: "base", + numeric: true + })); +}