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}
>
-
+
{
- 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() {