diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b4a9cc4..2df7d89fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 1.18.1 (2022-xx-xx) +## Add + +[Header] Add wallet selector [#1576](https://github.com/thorchain/asgardex-electron/issues/1576) + ## Fix Add validation of wallet names to avoid duplications [#2386](https://github.com/thorchain/asgardex-electron/issues/2386) diff --git a/package.json b/package.json index cf40692ba..5b8f966a1 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "concurrently": "^7.2.1", "cross-env": "^7.0.3", "crypto-browserify": "^3.12.0", - "electron": "^19.0.14", + "electron": "^19.0.15", "electron-builder": "^23.0.3", "electron-devtools-installer": "^3.2.0", "electron-notarize": "^1.2.1", diff --git a/src/renderer/components/header/Header.tsx b/src/renderer/components/header/Header.tsx index ceb4046a3..96449f2d0 100644 --- a/src/renderer/components/header/Header.tsx +++ b/src/renderer/components/header/Header.tsx @@ -6,7 +6,8 @@ import { useObservableState } from 'observable-hooks' import { useMidgardContext } from '../../contexts/MidgardContext' import { useThorchainContext } from '../../contexts/ThorchainContext' -import { useWalletContext } from '../../contexts/WalletContext' +import { useKeystoreState } from '../../hooks/useKeystoreState' +import { useKeystoreWallets } from '../../hooks/useKeystoreWallets' import { useNetwork } from '../../hooks/useNetwork' import { usePricePools } from '../../hooks/usePricePools' import { useRunePrice } from '../../hooks/useRunePrice' @@ -16,10 +17,9 @@ import { SelectedPricePoolAsset } from '../../services/midgard/types' import { HeaderComponent } from './HeaderComponent' export const Header: React.FC = (): JSX.Element => { - const { keystoreService } = useWalletContext() + const { lock, state: keystoreState, change$: changeWalletHandler$ } = useKeystoreState() + const { walletsUI } = useKeystoreWallets() const { mimir$ } = useThorchainContext() - const { lock } = keystoreService - const keystore = useObservableState(keystoreService.keystoreState$, O.none) const mimir = useObservableState(mimir$, RD.initial) const { service: midgardService } = useMidgardContext() const { @@ -44,8 +44,10 @@ export const Header: React.FC = (): JSX.Element => { return ( + changeWalletHandler$: ChangeKeystoreWalletHandler setSelectedPricePool: (asset: PricePoolAsset) => void pricePools: O.Option runePrice: PriceRD @@ -65,6 +68,7 @@ export type Props = { export const HeaderComponent: React.FC = (props): JSX.Element => { const { keystore, + wallets, network, pricePools: oPricePools, runePrice: runePriceRD, @@ -75,6 +79,7 @@ export const HeaderComponent: React.FC = (props): JSX.Element => { reloadVolume24Price, selectedPricePoolAsset: oSelectedPricePoolAsset, lockHandler, + changeWalletHandler$, setSelectedPricePool, midgardUrl: midgardUrlRD, thorchainNodeUrl, @@ -237,11 +242,6 @@ export const HeaderComponent: React.FC = (props): JSX.Element => { [hasPricePools, isDesktopView, oSelectedPricePoolAsset, pricePoolAssets, currencyChangeHandler] ) - const renderHeaderLock = useMemo( - () => , - [isDesktopView, clickLockHandler, keystore] - ) - const renderHeaderSettings = useMemo( () => , [isDesktopView, clickSettingsHandler] @@ -319,7 +319,12 @@ export const HeaderComponent: React.FC = (props): JSX.Element => { {renderHeaderNetStatus} {renderHeaderCurrency} - {renderHeaderLock} + {renderHeaderSettings} @@ -374,7 +379,9 @@ export const HeaderComponent: React.FC = (props): JSX.Element => { - {renderHeaderLock} + + + {renderHeaderSettings} {renderHeaderNetStatus} diff --git a/src/renderer/components/header/HeaderMenu.styles.ts b/src/renderer/components/header/HeaderMenu.styles.ts index ddf4e0f4f..711dacdbf 100644 --- a/src/renderer/components/header/HeaderMenu.styles.ts +++ b/src/renderer/components/header/HeaderMenu.styles.ts @@ -51,7 +51,7 @@ export const HeaderDropdownTitle = styled(Text)` export const HeaderDropdownMenuItemText = styled(Text)` text-transform: uppercase; font-family: 'MainFontBold'; - color: ${palette('text', 0)}; - font-size: 18px; + color: ${palette('text', 1)}; + font-size: 16px; background: transparent; ` diff --git a/src/renderer/components/header/lock/HeaderLock.stories.tsx b/src/renderer/components/header/lock/HeaderLock.stories.tsx index 5ae17339e..d0ea15726 100644 --- a/src/renderer/components/header/lock/HeaderLock.stories.tsx +++ b/src/renderer/components/header/lock/HeaderLock.stories.tsx @@ -13,11 +13,10 @@ const meta: ComponentMeta = { title: 'Components/HeaderLock', argTypes: { keystoreState: AT.keystore, - onPress: { action: 'onPress' } + lockHandler: { action: 'onPress' } }, args: { - keystoreState: O.none, - isDesktopView: false + keystoreState: O.none } } diff --git a/src/renderer/components/header/lock/HeaderLock.styles.tsx b/src/renderer/components/header/lock/HeaderLock.styles.tsx index c4fbf3d59..31dd88f37 100644 --- a/src/renderer/components/header/lock/HeaderLock.styles.tsx +++ b/src/renderer/components/header/lock/HeaderLock.styles.tsx @@ -6,8 +6,6 @@ import Text from 'antd/lib/typography/Text' import styled from 'styled-components' import { palette } from 'styled-theme' -import { ReactComponent as LockWarningIconUI } from '../../../assets/svg/icon-lock-warning.svg' -import { ReactComponent as UnlockWarningIconUI } from '../../../assets/svg/icon-unlock-warning.svg' import { media } from '../../../helpers/styleHelper' type Props = RowProps & { disabled: boolean } @@ -28,16 +26,6 @@ export const HeaderLockWrapper = styled(Wrapper)` `} ` -export const LockIcon = styled(LockWarningIconUI)` - font-size: '1.5em'; - cursor: pointer; -` - -export const UnlockIcon = styled(UnlockWarningIconUI)` - font-size: '1.5em'; - cursor: pointer; -` - export const Label = styled(Text)` text-transform: uppercase; color: ${palette('text', 0)}; diff --git a/src/renderer/components/header/lock/HeaderLock.tsx b/src/renderer/components/header/lock/HeaderLock.tsx index e0d289fb2..a56a10b00 100644 --- a/src/renderer/components/header/lock/HeaderLock.tsx +++ b/src/renderer/components/header/lock/HeaderLock.tsx @@ -1,75 +1,179 @@ import React, { useCallback, useMemo } from 'react' +import * as RD from '@devexperts/remote-data-ts' +import { Listbox } from '@headlessui/react' +import { PlusCircleIcon } from '@heroicons/react/outline' +import * as A from 'fp-ts/lib/Array' import * as FP from 'fp-ts/lib/function' import * as O from 'fp-ts/lib/Option' import { useIntl } from 'react-intl' +import { useNavigate } from 'react-router-dom' +import { KeystoreId } from '../../../../shared/api/types' import { truncateMiddle } from '../../../helpers/stringHelper' -import { KeystoreState } from '../../../services/wallet/types' +import { useSubscriptionState } from '../../../hooks/useSubscriptionState' +import * as walletRoutes from '../../../routes/wallet' +import { + ChangeKeystoreWalletHandler, + ChangeKeystoreWalletRD, + KeystoreState, + KeystoreWalletsUI +} from '../../../services/wallet/types' import * as WU from '../../../services/wallet/util' +import { DownIcon, LockIcon, UnlockIcon } from '../../icons' +import { BaseButton } from '../../uielements/button' import { Tooltip } from '../../uielements/common/Common.styles' -import { HeaderIconWrapper } from '../HeaderIcon.styles' -import * as Styled from './HeaderLock.styles' + +type WalletData = { id: KeystoreId; name: string } export type Props = { + wallets: KeystoreWalletsUI + changeWalletHandler$: ChangeKeystoreWalletHandler keystoreState: KeystoreState - onPress?: FP.Lazy - isDesktopView: boolean + lockHandler: FP.Lazy } export const HeaderLock: React.FC = (props): JSX.Element => { - const { keystoreState, onPress = FP.constVoid, isDesktopView } = props + const { keystoreState, wallets, changeWalletHandler$, lockHandler: onPress } = props const intl = useIntl() + const navigate = useNavigate() const isLocked = useMemo(() => WU.isLocked(keystoreState), [keystoreState]) - const clickHandler = useCallback((_: React.MouseEvent) => onPress(), [onPress]) + const hasWallets = wallets.length - const desktopView = useMemo( - () => ( -
-
{isLocked ? : }
- {FP.pipe( - keystoreState, - WU.getWalletName, - O.fold( - () => <>, - (name) => ( - -

- {truncateMiddle(name, { start: 3, end: 3, max: 11 })} -

-
- ) + // Data for Listbox + const walletData = FP.pipe( + wallets, + A.map(({ id, name }) => ({ id, name })) + ) + + // Selected wallet for Listbox + const oSelectedWallet: O.Option = useMemo( + () => + FP.pipe( + keystoreState, + WU.getKeystoreId, + O.chain((selectedId) => + FP.pipe( + walletData, + A.findFirst(({ id }) => id === selectedId) + ) + ) + ), + [keystoreState, walletData] + ) + + const { subscribe: subscribeChangeWalletState } = useSubscriptionState(RD.initial) + + const changeWalletHandler = useCallback( + ({ id }: WalletData) => { + // subscription is needed to run `changeWalletHandler$` + subscribeChangeWalletState(changeWalletHandler$(id)) + }, + [changeWalletHandler$, subscribeChangeWalletState] + ) + + const renderWallets = useMemo( + () => + FP.pipe( + oSelectedWallet, + O.fold( + () => <>no selected wallet, + (selectedWallet) => ( +
+
onPress()}> + {isLocked ? : } +
+ +
+ + {({ open }) => ( + <> + + {truncateMiddle(selectedWallet.name, { start: 3, end: 3, max: 6 })} + + + + )} + + + {FP.pipe( + walletData, + A.map((wallet) => ( + `select-none + px-20px py-10px + ${selected && 'text-gray2 dark:text-gray2d'} + ${selected ? 'cursor-disabled' : 'cursor-pointer'} + font-main text-14 + text-text1 + ${!selected && 'hover:bg-gray0 hover:text-gray2'} + dark:text-text1d + ${!selected && 'hover:dark:bg-gray0d hover:dark:text-gray2d'} + `} + key={wallet.id} + value={wallet}> + {truncateMiddle(wallet.name, { start: 9, end: 9, max: 20 })} + + )) + )} + +
+
+
) - )} -
+ ) + ), + + [changeWalletHandler, isLocked, oSelectedWallet, onPress, walletData] + ) + + const renderAddWallet = useMemo( + () => ( + + navigate(walletRoutes.noWallet.path())}> + + + ), - [clickHandler, isLocked, keystoreState] + [intl, navigate] ) - const mobileView = useMemo(() => { - const notImported = !WU.hasImportedKeystore(keystoreState) - const label = intl.formatMessage({ - id: notImported ? 'wallet.add.label' : isLocked ? 'wallet.unlock.label' : 'wallet.lock.label' - }) - - return ( - - {label} - {isLocked ? : } - - ) - }, [clickHandler, intl, isLocked, keystoreState]) - - return isDesktopView ? desktopView : mobileView + return
{hasWallets ? renderWallets : renderAddWallet}
} diff --git a/src/renderer/components/header/lock/HeaderLockMobile.stories.tsx b/src/renderer/components/header/lock/HeaderLockMobile.stories.tsx new file mode 100644 index 000000000..68e82d2ca --- /dev/null +++ b/src/renderer/components/header/lock/HeaderLockMobile.stories.tsx @@ -0,0 +1,23 @@ +import { ComponentMeta, StoryFn } from '@storybook/react' +import * as O from 'fp-ts/lib/Option' + +import * as AT from '../../../storybook/argTypes' +import { HeaderLockMobile as Component, Props } from './HeaderLockMobile' + +const Template: StoryFn = (args) => + +export const Default = Template.bind({}) + +const meta: ComponentMeta = { + component: Component, + title: 'Components/HeaderLockMobile', + argTypes: { + keystoreState: AT.keystore, + onPress: { action: 'onPress' } + }, + args: { + keystoreState: O.none + } +} + +export default meta diff --git a/src/renderer/components/header/lock/HeaderLockMobile.tsx b/src/renderer/components/header/lock/HeaderLockMobile.tsx new file mode 100644 index 000000000..7bdc1c799 --- /dev/null +++ b/src/renderer/components/header/lock/HeaderLockMobile.tsx @@ -0,0 +1,37 @@ +import React, { useMemo } from 'react' + +import * as FP from 'fp-ts/lib/function' +import { useIntl } from 'react-intl' + +import { KeystoreState } from '../../../services/wallet/types' +import * as WU from '../../../services/wallet/util' +import { LockIcon, UnlockIcon } from '../../icons' +import { HeaderIconWrapper } from '../HeaderIcon.styles' +import * as Styled from './HeaderLock.styles' + +export type Props = { + keystoreState: KeystoreState + onPress: FP.Lazy +} + +export const HeaderLockMobile: React.FC = (props): JSX.Element => { + const { keystoreState, onPress } = props + + const intl = useIntl() + + const isLocked = useMemo(() => WU.isLocked(keystoreState), [keystoreState]) + + const label = useMemo(() => { + const notImported = !WU.hasImportedKeystore(keystoreState) + return intl.formatMessage({ + id: notImported ? 'wallet.add.label' : isLocked ? 'wallet.unlock.label' : 'wallet.lock.label' + }) + }, [intl, isLocked, keystoreState]) + + return ( + onPress()}> + {label} + {isLocked ? : } + + ) +} diff --git a/src/renderer/components/header/settings/HeaderSettings.styles.ts b/src/renderer/components/header/settings/HeaderSettings.styles.ts index b59b93cc5..f7e2dfa8e 100644 --- a/src/renderer/components/header/settings/HeaderSettings.styles.ts +++ b/src/renderer/components/header/settings/HeaderSettings.styles.ts @@ -2,16 +2,6 @@ import Text from 'antd/lib/typography/Text' import styled from 'styled-components' import { palette } from 'styled-theme' -import { ReactComponent as SettingsIcon } from '../../../assets/svg/icon-settings.svg' - export const Label = styled(Text)` color: ${palette('text', 0)}; ` - -export const Icon = styled(SettingsIcon)` - font-size: '1.5em'; - - & path { - fill: ${palette('text', 2)}; - } -` diff --git a/src/renderer/components/header/settings/HeaderSettings.tsx b/src/renderer/components/header/settings/HeaderSettings.tsx index 080cada23..f55565040 100644 --- a/src/renderer/components/header/settings/HeaderSettings.tsx +++ b/src/renderer/components/header/settings/HeaderSettings.tsx @@ -1,7 +1,9 @@ import React from 'react' +import { CogIcon } from '@heroicons/react/solid' import { useIntl } from 'react-intl' +import { Tooltip } from '../../uielements/common/Common.styles' import { HeaderIconWrapper } from '../HeaderIcon.styles' import * as Styled from './HeaderSettings.styles' @@ -17,7 +19,9 @@ export const HeaderSettings: React.FC = (props): JSX.Element => { return ( {!isDesktopView && {intl.formatMessage({ id: 'common.settings' })} } - + + + ) } diff --git a/src/renderer/components/header/theme/HeaderTheme.styles.ts b/src/renderer/components/header/theme/HeaderTheme.styles.ts index c335b5f45..00c41ec2f 100644 --- a/src/renderer/components/header/theme/HeaderTheme.styles.ts +++ b/src/renderer/components/header/theme/HeaderTheme.styles.ts @@ -4,16 +4,9 @@ import { palette } from 'styled-theme' import { ReactComponent as DayThemeIconUI } from '../../../assets/svg/icon-theme-day.svg' import { ReactComponent as NightThemeIconUI } from '../../../assets/svg/icon-theme-night.svg' -import { media } from '../../../helpers/styleHelper' import { HeaderIconWrapper } from '../HeaderIcon.styles' -export const HeaderThemeWrapper = styled(HeaderIconWrapper)` - margin-right: 5px; - - ${media.lg` - margin-right: 10px; - `} -` +export const HeaderThemeWrapper = styled(HeaderIconWrapper)`` export const DayThemeIcon = styled(DayThemeIconUI)` font-size: '1.5em'; diff --git a/src/renderer/components/icons/index.ts b/src/renderer/components/icons/index.ts index fe75eb07b..dadb7d94d 100644 --- a/src/renderer/components/icons/index.ts +++ b/src/renderer/components/icons/index.ts @@ -20,6 +20,8 @@ import { ReactComponent as DownIcon } from '../../assets/svg/icon-down.svg' import { ReactComponent as EyeHideIcon } from '../../assets/svg/icon-eye-hide.svg' import { ReactComponent as EyeIcon } from '../../assets/svg/icon-eye.svg' import { ReactComponent as LoadingIcon } from '../../assets/svg/icon-loading.svg' +import { ReactComponent as LockIcon } from '../../assets/svg/icon-lock-warning.svg' +import { ReactComponent as UnlockIcon } from '../../assets/svg/icon-unlock-warning.svg' import { ReactComponent as LedgerIcon } from '../../assets/svg/ledger.svg' export { @@ -40,5 +42,7 @@ export { LoadingIcon, AttentionIcon, EyeIcon, - EyeHideIcon + EyeHideIcon, + LockIcon, + UnlockIcon } diff --git a/src/renderer/components/uielements/wallet/WalletSelector.tsx b/src/renderer/components/uielements/wallet/WalletSelector.tsx index 76ec6f834..0dfcae3da 100644 --- a/src/renderer/components/uielements/wallet/WalletSelector.tsx +++ b/src/renderer/components/uielements/wallet/WalletSelector.tsx @@ -36,14 +36,12 @@ export const WalletSelector: React.FC = (props): JSX.Element => { value={selectedWallet} disabled={disabled} onChange={({ id }) => { - console.log('on change listbox', id) onChange(id) }}>
= (props): JSX.Element => { pl-20px pr-10px font-main text-14 - uppercase text-text0 transition duration-300 ease-in-out @@ -93,10 +90,10 @@ export const WalletSelector: React.FC = (props): JSX.Element => { px-20px ${selected && 'text-gray2 dark:text-gray2d'} ${selected ? 'cursor-disabled' : 'cursor-pointer'} - font-main text-14 uppercase text-text0 - hover:bg-gray0 hover:text-gray2 - dark:text-text0d hover:dark:bg-gray0d - hover:dark:text-gray2d + font-main text-14 text-text0 + dark:text-text0d + ${!selected && 'hover:bg-gray0 hover:text-gray2'} + ${!selected && 'hover:dark:bg-gray0d hover:dark:text-gray2d'} ` } key={wallet.id} diff --git a/src/renderer/helpers/walletHelper.ts b/src/renderer/helpers/walletHelper.ts index b54ff0cdb..0b8321bc4 100644 --- a/src/renderer/helpers/walletHelper.ts +++ b/src/renderer/helpers/walletHelper.ts @@ -11,7 +11,7 @@ import { isLedgerWallet, isWalletType } from '../../shared/utils/guard' import { WalletAddress, WalletType } from '../../shared/wallet/types' import { ZERO_ASSET_AMOUNT } from '../const' import { WalletBalances } from '../services/clients' -import { KeystoreWalletsUI, NonEmptyWalletBalances, WalletBalance } from '../services/wallet/types' +import { NonEmptyWalletBalances, WalletBalance } from '../services/wallet/types' import { isBnbAsset, isEthAsset, isLtcAsset, isRuneNativeAsset } from './assetHelper' import { isBchChain, isDogeChain, isLtcChain, isThorChain } from './chainHelper' import { eqAddress, eqAsset, eqWalletType } from './fp/eq' @@ -175,7 +175,7 @@ export const getWalletIndexFromNullableString = (s?: string): O.Option => FP.pipe(s, optionFromNullableString, O.chain(O.fromPredicate(isWalletType))) -export const getWalletNamesFromKeystoreWallets = (wallets: KeystoreWalletsUI) => +export const getWalletNamesFromKeystoreWallets = (wallets: Array>) => FP.pipe( wallets, A.map(({ name }) => name) diff --git a/tailwind.config.js b/tailwind.config.js index dfa6d4d6f..25e289f7d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -64,7 +64,7 @@ module.exports = { error3d: t.dark.palette.error[3], // warning warning0: t.light.palette.warning[0], - warningr0d: t.dark.palette.warning[0], + warning0d: t.dark.palette.warning[0], // gray gray0: t.light.palette.gray[0], gray0d: t.dark.palette.gray[0], diff --git a/yarn.lock b/yarn.lock index 511d2c4f6..0c0e23993 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7674,7 +7674,7 @@ __metadata: crypto-browserify: ^3.12.0 dayjs: ^1.11.3 dotenv: ^16.0.1 - electron: ^19.0.14 + electron: ^19.0.15 electron-builder: ^23.0.3 electron-debug: ^3.2.0 electron-devtools-installer: ^3.2.0 @@ -11642,16 +11642,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:^19.0.14": - version: 19.0.14 - resolution: "electron@npm:19.0.14" +"electron@npm:^19.0.15": + version: 19.0.15 + resolution: "electron@npm:19.0.15" dependencies: "@electron/get": ^1.14.1 "@types/node": ^16.11.26 extract-zip: ^1.0.3 bin: electron: cli.js - checksum: af19de0a086f0e65f659a3f4530865b5f19a81462a6d818b32edc3ef7b29bb8d0f9428b8a92c3d8d0080e4db0cda45ff04b9fa811d1e793f15e9805a3d08b84b + checksum: f2c0a286bd13c83de63e341cf09dd2750808225ea96e062ccfe22f16a8a8de59c4f3953ddfa34116033d86c429ef04d829c7c3879e85e9d5ec701730099c996d languageName: node linkType: hard