diff --git a/src/renderer/components/header/lock/HeaderLock.tsx b/src/renderer/components/header/lock/HeaderLock.tsx index 81ab351eb..d937e13b6 100644 --- a/src/renderer/components/header/lock/HeaderLock.tsx +++ b/src/renderer/components/header/lock/HeaderLock.tsx @@ -44,7 +44,7 @@ export const HeaderLock: React.FC = (props): JSX.Element => { text-text2 dark:bg-gray0d dark:text-text2d "> - {truncateMiddle(name, { start: 4, end: 4, max: 16 })} + {truncateMiddle(name, { start: 3, end: 3, max: 11 })}

) ) diff --git a/src/renderer/components/settings/WalletSettings.tsx b/src/renderer/components/settings/WalletSettings.tsx index ab2d38127..2417d4cf5 100644 --- a/src/renderer/components/settings/WalletSettings.tsx +++ b/src/renderer/components/settings/WalletSettings.tsx @@ -60,7 +60,9 @@ import { WalletAddressAsync, KeystoreUnlocked, ChangeKeystoreWalletHandler, - ChangeKeystoreWalletRD + ChangeKeystoreWalletRD, + RenameKeystoreWalletHandler, + RenameKeystoreWalletRD } from '../../services/wallet/types' import { walletTypeToI18n } from '../../services/wallet/util' import { AttentionIcon } from '../icons' @@ -77,8 +79,9 @@ type Props = { network: Network walletAccounts: O.Option lockWallet: FP.Lazy - removeKeystore: RemoveKeystoreWalletHandler - changeKeystore$: ChangeKeystoreWalletHandler + removeKeystoreWallet: RemoveKeystoreWalletHandler + changeKeystoreWallet$: ChangeKeystoreWalletHandler + renameKeystoreWallet$: RenameKeystoreWalletHandler exportKeystore: () => Promise addLedgerAddress: (chain: Chain, walletIndex: number) => void verifyLedgerAddress: (chain: Chain, walletIndex: number) => Promise @@ -100,13 +103,14 @@ export const WalletSettings: React.FC = (props): JSX.Element => { network, walletAccounts: oWalletAccounts, lockWallet, - removeKeystore, - changeKeystore$, + removeKeystoreWallet, + changeKeystoreWallet$, + renameKeystoreWallet$, exportKeystore, addLedgerAddress, verifyLedgerAddress, removeLedgerAddress, - keystore: { phrase, name: walletName }, + keystore: { phrase, name: walletName, id: walletId }, wallets, clickAddressLinkHandler, validatePassword$, @@ -125,7 +129,7 @@ export const WalletSettings: React.FC = (props): JSX.Element => { const closeQrModal = useCallback(() => setShowQRModal(O.none), [setShowQRModal]) const removeWalletHandler = useCallback(async () => { - const noWallets = await removeKeystore() + const noWallets = await removeKeystoreWallet() if (noWallets >= 1) { // goto unlock screen to unlock another wallet navigate(walletRoutes.locked.path()) @@ -133,7 +137,7 @@ export const WalletSettings: React.FC = (props): JSX.Element => { // no wallet -> go to homepage navigate(appRoutes.base.template) } - }, [removeKeystore, navigate]) + }, [removeKeystoreWallet, navigate]) const onSuccessPassword = useCallback(() => { setShowPasswordModal(false) @@ -448,11 +452,11 @@ export const WalletSettings: React.FC = (props): JSX.Element => { const changeWalletHandler = useCallback( (id: KeystoreId) => { - subscribeChangeWalletState(changeKeystore$(id)) + subscribeChangeWalletState(changeKeystoreWallet$(id)) // Jump to `UnlockView` to avoid staying at wallet settings navigate(walletRoutes.locked.path()) }, - [changeKeystore$, navigate, subscribeChangeWalletState] + [changeKeystoreWallet$, navigate, subscribeChangeWalletState] ) const renderChangeWalletError = useMemo( @@ -474,9 +478,33 @@ export const WalletSettings: React.FC = (props): JSX.Element => { [changeWalletState, intl] ) - const changeWalletNameHandler = useCallback((walletName: string) => { - console.log('walletName:', walletName) - }, []) + const { state: renameWalletState, subscribe: subscribeRenameWalletState } = + useSubscriptionState(RD.initial) + + const changeWalletNameHandler = useCallback( + (walletName: string) => { + subscribeRenameWalletState(renameKeystoreWallet$(walletId, walletName)) + }, + [renameKeystoreWallet$, subscribeRenameWalletState, walletId] + ) + + const renderRenameWalletError = useMemo( + () => + FP.pipe( + renameWalletState, + RD.fold( + () => <>, + () => <>, + (error) => ( +

+ {intl.formatMessage({ id: 'wallet.name.error.rename' })} {error?.message ?? error.toString()} +

+ ), + () => <> + ) + ), + [intl, renameWalletState] + ) return (
@@ -518,8 +546,13 @@ export const WalletSettings: React.FC = (props): JSX.Element => {

{intl.formatMessage({ id: 'setting.wallet.management' })}

- -
+ + {renderRenameWalletError} +
void className?: string } @@ -21,9 +23,10 @@ type FormData = { } export const EditableWalletName: React.FC = (props): JSX.Element => { - const { name, onChange, className = '' } = props + const { name: initialName, onChange, loading = false, className = '' } = props const [editableName, setEditableName] = useState>(O.none) + const [name, setName] = useState(initialName) const intl = useIntl() @@ -39,18 +42,22 @@ export const EditableWalletName: React.FC = (props): JSX.Element => { setEditableName(O.some(name)) } return ( -
{name} -
+
) - }, [name]) + }, [loading, name]) const renderEditableName = useCallback( (name: string) => { - const confirm = () => FP.pipe(editableName, O.fold(FP.constVoid, onChange)) const cancel = () => { reset() setEditableName(O.none) @@ -58,25 +65,22 @@ export const EditableWalletName: React.FC = (props): JSX.Element => { const submit = ({ name }: FormData) => { setEditableName(O.none) onChange(name) + setName(name) reset() } const keyDownHandler = (e: React.KeyboardEvent) => { - console.log('key down', e.key) if (e.key === 'Escape') { cancel() } } - const error = !!errors.name - return (
= (props): JSX.Element => { error={!!errors.name} onKeyDown={keyDownHandler} /> - - + +
@@ -99,13 +103,14 @@ export const EditableWalletName: React.FC = (props): JSX.Element => { ) }, - [editableName, errors.name, handleSubmit, intl, onChange, register, reset] + [errors.name, handleSubmit, intl, onChange, register, reset] ) return (

- {intl.formatMessage({ id: 'wallet.name' })}{' '} + {intl.formatMessage({ id: 'wallet.name' })} + {/* show info about max. chars in editable mode only */} {FP.pipe( editableName, O.fold( @@ -118,7 +123,12 @@ export const EditableWalletName: React.FC = (props): JSX.Element => { ) )}

- {FP.pipe(editableName, O.fold(renderName, renderEditableName))} +
+ {FP.pipe(editableName, O.fold(renderName, renderEditableName))} +
) } diff --git a/src/renderer/hooks/useKeystoreState.ts b/src/renderer/hooks/useKeystoreState.ts index c978d6a84..3dd903f7b 100644 --- a/src/renderer/hooks/useKeystoreState.ts +++ b/src/renderer/hooks/useKeystoreState.ts @@ -9,7 +9,8 @@ import { KeystoreState, Phrase, RemoveKeystoreWalletHandler, - ChangeKeystoreWalletHandler + ChangeKeystoreWalletHandler, + RenameKeystoreWalletHandler } from '../services/wallet/types' import { getPhrase, getWalletName, isLocked } from '../services/wallet/util' @@ -19,12 +20,20 @@ export const useKeystoreState = (): { walletName: O.Option remove: RemoveKeystoreWalletHandler change$: ChangeKeystoreWalletHandler + rename$: RenameKeystoreWalletHandler unlock: (password: string) => Promise lock: FP.Lazy locked: boolean } => { const { - keystoreService: { keystore$, unlock, lock, removeKeystoreWallet: remove, changeKeystoreWallet: change$ } + keystoreService: { + keystore$, + unlock, + lock, + removeKeystoreWallet: remove, + changeKeystoreWallet: change$, + renameKeystoreWallet: rename$ + } } = useWalletContext() const state = useObservableState(keystore$, INITIAL_KEYSTORE_STATE) @@ -33,5 +42,5 @@ export const useKeystoreState = (): { const [walletName] = useObservableState(() => FP.pipe(keystore$, RxOp.map(FP.flow(getWalletName))), O.none) const [locked] = useObservableState(() => FP.pipe(keystore$, RxOp.map(FP.flow(isLocked))), false) - return { state, phrase, walletName, unlock, lock, locked, remove, change$ } + return { state, phrase, walletName, unlock, lock, locked, remove, change$, rename$ } } diff --git a/src/renderer/i18n/de/wallet.ts b/src/renderer/i18n/de/wallet.ts index c8b500e80..068ad78c2 100644 --- a/src/renderer/i18n/de/wallet.ts +++ b/src/renderer/i18n/de/wallet.ts @@ -4,6 +4,7 @@ const wallet: WalletMessages = { 'wallet.name': 'Wallet Name', 'wallet.name.maxChars': 'Max. {max} Zeichen', 'wallet.name.error.empty': 'Bitte Namen der Wallet angeben', + 'wallet.name.error.rename': 'Error beim Umbenennen der Wallet', 'wallet.nav.deposits': 'Einzahlungen', 'wallet.nav.bonds': 'Bonds', 'wallet.nav.poolshares': 'Anteile', diff --git a/src/renderer/i18n/en/wallet.ts b/src/renderer/i18n/en/wallet.ts index bb495d33f..e8cee3e2e 100644 --- a/src/renderer/i18n/en/wallet.ts +++ b/src/renderer/i18n/en/wallet.ts @@ -4,6 +4,7 @@ const wallet: WalletMessages = { 'wallet.name': 'Wallet name', 'wallet.name.maxChars': 'Max. {max} chars', 'wallet.name.error.empty': 'Please enter a name for your wallet', + 'wallet.name.error.rename': 'Error while renaming the wallet', 'wallet.nav.deposits': 'Deposits', 'wallet.nav.bonds': 'Bonds', 'wallet.nav.poolshares': 'Shares', diff --git a/src/renderer/i18n/fr/wallet.ts b/src/renderer/i18n/fr/wallet.ts index 80cbc5036..f54d71c40 100644 --- a/src/renderer/i18n/fr/wallet.ts +++ b/src/renderer/i18n/fr/wallet.ts @@ -4,6 +4,7 @@ const wallet: WalletMessages = { 'wallet.name': 'Wallet name - FR', 'wallet.name.maxChars': 'Max. {max} chars - FR', 'wallet.name.error.empty': 'Please enter a name for your wallet - FR', + 'wallet.name.error.rename': 'Error while renaming the wallet - FR', 'wallet.nav.deposits': 'Dépots', 'wallet.nav.bonds': 'Cautions', 'wallet.nav.poolshares': 'Quote-part', diff --git a/src/renderer/i18n/ru/wallet.ts b/src/renderer/i18n/ru/wallet.ts index ca5eec4a1..cafedd8f2 100644 --- a/src/renderer/i18n/ru/wallet.ts +++ b/src/renderer/i18n/ru/wallet.ts @@ -4,6 +4,7 @@ const wallet: WalletMessages = { 'wallet.name': 'Wallet name - RU', 'wallet.name.maxChars': 'Max. {max} chars - RU', 'wallet.name.error.empty': 'Please enter a name for your wallet - RU', + 'wallet.name.error.rename': 'Error while renaming the wallet - RU', 'wallet.nav.deposits': 'Вклады', 'wallet.nav.bonds': 'Бонды', 'wallet.nav.poolshares': 'Доли', diff --git a/src/renderer/i18n/types.ts b/src/renderer/i18n/types.ts index 4d5d5a977..1f6baea9b 100644 --- a/src/renderer/i18n/types.ts +++ b/src/renderer/i18n/types.ts @@ -152,6 +152,7 @@ type WalletMessageKey = | 'wallet.name' | 'wallet.name.maxChars' | 'wallet.name.error.empty' + | 'wallet.name.error.rename' | 'wallet.nav.deposits' | 'wallet.nav.bonds' | 'wallet.nav.poolshares' diff --git a/src/renderer/services/wallet/keystore.ts b/src/renderer/services/wallet/keystore.ts index 46807bb23..5e6a3ba42 100644 --- a/src/renderer/services/wallet/keystore.ts +++ b/src/renderer/services/wallet/keystore.ts @@ -23,7 +23,9 @@ import { AddKeystoreParams, KeystoreWalletsLD, KeystoreWalletsUI$, - ChangeKeystoreWalletLD + RenameKeystoreWalletHandler, + ChangeKeystoreWalletHandler, + isKeystoreUnlocked } from './types' import { getKeystore, @@ -122,7 +124,7 @@ export const removeKeystoreWallet = async () => { return wallets.length } -const changeKeystoreWallet = (keystoreId: KeystoreId): ChangeKeystoreWalletLD => { +const changeKeystoreWallet: ChangeKeystoreWalletHandler = (keystoreId: KeystoreId) => { const wallets = getKeystoreWallets() // Get selected wallet const selectedWallet = FP.pipe( @@ -136,7 +138,7 @@ const changeKeystoreWallet = (keystoreId: KeystoreId): ChangeKeystoreWalletLD => const { id, name } = selectedWallet const updatedWallets = FP.pipe( - getKeystoreWallets(), + wallets, A.map((wallet) => ({ ...wallet, selected: id === wallet.id })) ) @@ -145,13 +147,13 @@ const changeKeystoreWallet = (keystoreId: KeystoreId): ChangeKeystoreWalletLD => // encode wallets first ipcKeystoreWalletsIO.encode, // Save updated `wallets` to disk - (wallets) => Rx.of(window.apiKeystore.saveKeystoreWallets(wallets)), + (wallets) => Rx.from(window.apiKeystore.saveKeystoreWallets(wallets)), RxOp.catchError((err) => Rx.of(RD.failure(err))), RxOp.map(() => RD.success(true)), liveData.map((_) => { // Update states setKeystoreWallets(updatedWallets) - // set selected wallet + // set selected wallet as locked wallet setKeystoreState(O.some({ id, name })) return true }), @@ -159,6 +161,43 @@ const changeKeystoreWallet = (keystoreId: KeystoreId): ChangeKeystoreWalletLD => ) } +const renameKeystoreWallet: RenameKeystoreWalletHandler = (id, name) => { + // get keystore state - needs to be UNLOCKED + const updatedKeystoreState = FP.pipe( + getKeystoreState(), + O.chain(FP.flow(O.fromPredicate(isKeystoreUnlocked))), + // rename in state + O.map((state) => ({ ...state, name })), + O.toNullable + ) + + if (!updatedKeystoreState) + return Rx.of(RD.failure(Error(`Could not rename wallet with id ${id} - it seems to be locked`))) + + // update selected wallet in list of wallets + const updatedWallets = FP.pipe( + getKeystoreWallets(), + A.map((wallet) => (id === wallet.id ? { ...wallet, name } : wallet)) + ) + return FP.pipe( + updatedWallets, + // encode wallets first + ipcKeystoreWalletsIO.encode, + // Save updated `wallets` to disk + (wallets) => Rx.from(window.apiKeystore.saveKeystoreWallets(wallets)), + RxOp.catchError((err) => Rx.of(RD.failure(err))), + RxOp.map(() => RD.success(true)), + liveData.map((_) => { + // Update states of all wallets + setKeystoreWallets(updatedWallets) + // set selected wallet - still unlocked + setKeystoreState(O.some(updatedKeystoreState)) + return true + }), + RxOp.startWith(RD.pending) + ) +} + const importKeystore = async ({ keystore, password, name, id }: ImportKeystoreParams): Promise => { try { setImportingKeystoreState(RD.pending) @@ -311,6 +350,7 @@ export const keystoreService: KeystoreService = { addKeystoreWallet, removeKeystoreWallet, changeKeystoreWallet, + renameKeystoreWallet, importKeystore, exportKeystore, loadKeystore$, diff --git a/src/renderer/services/wallet/types.ts b/src/renderer/services/wallet/types.ts index ad3307c10..3fba2d1ae 100644 --- a/src/renderer/services/wallet/types.ts +++ b/src/renderer/services/wallet/types.ts @@ -58,6 +58,10 @@ export type ImportingKeystoreStateLD = Rx.Observable export type RemoveKeystoreWalletHandler = () => Promise +export type RenameKeystoreWalletRD = RD.RemoteData +export type RenameKeystoreWalletLD = LiveData +export type RenameKeystoreWalletHandler = (id: KeystoreId, name: string) => RenameKeystoreWalletLD + export type ChangeKeystoreWalletRD = RD.RemoteData export type ChangeKeystoreWalletLD = LiveData export type ChangeKeystoreWalletHandler = (id: KeystoreId) => ChangeKeystoreWalletLD @@ -67,6 +71,7 @@ export type KeystoreService = { addKeystoreWallet: (params: AddKeystoreParams) => Promise removeKeystoreWallet: RemoveKeystoreWalletHandler changeKeystoreWallet: ChangeKeystoreWalletHandler + renameKeystoreWallet: RenameKeystoreWalletHandler loadKeystore$: () => LoadKeystoreLD importKeystore: (params: ImportKeystoreParams) => Promise exportKeystore: () => Promise diff --git a/src/renderer/views/wallet/WalletSettingsView.tsx b/src/renderer/views/wallet/WalletSettingsView.tsx index baf5bf2a2..4eaacd197 100644 --- a/src/renderer/views/wallet/WalletSettingsView.tsx +++ b/src/renderer/views/wallet/WalletSettingsView.tsx @@ -69,7 +69,7 @@ export const WalletSettingsView: React.FC = (): JSX.Element => { keystoreService: { exportKeystore, validatePassword$ } } = useWalletContext() - const { state: keystore, lock, remove, change$ } = useKeystoreState() + const { state: keystore, lock, remove, change$, rename$ } = useKeystoreState() const { network } = useNetwork() @@ -417,8 +417,9 @@ export const WalletSettingsView: React.FC = (): JSX.Element => {