diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml index 711f63a3..5853d39a 100644 --- a/.github/workflows/package-and-publish.yml +++ b/.github/workflows/package-and-publish.yml @@ -312,10 +312,6 @@ jobs: WEB_EXT_API_KEY: ${{ secrets.FIREFOX_API_KEY }} WEB_EXT_API_SECRET: ${{ secrets.FIREFOX_API_SECRET }} # App env - TONHTTPAPI_MAINNET_URL: ${{ vars.TONHTTPAPI_MAINNET_URL }} - TONAPIIO_MAINNET_URL: ${{ vars.TONAPIIO_MAINNET_URL }} - TONHTTPAPI_TESTNET_URL: ${{ vars.TONHTTPAPI_TESTNET_URL }} - TONAPIIO_TESTNET_URL: ${{ vars.TONAPIIO_TESTNET_URL }} PROXY_HOSTS: ${{ vars.PROXY_HOSTS }} STAKING_POOLS: ${{ vars.STAKING_POOLS }} if: ${{ env.WEB_EXT_API_KEY != '' }} @@ -324,12 +320,8 @@ jobs: UNZIP_DIR=/tmp/${{ env.APP_NAME }}-firefox mkdir $UNZIP_DIR unzip ${{ env.FIREFOX_FILE_NAME }} -d $UNZIP_DIR - web-ext-submit --source-dir=$UNZIP_DIR/dist + npx web-ext-submit --source-dir=$UNZIP_DIR/dist echo "APP_NAME=\"${APP_NAME}\" - TONHTTPAPI_MAINNET_URL=\"${TONHTTPAPI_MAINNET_URL}\" - TONHTTPAPI_TESTNET_URL=\"${TONHTTPAPI_TESTNET_URL}\" - TONAPIIO_MAINNET_URL=\"${TONAPIIO_MAINNET_URL}\" - TONAPIIO_TESTNET_URL=\"${TONAPIIO_TESTNET_URL}\" PROXY_HOSTS=\"${PROXY_HOSTS}\" STAKING_POOLS=\"${STAKING_POOLS}\"" >.env bash deploy/firefox_pack_sources.sh diff --git a/changelogs/1.17.1.txt b/changelogs/1.17.1.txt new file mode 100644 index 00000000..5f2164e4 --- /dev/null +++ b/changelogs/1.17.1.txt @@ -0,0 +1 @@ +Some hotfixes diff --git a/package-lock.json b/package-lock.json index d0414122..1d5dba18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "1.17.0", + "version": "1.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "1.17.0", + "version": "1.17.1", "license": "GPL-3.0-or-later", "dependencies": { "@capacitor-mlkit/barcode-scanning": "^5.3.0", diff --git a/package.json b/package.json index a8a43a5f..6f1191ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "1.17.0", + "version": "1.17.1", "description": "The most feature-rich web wallet and browser extension for TON – with support of multi-accounts, tokens (jettons), NFT, TON DNS, TON Sites, TON Proxy, and TON Magic.", "main": "index.js", "scripts": { diff --git a/public/get/index.html b/public/get/index.html index e1d61e2e..35d48fe3 100644 --- a/public/get/index.html +++ b/public/get/index.html @@ -3,7 +3,7 @@ - MyTonWallet Desktop + Install MyTonWallet diff --git a/public/get/index.js b/public/get/index.js index eba462db..6c82b66b 100644 --- a/public/get/index.js +++ b/public/get/index.js @@ -2,6 +2,10 @@ const REPO = 'mytonwalletorg/mytonwallet'; const LATEST_RELEASE_API_URL = `https://api.github.com/repos/${REPO}/releases/latest`; const LATEST_RELEASE_WEB_URL = `https://github.com/${REPO}/releases/latest`; const WEB_APP_URL = '/'; +const MOBILE_URLS = { + ios: 'https://apps.apple.com/ru/app/mytonwallet-anyway-ton-wallet/id6464677844', + android: 'https://play.google.com/store/apps/details?id=org.mytonwallet.app', +}; const platform = getPlatform(); const currentPage = location.href.includes('mac.html') @@ -48,7 +52,7 @@ const packagesPromise = fetch(LATEST_RELEASE_API_URL) }); (function init() { - if (platform === 'Windows' || platform === 'Linux') { + if (['Windows', 'Linux', 'iOS', 'Android'].includes(platform)) { if (currentPage === 'index') { setupDownloadButton(); setupVersion(); @@ -103,10 +107,17 @@ function setupVersion() { document.addEventListener('DOMContentLoaded', () => { Promise.all([packagesPromise, areSignaturesPresent()]).then(([packages, areSignaturesPresentResult]) => { const versionEl = document.querySelector('.version'); - const signaturesHtml = areSignaturesPresentResult - ? 'Signatures' - : 'Missing signatures!'; - versionEl.innerHTML = `v. ${packages.$version} · ${signaturesHtml}`; + + let html = `v. ${packages.$version}`; + if (['Windows', 'macOS', 'Linux'].includes(platform)) { + const signaturesHtml = areSignaturesPresentResult + ? 'Signatures' + : 'Missing signatures!'; + + html += ` · ${signaturesHtml}`; + } + + versionEl.innerHTML = html; }); }); } @@ -127,6 +138,10 @@ function redirectToFullList() { location.href = LATEST_RELEASE_WEB_URL; } +function redirectToStore(platform) { + location.href = MOBILE_URLS[platform.toLowerCase()]; +} + function downloadDefault() { if (platform === 'Windows') { download('win'); @@ -134,6 +149,8 @@ function downloadDefault() { download('linux'); } else if (platform === 'macOS') { redirectToMac(); + } else if (platform === 'iOS' || platform === 'Android') { + redirectToStore(platform); } else { redirectToUnsupported(); } diff --git a/public/version.txt b/public/version.txt index 73d74673..511a76e6 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.17.0 \ No newline at end of file +1.17.1 diff --git a/src/api/types/backend.ts b/src/api/types/backend.ts index 23422136..8a3d7436 100644 --- a/src/api/types/backend.ts +++ b/src/api/types/backend.ts @@ -42,6 +42,7 @@ export type ApiSwapAsset = { blockchain: string; slug: string; decimals: number; + isPopular: boolean; image?: string; contract?: string; keywords?: string[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index 7a323fbc..cddaa763 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -8,7 +8,7 @@ import { INACTIVE_MARKER, IS_CAPACITOR } from '../config'; import { setActiveTabChangeListener } from '../util/activeTabMonitor'; import buildClassName from '../util/buildClassName'; import { - CAN_DELEGATE_BOTTOM_SHEET, IS_ANDROID, IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON, IS_IOS, IS_LINUX, + IS_ANDROID, IS_DELEGATED_BOTTOM_SHEET, IS_DELEGATING_BOTTOM_SHEET, IS_ELECTRON, IS_IOS, IS_LINUX, } from '../util/windowEnvironment'; import { updateSizes } from '../util/windowSize'; @@ -79,7 +79,7 @@ function App({ const lang = useLang(); const { isPortrait } = useDeviceScreen(); - const areSettingsInModal = !isPortrait || IS_ELECTRON || CAN_DELEGATE_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET; + const areSettingsInModal = !isPortrait || IS_ELECTRON || IS_DELEGATING_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET; const [isBarcodeSupported, setIsBarcodeSupported] = useState(false); const [isInactive, markInactive] = useFlag(false); diff --git a/src/components/Dialogs.tsx b/src/components/Dialogs.tsx index 27ce496f..3212905e 100644 --- a/src/components/Dialogs.tsx +++ b/src/components/Dialogs.tsx @@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../global'; import renderText from '../global/helpers/renderText'; import { pick } from '../util/iteratees'; -import { CAN_DELEGATE_BOTTOM_SHEET, IS_DELEGATED_BOTTOM_SHEET } from '../util/windowEnvironment'; +import { IS_DELEGATED_BOTTOM_SHEET, IS_DELEGATING_BOTTOM_SHEET } from '../util/windowEnvironment'; import useFlag from '../hooks/useFlag'; import useLang from '../hooks/useLang'; @@ -30,7 +30,7 @@ const Dialogs: FC = ({ dialogs }) => { const title = lang('Something went wrong'); useEffect(() => { - if (CAN_DELEGATE_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET) { + if (IS_DELEGATING_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET) { if (message) { Dialog.alert({ title, @@ -46,7 +46,7 @@ const Dialogs: FC = ({ dialogs }) => { } }, [dialogs, lang, message, openModal, title]); - if (!message || CAN_DELEGATE_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET) { + if (!message || IS_DELEGATING_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET) { return undefined; } diff --git a/src/components/common/TokenSelector.tsx b/src/components/common/TokenSelector.tsx index 87f1a8fa..981f9b85 100644 --- a/src/components/common/TokenSelector.tsx +++ b/src/components/common/TokenSelector.tsx @@ -14,8 +14,9 @@ import { import { ANIMATED_STICKER_MIDDLE_SIZE_PX, TON_BLOCKCHAIN } from '../../config'; import { Big } from '../../lib/big.js/index.js'; import { + selectCurrentAccountState, selectCurrentAccountTokens, - selectPopularTokensWithoutAccountTokens, + selectPopularTokens, selectSwapTokens, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; @@ -52,6 +53,7 @@ interface StateProps { swapTokens?: UserSwapToken[]; tokenInSlug?: string; pairsBySlug?: Record; + balancesBySlug?: Record; baseCurrency?: ApiBaseCurrency; isLoading?: boolean; } @@ -59,7 +61,7 @@ interface StateProps { interface OwnProps { isActive?: boolean; shouldFilter?: boolean; - onlyPopular?: boolean; + isInsideSettings?: boolean; onClose: NoneToVoidFunction; onBack: NoneToVoidFunction; } @@ -80,10 +82,11 @@ function TokenSelector({ swapTokens, popularTokens, shouldFilter, - onlyPopular, + isInsideSettings, baseCurrency, tokenInSlug, pairsBySlug, + balancesBySlug, isActive, isLoading, onBack, @@ -96,6 +99,7 @@ function TokenSelector({ setSwapTokenIn, setSwapTokenOut, addToken, + addSwapToken, } = getActions(); const lang = useLang(); @@ -124,14 +128,27 @@ function TokenSelector({ const [renderingKey, setRenderingKey] = useState(SearchState.Initial); const [searchTokenList, setSearchTokenList] = useState([]); - const popularTokensPrev = usePrevious(popularTokens); + const balancesBySlugPrev = usePrevious(balancesBySlug); const filterTokens = useLastCallback((tokens: Token[]) => filterAndSortTokens(tokens, tokenInSlug, pairsBySlug)); + const allUnimportedTonTokens = useMemo(() => { + const balances = balancesBySlugPrev ?? balancesBySlug ?? {}; + const tokens = (swapTokens ?? EMPTY_ARRAY).filter( + (popularToken) => { + const isTonBlockchain = 'blockchain' in popularToken && popularToken.blockchain === TON_BLOCKCHAIN; + const isTokenUnimported = balances[popularToken.slug] === undefined; + return isTonBlockchain && isTokenUnimported; + }, + ); + + return tokens; + }, [balancesBySlug, balancesBySlugPrev, swapTokens]); + const { userTokensWithFilter, popularTokensWithFilter, swapTokensWithFilter } = useMemo(() => { const currentUserTokens = userTokens ?? EMPTY_ARRAY; const currentSwapTokens = swapTokens ?? EMPTY_ARRAY; - const currentPopularTokens = popularTokensPrev ?? popularTokens ?? EMPTY_ARRAY; + const currentPopularTokens = popularTokens ?? EMPTY_ARRAY; if (!shouldFilter) { return { @@ -142,26 +159,18 @@ function TokenSelector({ } const filteredPopularTokens = filterTokens(currentPopularTokens); - let filteredUserTokens: Token[]; - let filteredSwapTokens: Token[]; - - if (onlyPopular) { - filteredUserTokens = EMPTY_ARRAY; - filteredSwapTokens = EMPTY_ARRAY; - } else { - filteredUserTokens = filterTokens(currentUserTokens); - filteredSwapTokens = filterTokens(currentSwapTokens); - } + const filteredUserTokens = filterTokens(currentUserTokens); + const filteredSwapTokens = filterTokens(currentSwapTokens); return { userTokensWithFilter: filteredUserTokens, popularTokensWithFilter: filteredPopularTokens, swapTokensWithFilter: filteredSwapTokens, }; - }, [filterTokens, onlyPopular, popularTokens, popularTokensPrev, shouldFilter, swapTokens, userTokens]); + }, [filterTokens, popularTokens, shouldFilter, swapTokens, userTokens]); const filteredTokenList = useMemo(() => { - const tokensToFilter = onlyPopular ? popularTokensWithFilter : swapTokensWithFilter; + const tokensToFilter = isInsideSettings ? allUnimportedTonTokens : swapTokensWithFilter; const lowerCaseSearchValue = searchValue.toLowerCase().trim(); return tokensToFilter.filter(({ @@ -177,7 +186,7 @@ function TokenSelector({ return isName || isSymbol || isKeyword; }).sort((a, b) => b.amount - a.amount) ?? []; - }, [onlyPopular, popularTokensWithFilter, searchValue, swapTokensWithFilter]); + }, [allUnimportedTonTokens, isInsideSettings, searchValue, swapTokensWithFilter]); const resetSearch = () => { setSearchValue(''); @@ -228,9 +237,10 @@ function TokenSelector({ onClose(); } - if (onlyPopular) { + if (isInsideSettings) { addToken({ token: selectedToken as UserToken }); } else { + addSwapToken({ token: selectedToken as UserSwapToken }); const setToken = shouldFilter ? setSwapTokenOut : setSwapTokenIn; setToken({ tokenSlug: selectedToken.slug }); } @@ -435,8 +445,8 @@ function TokenSelector({ } function renderTokenGroups() { - if (onlyPopular) { - return renderTokenGroup(popularTokensWithFilter, lang('POPULAR')); + if (isInsideSettings) { + return renderTokenGroup(allUnimportedTonTokens, lang('A-Z')); } return ( @@ -489,11 +499,12 @@ function TokenSelector({ } export default memo(withGlobal((global): StateProps => { + const balances = selectCurrentAccountState(global)?.balances; const { isLoading, token } = global.settings.importToken ?? {}; const { pairs, tokenInSlug } = global.currentSwap ?? {}; const userTokens = selectCurrentAccountTokens(global); - const popularTokens = selectPopularTokensWithoutAccountTokens(global); + const popularTokens = selectPopularTokens(global); const swapTokens = selectSwapTokens(global); const { baseCurrency } = global.settings; @@ -506,6 +517,7 @@ export default memo(withGlobal((global): StateProps => { tokenInSlug, baseCurrency, pairsBySlug: pairs?.bySlug, + balancesBySlug: balances?.bySlug, }; })(TokenSelector)); diff --git a/src/components/ledger/LedgerModal.module.scss b/src/components/ledger/LedgerModal.module.scss index 31d68e7d..44739e00 100644 --- a/src/components/ledger/LedgerModal.module.scss +++ b/src/components/ledger/LedgerModal.module.scss @@ -148,16 +148,20 @@ } .accounts { - overflow-y: auto; + overflow-y: scroll; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.5rem; + max-height: 13.25rem; + margin-bottom: 2rem; padding: 0.5rem; background-color: var(--color-background-first); border-radius: var(--border-radius-default); + @include adapt-padding-to-scrollbar(0.5rem, !important); + &_two { grid-template-columns: repeat(6, 1fr); } @@ -198,10 +202,6 @@ opacity: 1; } - .accounts_single & { - grid-column-start: 2; - } - .accounts_two & { grid-column: span 2; @@ -219,6 +219,41 @@ } } +.addAccountContainer { + flex-direction: column !important; + align-items: flex-start !important; + justify-content: space-between !important; + + width: 100% !important; + min-width: auto !important; + max-width: 100% !important; + height: 3.75rem !important; + padding: 0.5rem !important; + + font-size: 0.75rem !important; + line-height: 1 !important; + color: var(--color-add-wallet-text) !important; + + background-color: var(--color-add-wallet-background) !important; + border-radius: 0.5rem !important; + + &:focus, + &:hover { + color: var(--color-add-wallet-text-hover) !important; + + background-color: var(--color-add-wallet-background-hover) !important; + } + + &:active { + // Optimization + transition: none; + } +} + +.addAccountIcon { + font-size: 1.5rem; +} + .accountName { overflow: hidden; diff --git a/src/components/ledger/LedgerSelectWallets.tsx b/src/components/ledger/LedgerSelectWallets.tsx index 0c288be6..2c0a975c 100644 --- a/src/components/ledger/LedgerSelectWallets.tsx +++ b/src/components/ledger/LedgerSelectWallets.tsx @@ -40,6 +40,7 @@ function LedgerSelectWallets({ }: OwnProps) { const { afterSelectHardwareWallets, + loadMoreHardwareWallets, } = getActions(); const lang = useLang(); @@ -69,6 +70,25 @@ function LedgerSelectWallets({ [accounts], ); + const handleAddWalletClick = useLastCallback(() => { + const list = hardwareWallets ?? []; + const lastIndex = list[list.length - 1]?.index ?? 0; + + loadMoreHardwareWallets({ lastIndex }); + }); + + function renderAddAccount() { + return ( + + ); + } + function renderAccount(address: string, balance: string, index: number, isConnected: boolean) { const isActiveAccount = isConnected || selectedAccountIndices.includes(index); @@ -76,7 +96,6 @@ function LedgerSelectWallets({
handleAccountToggle(index)} > @@ -98,8 +117,8 @@ function LedgerSelectWallets({ const list = hardwareWallets ?? []; const fullClassName = buildClassName( styles.accounts, - list.length === 1 && styles.accounts_single, - list.length === 2 && styles.accounts_two, + list.length === 1 && styles.accounts_two, + 'custom-scroll', ); return ( @@ -112,6 +131,7 @@ function LedgerSelectWallets({ alreadyConnectedList.includes(address), ), )} + {renderAddAccount()}
); } diff --git a/src/components/main/modals/QrScannerModal.tsx b/src/components/main/modals/QrScannerModal.tsx index fb3dca7a..c34cbb30 100644 --- a/src/components/main/modals/QrScannerModal.tsx +++ b/src/components/main/modals/QrScannerModal.tsx @@ -9,7 +9,7 @@ import buildClassName from '../../../util/buildClassName'; import { vibrateOnSuccess } from '../../../util/capacitor'; import { pause } from '../../../util/schedulers'; import { - CAN_DELEGATE_BOTTOM_SHEET, DPR, IS_IOS, + DPR, IS_DELEGATING_BOTTOM_SHEET, IS_IOS, } from '../../../util/windowEnvironment'; import useEffectOnce from '../../../hooks/useEffectOnce'; @@ -105,7 +105,7 @@ function QrScannerModal({ isOpen, onScan, onClose }: OwnProps) { }); useEffectWithPrevDeps(([prevIsOpen]) => { - if (CAN_DELEGATE_BOTTOM_SHEET) return undefined; + if (IS_DELEGATING_BOTTOM_SHEET) return undefined; let startScanTimeoutId: number; let documentClassModifyTimeoutId: number; diff --git a/src/components/settings/SettingsTokenList.tsx b/src/components/settings/SettingsTokenList.tsx index 25ec48bd..ba869e18 100644 --- a/src/components/settings/SettingsTokenList.tsx +++ b/src/components/settings/SettingsTokenList.tsx @@ -26,7 +26,7 @@ function SettingsTokenList({ isActive={isActive} onBack={handleBackClick} onClose={handleBackClick} - onlyPopular + isInsideSettings /> ); diff --git a/src/components/settings/SettingsTokens.tsx b/src/components/settings/SettingsTokens.tsx index 8509144d..02d65342 100644 --- a/src/components/settings/SettingsTokens.tsx +++ b/src/components/settings/SettingsTokens.tsx @@ -185,7 +185,6 @@ function SettingsTokens({ {!isTON && ( )} diff --git a/src/components/swap/components/SwapSelectToken.tsx b/src/components/swap/components/SwapSelectToken.tsx index cb6f87a1..a7759e90 100644 --- a/src/components/swap/components/SwapSelectToken.tsx +++ b/src/components/swap/components/SwapSelectToken.tsx @@ -64,7 +64,7 @@ function SwapSelectToken({ token, shouldFilter }: OwnProps) { onClick={handleOpenSelectTokenModal} >
- {tokenToRender?.symbol} + { - if (CAN_DELEGATE_BOTTOM_SHEET || !isActive || !isBiometricAuthEnabled) { + if (IS_DELEGATING_BOTTOM_SHEET || !isActive || !isBiometricAuthEnabled) { return; } diff --git a/src/components/ui/Switcher.tsx b/src/components/ui/Switcher.tsx index a3aa5d82..c6768123 100644 --- a/src/components/ui/Switcher.tsx +++ b/src/components/ui/Switcher.tsx @@ -9,7 +9,7 @@ type OwnProps = { id?: string; name?: string; value?: string; - label: string; + label?: string; checked?: boolean; className?: string; onChange?: (e: ChangeEvent) => void; diff --git a/src/config.ts b/src/config.ts index bf498965..8acf602e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +import type { ApiSwapAsset } from './api/types'; import type { LangItem } from './global/types'; export const APP_ENV = process.env.APP_ENV; @@ -168,13 +169,14 @@ export const TOKEN_INFO = { export const TON_BLOCKCHAIN = 'ton'; -export const INIT_SWAP_ASSETS = { +export const INIT_SWAP_ASSETS: Record = { toncoin: { name: 'Toncoin', symbol: TON_SYMBOL, blockchain: TON_BLOCKCHAIN, slug: TON_TOKEN_SLUG, decimals: DEFAULT_DECIMAL_PLACES, + isPopular: true, }, 'ton-eqdcbkghmc': { name: 'jWBTC', @@ -185,6 +187,7 @@ export const INIT_SWAP_ASSETS = { // eslint-disable-next-line max-len image: 'https://cache.tonapi.io/imgproxy/LaFKdzahVX9epWT067gyVLd8aCa1lFrZd7Rp9siViEE/rs:fill:200:200:1/g:no/aHR0cHM6Ly9icmlkZ2UudG9uLm9yZy90b2tlbi8xLzB4MjI2MGZhYzVlNTU0MmE3NzNhYTQ0ZmJjZmVkZjdjMTkzYmMyYzU5OS5wbmc.webp', contract: 'EQDcBkGHmC4pTf34x3Gm05XvepO5w60DNxZ-XT4I6-UGG5L5', + isPopular: false, keywords: ['bitcoin'], }, }; diff --git a/src/global/actions/api/auth.ts b/src/global/actions/api/auth.ts index 22f8b710..16216c38 100644 --- a/src/global/actions/api/auth.ts +++ b/src/global/actions/api/auth.ts @@ -37,10 +37,11 @@ import { } from '../../reducers'; import { selectAccounts, + selectAllHardwareAccounts, selectCurrentNetwork, selectFirstNonHardwareAccount, selectIsOneAccount, - selectLastLedgerAccountIndex, + selectLedgerAccountIndexToImport, selectNetworkAccountsMemoized, selectNewestTxIds, } from '../../selectors'; @@ -590,8 +591,14 @@ addActionHandler('connectHardwareWallet', async (global, actions) => { global = getGlobal(); const { isRemoteTab } = global.hardware; const network = selectCurrentNetwork(global); - const lastIndex = selectLastLedgerAccountIndex(global, network); - const hardwareWallets = isRemoteTab ? [] : await ledgerApi.getNextLedgerWallets(network, lastIndex); + const lastIndex = selectLedgerAccountIndexToImport(global); + const currentHardwareAccounts = selectAllHardwareAccounts(global) ?? []; + const currentHardwareAddresses = currentHardwareAccounts.map((account) => account.address); + const hardwareWallets = isRemoteTab ? [] : await ledgerApi.getNextLedgerWallets( + network, + lastIndex, + currentHardwareAddresses, + ); global = getGlobal(); @@ -618,6 +625,25 @@ addActionHandler('connectHardwareWallet', async (global, actions) => { } }); +addActionHandler('loadMoreHardwareWallets', async (global, actions, { lastIndex }) => { + const network = selectCurrentNetwork(global); + const oldHardwareWallets = global.hardware.hardwareWallets ?? []; + const ledgerApi = await import('../../../util/ledger'); + const hardwareWallets = await ledgerApi.getNextLedgerWallets(network, lastIndex); + + global = getGlobal(); + + if ('error' in hardwareWallets) { + actions.showError({ error: hardwareWallets.error }); + throw Error(hardwareWallets.error); + } + + global = updateHardware(global, { + hardwareWallets: oldHardwareWallets.concat(hardwareWallets), + }); + setGlobal(global); +}); + addActionHandler('afterSelectHardwareWallets', (global, actions, { hardwareSelectedIndices }) => { global = updateAuth(global, { method: 'importHardwareWallet', diff --git a/src/global/actions/api/dapps.ts b/src/global/actions/api/dapps.ts index ddc70a25..c29cb754 100644 --- a/src/global/actions/api/dapps.ts +++ b/src/global/actions/api/dapps.ts @@ -41,14 +41,14 @@ addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { pa global = getGlobal(); global = updateIsPinPadPasswordAccepted(global); setGlobal(global); - - await vibrateOnSuccess(true); } if (IS_DELEGATED_BOTTOM_SHEET) { callActionInMain('submitDappConnectRequestConfirm', { password, accountId }); return; + } else if (IS_CAPACITOR) { + vibrateOnSuccess(true); } actions.switchAccount({ accountId }); diff --git a/src/global/actions/api/wallet.ts b/src/global/actions/api/wallet.ts index 68c5f600..2c6d20e1 100644 --- a/src/global/actions/api/wallet.ts +++ b/src/global/actions/api/wallet.ts @@ -714,6 +714,12 @@ addActionHandler('setActiveContentTab', (global, actions, { tab }) => { }); addActionHandler('addSwapToken', (global, actions, { token }) => { + const isAlreadyExist = token.slug in global.swapTokenInfo.bySlug; + + if (isAlreadyExist) { + return; + } + const apiSwapAsset: ApiSwapAsset = { name: token.name, symbol: token.symbol, @@ -723,6 +729,7 @@ addActionHandler('addSwapToken', (global, actions, { token }) => { image: token.image, contract: token.contract, keywords: token.keywords, + isPopular: false, }; setGlobal({ diff --git a/src/global/actions/apiUpdates/dapp.ts b/src/global/actions/apiUpdates/dapp.ts index 299c2cae..34ade47f 100644 --- a/src/global/actions/apiUpdates/dapp.ts +++ b/src/global/actions/apiUpdates/dapp.ts @@ -1,6 +1,7 @@ import { DappConnectState, TransferState } from '../../types'; import { TON_TOKEN_SLUG } from '../../../config'; +import { IS_DELEGATING_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { bigStrToHuman } from '../../helpers'; import { addActionHandler, setGlobal } from '../../index'; import { @@ -72,8 +73,11 @@ addActionHandler('apiUpdate', (global, actions, update) => { } case 'dappConnect': { + if (IS_DELEGATING_BOTTOM_SHEET) { + callActionInNative('apiUpdateDappConnect', update); + } + actions.apiUpdateDappConnect(update); - callActionInNative('apiUpdateDappConnect', update); break; } @@ -119,8 +123,11 @@ addActionHandler('apiUpdate', (global, actions, update) => { } case 'dappSendTransactions': { + if (IS_DELEGATING_BOTTOM_SHEET) { + callActionInNative('apiUpdateDappSendTransaction', update); + } + actions.apiUpdateDappSendTransaction(update); - callActionInNative('apiUpdateDappSendTransaction', update); break; } diff --git a/src/global/cache.ts b/src/global/cache.ts index 520d1103..95ec50da 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -343,6 +343,13 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { cached.stateVersion = 10; } + if (cached.stateVersion === 10) { + if (cached.settings.areTokensWithNoBalanceHidden === undefined) { + cached.settings.areTokensWithNoBalanceHidden = true; + } + cached.stateVersion = 11; + } + // When adding migration here, increase `STATE_VERSION` } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 7bb4fbe8..fdd81cde 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -18,7 +18,7 @@ import { } from '../config'; import { USER_AGENT_LANG_CODE } from '../util/windowEnvironment'; -export const STATE_VERSION = 10; +export const STATE_VERSION = 11; export const INITIAL_STATE: GlobalState = { appState: AppState.Auth, @@ -70,6 +70,7 @@ export const INITIAL_STATE: GlobalState = { dapps: [], byAccountId: {}, areTokensWithNoPriceHidden: true, + areTokensWithNoBalanceHidden: true, }, byAccountId: {}, diff --git a/src/global/selectors/index.ts b/src/global/selectors/index.ts index b8d4d8d3..5c6b4def 100644 --- a/src/global/selectors/index.ts +++ b/src/global/selectors/index.ts @@ -1,4 +1,4 @@ -import type { ApiNetwork, ApiTxIdBySlug } from '../../api/types'; +import type { ApiNetwork, ApiSwapAsset, ApiTxIdBySlug } from '../../api/types'; import type { Account, AccountSettings, @@ -103,59 +103,18 @@ export function selectCurrentAccountTokens(global: GlobalState) { ); } -export const selectPopularTokensMemoized = memoized(( - balancesBySlug: Record, - tokenInfo: GlobalState['tokenInfo'], -) => { - return Object.entries(tokenInfo.bySlug) - .filter(([slug, token]) => token.isPopular && !(slug in balancesBySlug)) - .map(([slug]) => { - const { - symbol, name, image, decimals, keywords, quote: { - price, percentChange24h, percentChange7d, percentChange30d, history7d, history24h, history30d, - }, - } = tokenInfo.bySlug[slug]; - const amount = bigStrToHuman('0', decimals); - - return { - symbol, - slug, - amount, - name, - image, - price, - decimals, - change24h: round(percentChange24h / 100, 4), - change7d: round(percentChange7d / 100, 4), - change30d: round(percentChange30d / 100, 4), - history24h, - history7d, - history30d, - keywords, - } as UserToken; - }); -}); - -export function selectPopularTokensWithoutAccountTokens(global: GlobalState) { - const balancesBySlug = selectCurrentAccountState(global)?.balances?.bySlug; - if (!balancesBySlug || !global.tokenInfo) { - return undefined; - } - - return selectPopularTokensMemoized(balancesBySlug, global.tokenInfo); -} - -const selectSwapTokensMemoized = memoized(( - balancesBySlug: Record, +function createTokenList( swapTokenInfo: GlobalState['swapTokenInfo'], -) => { - const tokenList: UserSwapToken[] = Object.entries(swapTokenInfo.bySlug) - .map(([slug]) => { - const { - symbol, name, image, decimals, keywords, blockchain, contract, - } = swapTokenInfo.bySlug[slug]; + balancesBySlug: Record, + sortFn: (tokenA: ApiSwapAsset, tokenB: ApiSwapAsset) => number, + filterFn?: (token: ApiSwapAsset) => boolean, +): UserSwapToken[] { + return Object.entries(swapTokenInfo.bySlug) + .filter(([, token]) => !filterFn || filterFn(token)) + .map(([slug, { + symbol, name, image, decimals, keywords, blockchain, contract, isPopular, + }]) => { const amount = bigStrToHuman(balancesBySlug[slug] ?? '0', decimals); - return { symbol, slug, @@ -165,17 +124,55 @@ const selectSwapTokensMemoized = memoized(( decimals, isDisabled: false, canSwap: true, + isPopular, keywords, blockchain, contract, } satisfies UserSwapToken; - }); + }) + .sort(sortFn); +} - const userTokenList = tokenList.slice() - .sort((a, b) => a.name.trim().toLowerCase().localeCompare(b.name.trim().toLowerCase())); +const selectPopularTokensMemoized = memoized( + (balancesBySlug, swapTokenInfo) => { + const popularTokenOrder = [ + 'TON', + 'BTC', + 'ETH', + 'USDT', + 'jUSDT', + 'jWBTC', + ]; + const orderMap = new Map(popularTokenOrder.map((item, index) => [item, index])); + + const filterFn = (token: ApiSwapAsset) => token.isPopular; + const sortFn = (tokenA: ApiSwapAsset, tokenB: ApiSwapAsset) => { + const orderIndexA = orderMap.has(tokenA.symbol) ? orderMap.get(tokenA.symbol)! : popularTokenOrder.length; + const orderIndexB = orderMap.has(tokenB.symbol) ? orderMap.get(tokenB.symbol)! : popularTokenOrder.length; + + return orderIndexA - orderIndexB; + }; + return createTokenList(swapTokenInfo, balancesBySlug, sortFn, filterFn); + }, +); + +const selectSwapTokensMemoized = memoized( + (balancesBySlug, swapTokenInfo) => { + const sortFn = (tokenA: ApiSwapAsset, tokenB: ApiSwapAsset) => ( + tokenA.name.trim().toLowerCase().localeCompare(tokenB.name.trim().toLowerCase()) + ); + return createTokenList(swapTokenInfo, balancesBySlug, sortFn); + }, +); + +export function selectPopularTokens(global: GlobalState) { + const balancesBySlug = selectCurrentAccountState(global)?.balances?.bySlug; + if (!balancesBySlug || !global.swapTokenInfo) { + return undefined; + } - return userTokenList; -}); + return selectPopularTokensMemoized(balancesBySlug, global.swapTokenInfo); +} export function selectSwapTokens(global: GlobalState) { const balancesBySlug = selectCurrentAccountState(global)?.balances?.bySlug; @@ -270,6 +267,16 @@ export function selectIsHardwareAccount(global: GlobalState) { return state?.isHardware; } +export function selectAllHardwareAccounts(global: GlobalState) { + const accounts = selectAccounts(global); + + if (!accounts) { + return undefined; + } + + return Object.values(accounts).filter((account) => account.isHardware); +} + export function selectIsOneAccount(global: GlobalState) { return Object.keys(selectAccounts(global) || {}).length === 1; } @@ -278,14 +285,26 @@ export const selectEnabledTokensCountMemoized = memoized((tokens?: UserToken[]) return (tokens ?? []).filter(({ isDisabled }) => !isDisabled).length; }); -export function selectLastLedgerAccountIndex(global: GlobalState, network: ApiNetwork) { - const byId = global.accounts?.byId ?? {}; - return Object.entries(byId).reduce((previousValue, [accountId, account]) => { - if (!account.ledger || parseAccountId(accountId).network !== network) { - return previousValue; +export function selectLedgerAccountIndexToImport(global: GlobalState) { + const hardwareAccounts = selectAllHardwareAccounts(global) ?? []; + const hardwareAccountIndexes = hardwareAccounts?.map((account) => account.ledger!.index) + .sort((a, b) => a - b); + + if (hardwareAccountIndexes.length === 0 || hardwareAccountIndexes[0] !== 0) { + return -1; + } + + if (hardwareAccountIndexes.length === 1) { + return 0; + } + + for (let i = 1; i < hardwareAccountIndexes.length; i++) { + if (hardwareAccountIndexes[i] - hardwareAccountIndexes[i - 1] !== 1) { + return i - 1; } - return Math.max(account.ledger.index, previousValue ?? 0); - }, undefined as number | undefined); + } + + return hardwareAccountIndexes.length - 1; } export function selectLocalTransactions(global: GlobalState, accountId: string) { diff --git a/src/global/types.ts b/src/global/types.ts index 467ec67e..5bd82dd1 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -212,6 +212,7 @@ export type UserToken = { export type UserSwapToken = { blockchain: string; + isPopular: boolean; contract?: string; } & Omit; @@ -449,7 +450,7 @@ export type GlobalState = { isPasswordNumeric?: boolean; // Backwards compatibility for non-numeric passwords from older versions isTestnet?: boolean; isSecurityWarningHidden?: boolean; - areTokensWithNoBalanceHidden?: boolean; + areTokensWithNoBalanceHidden: boolean; areTokensWithNoPriceHidden: boolean; isSortByValueEnabled?: boolean; importToken?: { @@ -508,6 +509,7 @@ export interface ActionPayloads { initializeHardwareWalletConnection: undefined; connectHardwareWallet: undefined; createHardwareAccounts: undefined; + loadMoreHardwareWallets: { lastIndex: number }; createAccount: { password: string; isImporting: boolean; isPasswordNumeric?: boolean }; afterSelectHardwareWallets: { hardwareSelectedIndices: number[] }; resetApiSettings: { areAllDisabled?: boolean } | undefined; diff --git a/src/hooks/useDelegatedBottomSheet.ts b/src/hooks/useDelegatedBottomSheet.ts index 1d72db7c..a8309cd5 100644 --- a/src/hooks/useDelegatedBottomSheet.ts +++ b/src/hooks/useDelegatedBottomSheet.ts @@ -93,6 +93,7 @@ export function useDelegatedBottomSheet( useLayoutEffect(() => { if (!IS_DELEGATED_BOTTOM_SHEET || !isOpen) return; + dialogRef.current!.style[forceFullNative ? 'maxHeight' : 'height'] = ''; dialogRef.current!.style[forceFullNative ? 'height' : 'maxHeight'] = `${maxHeight}px`; }, [dialogRef, forceFullNative, isOpen, maxHeight]); diff --git a/src/hooks/useDelegatingBottomSheet.ts b/src/hooks/useDelegatingBottomSheet.ts index 4974fb9f..86583977 100644 --- a/src/hooks/useDelegatingBottomSheet.ts +++ b/src/hooks/useDelegatingBottomSheet.ts @@ -7,7 +7,7 @@ import { getActions, getGlobal } from '../global'; import type { ActionPayloads } from '../global/types'; import { pause } from '../util/schedulers'; -import { CAN_DELEGATE_BOTTOM_SHEET } from '../util/windowEnvironment'; +import { IS_DELEGATING_BOTTOM_SHEET } from '../util/windowEnvironment'; import useEffectWithPrevDeps from './useEffectWithPrevDeps'; const RACE_TIMEOUT = 1000; @@ -15,7 +15,7 @@ const CLOSING_DURATION = 100; const controlledByNative = new Map(); -if (CAN_DELEGATE_BOTTOM_SHEET) { +if (IS_DELEGATING_BOTTOM_SHEET) { BottomSheet.prepare(); BottomSheet.addListener( @@ -43,7 +43,7 @@ export function useDelegatingBottomSheet( isOpen: boolean | undefined, onClose: AnyToVoidFunction, ) { - const isDelegating = CAN_DELEGATE_BOTTOM_SHEET && key; + const isDelegating = IS_DELEGATING_BOTTOM_SHEET && key; const shouldOpen = isOpen && isPortrait; useEffectWithPrevDeps(([prevShouldOpen]) => { @@ -90,7 +90,7 @@ export function useOpenFromNativeBottomSheet( open: NoneToVoidFunction, ) { useEffect(() => { - if (!CAN_DELEGATE_BOTTOM_SHEET) return undefined; + if (!IS_DELEGATING_BOTTOM_SHEET) return undefined; controlledByNative.set(key, open); diff --git a/src/index.tsx b/src/index.tsx index 1d8f0887..d4442039 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,7 @@ import { enableStrict } from './lib/fasterdom/stricterdom'; import { betterView } from './util/betterView'; import { initCapacitor } from './util/capacitor'; import { initMultitab } from './util/multitab'; -import { CAN_DELEGATE_BOTTOM_SHEET, IS_DELEGATED_BOTTOM_SHEET } from './util/windowEnvironment'; +import { IS_DELEGATED_BOTTOM_SHEET, IS_DELEGATING_BOTTOM_SHEET } from './util/windowEnvironment'; import App from './components/App'; @@ -31,7 +31,7 @@ if (IS_CAPACITOR) { void initCapacitor(); } -if (CAN_DELEGATE_BOTTOM_SHEET) { +if (IS_DELEGATING_BOTTOM_SHEET) { initMultitab({ noPub: true }); } else if (IS_DELEGATED_BOTTOM_SHEET) { initMultitab({ noSub: true }); diff --git a/src/util/ledger/index.ts b/src/util/ledger/index.ts index 50e1202a..f0d47d13 100644 --- a/src/util/ledger/index.ts +++ b/src/util/ledger/index.ts @@ -390,7 +390,11 @@ export async function signLedgerProof(accountId: string, proof: ApiTonConnectPro return result.signature.toString('base64'); } -export async function getNextLedgerWallets(network: ApiNetwork, lastExistingIndex = -1) { +export async function getNextLedgerWallets( + network: ApiNetwork, + lastExistingIndex = -1, + alreadyImportedAddresses: string[] = [], +) { const result: LedgerWalletInfo[] = []; let index = lastExistingIndex + 1; @@ -398,6 +402,12 @@ export async function getNextLedgerWallets(network: ApiNetwork, lastExistingInde // eslint-disable-next-line no-constant-condition while (true) { const walletInfo = await getLedgerWalletInfo(network, index, IS_BOUNCEABLE); + + if (alreadyImportedAddresses.includes(walletInfo.address)) { + index += 1; + continue; + } + if (walletInfo.balance !== '0') { result.push(walletInfo); index += 1; @@ -407,6 +417,7 @@ export async function getNextLedgerWallets(network: ApiNetwork, lastExistingInde if (!result.length) { result.push(walletInfo); } + return result; } } catch (err) { diff --git a/src/util/processDeeplink.ts b/src/util/processDeeplink.ts index 1e038055..a129907d 100644 --- a/src/util/processDeeplink.ts +++ b/src/util/processDeeplink.ts @@ -5,7 +5,7 @@ import { TON_TOKEN_SLUG } from '../config'; import { bigStrToHuman } from '../global/helpers'; import { parseTonDeeplink } from './ton/deeplinks'; import { pause } from './schedulers'; -import { CAN_DELEGATE_BOTTOM_SHEET } from './windowEnvironment'; +import { IS_DELEGATING_BOTTOM_SHEET } from './windowEnvironment'; // Both to close current Transfer Modal and delay when app launch const PAUSE = 700; @@ -13,7 +13,7 @@ export async function processDeeplink(url: string) { const params = parseTonDeeplink(url); if (!params) return false; - if (CAN_DELEGATE_BOTTOM_SHEET) { + if (IS_DELEGATING_BOTTOM_SHEET) { await BottomSheet.release({ key: '*' }); await pause(PAUSE); } diff --git a/src/util/windowEnvironment.ts b/src/util/windowEnvironment.ts index 6585b127..fecc08c1 100644 --- a/src/util/windowEnvironment.ts +++ b/src/util/windowEnvironment.ts @@ -59,7 +59,7 @@ export const IS_BIOMETRIC_AUTH_SUPPORTED = Boolean( !IS_CAPACITOR && window.navigator.credentials && (!IS_ELECTRON || IS_MAC_OS), ); export const IS_DELEGATED_BOTTOM_SHEET = IS_CAPACITOR && global.location.search.startsWith('?bottom-sheet'); -export const CAN_DELEGATE_BOTTOM_SHEET = IS_CAPACITOR && IS_IOS && !IS_DELEGATED_BOTTOM_SHEET; +export const IS_DELEGATING_BOTTOM_SHEET = IS_CAPACITOR && IS_IOS && !IS_DELEGATED_BOTTOM_SHEET; export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window && !IS_LEDGER_EXTENSION_TAB; export function setScrollbarWidthProperty() {