diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx index 6499b5728..e655b4bb8 100644 --- a/apps/desktop/src/app/App.tsx +++ b/apps/desktop/src/app/App.tsx @@ -326,8 +326,7 @@ export const Loader: FC = () => { tgAuthBotId: REACT_APP_TG_BOT_ID, stonfiReferralAddress: REACT_APP_STONFI_REFERRAL_ADDRESS }, - defaultWalletVersion: - isV5R1Enabled(config) || devSettings.enableV5 ? WalletVersion.V5R1 : WalletVersion.V4R2 + defaultWalletVersion: WalletVersion.V5R1 }; return ( diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 483a47a52..7ef079974 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,7 +1,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { localizationText } from '@tonkeeper/core/dist/entries/language'; -import { Network, getApiConfig } from '@tonkeeper/core/dist/entries/network'; -import { WalletState, WalletVersion } from "@tonkeeper/core/dist/entries/wallet"; +import { getApiConfig } from '@tonkeeper/core/dist/entries/network'; +import { WalletVersion } from "@tonkeeper/core/dist/entries/wallet"; import { InnerBody, useWindowsScroll } from '@tonkeeper/uikit/dist/components/Body'; import { CopyNotification } from '@tonkeeper/uikit/dist/components/CopyNotification'; import { Footer, FooterGlobalStyle } from '@tonkeeper/uikit/dist/components/Footer'; @@ -38,8 +38,8 @@ import Initialize, { InitializeContainer } from '@tonkeeper/uikit/dist/pages/imp import { useKeyboardHeight } from '@tonkeeper/uikit/dist/pages/import/hooks'; import { UserThemeProvider } from '@tonkeeper/uikit/dist/providers/UserThemeProvider'; import { useUserFiat } from '@tonkeeper/uikit/dist/state/fiat'; -import { isV5R1Enabled, useTonendpoint, useTonenpointConfig } from "@tonkeeper/uikit/dist/state/tonendpoint"; -import { useActiveAccountQuery, useAccountsStateQuery } from "@tonkeeper/uikit/dist/state/wallet"; +import { useTonendpoint, useTonenpointConfig } from "@tonkeeper/uikit/dist/state/tonendpoint"; +import { useActiveAccountQuery, useAccountsStateQuery, useActiveTonNetwork } from "@tonkeeper/uikit/dist/state/wallet"; import { Container, GlobalStyle } from '@tonkeeper/uikit/dist/styles/globalStyle'; import React, { FC, PropsWithChildren, Suspense, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -49,6 +49,8 @@ import { BrowserAppSdk } from './libs/appSdk'; import { useAnalytics, useAppHeight, useAppWidth } from './libs/hooks'; import { useUserLanguage } from "@tonkeeper/uikit/dist/state/language"; import { useDevSettings } from "@tonkeeper/uikit/dist/state/dev"; +import { ModalsRoot } from "@tonkeeper/uikit/dist/components/ModalsRoot"; +import { Account } from "@tonkeeper/core/dist/entries/account"; const ImportRouter = React.lazy(() => import('@tonkeeper/uikit/dist/pages/import')); const Settings = React.lazy(() => import('@tonkeeper/uikit/dist/pages/settings')); @@ -178,8 +180,9 @@ const Wrapper = styled(FullSizeWrapper)<{ standalone: boolean }>` `; export const Loader: FC = () => { - const { data: activeWallet, isLoading: activeWalletLoading } = useActiveAccountQuery(); - const { data: wallets, isLoading: isWalletsLoading } = useAccountsStateQuery(); + const network = useActiveTonNetwork(); + const { data: activeAccount, isLoading: activeWalletLoading } = useActiveAccountQuery(); + const { data: accounts, isLoading: isWalletsLoading } = useAccountsStateQuery(); const { data: lang, isLoading: isLangLoading } = useUserLanguage(); const { data: fiat } = useUserFiat(); const { data: devSettings } = useDevSettings(); @@ -194,7 +197,7 @@ export const Loader: FC = () => { const tonendpoint = useTonendpoint({ targetEnv: TARGET_ENV, build: sdk.version, - network: activeWallet?.network, + network, lang }); const { data: config } = useTonenpointConfig(tonendpoint); @@ -202,11 +205,11 @@ export const Loader: FC = () => { const navigate = useNavigate(); useAppHeight(); - const { data: tracker } = useAnalytics(activeWallet || undefined, wallets, sdk.version); + const { data: tracker } = useAnalytics(activeAccount || undefined, accounts, sdk.version); useEffect(() => { if ( - activeWallet && + activeAccount && lang && i18n.language !== localizationText(lang) ) { @@ -214,7 +217,7 @@ export const Loader: FC = () => { i18n.changeLanguage(localizationText(lang)) ); } - }, [activeWallet, i18n]); + }, [activeAccount, i18n]); if ( isWalletsLoading || @@ -228,7 +231,6 @@ export const Loader: FC = () => { return ; } - const network = activeWallet?.network ?? Network.MAINNET; const context: IAppContext = { api: getApiConfig(config, network), fiat, @@ -238,7 +240,7 @@ export const Loader: FC = () => { extension: false, proFeatures: false, ios, - defaultWalletVersion: (isV5R1Enabled(config) || devSettings.enableV5) ? WalletVersion.V5R1 : WalletVersion.V4R2 + defaultWalletVersion: WalletVersion.V5R1 }; return ( @@ -248,11 +250,12 @@ export const Loader: FC = () => { value={() => navigate(AppRoute.home, { replace: true })} > - + }> + @@ -261,10 +264,10 @@ export const Loader: FC = () => { }; export const Content: FC<{ - activeWallet?: WalletState | null; + activeAccount?: Account | null; lock: boolean; standalone: boolean; -}> = ({ activeWallet, lock, standalone }) => { +}> = ({ activeAccount, lock, standalone }) => { const location = useLocation(); useWindowsScroll(); useAppWidth(standalone); @@ -298,7 +301,7 @@ export const Content: FC<{ ); } - if (!activeWallet || location.pathname.startsWith(AppRoute.import)) { + if (!activeAccount || location.pathname.startsWith(AppRoute.import)) { return ( }> diff --git a/apps/web/src/libs/hooks.ts b/apps/web/src/libs/hooks.ts index 1b3702f1b..247f8a6d6 100644 --- a/apps/web/src/libs/hooks.ts +++ b/apps/web/src/libs/hooks.ts @@ -1,11 +1,12 @@ import { useQuery } from '@tanstack/react-query'; -import { WalletsState, WalletState } from "@tonkeeper/core/dist/entries/wallet"; +import { Account } from "@tonkeeper/core/dist/entries/account"; import { throttle } from '@tonkeeper/core/dist/utils/common'; import { Analytics, AnalyticsGroup, toWalletType } from '@tonkeeper/uikit/dist/hooks/analytics'; import { AptabaseWeb } from '@tonkeeper/uikit/dist/hooks/analytics/aptabase-web'; import { Gtag } from '@tonkeeper/uikit/dist/hooks/analytics/gtag'; import { QueryKey } from '@tonkeeper/uikit/dist/libs/queryKey'; import { useEffect } from 'react'; +import { useActiveTonNetwork } from "@tonkeeper/uikit/dist/state/wallet"; export const useAppHeight = () => { useEffect(() => { @@ -48,12 +49,13 @@ export const useAppWidth = (standalone: boolean) => { }; export const useAnalytics = ( - activeWallet?: WalletState, - wallets?: WalletsState, + activeAccount?: Account, + accounts?: Account[], version?: string ) => { + const network = useActiveTonNetwork(); return useQuery( - [QueryKey.analytics], + [QueryKey.analytics, network], async () => { const tracker = new AnalyticsGroup( new AptabaseWeb( @@ -64,12 +66,17 @@ export const useAnalytics = ( new Gtag(import.meta.env.VITE_APP_MEASUREMENT_ID) ); - tracker.init('Web', toWalletType(activeWallet), - activeWallet, - wallets,); + tracker.init({ + application: 'Web', + walletType: toWalletType(activeAccount?.activeTonWallet), + activeAccount: activeAccount!, + accounts: accounts!, + network + } + ); return tracker; }, - { enabled: wallets != null && activeWallet !== undefined } + { enabled: accounts != null && activeAccount !== undefined } ); }; diff --git a/packages/core/src/entries/account.ts b/packages/core/src/entries/account.ts index d0d1adeab..216c69f41 100644 --- a/packages/core/src/entries/account.ts +++ b/packages/core/src/entries/account.ts @@ -14,6 +14,7 @@ export interface DeprecatedAccountState { export type AccountId = string; export interface IAccount { + id: AccountId; name: string; emoji: string; @@ -51,6 +52,9 @@ export class AccountTonMnemonic extends Clonable implements IAccount { return this.tonWallets.find(w => w.id === this.activeTonWalletId)!; } + /** + * @param id ton public key hex string without 0x corresponding to the mnemonic + */ constructor( public readonly id: AccountId, public name: string, @@ -75,7 +79,12 @@ export class AccountTonMnemonic extends Clonable implements IAccount { } addTonWalletToActiveDerivation(wallet: TonWalletStandard) { - this.tonWallets.push(wallet); + const walletExists = this.tonWallets.findIndex(w => w.id === wallet.id); + if (walletExists === -1) { + this.tonWallets = this.tonWallets.concat(wallet); + } else { + this.tonWallets[walletExists] = wallet; + } } removeTonWalletFromActiveDerivation(walletId: WalletId) { @@ -90,6 +99,9 @@ export class AccountTonMnemonic extends Clonable implements IAccount { } setActiveTonWallet(walletId: WalletId) { + if (this.tonWallets.every(w => w.id !== walletId)) { + throw new Error('Wallet not found'); + } this.activeTonWalletId = walletId; } } @@ -116,6 +128,9 @@ export class AccountLedger extends Clonable implements IAccount { )!; } + /** + * @param id index 0 derivation ton public key hex string without 0x + */ constructor( public readonly id: AccountId, public name: string, @@ -143,7 +158,12 @@ export class AccountLedger extends Clonable implements IAccount { } addTonWalletToActiveDerivation(wallet: TonWalletStandard) { - this.activeDerivation.tonWallets.push(wallet); + const walletExists = this.activeDerivation.tonWallets.findIndex(w => w.id === wallet.id); + if (walletExists === -1) { + this.activeDerivation.tonWallets = this.activeDerivation.tonWallets.concat(wallet); + } else { + this.activeDerivation.tonWallets[walletExists] = wallet; + } } removeTonWalletFromActiveDerivation(walletId: WalletId) { @@ -161,8 +181,8 @@ export class AccountLedger extends Clonable implements IAccount { setActiveTonWallet(walletId: WalletId) { for (const derivation of this.derivations) { - const index = derivation.tonWallets.findIndex(w => w.id === walletId); - if (index !== -1) { + const walletInDerivation = derivation.tonWallets.some(w => w.id === walletId); + if (walletInDerivation) { derivation.activeTonWalletId = walletId; this.activeDerivationIndex = derivation.index; return; @@ -188,6 +208,9 @@ export class AccountKeystone extends Clonable implements IAccount { return this.tonWallet; } + /** + * @param id ton public key hex string without 0x + */ constructor( public readonly id: AccountId, public name: string, @@ -236,6 +259,9 @@ export class AccountTonOnly extends Clonable implements IAccount { return this.tonWallets.find(w => w.id === this.activeTonWalletId)!; } + /** + * @param id ton public key hex string without 0x + */ constructor( public readonly id: AccountId, public name: string, @@ -260,7 +286,12 @@ export class AccountTonOnly extends Clonable implements IAccount { } addTonWalletToActiveDerivation(wallet: TonWalletStandard) { - this.tonWallets.push(wallet); + const walletExists = this.tonWallets.findIndex(w => w.id === wallet.id); + if (walletExists === -1) { + this.tonWallets = this.tonWallets.concat(wallet); + } else { + this.tonWallets[walletExists] = wallet; + } } removeTonWalletFromActiveDerivation(walletId: WalletId) { @@ -275,6 +306,10 @@ export class AccountTonOnly extends Clonable implements IAccount { } setActiveTonWallet(walletId: WalletId) { + if (this.tonWallets.every(w => w.id !== walletId)) { + throw new Error('Wallet not found'); + } + this.activeTonWalletId = walletId; } } diff --git a/packages/core/src/service/accountsStorage.ts b/packages/core/src/service/accountsStorage.ts index 54c6e7f1b..fbf9a2b0f 100644 --- a/packages/core/src/service/accountsStorage.ts +++ b/packages/core/src/service/accountsStorage.ts @@ -18,6 +18,7 @@ import { DeprecatedAccountState } from '../entries/account'; import { AuthState, DeprecatedAuthState } from '../entries/password'; import { assertUnreachable, notNullish } from '../utils/types'; import { + getFallbackAccountEmoji, getFallbackDerivationItemEmoji, getFallbackTonStandardWalletEmoji, getWalletNameAddress @@ -69,6 +70,10 @@ export class AccountsStorage { }; setActiveAccountId = async (activeAccountId: AccountId | null) => { + const accounts = await this.getAccounts(); + if (accounts.every(a => a.id !== activeAccountId)) { + throw new Error('Account not found'); + } await this.storage.set(AppKey.ACTIVE_ACCOUNT_ID, activeAccountId); }; @@ -78,9 +83,15 @@ export class AccountsStorage { addAccountsToState = async (accounts: Account[]) => { const state = await this.getAccounts(); - await this.setAccounts( - state.concat(accounts.filter(acc => state.every(a => a.id !== acc.id))) - ); + accounts.forEach(account => { + const existingAccIndex = state.findIndex(a => a.id === account.id); + if (existingAccIndex !== -1) { + state[existingAccIndex] = account; + return; + } + state.push(account); + }); + await this.setAccounts(state); }; /** @@ -121,6 +132,14 @@ export class AccountsStorage { await this.setAccounts(state.filter(w => w.id !== id)); }; + async getNewAccountNameAndEmoji(accountId: AccountId) { + const existingAccounts = await this.getAccounts(); + const existingAccount = existingAccounts.find(a => a.id === accountId); + const name = existingAccount?.name || 'Account ' + (existingAccounts.length + 1); + const emoji = existingAccount?.emoji || getFallbackAccountEmoji(accountId); + return { name, emoji }; + } + private migrateToActiveAccountIdState = async (): Promise => { const state = await this.storage.get(AppKey.DEPRECATED_ACCOUNT); if (!state || !state.activePublicKey) { diff --git a/packages/core/src/service/devStorage.ts b/packages/core/src/service/devStorage.ts index 5a7eff0e0..fbcd909e2 100644 --- a/packages/core/src/service/devStorage.ts +++ b/packages/core/src/service/devStorage.ts @@ -3,12 +3,10 @@ import { Network } from '../entries/network'; import { AppKey } from '../Keys'; export interface DevSettings { - enableV5: boolean; tonNetwork: Network; } const defaultDevSettings: DevSettings = { - enableV5: false, tonNetwork: Network.MAINNET }; diff --git a/packages/core/src/service/walletService.ts b/packages/core/src/service/walletService.ts index 710e76e89..80877bb7f 100644 --- a/packages/core/src/service/walletService.ts +++ b/packages/core/src/service/walletService.ts @@ -18,9 +18,12 @@ import { AccountTonMnemonic, AccountTonOnly } from '../entries/account'; +import { IStorage } from '../Storage'; +import { accountsStorage } from './accountsStorage'; export const createStandardTonAccountByMnemonic = async ( appContext: { api: APIConfig; defaultWalletVersion: WalletVersion }, + storage: IStorage, mnemonic: string[], options: { versions?: WalletVersion[]; @@ -54,10 +57,12 @@ export const createStandardTonAccountByMnemonic = async ( walletAuth = options.auth; } + const { name, emoji } = await accountsStorage(storage).getNewAccountNameAndEmoji(publicKey); + return new AccountTonMnemonic( publicKey, - getFallbackAccountName(publicKey), - getFallbackAccountEmoji(publicKey), + name, + emoji, walletAuth, tonWallets[0].rawAddress, tonWallets.map(w => ({ @@ -160,6 +165,7 @@ export const getWalletsAddresses = ( export const accountBySignerQr = async ( appContext: { api: APIConfig; defaultWalletVersion: WalletVersion }, + storage: IStorage, qrCode: string ): Promise => { if (!qrCode.startsWith('tonkeeper://signer')) { @@ -182,10 +188,14 @@ export const accountBySignerQr = async ( // TODO support multiple wallets versions configuration const active = await findWalletAddress(appContext, publicKey); + const { name: fallbackName, emoji } = await accountsStorage(storage).getNewAccountNameAndEmoji( + publicKey + ); + return new AccountTonOnly( publicKey, - name || getFallbackAccountName(publicKey), - getFallbackAccountEmoji(publicKey), + name || fallbackName, + emoji, { kind: 'signer' }, active.rawAddress, [ @@ -203,15 +213,20 @@ export const accountBySignerQr = async ( export const accountBySignerDeepLink = async ( appContext: { api: APIConfig; defaultWalletVersion: WalletVersion }, + storage: IStorage, publicKey: string, name: string | null ): Promise => { const active = await findWalletAddress(appContext, publicKey); + const { name: fallbackName, emoji } = await accountsStorage(storage).getNewAccountNameAndEmoji( + publicKey + ); + return new AccountTonOnly( publicKey, - name || getFallbackAccountName(publicKey), - getFallbackAccountEmoji(publicKey), + name || fallbackName, + emoji, { kind: 'signer-deeplink' }, active.rawAddress, [ @@ -228,6 +243,7 @@ export const accountBySignerDeepLink = async ( }; export const accountByLedger = ( + accountId: string, walletsInfo: { address: string; publicKey: Buffer; @@ -236,9 +252,9 @@ export const accountByLedger = ( name: string, emoji: string ): AccountLedger => { - const zeroAccPublicKey = walletsInfo[0].publicKey.toString('hex'); + // const zeroAccPublicKey = walletsInfo[0].publicKey.toString('hex'); return new AccountLedger( - zeroAccPublicKey, + accountId, name, emoji, walletsInfo[0].accountIndex, @@ -267,7 +283,7 @@ export const accountByLedger = ( ); }; -export const accountByKeystone = (ur: UR): AccountKeystone => { +export const accountByKeystone = async (ur: UR, storage: IStorage): Promise => { const account = parseTonAccount(ur); const contact = WalletContractV4.create({ workchain: 0, @@ -277,20 +293,18 @@ export const accountByKeystone = (ur: UR): AccountKeystone => { const pathInfo = account.path && account.xfp ? { path: account.path, mfp: account.xfp } : undefined; - return new AccountKeystone( - account.publicKey, - getFallbackAccountName(account.publicKey), - getFallbackAccountEmoji(account.publicKey), - pathInfo, - { - id: contact.address.toRawString(), - publicKey: account.publicKey, - version: WalletVersion.V4R2, - rawAddress: contact.address.toRawString(), - name: getFallbackWalletName(contact.address.toRawString()), - emoji: getFallbackTonStandardWalletEmoji(account.publicKey, WalletVersion.V4R2) - } + const { name, emoji } = await accountsStorage(storage).getNewAccountNameAndEmoji( + account.publicKey ); + + return new AccountKeystone(account.publicKey, name, emoji, pathInfo, { + id: contact.address.toRawString(), + publicKey: account.publicKey, + version: WalletVersion.V4R2, + rawAddress: contact.address.toRawString(), + name: getFallbackWalletName(contact.address.toRawString()), + emoji: getFallbackTonStandardWalletEmoji(account.publicKey, WalletVersion.V4R2) + }); }; export function getFallbackAccountEmoji(publicKey: string) { diff --git a/packages/uikit/src/components/Header.tsx b/packages/uikit/src/components/Header.tsx index 8ddba6556..d4080ab99 100644 --- a/packages/uikit/src/components/Header.tsx +++ b/packages/uikit/src/components/Header.tsx @@ -9,7 +9,8 @@ import { useActiveWallet, useAccountsState, useMutateActiveTonWallet, - useActiveTonNetwork + useActiveTonNetwork, + useActiveAccount } from '../state/wallet'; import { DropDown } from './DropDown'; import { DoneIcon, DownIcon, PlusIcon, SettingsIcon } from './Icon'; @@ -20,7 +21,8 @@ import { ScanButton } from './connect/ScanButton'; import { ImportNotification } from './create/ImportNotification'; import { SkeletonText } from './shared/Skeleton'; import { WalletEmoji } from './shared/emoji/WalletEmoji'; -import { TonWalletStandard } from '@tonkeeper/core/dist/entries/wallet'; +import { TonWalletStandard, walletVersionText } from '@tonkeeper/core/dist/entries/wallet'; +import { Account } from '@tonkeeper/core/dist/entries/account'; const Block = styled.div<{ center?: boolean; @@ -101,6 +103,7 @@ const Icon = styled.span` padding-left: 0.5rem; color: ${props => props.theme.accentBlue}; display: flex; + margin-left: auto; `; const Row = styled.div` @@ -118,10 +121,40 @@ const Row = styled.div` } `; +const Badge = styled.div` + padding: 2px 4px; + margin-left: -4px; + height: fit-content; + background: ${p => p.theme.backgroundContentAttention}; + border-radius: 3px; + color: ${p => p.theme.textSecondary}; + font-size: 9px; + font-style: normal; + font-weight: 510; + line-height: 12px; +`; + +const ListItemPayloadStyled = styled(ListItemPayload)` + justify-content: flex-start; +`; + +const ColumnTextStyled = styled(ColumnText)` + flex-grow: 0; +`; + +const DropDownContainerStyle = createGlobalStyle` + .header-dd-container { + margin-left: -135px; + width: 270px; + } +`; + const WalletRow: FC<{ + account: Account; walletState: TonWalletStandard; onClose: () => void; -}> = ({ walletState, onClose }) => { + badge?: string; +}> = ({ account, walletState, onClose, badge }) => { const network = useActiveTonNetwork(); const { mutate } = useMutateActiveTonWallet(); const address = toShortValue(formatAddress(walletState.rawAddress, network)); @@ -134,15 +167,16 @@ const WalletRow: FC<{ onClose(); }} > - - - + + + + {badge && {badge}} {activeWallet?.id === walletState.id ? ( ) : undefined} - + ); }; @@ -153,13 +187,41 @@ const DropDownPayload: FC<{ onClose: () => void; onCreate: () => void }> = ({ }) => { const navigate = useNavigate(); const { t } = useTranslation(); - const wallets = useAccountsState().flatMap(a => a.allTonWallets); - - if (!wallets) { + const accountsWallets: { wallet: TonWalletStandard; account: Account; badge?: string }[] = + useAccountsState().flatMap(a => { + if (a.type === 'ledger') { + return a.derivations.map( + d => + ({ + wallet: d.tonWallets.find(w => w.id === d.activeTonWalletId)!, + account: a, + badge: `LEDGER #${d.index}` + } as { wallet: TonWalletStandard; account: Account; badge?: string }) + ); + } + + return a.allTonWallets.map( + w => + ({ + wallet: w, + account: a, + badge: + a.type === 'ton-only' + ? 'SIGNER' + : a.type === 'keystone' + ? 'KEYSTONE' + : a.allTonWallets.length > 1 + ? walletVersionText(w.version) + : undefined + } as { wallet: TonWalletStandard; account: Account; badge?: string }) + ); + }); + + if (!accountsWallets) { return null; } - if (wallets.length === 1) { + if (accountsWallets.length === 1) { return ( { @@ -176,8 +238,14 @@ const DropDownPayload: FC<{ onClose: () => void; onCreate: () => void }> = ({ } else { return ( <> - {wallets.map(wallet => ( - + {accountsWallets.map(({ wallet, account, badge }) => ( + ))} void; onCreate: () => void }> = ({ } }; +const TitleStyled = styled(Title)` + align-items: center; +`; + export const Header: FC<{ showQrScan?: boolean }> = ({ showQrScan = true }) => { - const { t } = useTranslation(); - const wallet = useActiveWallet(); + const account = useActiveAccount(); const [isOpen, setOpen] = useState(false); const wallets = useAccountsState(); const shouldShowIcon = wallets.length > 1; + const accountBadge = + account.allTonWallets.length === 1 + ? undefined + : account.type === 'ledger' + ? `LEDGER #${account.activeDerivation.index}` + : account.type === 'mnemonic' + ? walletVersionText(account.activeTonWallet.version) + : undefined; + return ( + ( setOpen(true)} /> )} + containerClassName="header-dd-container" > - - {shouldShowIcon && <WalletEmoji emoji={wallet.emoji} />} - <TitleName> {wallet.name ? wallet.name : t('wallet_title')}</TitleName> + <TitleStyled> + {shouldShowIcon && <WalletEmoji emoji={account.emoji} />} + <TitleName>{account.name}</TitleName> + {accountBadge && <Badge>{accountBadge}</Badge>} <DownIconWrapper> <DownIcon /> </DownIconWrapper> - + {showQrScan && } diff --git a/packages/uikit/src/components/create/ChoseWalletVersions.tsx b/packages/uikit/src/components/create/ChoseWalletVersions.tsx index 5e9a9e395..2c024ac97 100644 --- a/packages/uikit/src/components/create/ChoseWalletVersions.tsx +++ b/packages/uikit/src/components/create/ChoseWalletVersions.tsx @@ -9,7 +9,7 @@ import { } from '@tonkeeper/core/dist/entries/wallet'; import { formatAddress, toShortValue } from '@tonkeeper/core/dist/utils/common'; import React, { FC, useEffect, useLayoutEffect, useState } from 'react'; -import { useStandardTonWalletVersions } from '../../state/wallet'; +import { useAccountState, useStandardTonWalletVersions } from '../../state/wallet'; import { SkeletonList } from '../Skeleton'; import { toFormattedTonBalance } from '../../hooks/balance'; import { Checkbox } from '../fields/Checkbox'; @@ -76,6 +76,7 @@ export const ChoseWalletVersions: FC<{ const [publicKey, setPublicKey] = useState(undefined); const { data: wallets } = useStandardTonWalletVersions(publicKey); const [checkedVersions, setCheckedVersions] = useState([]); + const accountState = useAccountState(publicKey); useEffect(() => { mnemonicToWalletKey(mnemonic).then(keypair => @@ -85,6 +86,10 @@ export const ChoseWalletVersions: FC<{ useLayoutEffect(() => { if (wallets) { + if (accountState) { + return setCheckedVersions(accountState.allTonWallets.map(w => w.version)); + } + const versionsToCheck = wallets .filter(w => w.tonBalance || w.hasJettons) .map(w => w.version); @@ -93,7 +98,7 @@ export const ChoseWalletVersions: FC<{ } setCheckedVersions(versionsToCheck); } - }, [wallets]); + }, [wallets, accountState]); const toggleVersion = (version: WalletVersion, isChecked: boolean) => { setCheckedVersions(state => diff --git a/packages/uikit/src/components/desktop/aside/AsideMenu.tsx b/packages/uikit/src/components/desktop/aside/AsideMenu.tsx index fb2419af6..71343a1d2 100644 --- a/packages/uikit/src/components/desktop/aside/AsideMenu.tsx +++ b/packages/uikit/src/components/desktop/aside/AsideMenu.tsx @@ -198,7 +198,7 @@ export const AsideMenuAccount: FC<{ account: Account; isSelected: boolean }> = ( onClick={e => { e.preventDefault(); e.stopPropagation(); - openWalletVersionSettings({ account }); + openWalletVersionSettings({ accountId: account.id }); }} isShown={isHovered} > diff --git a/packages/uikit/src/components/desktop/header/DesktopHeader.tsx b/packages/uikit/src/components/desktop/header/DesktopHeader.tsx index f6d43e326..19718b2dc 100644 --- a/packages/uikit/src/components/desktop/header/DesktopHeader.tsx +++ b/packages/uikit/src/components/desktop/header/DesktopHeader.tsx @@ -9,15 +9,18 @@ import { useDisclosure } from '../../../hooks/useDisclosure'; import { usePreFetchRates } from '../../../state/rates'; import { useTonendpointBuyMethods } from '../../../state/tonendpoint'; import { fallbackRenderOver } from '../../Error'; -import { ArrowDownIcon, ArrowUpIcon, PlusIcon, PlusIconSmall } from "../../Icon"; -import { Num2 } from '../../Text'; +import { ArrowDownIcon, ArrowUpIcon, PlusIconSmall } from '../../Icon'; +import { Body2Class, Num2 } from '../../Text'; import { Button } from '../../fields/Button'; import { IconButton } from '../../fields/IconButton'; import { Link } from 'react-router-dom'; -import { AppProRoute } from '../../../libs/routes'; +import { AppProRoute, AppRoute, SettingsRoute } from '../../../libs/routes'; import { BuyNotification } from '../../home/BuyAction'; import { Skeleton } from '../../shared/Skeleton'; -import { useWalletTotalBalance } from "../../../state/asset"; +import { useWalletTotalBalance } from '../../../state/asset'; +import { hexToRGBA } from '../../../libs/css'; +import { useActiveTonNetwork } from '../../../state/wallet'; +import { Network } from '@tonkeeper/core/dist/entries/network'; const DesktopHeaderStyled = styled.div` padding-left: 1rem; @@ -96,6 +99,26 @@ const LinkStyled = styled(Link)` text-decoration: unset; `; +const TestnetBadge = styled(Link)` + background: ${p => hexToRGBA(p.theme.accentRed, 0.16)}; + color: ${p => p.theme.accentRed}; + padding: 4px 8px; + border-radius: ${p => p.theme.corner2xSmall}; + border: none; + text-transform: uppercase; + margin-left: 10px; + margin-right: auto; + text-decoration: none; + + transition: background 0.15s ease-in-out; + + &:hover { + background: ${p => hexToRGBA(p.theme.accentRed, 0.36)}; + } + + ${Body2Class}; +`; + const DesktopHeaderPayload = () => { usePreFetchRates(); const { fiat } = useAppContext(); @@ -104,6 +127,7 @@ const DesktopHeaderPayload = () => { const { isOpen, onClose, onOpen } = useDisclosure(); const { data: buy } = useTonendpointBuyMethods(); const { t } = useTranslation(); + const network = useActiveTonNetwork(); return ( @@ -114,7 +138,9 @@ const DesktopHeaderPayload = () => { {formatFiatCurrency(fiat, balance || 0)} )} - + {network === Network.TESTNET && ( + Testnet + )} (); +const { hook, control } = createModalControl<{ accountId?: AccountId }>(); export const useWalletVersionSettingsNotification = hook; @@ -17,7 +17,10 @@ export const WalletVersionSettingsNotification = () => { return ( onClose()}> {() => ( - + )} ); diff --git a/packages/uikit/src/pages/import/Ledger.tsx b/packages/uikit/src/pages/import/Ledger.tsx index 35b7347b4..fcceee506 100644 --- a/packages/uikit/src/pages/import/Ledger.tsx +++ b/packages/uikit/src/pages/import/Ledger.tsx @@ -3,9 +3,9 @@ import { Button } from '../../components/fields/Button'; import { useTranslation } from '../../hooks/translation'; import { LedgerAccount, - useAddLedgerAccountsMutation, + useAddLedgerAccountMutation, useConnectLedgerMutation, - useLedgerAccounts + useLedgerWallets } from '../../state/ledger'; import React, { FC, useCallback, useEffect, useState } from 'react'; import { LedgerTonTransport } from '@tonkeeper/core/dist/service/ledger/connector'; @@ -20,8 +20,7 @@ import { formatAddress } from '@tonkeeper/core/dist/utils/common'; import { Checkbox } from '../../components/fields/Checkbox'; import { LedgerConnectionSteps } from '../../components/ledger/LedgerConnectionSteps'; import { UpdateWalletName } from '../../components/create/WalletName'; -import { getFallbackAccountEmoji } from '@tonkeeper/core/dist/service/walletService'; -import { toFormattedTonBalance } from "../../hooks/balance"; +import { toFormattedTonBalance } from '../../hooks/balance'; const ConnectLedgerWrapper = styled.div` display: flex; @@ -155,15 +154,16 @@ const ChooseLedgerAccounts: FC<{ tonTransport: LedgerTonTransport; onCancel: () }) => { const { t } = useTranslation(); const totalAccounts = 10; - const { mutate: getLedgerAccounts, data: ledgerAccounts } = useLedgerAccounts(totalAccounts); + const { mutateAsync: getLedgerWallets, data: ledgerAccountData } = + useLedgerWallets(totalAccounts); const [selectedIndexes, setSelectedIndexes] = useState>({}); - const { mutate: addAccountsMutation, isLoading: isAdding } = useAddLedgerAccountsMutation(); + const { mutate: addAccountsMutation, isLoading: isAdding } = useAddLedgerAccountMutation(); const [accountsToAdd, setAccountsToAdd] = useState(); useEffect(() => { - getLedgerAccounts(tonTransport); + getLedgerWallets(tonTransport).then(data => setSelectedIndexes(data.preselectedIndexes)); }, [tonTransport]); const chosenSomeAccounts = !!Object.values(selectedIndexes).filter(Boolean).length; @@ -178,17 +178,24 @@ const ChooseLedgerAccounts: FC<{ tonTransport: LedgerTonTransport; onCancel: () .filter(([, v]) => v) .map(([k]) => Number(k)); setAccountsToAdd( - ledgerAccounts!.filter(account => chosenIndexes.includes(account.accountIndex)) + ledgerAccountData!.wallets.filter(account => + chosenIndexes.includes(account.accountIndex) + ) ); }; if (accountsToAdd) { - const fallbackEmoji = getFallbackAccountEmoji(accountsToAdd[0].publicKey.toString('hex')); return ( - addAccountsMutation({ name, emoji, accounts: accountsToAdd }) + addAccountsMutation({ + name, + emoji, + wallets: accountsToAdd, + accountId: ledgerAccountData!.accountId + }) } /> ); @@ -198,13 +205,13 @@ const ChooseLedgerAccounts: FC<{ tonTransport: LedgerTonTransport; onCancel: () {t('ledger_choose_wallets')} - {!ledgerAccounts ? ( + {!ledgerAccountData ? ( ) : ( - {ledgerAccounts.map(account => ( + {ledgerAccountData.wallets.map(account => ( {toFormattedAddress(account.address)}   @@ -233,7 +240,7 @@ const ChooseLedgerAccounts: FC<{ tonTransport: LedgerTonTransport; onCancel: ()