From c2d66af69ca3b559baf5bc5e0fc7042558fcbda8 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Thu, 1 Feb 2024 19:57:40 +0300 Subject: [PATCH 01/61] fix(mobile): Inscription subtitle (#694) * fix(mobile): Subscription subtitle * dev(mobile): Add "Copy FCM token" to DevMenu --- packages/mobile/src/core/DevMenu/DevMenu.tsx | 8 ++++++++ packages/mobile/src/core/InscriptionScreen.tsx | 12 ++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/mobile/src/core/DevMenu/DevMenu.tsx b/packages/mobile/src/core/DevMenu/DevMenu.tsx index 20478d1b9..3c9d50cd5 100644 --- a/packages/mobile/src/core/DevMenu/DevMenu.tsx +++ b/packages/mobile/src/core/DevMenu/DevMenu.tsx @@ -18,6 +18,8 @@ import { openLogs } from '$navigation'; import { Alert } from 'react-native'; import { Icon } from '$uikit'; import { tk } from '@tonkeeper/shared/tonkeeper'; +import Clipboard from '@react-native-community/clipboard'; +import { getToken } from '$utils/messaging'; export const DevMenu: FC = () => { const nav = useNavigation(); @@ -89,6 +91,11 @@ export const DevMenu: FC = () => { actions: { toggleFeature, setDevLanguage }, } = useDevFeaturesToggle(); + const handleCopyFCMToken = useCallback(async () => { + const token = await getToken(); + Clipboard.setString(String(token)); + }, []); + const toggleHttpProtocol = useCallback(() => { toggleFeature(DevFeature.UseHttpProtocol); }, [toggleFeature]); @@ -194,6 +201,7 @@ export const DevMenu: FC = () => { + diff --git a/packages/mobile/src/core/InscriptionScreen.tsx b/packages/mobile/src/core/InscriptionScreen.tsx index 2b0645fe6..325aec171 100644 --- a/packages/mobile/src/core/InscriptionScreen.tsx +++ b/packages/mobile/src/core/InscriptionScreen.tsx @@ -41,16 +41,8 @@ export const InscriptionScreen = memo(() => { return ( - - {inscription.ticker} - - - {inscription.type.toUpperCase()} - - - } + subtitle={inscription.type.toUpperCase()} + title={inscription.ticker} /> From db2839b781dc5f218b6307c21bf23a06f68b1f29 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Thu, 1 Feb 2024 19:57:48 +0300 Subject: [PATCH 02/61] feature(mobile): Add UnverifiedTokenDetailsModal (#693) * feature(mobile): Add UnverifiedTokenDetailsModal * Add goBack function for Button component * fix(mobile): Update translations * fix(mobile): Revert description * Revert UnverifiedTokenDetails text * bump(mobile): 3.6.1 --- packages/mobile/android/app/build.gradle | 2 +- .../ios/ton_keeper.xcodeproj/project.pbxproj | 4 +- packages/mobile/src/core/Jetton/Jetton.tsx | 21 ++++-- .../shared/i18n/locales/tonkeeper/en.json | 11 ++++ .../shared/i18n/locales/tonkeeper/ru-RU.json | 11 ++++ packages/shared/modals/RefillBatteryModal.tsx | 16 +---- .../modals/UnverifiedTokenDetailsModal.tsx | 60 ++++++++++++++++++ .../icons/png/ic-information-circle-12@4x.png | Bin 0 -> 914 bytes .../icons/svg/12/ic-information-circle-12.svg | 10 +++ .../uikit/src/components/Icon/Icon.types.ts | 3 + .../src/components/Icon/IconList.native.ts | 1 + .../uikit/src/components/Table/Paragraph.tsx | 48 ++++++++++++++ .../src/components/Table/TableContainer.tsx | 19 ++++++ packages/uikit/src/components/Table/index.ts | 6 ++ packages/uikit/src/index.ts | 1 + 15 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 packages/shared/modals/UnverifiedTokenDetailsModal.tsx create mode 100644 packages/uikit/assets/icons/png/ic-information-circle-12@4x.png create mode 100644 packages/uikit/assets/icons/svg/12/ic-information-circle-12.svg create mode 100644 packages/uikit/src/components/Table/Paragraph.tsx create mode 100644 packages/uikit/src/components/Table/TableContainer.tsx create mode 100644 packages/uikit/src/components/Table/index.ts diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 48ceaafac..b87b4c528 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -92,7 +92,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 433 - versionName "3.6" + versionName "3.6.1" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'store', 'play' } diff --git a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj index 8816338ea..0cb37367e 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -1294,7 +1294,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.6; + MARKETING_VERSION = 3.6.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1328,7 +1328,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.6; + MARKETING_VERSION = 3.6.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/packages/mobile/src/core/Jetton/Jetton.tsx b/packages/mobile/src/core/Jetton/Jetton.tsx index 93476652f..0108523bd 100644 --- a/packages/mobile/src/core/Jetton/Jetton.tsx +++ b/packages/mobile/src/core/Jetton/Jetton.tsx @@ -20,13 +20,16 @@ import { Events, JettonVerification, SendAnalyticsFrom } from '$store/models'; import { t } from '@tonkeeper/shared/i18n'; import { trackEvent } from '$utils/stats'; import { Address } from '@tonkeeper/core'; -import { Screen, Steezy, View, Icon, Spacer } from '@tonkeeper/uikit'; +import { Screen, Steezy, View, Icon, Spacer, TouchableOpacity } from '@tonkeeper/uikit'; import { useJettonActivityList } from '@tonkeeper/shared/query/hooks/useJettonActivityList'; import { ActivityList } from '@tonkeeper/shared/components'; import { openReceiveJettonModal } from '@tonkeeper/shared/modals/ReceiveJettonModal'; import { TokenType } from '$core/Send/Send.interface'; import { config } from '@tonkeeper/shared/config'; +import { openUnverifiedTokenDetailsModal } from '@tonkeeper/shared/modals/UnverifiedTokenDetailsModal'; + +const unverifiedTokenHitSlop = { top: 4, left: 4, bottom: 4, right: 4 }; export const Jetton: React.FC = ({ route }) => { const flags = useFlags(['disable_swap']); @@ -139,15 +142,19 @@ export const Jetton: React.FC = ({ route }) => { subtitle={ !config.get('disable_show_unverified_token') && jetton.verification === JettonVerification.NONE && ( - - - - - + {t('approval.unverified_token')} - + + + + + ) } title={jetton.metadata?.name || Address.toShort(jetton.jettonAddress)} diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index e3c4fae4e..9731bacc1 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -1067,5 +1067,16 @@ "button": "Decrypt the comment", "checkboxLabel": "Do not show again" } + }, + "unverifiedTokenDetails": { + "title": "Unverified Token", + "description": "This token looks suspicious for one or several reasons.", + "paragraphs": { + "p1": "Low liquidity. Token may have some value, but it is extremely low.", + "p2": "Token is not listed on trading platforms and has limited demand.", + "p3": "Used for spam. Employed for sending unwanted and often irrelevant messages at scale.", + "p4": "Used for scam. Token's name or image can lead users into deception." + }, + "button": "OK" } } diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 002dea874..45fca2666 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -1113,5 +1113,16 @@ "button": "Расшифровать комментарий", "checkboxLabel": "Не показывать снова" } + }, + "unverifiedTokenDetails": { + "title": "Непроверенный токен", + "description": "Токен выглядит подозрительно по одной или нескольким причинам.", + "paragraphs": { + "p1": "Низкая ликвидность. У токена может быть какая-то стоимость, но она крайне мала.", + "p2": "Токен не представлен на торговых площадках, имеет ограниченный спрос.", + "p3": "Используется для спама. Применяется для отправки нерелевантных сообщений в больших масштабах.", + "p4": "Используется в мошенничестве. Название токена или изображение может вводить пользователей в заблуждение." + }, + "button": "Понятно" } } diff --git a/packages/shared/modals/RefillBatteryModal.tsx b/packages/shared/modals/RefillBatteryModal.tsx index 9cdf0f2d8..48c5f5d16 100644 --- a/packages/shared/modals/RefillBatteryModal.tsx +++ b/packages/shared/modals/RefillBatteryModal.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import { Modal, Steezy } from '@tonkeeper/uikit'; +import { Modal } from '@tonkeeper/uikit'; import { navigation, SheetActions } from '@tonkeeper/router'; import { RefillBattery } from '../components/RefillBattery/RefillBattery'; @@ -22,17 +22,3 @@ export function openRefillBatteryModal() { path: '/refill-battery', }); } - -export const styles = Steezy.create({ - contentContainer: { - paddingTop: 48, - alignItems: 'center', - paddingHorizontal: 32, - }, - iconContainer: { - marginBottom: 24, - }, - indent: { - paddingHorizontal: 16, - }, -}); diff --git a/packages/shared/modals/UnverifiedTokenDetailsModal.tsx b/packages/shared/modals/UnverifiedTokenDetailsModal.tsx new file mode 100644 index 000000000..4728236fa --- /dev/null +++ b/packages/shared/modals/UnverifiedTokenDetailsModal.tsx @@ -0,0 +1,60 @@ +import { memo } from 'react'; +import { Button, Modal, Spacer, Steezy, Table, Text, View } from '@tonkeeper/uikit'; +import { navigation, SheetActions, useNavigation } from '@tonkeeper/router'; +import { t } from '@tonkeeper/shared/i18n'; + +export const UnverifiedTokenDetailsModal = memo(() => { + const { goBack } = useNavigation(); + + return ( + + + + + + {t('unverifiedTokenDetails.title')} + + + + {t('unverifiedTokenDetails.description')} + + + + + + {t('unverifiedTokenDetails.paragraphs.p1')} + {t('unverifiedTokenDetails.paragraphs.p2')} + {t('unverifiedTokenDetails.paragraphs.p3')} + {t('unverifiedTokenDetails.paragraphs.p4')} +
+ + ); - } else { - return ( - - ); } + + return null; } return ( @@ -271,7 +239,7 @@ export const AccessConfirmation: FC = () => { disabled={value.length === 4} onChange={handleKeyboard} value={value} - biometryEnabled={!isBiometryFailed} + biometryEnabled={biometryEnabled && !isBiometryFailed && !generatedVault} onBiometryPress={handleBiometry} /> diff --git a/packages/mobile/src/core/AddWatchOnly/AddWatchOnly.tsx b/packages/mobile/src/core/AddWatchOnly/AddWatchOnly.tsx new file mode 100644 index 000000000..a88c22da5 --- /dev/null +++ b/packages/mobile/src/core/AddWatchOnly/AddWatchOnly.tsx @@ -0,0 +1,199 @@ +import { SendRecipient } from '$core/Send/Send.interface'; +import { AddressInput } from '$core/Send/steps/AddressStep/components'; +import { Tonapi } from '$libs/Tonapi'; +import { openSetupNotifications, openSetupWalletDone } from '$navigation'; +import { asyncDebounce, isTransferOp, parseTonLink } from '$utils'; +import { tk } from '$wallet'; +import { Address } from '@tonkeeper/core'; +import { t } from '@tonkeeper/shared/i18n'; +import { + Button, + Screen, + Spacer, + Steezy, + Text, + Toast, + View, + useReanimatedKeyboardHeight, +} from '@tonkeeper/uikit'; +import React, { FC, useCallback, useMemo, useState } from 'react'; +import Animated from 'react-native-reanimated'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +let dnsAbortController: null | AbortController = null; + +export const AddWatchOnly: FC = () => { + const [account, setAccount] = useState | null>(null); + const [dnsLoading, setDnsLoading] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + + const getAddressByDomain = useMemo( + () => + asyncDebounce(async (value: string, signal: AbortSignal) => { + try { + const domain = value.toLowerCase(); + const resolvedDomain = await Tonapi.resolveDns(domain, signal); + + if (resolvedDomain === 'aborted') { + return 'aborted'; + } else if (resolvedDomain?.wallet?.address) { + return resolvedDomain.wallet.address as string; + } + + return null; + } catch (e) { + console.log('err', e); + + return null; + } + }, 1000), + [], + ); + + const validate = useCallback( + async (value: string) => { + setError(false); + if (value.length === 0) { + setAccount(null); + + return false; + } + + try { + const link = parseTonLink(value); + + if (dnsAbortController) { + dnsAbortController.abort(); + dnsAbortController = null; + setDnsLoading(false); + } + + if (link.match && isTransferOp(link.operation) && Address.isValid(link.address)) { + if (link.query.bin) { + return false; + } + + value = link.address; + } + + if (Address.isValid(value)) { + setAccount({ address: value }); + + return true; + } + + const domain = value.toLowerCase(); + + if (!Address.isValid(domain)) { + setDnsLoading(true); + const abortController = new AbortController(); + dnsAbortController = abortController; + + const zone = domain.indexOf('.') === -1 ? '.ton' : ''; + const resolvedDomain = await getAddressByDomain( + domain + zone, + abortController.signal, + ); + + if (resolvedDomain === 'aborted') { + setDnsLoading(false); + dnsAbortController = null; + return true; + } else if (resolvedDomain) { + setAccount({ address: resolvedDomain, domain }); + setDnsLoading(false); + dnsAbortController = null; + return true; + } else { + setDnsLoading(false); + dnsAbortController = null; + } + } + + setAccount(null); + + return false; + } catch (e) { + return false; + } + }, + [getAddressByDomain, setAccount], + ); + + const handleContinue = useCallback(async () => { + if (!account) { + return; + } + + setLoading(true); + + try { + const identifiers = await tk.addWatchOnlyWallet(account.address); + const isNotificationsDenied = await tk.wallet.notifications.getIsDenied(); + + if (isNotificationsDenied) { + openSetupWalletDone(identifiers); + } else { + openSetupNotifications(identifiers); + } + } catch (e) { + if (e.error) { + Toast.fail(t('add_watch_only.wallet_not_found')); + } + setLoading(false); + setError(true); + } + }, [account]); + + const { spacerStyle } = useReanimatedKeyboardHeight(); + + return ( + + + + + + + {t('add_watch_only.title')} + + + + {t('add_watch_only.subtitle')} + + + + + + + - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/Exchange/Exchange.tsx b/packages/mobile/src/core/Exchange/Exchange.tsx index d8e9e0f1f..7d061f646 100644 --- a/packages/mobile/src/core/Exchange/Exchange.tsx +++ b/packages/mobile/src/core/Exchange/Exchange.tsx @@ -3,20 +3,20 @@ import React, { FC, useCallback } from 'react'; import { InlineHeader, Loader } from '$uikit'; import * as S from './Exchange.style'; import { ExchangeItem } from './ExchangeItem/ExchangeItem'; -import { getServerConfig, getServerConfigSafe } from '$shared/constants'; import { Linking } from 'react-native'; import { Modal } from '@tonkeeper/uikit'; import { useMethodsToBuyStore } from '$store/zustand/methodsToBuy/useMethodsToBuyStore'; import { t } from '@tonkeeper/shared/i18n'; +import { config } from '$config'; export const OldExchange: FC = () => { const categories = useMethodsToBuyStore((state) => state.categories); - const otherWaysAvailable = getServerConfigSafe('exchangePostUrl') !== 'none'; + const otherWaysAvailable = !!config.get('exchangePostUrl'); const openOtherWays = useCallback(() => { try { - const url = getServerConfig('exchangePostUrl'); + const url = config.get('exchangePostUrl'); Linking.openURL(url); } catch {} diff --git a/packages/mobile/src/core/ImportWallet/ImportWallet.tsx b/packages/mobile/src/core/ImportWallet/ImportWallet.tsx index 203c14841..4384bcd0f 100644 --- a/packages/mobile/src/core/ImportWallet/ImportWallet.tsx +++ b/packages/mobile/src/core/ImportWallet/ImportWallet.tsx @@ -1,31 +1,53 @@ import React, { FC, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; import * as S from './ImportWallet.style'; import { NavBar } from '$uikit'; import { useKeyboardHeight } from '$hooks/useKeyboardHeight'; -import { walletActions } from '$store/wallet'; -import { openCreatePin } from '$navigation'; import { ImportWalletForm } from '$shared/components'; +import { RouteProp } from '@react-navigation/native'; +import { + ImportWalletStackParamList, + ImportWalletStackRouteNames, +} from '$navigation/ImportWalletStack/types'; +import { useNavigation } from '@tonkeeper/router'; +import { useImportWallet } from '$hooks/useImportWallet'; +import { tk } from '$wallet'; -export const ImportWallet: FC = () => { - const dispatch = useDispatch(); +export const ImportWallet: FC<{ + route: RouteProp; +}> = (props) => { const keyboardHeight = useKeyboardHeight(); + const nav = useNavigation(); + const doImportWallet = useImportWallet(); + + const isTestnet = !!props.route.params?.testnet; const handleWordsFilled = useCallback( - (mnemonics: string, config: any, onEnd: () => void) => { - dispatch( - walletActions.restoreWallet({ - mnemonics, - config, - onDone: () => { - onEnd(); - openCreatePin(); - }, - onFail: () => onEnd(), - }), - ); + async (mnemonic: string, lockupConfig: any, onEnd: () => void) => { + try { + const walletsInfo = await tk.getWalletsInfo(mnemonic, isTestnet); + + const shouldChooseWallets = !lockupConfig && walletsInfo.length > 1; + + if (shouldChooseWallets) { + nav.navigate(ImportWalletStackRouteNames.ChooseWallets, { + walletsInfo, + mnemonic, + lockupConfig, + isTestnet, + }); + onEnd(); + return; + } + + const versions = walletsInfo.map((item) => item.version); + + await doImportWallet(mnemonic, lockupConfig, versions, isTestnet); + onEnd(); + } catch { + onEnd(); + } }, - [dispatch], + [doImportWallet, isTestnet, nav], ); return ( diff --git a/packages/mobile/src/core/Intro/Intro.style.ts b/packages/mobile/src/core/Intro/Intro.style.ts deleted file mode 100644 index 6b75643bd..000000000 --- a/packages/mobile/src/core/Intro/Intro.style.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SafeAreaView } from 'react-native-safe-area-context'; - -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled(SafeAreaView)` - flex: 1; -`; - -export const Content = styled.ScrollView.attrs({ - contentContainerStyle: { - padding: ns(32), - }, -})` - flex: 1; -`; - -export const Items = styled.View``; - -export const Item = styled.View` - margin-top: ${ns(32)}px; - flex-direction: row; -`; - -export const ItemCont = styled.View` - margin-left: ${ns(16)}px; - flex: 1; -`; diff --git a/packages/mobile/src/core/Intro/Intro.tsx b/packages/mobile/src/core/Intro/Intro.tsx deleted file mode 100644 index c545d1827..000000000 --- a/packages/mobile/src/core/Intro/Intro.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { FC, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; - -import * as S from './Intro.style'; -import { BottomButtonWrap, BottomButtonWrapHelper } from '$shared/components'; -import {Button, Icon, Text} from '$uikit'; -import { useTheme } from '$hooks/useTheme'; -import { mainActions } from '$store/main'; -import { ns } from '$utils'; -import { t } from '@tonkeeper/shared/i18n'; - -export const Intro: FC = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - - const handleContinue = useCallback(() => { - dispatch(mainActions.completeIntro()); - }, [dispatch]); - - return ( - - - - {t('intro_title')} - - {t('app_name')} - - - - - - - {t('intro_item1_title')} - - {t('intro_item1_caption')} - - - - - - - {t('intro_item2_title')} - - {t('intro_item2_caption')} - - - - {/* - - - - {t('intro_item3_title')} - {t('intro_item3_caption')} - - - */} - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/Jetton/Jetton.tsx b/packages/mobile/src/core/Jetton/Jetton.tsx index 0108523bd..8e7876764 100644 --- a/packages/mobile/src/core/Jetton/Jetton.tsx +++ b/packages/mobile/src/core/Jetton/Jetton.tsx @@ -6,10 +6,7 @@ import { ns } from '$utils'; import { useJetton } from '$hooks/useJetton'; import { useTokenPrice } from '$hooks/useTokenPrice'; import { openDAppBrowser, openSend } from '$navigation'; -import { getServerConfig } from '$shared/constants'; -import { useSelector } from 'react-redux'; -import { walletAddressSelector } from '$store/wallet'; import { formatter } from '$utils/formatter'; import { useNavigation } from '@tonkeeper/router'; import { useSwapStore } from '$store/zustand/swap'; @@ -26,8 +23,10 @@ import { useJettonActivityList } from '@tonkeeper/shared/query/hooks/useJettonAc import { ActivityList } from '@tonkeeper/shared/components'; import { openReceiveJettonModal } from '@tonkeeper/shared/modals/ReceiveJettonModal'; import { TokenType } from '$core/Send/Send.interface'; -import { config } from '@tonkeeper/shared/config'; +import { config } from '$config'; import { openUnverifiedTokenDetailsModal } from '@tonkeeper/shared/modals/UnverifiedTokenDetailsModal'; +import { useWallet } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; const unverifiedTokenHitSlop = { top: 4, left: 4, bottom: 4, right: 4 }; @@ -35,8 +34,10 @@ export const Jetton: React.FC = ({ route }) => { const flags = useFlags(['disable_swap']); const jetton = useJetton(route.params.jettonAddress); const jettonActivityList = useJettonActivityList(jetton.jettonAddress); - const address = useSelector(walletAddressSelector); const jettonPrice = useTokenPrice(jetton.jettonAddress, jetton.balance); + const wallet = useWallet(); + + const isWatchOnly = wallet && wallet.isWatchOnly; const nav = useNavigation(); @@ -61,10 +62,11 @@ export const Jetton: React.FC = ({ route }) => { const handleOpenExplorer = useCallback(async () => { openDAppBrowser( - getServerConfig('accountExplorer').replace('%s', address.ton) + - `/jetton/${jetton.jettonAddress}`, + config + .get('accountExplorer', tk.wallet.isTestnet) + .replace('%s', wallet.address.ton.friendly) + `/jetton/${jetton.jettonAddress}`, ); - }, [address.ton, jetton.jettonAddress]); + }, [jetton.jettonAddress, wallet]); const renderHeader = useMemo(() => { if (!jetton) { @@ -100,17 +102,19 @@ export const Jetton: React.FC = ({ route }) => { - + {!isWatchOnly ? ( + + ) : null} - {showSwap && !flags.disable_swap ? ( + {!isWatchOnly && showSwap && !flags.disable_swap ? ( } @@ -123,8 +127,8 @@ export const Jetton: React.FC = ({ route }) => { ); }, [ jetton, - t, jettonPrice, + isWatchOnly, handleSend, handleReceive, showSwap, diff --git a/packages/mobile/src/core/JettonsList/JettonsList.interface.ts b/packages/mobile/src/core/JettonsList/JettonsList.interface.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/mobile/src/core/JettonsList/JettonsList.style.ts b/packages/mobile/src/core/JettonsList/JettonsList.style.ts deleted file mode 100644 index c4d71ced5..000000000 --- a/packages/mobile/src/core/JettonsList/JettonsList.style.ts +++ /dev/null @@ -1,74 +0,0 @@ -import styled, { RADIUS } from '$styled'; -import { hNs, nfs, ns } from '$utils'; -import FastImage from 'react-native-fast-image'; - -const borders = (borderStart: boolean, borderEnd: boolean) => { - return ` - ${ - borderStart - ? ` - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - ` - : '' - } - ${ - borderEnd - ? ` - border-bottom-left-radius: ${ns(RADIUS.normal)}px; - border-bottom-right-radius: ${ns(RADIUS.normal)}px; - ` - : '' - } - `; -}; - -export const Wrap = styled.View` - flex: 1; -`; - -export const JettonInner = styled.View<{ isFirst: boolean; isLast: boolean }>` - flex-direction: row; - align-items: center; - padding: ${ns(16)}px; - background: ${({ theme }) => theme.colors.backgroundSecondary}; - ${({ isFirst, isLast }) => borders(isFirst, isLast)} -`; - -export const JettonCont = styled.View` - flex: 1; -`; - -export const JettonName = styled.Text.attrs({ - numberOfLines: 1, -})` - font-family: ${({ theme }) => theme.font.medium}; - color: ${({ theme }) => theme.colors.foregroundPrimary}; - font-size: ${nfs(16)}px; - line-height: 24px; - margin-right: ${ns(16)}px; -`; - -export const JettonInfo = styled.Text.attrs({ - numberOfLines: 1, -})` - font-family: ${({ theme }) => theme.font.regular}; - color: ${({ theme }) => theme.colors.foregroundSecondary}; - font-size: ${nfs(14)}px; - line-height: 20px; -`; - -export const BalanceWrapper = styled.View` - margin-top: ${ns(2)}px; -`; - -export const JettonLogo = styled(FastImage).attrs({ - resizeMode: 'stretch', -})` - z-index: 2; - height: ${ns(44)}px; - width: ${hNs(44)}px; - border-radius: ${ns(44 / 2)}px; - background: ${({ theme }) => theme.colors.backgroundTertiary}; - margin-right: ${ns(16)}px; -`; diff --git a/packages/mobile/src/core/JettonsList/JettonsList.tsx b/packages/mobile/src/core/JettonsList/JettonsList.tsx deleted file mode 100644 index 769b4a0c8..000000000 --- a/packages/mobile/src/core/JettonsList/JettonsList.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { FC, useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import * as S from './JettonsList.style'; -import { AnimatedFlatList, ScrollHandler, Separator } from '$uikit'; -import { ns, formatAmount } from '$utils'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { jettonsActions, jettonsSelector } from '$store/jettons'; -import { Switch } from 'react-native'; -import { JettonBalanceModel, JettonVerification } from '$store/models'; -import { useBottomTabBarHeight } from '$hooks/useBottomTabBarHeight'; -import { useJettonBalancesLegacy } from '$hooks/useJettonBalancesLegacy'; -import { t } from '@tonkeeper/shared/i18n'; - -export const JettonsList: FC = () => { - const { excludedJettons } = useSelector(jettonsSelector); - const { bottom: bottomInset } = useSafeAreaInsets(); - const tabBarHeight = useBottomTabBarHeight(); - const dispatch = useDispatch(); - - const onSwitchExcludedJetton = useCallback( - (jettonAddress: string, value: boolean) => () => - dispatch(jettonsActions.switchExcludedJetton({ jetton: jettonAddress, value })), - [dispatch], - ); - - const data = useJettonBalancesLegacy(true); - - function renderJetton({ - item: jetton, - index, - }: { - item: JettonBalanceModel; - index: number; - }) { - const isWhitelisted = jetton.verification === JettonVerification.WHITELIST; - const isEnabled = - (isWhitelisted && !excludedJettons[jetton.jettonAddress]) || - excludedJettons[jetton.jettonAddress] === false; - - return ( - - - - {jetton.metadata.name} - - {formatAmount(jetton.balance, jetton.metadata.decimals)}{' '} - {jetton.metadata.symbol} - - - - - ); - } - - return ( - - - 0 ? tabBarHeight : bottomInset), - paddingHorizontal: ns(16), - paddingTop: ns(16), - }} - ItemSeparatorComponent={Separator} - data={data} - renderItem={renderJetton} - /> - - - ); -}; diff --git a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx index c9c66bf79..0a69352ca 100644 --- a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx +++ b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx @@ -17,14 +17,14 @@ import { ScaleDecorator } from '$uikit/DraggableFlashList'; import { NestableDraggableFlatList } from '$uikit/DraggableFlashList/components/NestableDraggableFlatList'; import { NestableScrollContainer } from '$uikit/DraggableFlashList/components/NestableScrollContainer'; import { Haptics } from '$utils'; -import { useTokenApprovalStore } from '$store/zustand/tokenApproval/useTokenApprovalStore'; import Animated, { useAnimatedScrollHandler, useSharedValue, } from 'react-native-reanimated'; import { useParams } from '$navigation/imperative'; import { Address } from '@tonkeeper/shared/Address'; - +import { useTokenApproval } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; const AnimatedFlashList = Animated.createAnimatedComponent(FlashList); @@ -115,12 +115,9 @@ export const ManageTokens: FC = () => { const [tab, setTab] = useState(params?.initialTab || 'tokens'); const jettonData = useJettonData(); const nftData = useNftData(); - const hasWatchedCollectiblesTab = useTokenApprovalStore( + const hasWatchedCollectiblesTab = useTokenApproval( (state) => state.hasWatchedCollectiblesTab, ); - const setHasWatchedCollectiblesTab = useTokenApprovalStore( - (state) => state.actions.setHasWatchedCollectiblesTab, - ); const scrollY = useSharedValue(0); const scrollHandler = useAnimatedScrollHandler({ onScroll: (event) => { @@ -210,7 +207,7 @@ export const ManageTokens: FC = () => { onChange={({ value }) => { setTab(value); if (value === 'collectibles') { - setHasWatchedCollectiblesTab(true); + tk.wallet.tokenApproval.setHasWatchedCollectiblesTab(true); } }} value={tab} @@ -251,7 +248,6 @@ export const ManageTokens: FC = () => { renderJettonList, scrollHandler, scrollY, - setHasWatchedCollectiblesTab, tab, withCollectibleDot, ]); diff --git a/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx b/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx index c9bf5c5e2..6f7076297 100644 --- a/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx +++ b/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx @@ -2,15 +2,15 @@ import React, { useMemo, useState } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { formatter } from '$utils/formatter'; import { openApproveTokenModal } from '$core/ModalContainer/ApproveToken/ApproveToken'; +import { tk } from '$wallet'; import { TokenApprovalStatus, TokenApprovalType, } from '$store/zustand/tokenApproval/types'; import { ListButton, Spacer } from '$uikit'; import { CellItem, Content, ContentType } from '$core/ManageTokens/ManageTokens.types'; -import { useTokenApprovalStore } from '$store/zustand/tokenApproval/useTokenApprovalStore'; import { useJettonBalances } from '$hooks/useJettonBalances'; -import { config } from '@tonkeeper/shared/config'; +import { config } from '$config'; import { JettonVerification } from '$store/models'; import { Text } from '@tonkeeper/uikit'; @@ -44,9 +44,6 @@ const baseJettonCellData = (jettonBalance) => ({ export function useJettonData() { const [isExtendedEnabled, setIsExtendedEnabled] = useState(false); const [isExtendedDisabled, setIsExtendedDisabled] = useState(false); - const updateTokenStatus = useTokenApprovalStore( - (state) => state.actions.updateTokenStatus, - ); const { enabled, disabled } = useJettonBalances(); const data = useMemo(() => { const content: Content[] = []; @@ -68,7 +65,7 @@ export function useJettonData() { - updateTokenStatus( + tk.wallet.tokenApproval.updateTokenStatus( jettonBalance.jettonAddress, TokenApprovalStatus.Declined, TokenApprovalType.Token, @@ -114,7 +111,7 @@ export function useJettonData() { - updateTokenStatus( + tk.wallet.tokenApproval.updateTokenStatus( jettonBalance.jettonAddress, TokenApprovalStatus.Approved, TokenApprovalType.Token, @@ -142,7 +139,7 @@ export function useJettonData() { } return content; - }, [disabled, enabled, isExtendedDisabled, isExtendedEnabled, updateTokenStatus]); + }, [disabled, enabled, isExtendedDisabled, isExtendedEnabled]); return data; } diff --git a/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx b/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx index 92f25b36d..36a3fd9a8 100644 --- a/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx +++ b/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx @@ -4,15 +4,15 @@ import { ImageType, openApproveTokenModal, } from '$core/ModalContainer/ApproveToken/ApproveToken'; -import { - TokenApprovalStatus, - TokenApprovalType, -} from '$store/zustand/tokenApproval/types'; import { ListButton, Spacer } from '$uikit'; import { CellItem, Content, ContentType } from '$core/ManageTokens/ManageTokens.types'; -import { useTokenApprovalStore } from '$store/zustand/tokenApproval/useTokenApprovalStore'; import { useApprovedNfts } from '$hooks/useApprovedNfts'; import { JettonVerification, NFTModel } from '$store/models'; +import { tk } from '$wallet'; +import { + TokenApprovalType, + TokenApprovalStatus, +} from '$wallet/managers/TokenApprovalManager'; const baseNftCellData = (nft: NFTModel) => ({ type: ContentType.Cell, @@ -60,9 +60,6 @@ export function groupByCollection( export function useNftData() { const [isExtendedEnabled, setIsExtendedEnabled] = useState(false); const [isExtendedDisabled, setIsExtendedDisabled] = useState(false); - const updateTokenStatus = useTokenApprovalStore( - (state) => state.actions.updateTokenStatus, - ); const { enabled, disabled } = useApprovedNfts(); return useMemo(() => { const content: Content[] = []; @@ -86,7 +83,7 @@ export function useNftData() { - updateTokenStatus( + tk.wallet.tokenApproval.updateTokenStatus( nft.collection?.address || nft.address, TokenApprovalStatus.Declined, nft.collection?.address @@ -137,7 +134,7 @@ export function useNftData() { - updateTokenStatus( + tk.wallet.tokenApproval.updateTokenStatus( nft.collection?.address || nft.address, TokenApprovalStatus.Approved, nft.collection?.address @@ -167,5 +164,5 @@ export function useNftData() { } return content; - }, [disabled, enabled, isExtendedDisabled, isExtendedEnabled, updateTokenStatus]); + }, [disabled, enabled, isExtendedDisabled, isExtendedEnabled]); } diff --git a/packages/mobile/src/core/Migration/Card/Card.interface.ts b/packages/mobile/src/core/Migration/Card/Card.interface.ts deleted file mode 100644 index 321bd9a48..000000000 --- a/packages/mobile/src/core/Migration/Card/Card.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface CardProps { - mode: 'old' | 'new'; - address: string; - amount: string; - startValue: string; -} diff --git a/packages/mobile/src/core/Migration/Card/Card.style.ts b/packages/mobile/src/core/Migration/Card/Card.style.ts deleted file mode 100644 index 1607801c9..000000000 --- a/packages/mobile/src/core/Migration/Card/Card.style.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Animated from 'react-native-reanimated'; - -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled(Animated.View)` - padding: ${ns(16)}px ${ns(16)}px ${ns(16)}px ${ns(16)}px; - justify-content: space-between; - background: ${({ theme }) => theme.colors.backgroundSecondary}; - border-radius: ${({ theme }) => ns(theme.radius.normal)}px; - width: ${ns(136)}px; - height: ${ns(136)}px; - position: absolute; - left: ${ns(57)}px; - top: ${ns(28)}px; -`; - -export const AmountWrap = styled.View``; diff --git a/packages/mobile/src/core/Migration/Card/Card.tsx b/packages/mobile/src/core/Migration/Card/Card.tsx deleted file mode 100644 index bbff7a063..000000000 --- a/packages/mobile/src/core/Migration/Card/Card.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { FC, useEffect, useMemo } from 'react'; -import { - Easing, - interpolate, - useAnimatedStyle, - useSharedValue, - withDelay, - withTiming, -} from 'react-native-reanimated'; - -import * as S from './Card.style'; -import { CardProps } from '$core/Migration/Card/Card.interface'; -import { useTheme } from '$hooks/useTheme'; -import { ns } from '$utils'; -import { useCounter } from '$core/Migration/Card/useCounter'; -import { CryptoCurrencies, Decimals } from '$shared/constants'; -import { formatCryptoCurrency } from '$utils/currency'; -import { Text } from '$uikit'; -import { t } from '@tonkeeper/shared/i18n'; - -const PositionOffsetHorizontal = ns(57); -const PositionOffsetVertical = ns(28); - -const positionDelay = 1000; -const positionDuration = 200; - -const amountDelay = 100; -const amountDuration = 800; - -export const Card: FC = (props) => { - const { mode, address, amount, startValue } = props; - const theme = useTheme(); - - const positionValue = useSharedValue(0); - const amountValue = useCounter( - positionDelay + positionDuration + amountDelay, - amountDuration, - amount, - mode === 'old' ? 'decr' : 'incr', - startValue, - ); - - useEffect(() => { - positionValue.value = withDelay( - positionDelay, - withTiming(1, { - duration: positionDuration, - easing: Easing.inOut(Easing.ease), - }), - ); - }, []); - - const label = useMemo(() => { - return t(mode === 'old' ? 'migration_old_wallet' : 'migration_new_wallet'); - }, [mode, t]); - - const backgroundColor = useMemo(() => { - return theme.colors[mode === 'old' ? 'backgroundSecondary' : 'backgroundTertiary']; - }, [mode, theme]); - - const positionStyle = useAnimatedStyle(() => { - const y = mode === 'old' ? -PositionOffsetVertical : PositionOffsetVertical; - const x = mode === 'old' ? -PositionOffsetHorizontal : PositionOffsetHorizontal; - return { - transform: [ - { - translateY: interpolate(positionValue.value, [0, 1], [0, y]), - }, - { - translateX: interpolate(positionValue.value, [0, 1], [0, x]), - }, - ], - }; - }); - - return ( - - - {label} - - - - {address} - - - {formatCryptoCurrency( - amountValue, - CryptoCurrencies.Ton, - Decimals[CryptoCurrencies.Ton], - )} - - - - ); -}; diff --git a/packages/mobile/src/core/Migration/Card/useCounter.ts b/packages/mobile/src/core/Migration/Card/useCounter.ts deleted file mode 100644 index d2f761d3f..000000000 --- a/packages/mobile/src/core/Migration/Card/useCounter.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import BigNumber from 'bignumber.js'; - -export function useCounter( - delayMs: number, - duration: number, - amount: string, - mode: 'incr' | 'decr', - startValue: string = '0', -) { - const [isStarted, setStarted] = useState(false); - const [value, setValue] = useState(mode === 'decr' ? amount : startValue); - const timer = useRef(0); - const interval = useRef(0); - - const onUpdate = useCallback( - (step) => () => { - setValue((oldValue) => { - let newVal = new BigNumber(oldValue); - if (mode === 'incr') { - newVal = newVal.plus(step); - - const maxVal = new BigNumber(amount).plus(startValue); - if (newVal.isGreaterThan(maxVal)) { - newVal = maxVal; - clearInterval(interval.current); - } - } else { - newVal = newVal.minus(step); - - if (newVal.isLessThan(0)) { - newVal = new BigNumber(0); - clearInterval(interval.current); - } - } - - return newVal.toString(); - }); - }, - [amount, mode, setValue], - ); - - useEffect(() => { - if (isStarted) { - const stepDuration = 50; - const step = new BigNumber(amount).dividedBy(duration / stepDuration); - interval.current = setInterval(onUpdate(step), stepDuration); - } - - return () => { - clearInterval(interval.current); - }; - }, [isStarted]); - - useEffect(() => { - timer.current = setTimeout(() => { - setStarted(true); - }, delayMs); - - return () => { - clearTimeout(timer.current); - }; - }, []); - - return value; -} diff --git a/packages/mobile/src/core/Migration/Migration.interface.ts b/packages/mobile/src/core/Migration/Migration.interface.ts deleted file mode 100644 index 82df79e0d..000000000 --- a/packages/mobile/src/core/Migration/Migration.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RouteProp } from '@react-navigation/native'; - -import { AppStackRouteNames } from '$navigation'; -import { AppStackParamList } from '$navigation/AppStack'; - -export interface MigrationProps { - route: RouteProp; -} diff --git a/packages/mobile/src/core/Migration/Migration.style.ts b/packages/mobile/src/core/Migration/Migration.style.ts deleted file mode 100644 index 2c4be934b..000000000 --- a/packages/mobile/src/core/Migration/Migration.style.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { SafeAreaView } from 'react-native-safe-area-context'; -import Animated from 'react-native-reanimated'; - -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled(SafeAreaView)` - flex: 1; - padding: ${ns(32)}px; -`; - -export const Header = styled.View` - flex: 0 0 auto; - padding-bottom: ${ns(16)}px; -`; - -export const CaptionWrapper = styled.View` - margin-top: ${ns(4)}px; -`; - -export const Footer = styled.View` - flex: 0 0 auto; - padding-top: ${ns(36)}px; -`; - -export const CardsWrap = styled.View` - flex: 1; - align-items: center; - justify-content: center; -`; - -export const Cards = styled.View` - flex: 0 0 auto; - width: ${ns(250)}px; - height: ${ns(190)}px; - position: relative; -`; - -export const Step = styled(Animated.View)` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -`; - -export const StateWrap = styled(SafeAreaView)` - flex: 1; - align-items: center; - justify-content: center; - padding: ${ns(32)}px; -`; - -export const StateIcon = styled(Animated.View)` - width: ${ns(84)}px; - height: ${ns(84)}px; -`; - -export const StateTitleWrapper = styled.View` - margin-top: ${ns(16)}px; -`; diff --git a/packages/mobile/src/core/Migration/Migration.tsx b/packages/mobile/src/core/Migration/Migration.tsx deleted file mode 100644 index 80b690249..000000000 --- a/packages/mobile/src/core/Migration/Migration.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { - cancelAnimation, - Easing, - interpolate, - useAnimatedStyle, - useSharedValue, - withDelay, - withRepeat, - withTiming, -} from 'react-native-reanimated'; - -import * as S from './Migration.style'; -import { Button, Icon, Text } from '$uikit'; -import { deviceWidth, ns, toLocaleNumber, triggerNotificationSuccess } from '$utils'; -import { Card } from '$core/Migration/Card/Card'; - -import { MigrationProps } from './Migration.interface'; -import { walletActions } from '$store/wallet'; -import { CryptoCurrencies } from '$shared/constants'; -import { useTheme } from '$hooks/useTheme'; -import { useTokenPrice } from '$hooks/useTokenPrice'; -import { formatFiatCurrencyAmount } from '$utils/currency'; -import { mainSelector } from '$store/main'; -import { goBack } from '$navigation/imperative'; -import { t } from '@tonkeeper/shared/i18n'; - -export const Migration: FC = ({ route }) => { - const { - oldAddress, - newAddress, - migrationInProgress, - oldBalance, - newBalance, - isTransfer, - fromVersion, - } = route.params; - - const dispatch = useDispatch(); - const theme = useTheme(); - const [step, setStep] = useState(migrationInProgress ? 1 : 0); - const [cardsScale, setCardsScale] = useState(1); - const { fiatCurrency } = useSelector(mainSelector); - - const iconAnimation = useSharedValue(0); - const slideAnimation = useSharedValue(migrationInProgress ? 1 : 0); - const feePrice = useTokenPrice(CryptoCurrencies.Ton, '0.01'); - - useEffect(() => { - if (migrationInProgress) { - dispatch( - walletActions.waitMigration({ - onDone: () => setStep(2), - onFail: () => setStep(0), - }), - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - slideAnimation.value = withTiming(step, { - duration: 350, - easing: Easing.inOut(Easing.ease), - }); - - if (step === 1) { - iconAnimation.value = withRepeat( - withDelay( - 600, - withTiming(1, { - duration: 1400, - }), - ), - Infinity, - false, - ); - } else { - cancelAnimation(iconAnimation); - iconAnimation.value = 0; - } - - let successTimer: any; - if (step === 2) { - triggerNotificationSuccess(); - successTimer = setTimeout(() => { - goBack(); - }, 3000); - } - - return () => clearTimeout(successTimer); - }, [step]); - - const handleUpgrade = useCallback(() => { - setStep(1); - dispatch( - walletActions.migrate({ - fromVersion, - oldAddress, - newAddress, - onDone: () => setStep(2), - onFail: () => setStep(0), - }), - ); - }, [dispatch, newAddress, oldAddress]); - - const handleSkip = useCallback(() => { - goBack(); - }, []); - - // Scale cards for small devices - const handleCardsLayout = useCallback(({ nativeEvent }) => { - const height = nativeEvent.layout.height; - const minHeight = ns(192); - - const scaleFactor = Math.min(height / minHeight, 1); - setCardsScale(scaleFactor); - }, []); - - const iconStyle = useAnimatedStyle(() => ({ - transform: [ - { - rotate: `${interpolate(iconAnimation.value, [0, 1], [0, 180])}deg`, - }, - ], - })); - - const step1Style = useAnimatedStyle(() => ({ - opacity: interpolate(Math.min(slideAnimation.value, 1), [0, 1], [1, 0]), - transform: [ - { - translateX: interpolate( - Math.min(slideAnimation.value, 1), - [0, 1], - [0, -deviceWidth], - ), - }, - ], - })); - - const step2Style = useAnimatedStyle(() => ({ - opacity: interpolate(Math.min(slideAnimation.value, 2), [0, 1, 2], [0, 1, 0]), - transform: [ - { - translateX: interpolate( - Math.min(slideAnimation.value, 2), - [0, 1, 2], - [deviceWidth, 0, -deviceWidth], - ), - }, - ], - })); - - const step3Style = useAnimatedStyle(() => { - const value = Math.min(Math.max(1, slideAnimation.value), 2); - return { - opacity: interpolate(value, [1, 2], [0, 1]), - transform: [ - { - translateX: interpolate(value, [1, 2], [deviceWidth, 0]), - }, - ], - }; - }); - - return ( - <> - - - - - {t(isTransfer ? 'transfer_from_old_wallet_title' : 'migration_title')} - - - - {t(isTransfer ? 'transfer_from_old_wallet_caption' : 'migration_caption')} - - - - - - - - - - - {t('migration_fee_info', { - tonFee: toLocaleNumber('0.01'), - fiatFee: `${formatFiatCurrencyAmount( - feePrice.totalFiat.toFixed(2), - fiatCurrency, - )}`, - })} - - - - - - - - - - - - - - - {t( - isTransfer - ? 'transfer_from_old_wallet_in_progress' - : 'migration_in_progress', - )} - - - - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/AddressMismatch/AddressMismatch.tsx b/packages/mobile/src/core/ModalContainer/AddressMismatch/AddressMismatch.tsx index 8b743aa09..864cc6e88 100644 --- a/packages/mobile/src/core/ModalContainer/AddressMismatch/AddressMismatch.tsx +++ b/packages/mobile/src/core/ModalContainer/AddressMismatch/AddressMismatch.tsx @@ -1,59 +1,31 @@ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { Modal } from '@tonkeeper/uikit'; import { Button, Icon, Text } from '$uikit'; import * as S from './AddressMismatch.style'; -import { useWallet } from '$hooks/useWallet'; import { useNavigation, SheetActions } from '@tonkeeper/router'; import { delay } from '$utils'; -import { walletActions } from '$store/wallet'; -import { useDispatch } from 'react-redux'; -import { SelectableVersion } from '$shared/constants'; import { push } from '$navigation/imperative'; import { Address } from '@tonkeeper/shared/Address'; export const AddressMismatchModal = memo<{ source: string; onSwitchAddress: () => void }>( (props) => { - const [allVersions, setAllVersions] = useState( - null, - ); - const wallet = useWallet(); const nav = useNavigation(); - const dispatch = useDispatch(); - useEffect(() => { - wallet.ton.getAllAddresses().then((allAddresses) => setAllVersions(allAddresses)); - }, [wallet.ton]); - - const foundVersion = useMemo(() => { - if (!allVersions) { - return false; - } - let found = Object.entries(allVersions).find(([_, address]) => - Address.compare(address, props.source), - ); - if (!found) { - return false; - } - return found[0]; - }, [allVersions, props.source]); + // MULTIWALLET TODO + const foundVersion = false; const handleCloseModal = useCallback(() => nav.goBack(), [nav]); - const handleSwitchVersion = useCallback(async () => { + const handleSwitchWallet = useCallback(async () => { if (!foundVersion) { return; } nav.goBack(); - dispatch(walletActions.switchVersion(foundVersion as SelectableVersion)); + // tk.updateWallet({ version: foundVersion }); await delay(100); props.onSwitchAddress(); - }, [dispatch, foundVersion, nav, props]); - - // Wait to get all versions - if (!allVersions) { - return null; - } + }, [foundVersion, nav, props]); return ( @@ -83,7 +55,7 @@ export const AddressMismatchModal = memo<{ source: string; onSwitchAddress: () = diff --git a/packages/mobile/src/core/ModalContainer/AppearanceModal/AppearanceModal.tsx b/packages/mobile/src/core/ModalContainer/AppearanceModal/AppearanceModal.tsx index 5d550295b..212519c0c 100644 --- a/packages/mobile/src/core/ModalContainer/AppearanceModal/AppearanceModal.tsx +++ b/packages/mobile/src/core/ModalContainer/AppearanceModal/AppearanceModal.tsx @@ -1,8 +1,7 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDimensions } from '$hooks/useDimensions'; -import { mainActions, accentSelector, accentTonIconSelector } from '$store/main'; +import { accentSelector } from '$store/main'; import { NFTModel, TonDiamondMetadata } from '$store/models'; -import { nftsSelector } from '$store/nfts'; import { AccentKey, AccentModel, @@ -10,10 +9,9 @@ import { AppearanceAccents, getAccentIdByDiamondsNFT, } from '$styled'; -import { checkIsTonDiamondsNFT, ns } from '$utils'; +import { checkIsTonDiamondsNFT, delay, ns } from '$utils'; import { ListRenderItem } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; -import { useDispatch, useSelector } from 'react-redux'; import { AccentItem, ACCENT_ITEM_WIDTH } from './AccentItem/AccentItem'; import { AppearanceModalProps } from './AppearanceModal.interface'; import * as S from './AppearanceModal.style'; @@ -22,7 +20,11 @@ import { t } from '@tonkeeper/shared/i18n'; import { Modal, View } from '@tonkeeper/uikit'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { push } from '$navigation/imperative'; -import { openMarketplaces } from '../Marketplaces/Marketplaces'; +import { useNftsState, useWallet } from '@tonkeeper/shared/hooks'; +import { mapNewNftToOldNftData } from '$utils/mapNewNftToOldNftData'; +import { BrowserStackRouteNames, TabsStackRouteNames } from '$navigation'; +import { tk } from '$wallet'; +import { Address } from '@tonkeeper/shared/Address'; const AppearanceModal = memo((props) => { const { selectedAccentNFTAddress } = props; @@ -34,15 +36,15 @@ const AppearanceModal = memo((props) => { window: { width: windowWidth }, } = useDimensions(); - const dispatch = useDispatch(); - - const { myNfts } = useSelector(nftsSelector); - const currentAccent = useSelector(accentSelector); - const accentTonIcon = useSelector(accentTonIconSelector); + const { accountNfts, selectedDiamond } = useNftsState(); + const wallet = useWallet(); const diamondNFTs = useMemo( - () => Object.values(myNfts).filter(checkIsTonDiamondsNFT), - [myNfts], + () => + Object.values(accountNfts) + .map((item) => mapNewNftToOldNftData(item, wallet.address.ton.friendly)) + .filter(checkIsTonDiamondsNFT), + [accountNfts, wallet], ); const getNFTIcon = useCallback((nft: NFTModel) => { @@ -60,6 +62,7 @@ const AppearanceModal = memo((props) => { ...AppearanceAccents[getAccentIdByDiamondsNFT(nft)], available: true, nftIcon: getNFTIcon(nft), + nft, })); const otherAccents = Object.values(AppearanceAccents) @@ -97,11 +100,11 @@ const AppearanceModal = memo((props) => { } const index = accents.findIndex((item) => { - if (accentTonIcon) { - return item.nftIcon?.uri === accentTonIcon.uri; + if (selectedDiamond && item.nft) { + return Address.compare(item.nft.address, selectedDiamond.address); } - return item.id === currentAccent; + return false; }); return index !== -1 ? index : 0; @@ -130,17 +133,19 @@ const AppearanceModal = memo((props) => { : t('nft_open_in_marketplace'); const changeAccent = useCallback(() => { - dispatch(mainActions.setAccent(selectedAccent.id)); - dispatch(mainActions.setTonCustomIcon(selectedAccent?.nftIcon || null)); + tk.wallet.nfts.setSelectedDiamond(selectedAccent.nft?.address ?? null); nav.goBack(); - }, [dispatch, nav, selectedAccent.id, selectedAccent?.nftIcon]); + }, [nav, selectedAccent]); - const openDiamondsNFTCollection = useCallback(() => { + const openDiamondsNFTCollection = useCallback(async () => { if (selectedAccent) { - openMarketplaces({ accentKey: selectedAccent.id }); + nav.goBack(); + await delay(300); + nav.navigate(TabsStackRouteNames.BrowserStack); + nav.push(BrowserStackRouteNames.Category, { categoryId: 'nft' }); } - }, [selectedAccent]); + }, [nav, selectedAccent]); useEffect(() => { setTimeout(() => { diff --git a/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx b/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx index 3218e8d2e..f162c0470 100644 --- a/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx +++ b/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx @@ -1,12 +1,6 @@ import { Modal } from '@tonkeeper/uikit'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import React, { memo, useCallback, useMemo } from 'react'; -import { - TokenApprovalStatus, - TokenApprovalType, -} from '$store/zustand/tokenApproval/types'; -import { useTokenApprovalStore } from '$store/zustand/tokenApproval/useTokenApprovalStore'; -import { getTokenStatus } from '$store/zustand/tokenApproval/selectors'; import { JettonVerification } from '$store/models'; import { Button, Icon, Spacer, View, List } from '$uikit'; import { Steezy } from '$styles'; @@ -19,6 +13,12 @@ import Clipboard from '@react-native-community/clipboard'; import { TranslateOptions } from 'i18n-js'; import { push } from '$navigation/imperative'; import { Address } from '@tonkeeper/core'; +import { useTokenApproval } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; +import { + TokenApprovalType, + TokenApprovalStatus, +} from '$wallet/managers/TokenApprovalManager'; export enum ImageType { ROUND = 'round', @@ -34,19 +34,21 @@ export interface ApproveTokenModalParams { } export const ApproveToken = memo((props: ApproveTokenModalParams) => { const nav = useNavigation(); - const currentStatus = useTokenApprovalStore((state) => - getTokenStatus(state, props.tokenAddress), - ); - const updateTokenStatus = useTokenApprovalStore( - (state) => state.actions.updateTokenStatus, - ); + const currentStatus = useTokenApproval((state) => { + const rawAddress = Address.parse(props.tokenAddress).toRaw(); + return state.tokens[rawAddress]; + }); const handleUpdateStatus = useCallback( (approvalStatus: TokenApprovalStatus) => () => { - updateTokenStatus(props.tokenAddress, approvalStatus, props.type); + tk.wallet.tokenApproval.updateTokenStatus( + props.tokenAddress, + approvalStatus, + props.type, + ); nav.goBack(); }, - [nav, props.tokenAddress, props.type, updateTokenStatus], + [nav, props.tokenAddress, props.type], ); const handleCopyAddress = useCallback(() => { diff --git a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx index ae01c9dd6..8f57058ef 100644 --- a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx +++ b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx @@ -15,7 +15,7 @@ import { triggerNotificationSuccess, } from '$utils'; import { subscriptionsActions } from '$store/subscriptions'; -import { CryptoCurrencies, Decimals, getServerConfig } from '$shared/constants'; +import { CryptoCurrencies, Decimals } from '$shared/constants'; import { formatCryptoCurrency } from '$utils/currency'; import { useWalletInfo } from '$hooks/useWalletInfo'; import { walletWalletSelector } from '$store/wallet'; @@ -27,6 +27,8 @@ import { t } from '@tonkeeper/shared/i18n'; import { push } from '$navigation/imperative'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Modal, View } from '@tonkeeper/uikit'; +import { config } from '$config'; +import { tk } from '$wallet'; export const CreateSubscription: FC = ({ invoiceId = null, @@ -38,7 +40,7 @@ export const CreateSubscription: FC = ({ const nav = useNavigation(); const wallet = useSelector(walletWalletSelector); - const { amount: balance } = useWalletInfo(CryptoCurrencies.Ton); + const { amount: balance } = useWalletInfo(); const [isLoading, setLoading] = useState(!isEdit); const [failed, setFailed] = useState(0); @@ -93,7 +95,7 @@ export const CreateSubscription: FC = ({ }, [isSuccess]); const loadInfo = useCallback(() => { - const host = getServerConfig('subscriptionsHost'); + const host = config.get('subscriptionsHost', tk.wallet.isTestnet); network .get(`${host}/v1/subscribe/invoice/${invoiceId}`, { params: { diff --git a/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx b/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx index 2f90d3397..e12035aa0 100644 --- a/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx +++ b/packages/mobile/src/core/ModalContainer/ExchangeMethod/ExchangeMethod.tsx @@ -9,7 +9,6 @@ import * as S from './ExchangeMethod.style'; import { openBuyFiat } from '$navigation'; import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; import { CryptoCurrencies } from '$shared/constants'; -import { walletSelector } from '$store/wallet'; import { CheckmarkItem } from '$uikit/CheckmarkItem'; import { t } from '@tonkeeper/shared/i18n'; import { ExchangeDB } from './ExchangeDB'; @@ -17,10 +16,11 @@ import { trackEvent } from '$utils/stats'; import { push } from '$navigation/imperative'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Modal, View } from '@tonkeeper/uikit'; +import { useWallet } from '@tonkeeper/shared/hooks'; export const ExchangeMethod: FC = ({ methodId, onContinue }) => { const method = useExchangeMethodInfo(methodId); - const { wallet } = useSelector(walletSelector); + const wallet = useWallet(); const [isDontShow, setIsDontShow] = React.useState(false); const nav = useNavigation(); diff --git a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx index 318febdfd..1b57684da 100644 --- a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx +++ b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx @@ -8,13 +8,13 @@ import * as S from './InsufficientFunds.style'; import { delay, fromNano } from '$utils'; import { debugLog } from '$utils/debugLog'; import BigNumber from 'bignumber.js'; -import { Tonapi } from '$libs/Tonapi'; import { store } from '$store'; import { formatter } from '$utils/formatter'; import { push } from '$navigation/imperative'; import { useBatteryBalance } from '@tonkeeper/shared/query/hooks/useBatteryBalance'; -import { config } from '@tonkeeper/shared/config'; +import { config } from '$config'; import { openRefillBatteryModal } from '@tonkeeper/shared/modals/RefillBatteryModal'; +import { tk } from '$wallet'; export interface InsufficientFundsParams { /** @@ -164,7 +164,7 @@ export async function checkIsInsufficient(amount: string | number) { try { const wallet = store.getState().wallet.wallet; const address = await wallet.ton.getAddress(); - const { balance } = await Tonapi.getWalletInfo(address); + const { balance } = await tk.wallet.tonapi.accounts.getAccount(address); return { insufficient: new BigNumber(amount).gt(new BigNumber(balance)), balance }; } catch (e) { debugLog('[checkIsInsufficient]: error', e); diff --git a/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx b/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx index 30daabde6..2f6dce143 100644 --- a/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx +++ b/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx @@ -14,13 +14,12 @@ import { TouchableOpacity } from 'react-native'; import { store, Toast } from '$store'; import { Wallet } from 'blockchain'; -import { Tonapi } from '$libs/Tonapi'; import { Modal } from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; import { SheetActions } from '@tonkeeper/router'; import { openReplaceDomainAddress } from './NFTOperations/ReplaceDomainAddressModal'; import { Address } from '@tonkeeper/core'; -import { tonapi } from '@tonkeeper/shared/tonkeeper'; +import { tk } from '$wallet'; const TonWeb = require('tonweb'); @@ -62,7 +61,7 @@ export class LinkingDomainActions { public async calculateFee() { try { const boc = await this.createBoc(); - const feeInfo = await tonapi.wallet.emulateMessageToWallet({ boc }); + const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); const feeNano = new BigNumber(feeInfo.event.extra).multipliedBy(-1); return truncateDecimal(Ton.fromNano(feeNano.toString()), 1); @@ -137,7 +136,7 @@ export const LinkingDomainModal: React.FC = ({ setIsDisabled(true); const boc = await linkingActions.createBoc(privateKey); - await tonapi.blockchain.sendBlockchainMessage({ boc }, { format: 'text' }); + await tk.wallet.tonapi.blockchain.sendBlockchainMessage({ boc }, { format: 'text' }); }); const handleReplace = React.useCallback(() => { diff --git a/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.interface.ts b/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.interface.ts deleted file mode 100644 index 93222f972..000000000 --- a/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface MarketplaceItemProps { - topRadius: boolean; - bottomRadius: boolean; - marketplaceUrl: string; - iconUrl: string; - description: string; - title: string; - internalId: string; -} diff --git a/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.style.ts b/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.style.ts deleted file mode 100644 index 78795a0dc..000000000 --- a/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.style.ts +++ /dev/null @@ -1,75 +0,0 @@ -import FastImage from 'react-native-fast-image'; - -import styled, { RADIUS } from '$styled'; -import { Highlight } from '$uikit'; -import { hNs, nfs, ns } from '$utils'; - -export const Wrap = styled.View` - position: relative; -`; - -const radius = (topRadius: boolean, bottomRadius: boolean) => { - return ` - ${ - topRadius - ? ` - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - ` - : '' - } - ${ - bottomRadius - ? ` - border-bottom-left-radius: ${ns(RADIUS.normal)}px; - border-bottom-right-radius: ${ns(RADIUS.normal)}px; - ` - : '' - } - `; -}; - -export const Card = styled(Highlight)<{ topRadius: boolean; bottomRadius: boolean }>` - overflow: hidden; - padding: ${hNs(16)}px ${ns(16)}px; - ${({ bottomRadius, topRadius }) => radius(topRadius, bottomRadius)} -`; - -export const CardIn = styled.View` - flex-direction: row; - align-items: center; - justify-content: space-between; -`; - -export const Divider = styled.View` - height: ${ns(0.5)}px; - background: ${({ theme }) => theme.colors.border}; - margin-left: ${ns(16)}px; -`; - -export const Icon = styled(FastImage).attrs({ - resizeMode: 'cover', - priority: FastImage.priority.high, -})` - width: ${ns(44)}px; - height: ${hNs(44)}px; - border-radius: ${ns(44 / 2)}px; - margin-right: ${ns(16)}px; -`; - -export const Contain = styled.View` - flex: 1; - margin-right: ${ns(24.5)}px; -`; - -export const IconContain = styled.View``; - -export const Badge = styled.View` - padding: ${hNs(4)}px ${ns(8)}px; - background: ${({ theme }) => theme.colors.accentPrimary}; - border-radius: ${ns(8)}px; - position: absolute; - top: ${hNs(16 + 8)}px; - right: ${ns(3)}px; - z-index: 3; -`; \ No newline at end of file diff --git a/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.tsx b/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.tsx deleted file mode 100644 index 097e6fc8b..000000000 --- a/packages/mobile/src/core/ModalContainer/Marketplaces/MarketplaceItem/MarketplaceItem.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { FC, useCallback } from 'react'; - -import { MarketplaceItemProps } from './MarketplaceItem.interface'; -import * as S from './MarketplaceItem.style'; -import { Icon, Text } from '$uikit'; -import { trackEvent } from '$utils/stats'; -import { openDAppBrowser } from '$navigation'; - -export const MarketplaceItem: FC = ({ - marketplaceUrl, - iconUrl, - title, - description, - topRadius, - bottomRadius, - internalId, -}) => { - const handlePress = useCallback(async () => { - openDAppBrowser(marketplaceUrl); - trackEvent('marketplace_open', { internal_id: internalId }); - }, [marketplaceUrl, internalId]); - - return ( - - - - - - {title} - - {description} - - - - - - - - {!bottomRadius ? : null} - - ); -}; diff --git a/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.interface.ts b/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.interface.ts deleted file mode 100644 index b38d2fd35..000000000 --- a/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { AccentKey } from '$styled'; - -export interface MarketplacesModalProps { - accentKey?: AccentKey; -} diff --git a/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.style.ts b/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.style.ts deleted file mode 100644 index 45aab8d5f..000000000 --- a/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.style.ts +++ /dev/null @@ -1,14 +0,0 @@ -import styled from '$styled'; -import { hNs, ns } from '$utils'; - -export const LoaderWrap = styled.View` - flex: 1; - align-items: center; - justify-content: center; -`; - -export const Contain = styled.View` - background: ${({ theme }) => theme.colors.backgroundSecondary}; - margin: 0 ${hNs(16)}px; - border-radius: ${({ theme }) => ns(theme.radius.normal)}px; -`; diff --git a/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.tsx b/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.tsx deleted file mode 100644 index cbe8eea1d..000000000 --- a/packages/mobile/src/core/ModalContainer/Marketplaces/Marketplaces.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { FC, useMemo } from 'react'; -import { useSelector } from 'react-redux'; - -import { Loader } from '$uikit'; -import * as S from './Marketplaces.style'; -import { MarketplaceItem } from './MarketplaceItem/MarketplaceItem'; -import { t } from '@tonkeeper/shared/i18n'; -import { nftsSelector } from '$store/nfts'; -import { getDiamondsCollectionMarketUrl } from '$utils'; -import { MarketplacesModalProps } from './Marketplaces.interface'; -import { Modal, View } from '@tonkeeper/uikit'; -import { push } from '$navigation/imperative'; -import { SheetActions } from '@tonkeeper/router'; - -export const Marketplaces: FC = (props) => { - const { accentKey } = props; - - const { isMarketplacesLoading, marketplaces: data } = useSelector(nftsSelector); - - const marketplaces = useMemo(() => { - if (accentKey) { - return data - .filter((item) => ['getgems', 'tonDiamonds'].includes(item.id)) - .map((market) => ({ - ...market, - marketplace_url: getDiamondsCollectionMarketUrl(market, accentKey), - })); - } - - return data; - }, [accentKey, data]); - - function renderContent() { - // don't show spinner if we have loaded marketplaces - if (!marketplaces.length && isMarketplacesLoading) { - return ( - - - - ); - } - - return ( - - {marketplaces.map((item, idx, arr) => ( - - ))} - - ); - } - - return ( - - - - {renderContent()} - - - ); -}; - -export function openMarketplaces(props?: MarketplacesModalProps) { - push('SheetsProvider', { - $$action: SheetActions.ADD, - component: Marketplaces, - params: props, - path: 'MARKETPLACES', - }); -} diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 087ffa2a6..7008a928d 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -15,28 +15,29 @@ import { } from '$core/ModalContainer/InsufficientFunds/InsufficientFunds'; import { TonConnectRemoteBridge } from '$tonconnect/TonConnectRemoteBridge'; import { formatter } from '$utils/formatter'; -import { tk, tonapi } from '@tonkeeper/shared/tonkeeper'; +import { tk } from '$wallet'; import { MessageConsequences } from '@tonkeeper/core/src/TonAPI'; import { - ActionAmountType, - ActionSource, - ActionType, - ActivityModel, Address, - AnyActionItem, ContractService, contractVersionsMap, TransactionService, } from '@tonkeeper/core'; import { ActionListItemByType } from '@tonkeeper/shared/components/ActivityList/ActionListItemByType'; -import { useSelector } from 'react-redux'; -import { fiatCurrencySelector } from '$store/main'; import { useGetTokenPrice } from '$hooks/useTokenPrice'; import { formatValue, getActionTitle } from '@tonkeeper/shared/utils/signRaw'; import { Buffer } from 'buffer'; import { trackEvent } from '$utils/stats'; import { Events, SendAnalyticsFrom } from '$store/models'; import { getWalletSeqno } from '@tonkeeper/shared/utils/wallet'; +import { useWalletCurrency } from '@tonkeeper/shared/hooks'; +import { + ActionAmountType, + ActionSource, + ActionType, + ActivityModel, + AnyActionItem, +} from '$wallet/models/ActivityModel'; interface SignRawModalProps { consequences?: MessageConsequences; @@ -61,7 +62,7 @@ export const SignRawModal = memo((props) => { const { footerRef, onConfirm } = useNFTOperationState(options); const unlockVault = useUnlockVault(); - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const getTokenPrice = useGetTokenPrice(); const handleConfirm = onConfirm(async ({ startLoading }) => { @@ -82,7 +83,7 @@ export const SignRawModal = memo((props) => { secretKey: Buffer.from(privateKey), }); - await tonapi.blockchain.sendBlockchainMessage( + await tk.wallet.tonapi.blockchain.sendBlockchainMessage( { boc, }, @@ -119,16 +120,18 @@ export const SignRawModal = memo((props) => { sender: { address: message.address, is_scam: false, + is_wallet: true, }, recipient: { address: message.address, is_scam: false, + is_wallet: true, }, }, }); }); } - }, [consequences]); + }, [consequences, params.messages]); const extra = useMemo(() => { if (consequences) { @@ -269,7 +272,7 @@ export const openSignRawModal = async ( seqno: await getWalletSeqno(), secretKey: Buffer.alloc(64), }); - consequences = await tonapi.wallet.emulateMessageToWallet({ + consequences = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc, }); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx index 0256971a5..417218c3e 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx @@ -16,7 +16,7 @@ import { CanceledActionError, DismissedActionError, } from '$core/Send/steps/ConfirmStep/ActionErrors'; -import { tk } from '@tonkeeper/shared/tonkeeper'; +import { tk } from '$wallet'; import { TabsStackRouteNames } from '$navigation'; enum States { diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts index 61d0df223..422469074 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts @@ -12,9 +12,9 @@ import { Address as AddressType } from 'tonweb/dist/types/utils/address'; import { Address } from '@ton/core'; import { t } from '@tonkeeper/shared/i18n'; import { Ton } from '$libs/Ton'; -import { getServerConfig } from '$shared/constants'; import { Configuration, NFTApi } from '@tonkeeper/core/src/legacy'; -import { tonapi } from '@tonkeeper/shared/tonkeeper'; +import { tk } from '$wallet'; +import { config } from '$config'; const { NftItem } = TonWeb.token.nft; @@ -26,9 +26,9 @@ export class NFTOperations { private wallet: Wallet; private nftApi = new NFTApi( new Configuration({ - basePath: getServerConfig('tonapiV2Endpoint'), + basePath: config.get('tonapiV2Endpoint', tk.wallet.isTestnet), headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, }, }), ); @@ -160,7 +160,7 @@ export class NFTOperations { const methods = await signRawMethods(); const queryMsg = await methods.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - const feeInfo = await tonapi.wallet.emulateMessageToWallet({ boc }); + const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); const fee = new BigNumber(feeInfo.event.extra).multipliedBy(-1).toNumber(); return truncateDecimal(Ton.fromNano(fee.toString()), 2, true); @@ -171,7 +171,10 @@ export class NFTOperations { const queryMsg = await methods.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - await tonapi.blockchain.sendBlockchainMessage({ boc }, { format: 'text' }); + await tk.wallet.tonapi.blockchain.sendBlockchainMessage( + { boc }, + { format: 'text' }, + ); onDone?.(boc); }, @@ -231,7 +234,7 @@ export class NFTOperations { const methods = transfer(params); const queryMsg = await methods.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - const feeInfo = await tonapi.wallet.emulateMessageToWallet({ boc }); + const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); const fee = new BigNumber(feeInfo.event.extra).multipliedBy(-1).toNumber(); return truncateDecimal(Ton.fromNano(fee.toString()), 2, true); @@ -257,7 +260,7 @@ export class NFTOperations { try { const query = await transfer.getQuery(); const boc = Base64.encodeBytes(await query.toBoc(false)); - const feeInfo = await tonapi.wallet.emulateMessageToWallet({ boc }); + const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); feeNano = new BigNumber(feeInfo.event.extra).multipliedBy(-1); } catch (e) { throw new NFTOperationError(t('send_fee_estimation_error')); @@ -274,7 +277,10 @@ export class NFTOperations { const queryMsg = await transfer.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - await tonapi.blockchain.sendBlockchainMessage({ boc }, { format: 'text' }); + await tk.wallet.tonapi.blockchain.sendBlockchainMessage( + { boc }, + { format: 'text' }, + ); }, }; } diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadCollectionMeta.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadCollectionMeta.ts index 45d2a1a57..057f649fb 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadCollectionMeta.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadCollectionMeta.ts @@ -1,7 +1,8 @@ import { debugLog } from '$utils/debugLog'; import React from 'react'; -import { getServerConfig } from '$shared/constants'; import { NFTApi, Configuration } from '@tonkeeper/core/src/legacy'; +import { tk } from '$wallet'; +import { config } from '$config'; export type NFTCollectionMeta = { name: string; @@ -18,12 +19,12 @@ export function useDownloadCollectionMeta(addr?: string) { const download = React.useCallback(async (address: string) => { try { - const endpoint = getServerConfig('tonapiV2Endpoint'); + const endpoint = config.get('tonapiV2Endpoint', tk.wallet.isTestnet); const nftApi = new NFTApi( new Configuration({ basePath: endpoint, headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, }, }), ); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadNFT.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadNFT.ts index 5168f094e..2b0bc64fe 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadNFT.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/useDownloadNFT.ts @@ -1,8 +1,9 @@ import { debugLog } from '$utils/debugLog'; import axios from 'axios'; import React from 'react'; -import { getServerConfig } from '$shared/constants'; import { proxyMedia } from '$utils/proxyMedia'; +import { config } from '$config'; +import { tk } from '$wallet'; export type NFTItemMeta = { name: string; @@ -17,7 +18,7 @@ export function useDownloadNFT(addr?: string) { const download = React.useCallback(async (address: string) => { try { - const endpoint = getServerConfig('tonapiV2Endpoint'); + const endpoint = config.get('tonapiV2Endpoint', tk.wallet.isTestnet); const response: any = await axios.post( `${endpoint}/v2/nfts/_bulk`, @@ -26,7 +27,7 @@ export function useDownloadNFT(addr?: string) { }, { headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, }, }, ); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/useUnlockVault.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/useUnlockVault.ts index 6603e43bf..f635ba0b9 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/useUnlockVault.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/useUnlockVault.ts @@ -8,12 +8,14 @@ export const useUnlockVault = () => { const unlockVault = React.useCallback(async () => { return new Promise((resolve, reject) => { - dispatch(walletActions.walletGetUnlockedVault({ - onDone: (vault) => resolve(vault), - onFail: (err) => reject(err) - })); + dispatch( + walletActions.walletGetUnlockedVault({ + onDone: (vault) => resolve(vault), + onFail: (err) => reject(err), + }), + ); }); - }, []); + }, [dispatch]); return unlockVault; -}; \ No newline at end of file +}; diff --git a/packages/mobile/src/core/ModalContainer/NewConfirmSending/NewConfirmSending.tsx b/packages/mobile/src/core/ModalContainer/NewConfirmSending/NewConfirmSending.tsx index 4b0acd090..10a9f7604 100644 --- a/packages/mobile/src/core/ModalContainer/NewConfirmSending/NewConfirmSending.tsx +++ b/packages/mobile/src/core/ModalContainer/NewConfirmSending/NewConfirmSending.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { Alert } from 'react-native'; import BigNumber from 'bignumber.js'; @@ -7,9 +7,8 @@ import { ConfirmSendingProps } from './ConfirmSending.interface'; import * as S from './ConfirmSending.style'; import { useExchangeMethodInfo } from '$hooks/useExchangeMethodInfo'; import { CryptoCurrencies, Decimals } from '$shared/constants'; -import { walletActions, walletSelector } from '$store/wallet'; +import { walletActions } from '$store/wallet'; import { formatCryptoCurrency } from '$utils/currency'; -import { getTokenConfig } from '$shared/dynamicConfig'; import { useCurrencyToSend } from '$hooks/useCurrencyToSend'; import { Modal } from '@tonkeeper/uikit'; import { @@ -18,7 +17,7 @@ import { } from '../NFTOperations/NFTOperationFooter'; import { Separator } from '$uikit'; import { t } from '@tonkeeper/shared/i18n'; -import { TokenType } from '$core/Send/Send.interface'; +import { useBalancesState, useWallet } from '@tonkeeper/shared/hooks'; export const NewConfirmSending: FC = (props) => { const { currency, address, amount, comment, fee, tokenType, methodId } = props; @@ -29,7 +28,8 @@ export const NewConfirmSending: FC = (props) => { const { footerRef, onConfirm } = useNFTOperationState(); - const { balances, wallet } = useSelector(walletSelector); + const wallet = useWallet(); + const balances = useBalancesState(); const { decimals, jettonWalletAddress, currencyTitle } = useCurrencyToSend( currency, @@ -61,8 +61,8 @@ export const NewConfirmSending: FC = (props) => { if ( currency === CryptoCurrencies.Ton && wallet && - wallet.ton.isLockup() && - new BigNumber(balances[currency]).isLessThan(amountWithFee) + wallet.isLockup && + new BigNumber(balances.ton).isLessThan(amountWithFee) ) { Alert.alert(t('send_lockup_warning_title'), t('send_lockup_warning_caption'), [ { @@ -80,16 +80,7 @@ export const NewConfirmSending: FC = (props) => { } }, [amount, fee, currency, wallet, balances, doSend]); - const feeCurrency = useMemo(() => { - const tokenConfig = getTokenConfig(currency); - if (tokenConfig && tokenConfig.blockchain === 'ethereum') { - return CryptoCurrencies.Eth; - } else if (tokenType === TokenType.Jetton) { - return CryptoCurrencies.Ton; - } else { - return currency; - } - }, [currency, tokenType]); + const feeCurrency = CryptoCurrencies.Ton; const feeValue = React.useMemo(() => { if (fee === '0') { diff --git a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx b/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx index 6b6177ab7..fa696c4af 100644 --- a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx +++ b/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx @@ -3,12 +3,6 @@ import { t } from '@tonkeeper/shared/i18n'; import { Button, Icon, Text } from '$uikit'; import * as S from './ReminderEnableNotificationsModal.styles'; import { useNotifications } from '$hooks/useNotifications'; -import { - removeReminderNotifications, - saveDontShowReminderNotifications, - saveReminderNotifications, - shouldOpenReminderNotifications, -} from '$utils/messaging'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Modal, View } from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; @@ -17,20 +11,14 @@ export const ReminderEnableNotificationsModal = () => { const nav = useNavigation(); const notifications = useNotifications(); - React.useEffect(() => { - saveDontShowReminderNotifications(); - }, []); - const handleEnable = React.useCallback(async () => { const isSubscribe = await notifications.subscribe(); if (isSubscribe) { - removeReminderNotifications(); nav.goBack(); } }, []); const handleLater = React.useCallback(async () => { - saveReminderNotifications(); nav.goBack(); }, []); @@ -76,13 +64,10 @@ export const ReminderEnableNotificationsModal = () => { }; export async function openReminderEnableNotificationsModal() { - const shouldOpen = await shouldOpenReminderNotifications(); - if (shouldOpen) { - push('SheetsProvider', { - $$action: SheetActions.ADD, - component: ReminderEnableNotificationsModal, - params: {}, - path: 'MARKETPLACES', - }); - } + push('SheetsProvider', { + $$action: SheetActions.ADD, + component: ReminderEnableNotificationsModal, + params: {}, + path: 'MARKETPLACES', + }); } diff --git a/packages/mobile/src/core/ModalContainer/RequireWallet/RequireWallet.tsx b/packages/mobile/src/core/ModalContainer/RequireWallet/RequireWallet.tsx index 47fd7888c..4b9a91581 100644 --- a/packages/mobile/src/core/ModalContainer/RequireWallet/RequireWallet.tsx +++ b/packages/mobile/src/core/ModalContainer/RequireWallet/RequireWallet.tsx @@ -1,94 +1,11 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import { useDispatch } from 'react-redux'; -import LottieView from 'lottie-react-native'; - -import * as S from './RequireWallet.style'; -import { Text, Button, Modal, View } from '@tonkeeper/uikit'; - -import { openImportWallet } from '$navigation/helper'; -import { walletActions } from '$store/wallet'; -import { SheetActions, useNavigation } from '@tonkeeper/router'; -import { t } from '@tonkeeper/shared/i18n'; -import { openCreateWallet } from '$core/CreateWallet/CreateWallet'; -import { push } from '$navigation/imperative'; - -export const RequireWallet: FC = () => { - const iconRef = useRef(null); - const nav = useNavigation(); - const dispatch = useDispatch(); - const destination = useRef(null); - - useEffect(() => { - dispatch(walletActions.clearGeneratedVault()); - - const timer = setTimeout(() => { - iconRef.current?.play(); - }, 400); - - return () => clearTimeout(timer); - }, [dispatch]); - - const handleClose = useCallback(() => { - if (destination.current === 'Create') { - openCreateWallet(); - } else if (destination.current === 'Import') { - openImportWallet(); - } - }, []); - - return ( - - - - - - - - - {t('require_create_wallet_modal_title')} - - - - - {t('require_create_wallet_modal_caption')} - - - - - - )} - {isOnSale ? ( - - - {isDNS ? t('dns_on_sale_text') : t('nft_on_sale_text')} - - - ) : null} - {(isDNS || isTG) && ( - + {nft.ownerAddress && ( + + )} + {isOnSale ? ( + + + {isDNS ? t('dns_on_sale_text') : t('nft_on_sale_text')} + + + ) : null} + {(isDNS || isTG) && ( + + )} + {isDNS && ( + + )} + {nft.marketplaceURL && !flags.disable_nft_markets ? ( + + ) : null} + - )} - {isDNS && ( - - )} - {nft.marketplaceURL && !flags.disable_nft_markets ? ( - - ) : null} - - + + ) : null} {!hiddenAmounts && }
{ - const address = new TonWeb.utils.Address(nftItem.address).toString(true, true, true); - const ownerAddress = nftItem.owner?.address - ? Address.parse(nftItem.owner.address, { - bounceable: !getFlag('address_style_nobounce'), - }).toFriendly() - : ''; - const name = - typeof nftItem.metadata?.name === 'string' - ? nftItem.metadata.name.trim() - : nftItem.metadata?.name; - - const baseUrl = (nftItem.previews && - nftItem.previews.find((preview) => preview.resolution === '500x500')!.url)!; - - return { - ...nftItem, - ownerAddressToDisplay: nftItem.sale ? walletFriendlyAddress : undefined, - isApproved: !!nftItem.approved_by?.length ?? false, - internalId: `${CryptoCurrencies.Ton}_${address}`, - currency: CryptoCurrencies.Ton, - provider: 'TonProvider', - content: { - image: { - baseUrl, - }, - }, - description: nftItem.metadata?.description, - marketplaceURL: nftItem.metadata?.marketplace && nftItem.metadata?.external_url, - attributes: nftItem.metadata?.attributes, - address, - name, - ownerAddress, - collection: nftItem.collection, - }; -}; diff --git a/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx b/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx index 92fc8b597..57d581dfc 100644 --- a/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx +++ b/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx @@ -12,9 +12,9 @@ import { createTonProof } from '$utils/proof'; import { useSelector } from 'react-redux'; import { walletWalletSelector } from '$store/wallet'; import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; -import { isTestnetSelector } from '$store/main'; import { getDomainFromURL } from '$utils'; import { Address } from '@tonkeeper/core'; +import { tk } from '$wallet'; export interface ProgrammableButton { label?: string; @@ -32,7 +32,6 @@ export interface ProgrammableButtonsProps { const ProgrammableButtonsComponent = (props: ProgrammableButtonsProps) => { const wallet = useSelector(walletWalletSelector); const unlockVault = useUnlockVault(); - const isTestnet = useSelector(isTestnetSelector); const buttons = useMemo(() => { if (!props.buttons || !isArray(props.buttons)) { @@ -47,7 +46,7 @@ const ProgrammableButtonsComponent = (props: ProgrammableButtonsProps) => { try { const nftAddress = Address.parse(props.nftAddress).toRaw(); const vault = await unlockVault(); - const address = await vault.getTonAddress(isTestnet); + const address = await vault.getTonAddress(tk.wallet.isTestnet); let walletStateInit = ''; if (wallet) { const tonWallet = wallet.vault.tonWallet; @@ -83,7 +82,7 @@ const ProgrammableButtonsComponent = (props: ProgrammableButtonsProps) => { console.log(e); } }, - [isTestnet, props.nftAddress, unlockVault, wallet], + [props.nftAddress, unlockVault, wallet], ); const handleOpenLink = useCallback( diff --git a/packages/mobile/src/core/NFT/RenewDomainButton.tsx b/packages/mobile/src/core/NFT/RenewDomainButton.tsx index a642da418..a8a5420ef 100644 --- a/packages/mobile/src/core/NFT/RenewDomainButton.tsx +++ b/packages/mobile/src/core/NFT/RenewDomainButton.tsx @@ -12,9 +12,9 @@ import { Ton } from '$libs/Ton'; import TonWeb from 'tonweb'; import { openAddressMismatchModal } from '$core/ModalContainer/AddressMismatch/AddressMismatch'; -import { useWallet } from '$hooks/useWallet'; import { Base64 } from '$utils'; import { Address } from '@tonkeeper/core'; +import { useWallet } from '@tonkeeper/shared/hooks'; export type RenewDomainButtonRef = { renewUpdated: () => void; @@ -40,7 +40,7 @@ export const RenewDomainButton = forwardRef { - if (!wallet || !wallet.address?.rawAddress) { + if (!wallet) { return; } @@ -54,7 +54,7 @@ export const RenewDomainButton = forwardRef { - if (!wallet || !wallet.address?.rawAddress) { + if (!wallet) { return; } - if (!Address.compare(wallet.address.rawAddress, ownerAddress)) { + if (!Address.compare(wallet.address.ton.raw, ownerAddress)) { return openAddressMismatchModal(openRenew, ownerAddress!); } else { openRenew(); diff --git a/packages/mobile/src/core/NFTSend/NFTSend.tsx b/packages/mobile/src/core/NFTSend/NFTSend.tsx index 5b9bbfa9b..b12678d73 100644 --- a/packages/mobile/src/core/NFTSend/NFTSend.tsx +++ b/packages/mobile/src/core/NFTSend/NFTSend.tsx @@ -17,7 +17,6 @@ import { t } from '@tonkeeper/shared/i18n'; import { AddressStep } from '$core/Send/steps/AddressStep/AddressStep'; import { NFTSendSteps } from '$core/NFTSend/types'; import { ConfirmStep } from '$core/NFTSend/steps/ConfirmStep/ConfirmStep'; -import { useNFT } from '$hooks/useNFT'; import { BASE_FORWARD_AMOUNT, ContractService, @@ -26,7 +25,6 @@ import { ONE_TON, TransactionService, } from '@tonkeeper/core'; -import { tk, tonapi } from '@tonkeeper/shared/tonkeeper'; import { getWalletSeqno, setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; import { Buffer } from 'buffer'; import { MessageConsequences } from '@tonkeeper/core/src/TonAPI'; @@ -36,7 +34,6 @@ import { Ton } from '$libs/Ton'; import { delay } from '$utils'; import { Toast } from '$store'; import axios from 'axios'; -import { useWallet } from '$hooks/useWallet'; import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; import { emulateWithBattery, @@ -51,7 +48,9 @@ import { Keyboard } from 'react-native'; import nacl from 'tweetnacl'; import { useInstance } from '$hooks/useInstance'; import { AccountsApi, Configuration } from '@tonkeeper/core/src/legacy'; -import { getServerConfig } from '$shared/constants'; +import { useWallet } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; +import { config } from '$config'; interface Props { route: RouteProp; @@ -82,7 +81,10 @@ export const NFTSend: FC = (props) => { ), ); - const nft = useNFT({ currency: 'ton', address: nftAddress }); + const nft = useMemo( + () => wallet.nfts.getCachedByAddress(nftAddress), + [nftAddress, wallet], + ); const scrollTop = useDerivedValue( () => stepsScrollTop.value[currentStep.id] || 0, ); @@ -124,7 +126,7 @@ export const NFTSend: FC = (props) => { tempKeyPair.publicKey, tempKeyPair.publicKey, tempKeyPair.secretKey, - tk.wallet.address.ton.raw, + wallet.address.ton.raw, ); } @@ -135,7 +137,7 @@ export const NFTSend: FC = (props) => { body: ContractService.createNftTransferBody({ queryId: Date.now(), newOwnerAddress: recipient!.address, - excessesAddress: tk.wallet.address.ton.raw, + excessesAddress: wallet.address.ton.raw, forwardBody: commentValue, }), bounce: true, @@ -143,9 +145,9 @@ export const NFTSend: FC = (props) => { ]; const contract = ContractService.getWalletContract( - contractVersionsMap[wallet.ton.version ?? 'v4R2'], - Buffer.from(await wallet.ton.getTonPublicKey()), - wallet.ton.workchain, + contractVersionsMap[wallet.config.version ?? 'v4R2'], + Buffer.from(wallet.pubkey, 'hex'), + wallet.config.workchain, ); const boc = TransactionService.createTransfer(contract, { @@ -177,13 +179,13 @@ export const NFTSend: FC = (props) => { } finally { setPreparing(false); } - }, [comment, isCommentEncrypted, nftAddress, recipient, wallet.ton]); + }, [comment, isCommentEncrypted, nftAddress, recipient, wallet]); const accountsApi = useInstance(() => { const tonApiConfiguration = new Configuration({ - basePath: getServerConfig('tonapiV2Endpoint'), + basePath: config.get('tonapiV2Endpoint', tk.wallet.isTestnet), headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, }, }); @@ -243,15 +245,15 @@ export const NFTSend: FC = (props) => { if (isCommentEncrypted && comment.length) { const secretKey = await vault.getTonPrivateKey(); const recipientPubKey = ( - await tonapi.accounts.getAccountPublicKey(recipient!.address) + await tk.wallet.tonapi.accounts.getAccountPublicKey(recipient!.address) ).public_key; commentValue = await encryptMessageComment( comment, - wallet.vault.tonPublicKey, + vault.tonPublicKey, Buffer.from(recipientPubKey!, 'hex'), secretKey, - tk.wallet.address.ton.raw, + wallet.address.ton.raw, ); } @@ -270,7 +272,7 @@ export const NFTSend: FC = (props) => { throw new CanceledActionError(); } - const excessesAccount = isBattery && (await tk.wallet.battery.getExcessesAccount()); + const excessesAccount = isBattery && (await wallet.battery.getExcessesAccount()); const nftTransferMessages = [ internal({ @@ -279,7 +281,7 @@ export const NFTSend: FC = (props) => { body: ContractService.createNftTransferBody({ queryId: Date.now(), newOwnerAddress: recipient!.address, - excessesAddress: excessesAccount || tk.wallet.address.ton.raw, + excessesAddress: excessesAccount || wallet.address.ton.raw, forwardBody: commentValue, }), bounce: true, @@ -287,8 +289,8 @@ export const NFTSend: FC = (props) => { ]; const contract = ContractService.getWalletContract( - contractVersionsMap[wallet.ton.version ?? 'v4R2'], - Buffer.from(await wallet.ton.getTonPublicKey()), + contractVersionsMap[wallet.config.version ?? 'v4R2'], + Buffer.from(wallet.pubkey, 'hex'), vault.workchain, ); @@ -312,13 +314,15 @@ export const NFTSend: FC = (props) => { isCommentEncrypted, nftAddress, recipient, - recipientAccountInfo?.publicKey, total.isRefund, unlockVault, - wallet.ton, - wallet.vault.tonPublicKey, + wallet, ]); + if (!nft) { + return null; + } + return ( <> = (props) => { total={total} nftCollection={nft.collection?.name} nftName={nft.name} - nftIcon={nft.content.image.baseUrl} + nftIcon={nft.image.medium!} stepsScrollTop={stepsScrollTop} isPreparing={isPreparing} sendTx={sendTx} diff --git a/packages/mobile/src/core/NFTs/NFTs.interface.ts b/packages/mobile/src/core/NFTs/NFTs.interface.ts deleted file mode 100644 index 436a0dc10..000000000 --- a/packages/mobile/src/core/NFTs/NFTs.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RouteProp } from '@react-navigation/native'; -import { TabsStackRouteNames } from '$navigation'; -import { TabStackParamList } from '$navigation/MainStack/TabStack/TabStack.interface'; - -export interface NFTsProps { - route: RouteProp; -} diff --git a/packages/mobile/src/core/NFTs/NFTs.style.ts b/packages/mobile/src/core/NFTs/NFTs.style.ts deleted file mode 100644 index 836e98bfe..000000000 --- a/packages/mobile/src/core/NFTs/NFTs.style.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IsTablet } from '$shared/constants'; -import styled, { css } from '$styled'; -import { ns } from '$utils'; - -export const Wrap = styled.View` - flex: 1; -`; - -export const RightButtonIconWrap = styled.View` - margin-left: ${ns(-4)}px; - margin-right: ${ns(4)}px; -`; - -export const RightButtonContainer = styled.View` - ${() => - IsTablet && - css` - margin-right: ${ns(16)}px; - `} -`; diff --git a/packages/mobile/src/core/NFTs/NFTs.tsx b/packages/mobile/src/core/NFTs/NFTs.tsx deleted file mode 100644 index 44414a5c7..000000000 --- a/packages/mobile/src/core/NFTs/NFTs.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { FC, useCallback, useMemo } from 'react'; - -import * as S from './NFTs.style'; -import { Button, ScrollHandler, AnimatedFlatList } from '$uikit'; -import { useTheme } from '$hooks/useTheme'; -import { RefreshControl } from 'react-native'; -import { MarketplaceBanner } from '$core/NFTs/MarketplaceBanner/MarketplaceBanner'; -import { hNs, ns } from '$utils'; -import { IsTablet, LargeNavBarHeight } from '$shared/constants'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -import { NFTItem } from '$core/NFTs/NFTItem/NFTItem'; -import { useDispatch, useSelector } from 'react-redux'; -import { nftsActions, nftsSelector } from '$store/nfts'; -import { useIsFocused } from '@react-navigation/native'; -import { openMarketplaces } from '$navigation'; -import { NUM_OF_COLUMNS } from '$core/NFTs/NFTItem/NFTItem.style'; -import { useFlags } from '$utils/flags'; -import { t } from '@tonkeeper/shared/i18n'; - -export const NFTs: FC = () => { - const flags = useFlags(['disable_nft_markets']); - - const theme = useTheme(); - const tabBarHeight = useBottomTabBarHeight(); - - const { myNfts, isLoading, canLoadMore } = useSelector(nftsSelector); - const dispatch = useDispatch(); - const isFocused = useIsFocused(); - - const data = useMemo(() => Object.values(myNfts), [myNfts]); - - const handleOpenMarketplace = useCallback(() => { - openMarketplaces(); - }, []); - - const handleRefresh = useCallback(() => { - dispatch(nftsActions.loadNFTs({ isReplace: true })); - }, [dispatch]); - - const handleLoadMore = useCallback(() => { - if (isLoading || !canLoadMore) { - return; - } - - dispatch(nftsActions.loadNFTs({ isLoadMore: true })); - }, [isLoading, canLoadMore, dispatch]); - - function renderRightButton() { - if (!flags.disable_nft_markets) { - return ( - - - - ); - } - } - - function renderItem({ item, index }) { - return ( - - ); - } - - const keyExtractor = useCallback((item) => item.address, []); - - if (!data.length) { - return ; - } - - return ( - - - - } - numColumns={NUM_OF_COLUMNS} - showsVerticalScrollIndicator={false} - data={data} - scrollEventThrottle={16} - maxToRenderPerBatch={8} - style={{ alignSelf: IsTablet ? 'center' : 'auto' }} - contentContainerStyle={{ - paddingTop: IsTablet ? ns(8) : hNs(LargeNavBarHeight - 4), - paddingHorizontal: ns(16), - paddingBottom: tabBarHeight, - }} - keyExtractor={keyExtractor} - renderItem={renderItem} - onEndReachedThreshold={0.01} - onEndReached={isLoading || !canLoadMore ? undefined : handleLoadMore} - /> - - - ); -}; diff --git a/packages/mobile/src/core/Notifications/Notification.tsx b/packages/mobile/src/core/Notifications/Notification.tsx index 4c38f7937..33b391783 100644 --- a/packages/mobile/src/core/Notifications/Notification.tsx +++ b/packages/mobile/src/core/Notifications/Notification.tsx @@ -11,11 +11,9 @@ import { t } from '@tonkeeper/shared/i18n'; import { isToday } from 'date-fns'; import { useActionSheet } from '@expo/react-native-action-sheet'; import { TonConnect } from '$tonconnect'; -import messaging from '@react-native-firebase/messaging'; -import { useSelector } from 'react-redux'; -import { walletAddressSelector } from '$store/wallet'; import { openDAppBrowser } from '$navigation'; import { Alert, Animated } from 'react-native'; +import { useWallet } from '@tonkeeper/shared/hooks'; interface NotificationProps { notification: INotification; @@ -47,7 +45,7 @@ export const Notification: React.FC = (props) => { props.notification.dapp_url && getDomainFromURL(app.url) === getDomainFromURL(props.notification.dapp_url), ); - const walletAddress = useSelector(walletAddressSelector); + const wallet = useWallet(); const { showActionSheetWithOptions } = useActionSheet(); const deleteNotification = useNotificationsStore( (state) => state.actions.deleteNotificationByReceivedAt, @@ -79,7 +77,7 @@ export const Notification: React.FC = (props) => { app?.notificationsEnabled && { option: t('notifications.mute_notifications'), action: async () => { - disableNotifications(walletAddress.ton, app.url); + disableNotifications(wallet.address.ton.friendly, app.url); }, }, app && { @@ -111,7 +109,7 @@ export const Notification: React.FC = (props) => { return; }, ); - }, [app, showActionSheetWithOptions, walletAddress.ton]); + }, [app, showActionSheetWithOptions, wallet]); const renderRightActions = useCallback( (progress) => { diff --git a/packages/mobile/src/core/Notifications/Notifications.tsx b/packages/mobile/src/core/Notifications/Notifications.tsx index e12e21553..a4c289ae6 100644 --- a/packages/mobile/src/core/Notifications/Notifications.tsx +++ b/packages/mobile/src/core/Notifications/Notifications.tsx @@ -13,8 +13,6 @@ import { debugLog } from '$utils/debugLog'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { Linking } from 'react-native'; import { CellSection } from '$shared/components'; -import { getSubscribeStatus, SUBSCRIBE_STATUS } from '$utils/messaging'; -import { useSelector } from 'react-redux'; import { NotificationsStatus, useNotificationStatus } from '$hooks/useNotificationStatus'; import messaging from '@react-native-firebase/messaging'; import { useNotifications } from '$hooks/useNotifications'; @@ -23,11 +21,11 @@ import { useNotificationsBadge } from '$hooks/useNotificationsBadge'; import { Toast, ToastSize, useConnectedAppsList, useConnectedAppsStore } from '$store'; import { Steezy } from '$styles'; import { getChainName } from '$shared/dynamicConfig'; -import { walletAddressSelector } from '$store/wallet'; import { SwitchDAppNotifications } from '$core/Notifications/SwitchDAppNotifications'; +import { useWallet } from '@tonkeeper/shared/hooks'; export const Notifications: React.FC = () => { - const address = useSelector(walletAddressSelector); + const wallet = useWallet(); const handleOpenSettings = useCallback(() => Linking.openSettings(), []); const notifications = useNotifications(); const tabBarHeight = useBottomTabBarHeight(); @@ -42,18 +40,17 @@ export const Notifications: React.FC = () => { React.useEffect(() => { const init = async () => { - const subscribeStatus = await getSubscribeStatus(); const status = await messaging().hasPermission(); - const isGratend = + const isGranted = status === NotificationsStatus.AUTHORIZED || status === NotificationsStatus.PROVISIONAL; - const initialValue = isGratend && subscribeStatus === SUBSCRIBE_STATUS.SUBSCRIBED; - setIsSubscribeNotifications(initialValue); + setIsSubscribeNotifications(isGranted && notifications.isSubscribed); }; init(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { @@ -76,7 +73,7 @@ export const Notifications: React.FC = () => { ? await notifications.subscribe() : await notifications.unsubscribe(); - updateNotificationsSubscription(getChainName(), address.ton); + updateNotificationsSubscription(getChainName(), wallet.address.ton.friendly); if (!isSuccess) { // Revert @@ -90,7 +87,7 @@ export const Notifications: React.FC = () => { isSwitchFrozen.current = false; } }, - [address.ton, notifications, updateNotificationsSubscription], + [notifications, updateNotificationsSubscription, wallet], ); const connectedApps = useConnectedAppsList(); diff --git a/packages/mobile/src/core/Notifications/SwitchDAppNotifications.tsx b/packages/mobile/src/core/Notifications/SwitchDAppNotifications.tsx index 2788e1430..8e168a438 100644 --- a/packages/mobile/src/core/Notifications/SwitchDAppNotifications.tsx +++ b/packages/mobile/src/core/Notifications/SwitchDAppNotifications.tsx @@ -5,12 +5,11 @@ import { IConnectedApp, useConnectedAppsStore } from '$store'; import { Steezy } from '$styles'; import { getChainName } from '$shared/dynamicConfig'; import { useObtainProofToken } from '$hooks/useObtainProofToken'; -import { useSelector } from 'react-redux'; -import { walletAddressSelector } from '$store/wallet'; import { useIsFocused } from '@react-navigation/native'; +import { useWallet } from '@tonkeeper/shared/hooks'; const SwitchDAppNotificationsComponent: React.FC<{ app: IConnectedApp }> = ({ app }) => { - const address = useSelector(walletAddressSelector); + const wallet = useWallet(); const [switchValue, setSwitchValue] = React.useState(!!app.notificationsEnabled); const [isFrozen, setIsFrozen] = React.useState(false); const isFocused = useIsFocused(); @@ -37,9 +36,14 @@ const SwitchDAppNotificationsComponent: React.FC<{ app: IConnectedApp }> = ({ ap return setSwitchValue(!value); } if (value) { - await enableNotifications(getChainName(), address.ton, url, session_id); + await enableNotifications( + getChainName(), + wallet.address.ton.friendly, + url, + session_id, + ); } else { - await disableNotifications(getChainName(), address.ton, url); + await disableNotifications(getChainName(), wallet.address.ton.friendly, url); } setIsFrozen(false); } catch (error) { @@ -47,13 +51,7 @@ const SwitchDAppNotificationsComponent: React.FC<{ app: IConnectedApp }> = ({ ap setSwitchValue(!value); } }, - [ - setIsFrozen, - obtainProofToken, - disableNotifications, - address.ton, - enableNotifications, - ], + [obtainProofToken, enableNotifications, wallet, disableNotifications], ); return ( diff --git a/packages/mobile/src/core/ResetPin/ResetPin.style.ts b/packages/mobile/src/core/ResetPin/ResetPin.style.ts deleted file mode 100644 index c79bf2efc..000000000 --- a/packages/mobile/src/core/ResetPin/ResetPin.style.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Animated } from 'react-native'; -import Renimated from 'react-native-reanimated'; - -import styled from '$styled'; -import { deviceWidth } from '$utils'; - -export const Steps = styled(Renimated.View)` - flex-direction: row; - flex: 1; -`; - -export const Step = styled.View` - flex: 0 0 auto; - width: ${deviceWidth}px; -`; - -export const ImportWrap = styled(Animated.View)` - flex: 1; -`; diff --git a/packages/mobile/src/core/ResetPin/ResetPin.tsx b/packages/mobile/src/core/ResetPin/ResetPin.tsx deleted file mode 100644 index 3a0366ddc..000000000 --- a/packages/mobile/src/core/ResetPin/ResetPin.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; -import { - Easing, - interpolate, - useAnimatedStyle, - useSharedValue, - withDelay, - withTiming, -} from 'react-native-reanimated'; -import { Keyboard } from 'react-native'; -import { useDispatch } from 'react-redux'; -import * as LocalAuthentication from 'expo-local-authentication'; -import * as SecureStore from 'expo-secure-store'; - -import { NavBar } from '$uikit'; -import * as S from './ResetPin.style'; -import { CreatePinForm, ImportWalletForm } from '$shared/components'; -import { detectBiometryType, deviceWidth } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { useKeyboardHeight } from '$hooks/useKeyboardHeight'; -import { walletActions } from '$store/wallet'; -import { goBack, popToTop } from '$navigation/imperative'; -import { openSetupBiometryAfterRestore } from '$navigation'; -import { Toast } from '$store'; - -export const ResetPin: FC = () => { - const [step, setStep] = useState(0); - const keyboardHeight = useKeyboardHeight(); - const dispatch = useDispatch(); - - const stepsValue = useSharedValue(0); - - useEffect(() => { - stepsValue.value = withDelay( - 500, - withTiming(step === 0 ? 0 : 1, { - duration: 300, - easing: Easing.inOut(Easing.ease), - }), - ); - }, [step, stepsValue]); - - const handleWordsFilled = useCallback( - (mnemonics: string, config: any, onEnd: () => void) => { - dispatch( - walletActions.restoreWallet({ - mnemonics, - config, - onDone: () => { - Keyboard.dismiss(); - setStep(1); - }, - onFail: () => onEnd(), - }), - ); - }, - [dispatch], - ); - - const doCreateWallet = useCallback( - (pin: string) => { - dispatch( - walletActions.createWallet({ - pin, - onDone: () => { - popToTop(); - Toast.success(); - setTimeout(() => goBack(), 20); - }, - onFail: () => {}, - }), - ); - }, - [dispatch], - ); - - const handlePinCreated = useCallback( - (pin: string) => { - Promise.all([ - LocalAuthentication.supportedAuthenticationTypesAsync(), - SecureStore.isAvailableAsync(), - ]) - .then(([types, isProtected]) => { - const biometryType = detectBiometryType(types); - if (biometryType && isProtected) { - openSetupBiometryAfterRestore(pin, biometryType); - } else { - doCreateWallet(pin); - } - }) - .catch((err) => { - console.log('ERR2', err); - debugLog('supportedAuthenticationTypesAsync', err.message); - doCreateWallet(pin); - }); - }, - [doCreateWallet], - ); - - const stepsStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateX: interpolate(stepsValue.value, [0, 1], [0, -deviceWidth]), - }, - ], - }; - }); - - return ( - <> - - - - - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/SecretWords/SecretWords.tsx b/packages/mobile/src/core/SecretWords/SecretWords.tsx index 2c12a8276..1a38dca79 100644 --- a/packages/mobile/src/core/SecretWords/SecretWords.tsx +++ b/packages/mobile/src/core/SecretWords/SecretWords.tsx @@ -4,17 +4,17 @@ import { ScrollView, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as CreateWalletStyle from '../CreateWallet/CreateWallet.style'; -import {Button, NavBar, NavBarHelper, Text} from '$uikit'; +import { Button, NavBar, NavBarHelper, Text } from '$uikit'; import { ns } from '$utils'; -import { walletSelector } from '$store/wallet'; +import { walletGeneratedVaultSelector } from '$store/wallet'; import * as S from './SecretWords.style'; import { t } from '@tonkeeper/shared/i18n'; import { openCheckSecretWords } from '$navigation'; -import {WordsItemNumberWrapper} from "./SecretWords.style"; +import { tk } from '$wallet'; +import { popToTop } from '$navigation/imperative'; export const SecretWords: FC = () => { - - const { generatedVault } = useSelector(walletSelector); + const generatedVault = useSelector(walletGeneratedVaultSelector); const { bottom: bottomInset } = useSafeAreaInsets(); const data = useMemo(() => { @@ -52,10 +52,10 @@ export const SecretWords: FC = () => { return ( - + @@ -75,12 +75,13 @@ export const SecretWords: FC = () => { - + paddingTop: 16, + }} + > diff --git a/packages/mobile/src/core/Security/Security.tsx b/packages/mobile/src/core/Security/Security.tsx index 937a0e501..a3cc137c8 100644 --- a/packages/mobile/src/core/Security/Security.tsx +++ b/packages/mobile/src/core/Security/Security.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import Animated from 'react-native-reanimated'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import Clipboard from '@react-native-community/clipboard'; @@ -7,52 +7,41 @@ import * as LocalAuthentication from 'expo-local-authentication'; import { Switch } from 'react-native'; import * as S from './Security.style'; -import {NavBar, ScrollHandler, Text} from '$uikit'; +import { NavBar, ScrollHandler, Text } from '$uikit'; import { CellSection, CellSectionItem } from '$shared/components'; -import { walletActions, walletSelector } from '$store/wallet'; -import { openChangePin, openResetPin } from '$navigation'; +import { walletActions } from '$store/wallet'; +import { openChangePin } from '$navigation'; import { detectBiometryType, ns, platform, triggerImpactLight } from '$utils'; -import { MainDB } from '$database'; import { Toast } from '$store'; -import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; import { t } from '@tonkeeper/shared/i18n'; +import { useBiometrySettings, useWallet } from '@tonkeeper/shared/hooks'; export const Security: FC = () => { const dispatch = useDispatch(); const tabBarHeight = useBottomTabBarHeight(); - const { wallet } = useSelector(walletSelector); - const [isBiometryEnabled, setBiometryEnabled] = useState(false); + const wallet = useWallet(); + + const { biometryEnabled } = useBiometrySettings(); + + const [isBiometryEnabled, setBiometryEnabled] = useState(biometryEnabled); const [biometryAvail, setBiometryAvail] = useState(-1); const isTouchId = biometryAvail !== LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION; useEffect(() => { - Promise.all([ - MainDB.isBiometryEnabled(), - LocalAuthentication.supportedAuthenticationTypesAsync(), - ]).then(([isEnabled, types]) => { - setBiometryEnabled(isEnabled); - setBiometryAvail(detectBiometryType(types) || -1); - }); + LocalAuthentication.supportedAuthenticationTypesAsync().then((types) => + setBiometryAvail(detectBiometryType(types) || -1), + ); }, []); - const handleBackupSettings = useCallback(() => { - if (!wallet) { - return openRequireWalletModal(); - } - - // TODO: wrap this into something that support UI for password decryption for EncryptedVault. - dispatch(walletActions.backupWallet()); - }, [dispatch, wallet]); - const handleCopyLockupConfig = useCallback(() => { try { - Clipboard.setString(JSON.stringify(wallet!.vault.getLockupConfig())); + Clipboard.setString(JSON.stringify(wallet.getLockupConfig())); Toast.success(t('copied')); } catch (e) { Toast.fail(e.message); } - }, [t, wallet]); + }, [wallet]); const handleBiometry = useCallback( (triggerHaptic: boolean) => () => { @@ -73,14 +62,14 @@ export const Security: FC = () => { [dispatch, isBiometryEnabled], ); + useEffect(() => { + setBiometryEnabled(biometryEnabled); + }, [biometryEnabled]); + const handleChangePasscode = useCallback(() => { openChangePin(); }, []); - const handleResetPasscode = useCallback(() => { - openResetPin(); - }, []); - function renderBiometryToggler() { if (biometryAvail === -1) { return null; @@ -96,8 +85,8 @@ export const Security: FC = () => { } > {t('security_use_biometry_switch', { - biometryType: isTouchId - ? t(`platform.${platform}.fingerprint`) + biometryType: isTouchId + ? t(`platform.${platform}.fingerprint`) : t(`platform.${platform}.face_recognition`), })} @@ -132,20 +121,9 @@ export const Security: FC = () => { {t('security_change_passcode')} - - {t('security_reset_passcode')} - - {!!wallet && ( - - {t('settings_backup_seed')} - - )} - {!!wallet && wallet.ton.isLockup() && ( + {!!wallet && wallet.isLockup && ( Copy lockup config diff --git a/packages/mobile/src/core/SecurityMigration/SecurityMigration.style.ts b/packages/mobile/src/core/SecurityMigration/SecurityMigration.style.ts deleted file mode 100644 index 0207eb279..000000000 --- a/packages/mobile/src/core/SecurityMigration/SecurityMigration.style.ts +++ /dev/null @@ -1,33 +0,0 @@ -import LottieView from 'lottie-react-native'; - -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled.SafeAreaView` - flex: 1; -`; - -export const Info = styled.View` - flex: 1; - align-items: center; - justify-content: center; - padding: ${ns(32)}px; -`; - -export const LottieIcon = styled(LottieView)` - width: ${ns(120)}px; - height: ${ns(120)}px; -`; - -export const TitleWrapper = styled.View` - margin-top: ${ns(16)}px; -`; - -export const CaptionWrapper = styled.View` - margin-top: ${ns(4)}px; -`; - -export const Footer = styled.View` - flex: 0 0 auto; - padding: ${ns(32)}px; -`; diff --git a/packages/mobile/src/core/SecurityMigration/SecurityMigration.tsx b/packages/mobile/src/core/SecurityMigration/SecurityMigration.tsx deleted file mode 100644 index e2d546c05..000000000 --- a/packages/mobile/src/core/SecurityMigration/SecurityMigration.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import LottieView from 'lottie-react-native'; -import { useDispatch } from 'react-redux'; - -import * as S from './SecurityMigration.style'; -import {Button, Text} from '$uikit'; -import { ns, platform } from '$utils'; -import { goBack } from '$navigation/imperative'; -import { walletActions } from '$store/wallet'; -import { t } from '@tonkeeper/shared/i18n'; - -export const SecurityMigration: FC = () => { - const dispatch = useDispatch(); - const iconRef = useRef(null); - - useEffect(() => { - const timer = setTimeout(() => { - iconRef.current?.play(); - }, 100); - - return () => clearTimeout(timer); - }, []); - - const handleMigrate = useCallback(() => { - dispatch(walletActions.securityMigrate()); - }, [dispatch]); - - const handleSkip = useCallback(() => { - goBack(); - }, []); - - return ( - - - - - - {t('security_migration_title')} - - - - - {t('security_migration_caption', { - faceRecognition: t(`platform.${platform}.face_recognition`) - })} - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/Send/Send.tsx b/packages/mobile/src/core/Send/Send.tsx index d6c585eb4..03b53a4de 100644 --- a/packages/mobile/src/core/Send/Send.tsx +++ b/packages/mobile/src/core/Send/Send.tsx @@ -2,12 +2,7 @@ import { useInstance } from '$hooks/useInstance'; import { useTokenPrice } from '$hooks/useTokenPrice'; import { useCurrencyToSend } from '$hooks/useCurrencyToSend'; import { StepView, StepViewItem, StepViewRef } from '$shared/components'; -import { - CryptoCurrencies, - CryptoCurrency, - Decimals, - getServerConfig, -} from '$shared/constants'; +import { CryptoCurrencies, CryptoCurrency, Decimals } from '$shared/constants'; import { walletActions } from '$store/wallet'; import { NavBar, Text } from '$uikit'; import { parseLocaleNumber } from '$utils'; @@ -46,7 +41,7 @@ import { Events } from '$store/models'; import { trackEvent } from '$utils/stats'; import { t } from '@tonkeeper/shared/i18n'; import { Address } from '@tonkeeper/core'; -import { tk } from '@tonkeeper/shared/tonkeeper'; +import { tk } from '$wallet'; import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; import { useValueRef } from '@tonkeeper/uikit'; import { RequestData } from '@tonkeeper/core/src/TronAPI/TronAPIGenerated'; @@ -56,6 +51,7 @@ import { } from '$core/ModalContainer/InsufficientFunds/InsufficientFunds'; import { getTimeSec } from '$utils/getTimeSec'; import { Toast } from '$store'; +import { config } from '$config'; const tokensWithAllowedEncryption = [TokenType.TON, TokenType.Jetton]; @@ -89,9 +85,9 @@ export const Send: FC = ({ route }) => { const accountsApi = useInstance(() => { const tonApiConfiguration = new Configuration({ - basePath: getServerConfig('tonapiV2Endpoint'), + basePath: config.get('tonapiV2Endpoint', tk.wallet.isTestnet), headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, }, }); diff --git a/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts b/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts index c18d7d3e4..994787236 100644 --- a/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts +++ b/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts @@ -4,11 +4,10 @@ import uniqBy from 'lodash/uniqBy'; import { useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { SuggestedAddress, SuggestedAddressType } from '../Send.interface'; -import { walletAddressSelector } from '$store/wallet'; -import { CryptoCurrencies } from '$shared/constants'; -import { Tonapi } from '$libs/Tonapi'; -import { ActionItem, ActionType, Address } from '@tonkeeper/core'; -import { tk } from '@tonkeeper/shared/tonkeeper'; +import { Address } from '@tonkeeper/core'; +import { tk } from '$wallet'; +import { useWallet } from '@tonkeeper/shared/hooks'; +import { ActionItem, ActionType } from '$wallet/models/ActivityModel'; export const DOMAIN_ADDRESS_NOT_FOUND = 'DOMAIN_ADDRESS_NOT_FOUND'; @@ -19,7 +18,7 @@ export const useSuggestedAddresses = () => { const dispatch = useDispatch(); const { favorites, hiddenRecentAddresses, updatedDnsAddresses } = useSelector(favoritesSelector); - const address = useSelector(walletAddressSelector); + const wallet = useWallet(); const favoriteAddresses = useMemo( (): SuggestedAddress[] => @@ -45,7 +44,7 @@ export const useSuggestedAddresses = () => { ActionType.TonTransfer, ] as const; - const walletAddress = address[CryptoCurrencies.Ton]; + const walletAddress = wallet.address.ton.raw; const addresses = ( actions.filter((action) => { if ( @@ -97,7 +96,7 @@ export const useSuggestedAddresses = () => { ); return uniqBy(addresses, (item) => item.address).slice(0, 8); - }, [address, favoriteAddresses, hiddenRecentAddresses]); + }, [favoriteAddresses, hiddenRecentAddresses, wallet]); const suggestedAddresses = useMemo( () => [...favoriteAddresses, ...recentAddresses], @@ -117,7 +116,7 @@ export const useSuggestedAddresses = () => { } for (const favorite of dnsFavorites) { - const resolved = await Tonapi.resolveDns(favorite.domain!); + const resolved = await tk.wallet.tonapi.dns.dnsResolve(favorite.domain!); const fetchedAddress = resolved?.wallet?.address; if (fetchedAddress && !Address.compare(favorite.address, fetchedAddress)) { diff --git a/packages/mobile/src/core/Send/steps/AddressStep/components/AddressInput/AddressInput.tsx b/packages/mobile/src/core/Send/steps/AddressStep/components/AddressInput/AddressInput.tsx index ecfd853c4..df6355eaf 100644 --- a/packages/mobile/src/core/Send/steps/AddressStep/components/AddressInput/AddressInput.tsx +++ b/packages/mobile/src/core/Send/steps/AddressStep/components/AddressInput/AddressInput.tsx @@ -22,13 +22,14 @@ import { TextInput } from 'react-native-gesture-handler'; import { Address } from '@tonkeeper/core'; interface Props { - wordHintsRef: RefObject; + wordHintsRef?: RefObject; shouldFocus: boolean; recipient: SendRecipient | null; dnsLoading: boolean; editable: boolean; + error?: boolean; updateRecipient: (value: string) => Promise; - onSubmit: () => void; + onSubmit?: () => void; } const AddressInputComponent: FC = (props) => { @@ -36,6 +37,7 @@ const AddressInputComponent: FC = (props) => { wordHintsRef, shouldFocus, recipient, + error, dnsLoading, editable, updateRecipient, @@ -49,7 +51,7 @@ const AddressInputComponent: FC = (props) => { const [showFailed, setShowFailed] = useState(true); - const isFailed = showFailed && !dnsLoading && value.length > 0 && !recipient; + const isFailed = error || (showFailed && !dnsLoading && value.length > 0 && !recipient); const canScanQR = value.length === 0; @@ -59,7 +61,7 @@ const AddressInputComponent: FC = (props) => { const offsetTop = S.INPUT_HEIGHT + ns(isAndroid ? 20 : 16); const offsetLeft = ns(-16); - wordHintsRef.current?.search({ + wordHintsRef?.current?.search({ input: 0, query: inputValue.current, offsetTop, @@ -88,18 +90,18 @@ const AddressInputComponent: FC = (props) => { ); const handleBlur = useCallback(() => { - wordHintsRef.current?.clear(); + wordHintsRef?.current?.clear(); }, [wordHintsRef]); const handleSubmit = useCallback(() => { - const hint = wordHintsRef.current?.getCurrentSuggests()?.[0]; + const hint = wordHintsRef?.current?.getCurrentSuggests()?.[0]; if (hint) { updateRecipient(hint); return; } - onSubmit(); + onSubmit?.(); }, [onSubmit, updateRecipient, wordHintsRef]); const contentWidth = useSharedValue(0); @@ -185,12 +187,12 @@ const AddressInputComponent: FC = (props) => { inputValue.current = nextValue; - wordHintsRef.current?.clear(); + wordHintsRef?.current?.clear(); }, [recipient, wordHintsRef]); const preparedAddress = recipient && (recipient.name || recipient.domain) - ? Address.toShort(recipient.address) + ? Address.parse(recipient.address, { bounceable: false }).toShort() : ''; const isFirstRender = useRef(true); diff --git a/packages/mobile/src/core/Send/steps/AmountStep/AmountStep.tsx b/packages/mobile/src/core/Send/steps/AmountStep/AmountStep.tsx index d285cfb76..edc2dd2b0 100644 --- a/packages/mobile/src/core/Send/steps/AmountStep/AmountStep.tsx +++ b/packages/mobile/src/core/Send/steps/AmountStep/AmountStep.tsx @@ -4,14 +4,13 @@ import React, { FC, memo, useEffect, useMemo, useRef } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as S from './AmountStep.style'; import { parseLocaleNumber } from '$utils'; -import { useSelector } from 'react-redux'; import BigNumber from 'bignumber.js'; import { AmountStepProps } from './AmountStep.interface'; -import { walletWalletSelector } from '$store/wallet'; import { AmountInput, AmountInputRef } from '$shared/components'; import { CoinDropdown } from './CoinDropdown'; import { t } from '@tonkeeper/shared/i18n'; import { Steezy, View, Text } from '@tonkeeper/uikit'; +import { useWallet } from '@tonkeeper/shared/hooks'; const AmountStepComponent: FC = (props) => { const { @@ -29,9 +28,9 @@ const AmountStepComponent: FC = (props) => { onChangeCurrency, } = props; - const wallet = useSelector(walletWalletSelector); + const wallet = useWallet(); - const isLockup = !!wallet?.ton.isLockup(); + const isLockup = !!wallet?.isLockup; const { isReadyToContinue } = useMemo(() => { const bigNum = new BigNumber(parseLocaleNumber(amount.value)); diff --git a/packages/mobile/src/core/Send/steps/AmountStep/CoinDropdown/CoinDropdown.tsx b/packages/mobile/src/core/Send/steps/AmountStep/CoinDropdown/CoinDropdown.tsx index 06350d919..36233f4c0 100644 --- a/packages/mobile/src/core/Send/steps/AmountStep/CoinDropdown/CoinDropdown.tsx +++ b/packages/mobile/src/core/Send/steps/AmountStep/CoinDropdown/CoinDropdown.tsx @@ -1,12 +1,10 @@ import { useJettonBalances } from '$hooks/useJettonBalances'; -import { CryptoCurrencies, Decimals, SecondaryCryptoCurrencies } from '$shared/constants'; +import { CryptoCurrencies, Decimals } from '$shared/constants'; import { JettonBalanceModel } from '$store/models'; -import { walletSelector } from '$store/wallet'; import { Steezy } from '$styles'; import { Highlight, Icon, PopupSelect, Spacer, Text, View } from '$uikit'; import { ns } from '$utils'; import React, { FC, memo, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { useHideableFormatter } from '$core/HideableAmount/useHideableFormatter'; import { DEFAULT_TOKEN_LOGO, JettonIcon, TonIcon } from '@tonkeeper/uikit'; import { @@ -16,6 +14,7 @@ import { } from '$core/Send/Send.interface'; import { useTonInscriptions } from '@tonkeeper/shared/query/hooks/useTonInscriptions'; import { formatter } from '@tonkeeper/shared/formatter'; +import { useBalancesState } from '@tonkeeper/shared/hooks'; type CoinItem = | { @@ -55,37 +54,20 @@ interface Props { const CoinDropdownComponent: FC = (props) => { const { currency, currencyTitle, onChangeCurrency } = props; - const { currencies, balances } = useSelector(walletSelector); + const balances = useBalancesState(); const { enabled: jettons } = useJettonBalances(false, true); const inscriptions = useTonInscriptions(); const { format } = useHideableFormatter(); const coins = useMemo((): CoinItem[] => { - const list = [ - CryptoCurrencies.Ton, - ...SecondaryCryptoCurrencies.filter((item) => { - if (item === CryptoCurrencies.Ton) { - return false; - } - - if (+balances[item] > 0) { - return true; - } - - return currencies.indexOf(item) > -1; - }), - ].map( - (item): CoinItem => ({ - tokenType: TokenType.TON, - currency: item, - balance: balances[item], - decimals: Decimals[item], - }), - ); - return [ - ...list, + { + tokenType: TokenType.TON, + currency: CryptoCurrencies.Ton, + balance: balances.ton, + decimals: Decimals[CryptoCurrencies.Ton], + }, ...jettons.map((jetton): CoinItem => { return { tokenType: TokenType.Jetton, @@ -107,7 +89,7 @@ const CoinDropdownComponent: FC = (props) => { }; }), ]; - }, [jettons, inscriptions.items, balances, currencies]); + }, [jettons, inscriptions.items, balances]); const selectedCoin = useMemo( () => coins.find((item) => item.currency === currency), diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx index d70aecb3f..a7fc583fc 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx @@ -2,7 +2,6 @@ import { useCopyText } from '$hooks/useCopyText'; import { useFiatValue } from '$hooks/useFiatValue'; import { BottomButtonWrapHelper, StepScrollView } from '$shared/components'; import { CryptoCurrencies, CryptoCurrency, Decimals } from '$shared/constants'; -import { getTokenConfig } from '$shared/dynamicConfig'; import { Highlight, Icon, Separator, Spacer, StakedTonIcon, Text } from '$uikit'; import { parseLocaleNumber } from '$utils'; import React, { FC, memo, useCallback, useEffect, useMemo } from 'react'; @@ -17,8 +16,6 @@ import { useActionFooter, } from '$core/ModalContainer/NFTOperations/NFTOperationFooter'; import { Alert } from 'react-native'; -import { walletBalancesSelector, walletWalletSelector } from '$store/wallet'; -import { useSelector } from 'react-redux'; import { SkeletonLine } from '$uikit/Skeleton/SkeletonLine'; import { t } from '@tonkeeper/shared/i18n'; import { openInactiveInfo } from '$core/ModalContainer/InfoAboutInactive/InfoAboutInactive'; @@ -26,6 +23,7 @@ import { Address } from '@tonkeeper/core'; import { useBatteryState } from '@tonkeeper/shared/query/hooks/useBatteryState'; import { BatteryState } from '@tonkeeper/shared/utils/battery'; import { TokenType } from '$core/Send/Send.interface'; +import { useBalancesState, useWallet } from '@tonkeeper/shared/hooks'; const ConfirmStepComponent: FC = (props) => { const { @@ -53,8 +51,8 @@ const ConfirmStepComponent: FC = (props) => { const copyText = useCopyText(); - const balances = useSelector(walletBalancesSelector); - const wallet = useSelector(walletWalletSelector); + const balances = useBalancesState(); + const wallet = useWallet(); const batteryState = useBatteryState(); const { Logo, liquidJettonPool } = useCurrencyToSend(currency, tokenType); @@ -103,9 +101,9 @@ const ConfirmStepComponent: FC = (props) => { if ( currency === CryptoCurrencies.Ton && wallet && - wallet.ton.isLockup() && + wallet.isLockup && !amount.all && - new BigNumber(balances[currency]).isLessThan(amountWithFee) + new BigNumber(balances.ton).isLessThan(amountWithFee) ) { await showLockupAlert(); } @@ -133,18 +131,7 @@ const ConfirmStepComponent: FC = (props) => { sendTx, ]); - const feeCurrency = useMemo(() => { - const tokenConfig = getTokenConfig(currency as CryptoCurrency); - if (currency === 'usdt') { - return 'USDT'; - } else if (tokenConfig && tokenConfig.blockchain === 'ethereum') { - return CryptoCurrencies.Eth; - } else if ([TokenType.Jetton, TokenType.Inscription].includes(tokenType)) { - return CryptoCurrencies.Ton; - } else { - return currency; - } - }, [currency, tokenType]); + const feeCurrency = CryptoCurrencies.Ton; const calculatedValue = useMemo(() => { if (amount.all && tokenType === TokenType.TON) { diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index c2189cda5..7f892ba21 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -12,10 +12,9 @@ import { Icon, PopupSelect, ScrollHandler, Spacer, Text } from '$uikit'; import { Icon as NewIcon } from '@tonkeeper/uikit'; import { useShouldShowTokensButton } from '$hooks/useShouldShowTokensButton'; import { useNavigation } from '@tonkeeper/router'; -import { fiatCurrencySelector, showV4R1Selector } from '$store/main'; -import { hasSubscriptionsSelector } from '$store/subscriptions'; import { List } from '@tonkeeper/uikit'; import { + AppStackRouteNames, MainStackRouteNames, openDeleteAccountDone, openDevMenu, @@ -24,31 +23,19 @@ import { openNotifications, openRefillBattery, openSecurity, - openSecurityMigration, openSubscriptions, } from '$navigation'; -import { - walletActions, - walletVersionSelector, - walletWalletSelector, -} from '$store/wallet'; +import { walletActions } from '$store/wallet'; import { APPLE_STORE_ID, - getServerConfig, GOOGLE_PACKAGE_NAME, LargeNavBarHeight, - SelectableVersion, - SelectableVersionsConfig, IsTablet, - SelectableVersions, } from '$shared/constants'; -import { hNs, ns, throttle, useHasDiamondsOnBalance } from '$utils'; +import { checkIsTonDiamondsNFT, hNs, ns, throttle } from '$utils'; import { LargeNavBarInteractiveDistance } from '$uikit/LargeNavBar/LargeNavBar'; import { CellSectionItem } from '$shared/components'; -import { MainDB } from '$database'; -import { useNotifications } from '$hooks/useNotifications'; import { useNotificationsBadge } from '$hooks/useNotificationsBadge'; -import { useAllAddresses } from '$hooks/useAllAddresses'; import { useFlags } from '$utils/flags'; import { SearchEngine, useBrowserStore, useNotificationsStore } from '$store'; import AnimatedLottieView from 'lottie-react-native'; @@ -56,9 +43,18 @@ import { Steezy } from '$styles'; import { t } from '@tonkeeper/shared/i18n'; import { trackEvent } from '$utils/stats'; import { openAppearance } from '$core/ModalContainer/AppearanceModal'; -import { Address } from '@tonkeeper/core'; +import { config } from '$config'; import { shouldShowNotifications } from '$store/zustand/notifications/selectors'; -import { config } from '@tonkeeper/shared/config'; +import { + useNftsState, + useWallet, + useWalletCurrency, + useWallets, +} from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; +import { mapNewNftToOldNftData } from '$utils/mapNewNftToOldNftData'; +import { WalletListItem } from '@tonkeeper/shared/components'; +import { useSubscriptions } from '@tonkeeper/shared/hooks/useSubscriptions'; export const Settings: FC = () => { const animationRef = useRef(null); @@ -75,15 +71,13 @@ export const Settings: FC = () => { const nav = useNavigation(); const tabBarHeight = useBottomTabBarHeight(); const notificationsBadge = useNotificationsBadge(); - const notifications = useNotifications(); - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const dispatch = useDispatch(); - const hasSubscriptions = useSelector(hasSubscriptionsSelector); - const wallet = useSelector(walletWalletSelector); - const version = useSelector(walletVersionSelector); - const allTonAddesses = useAllAddresses(); - const showV4R1 = useSelector(showV4R1Selector); + const hasSubscriptions = useSubscriptions( + (state) => Object.values(state.subscriptions).length > 0, + ); + const wallet = useWallet(); const shouldShowTokensButton = useShouldShowTokensButton(); const showNotifications = useNotificationsStore(shouldShowNotifications); @@ -114,7 +108,7 @@ export const Settings: FC = () => { }, []); const handleFeedback = useCallback(() => { - Linking.openURL(getServerConfig('supportLink')).catch((err) => console.log(err)); + Linking.openURL(config.get('supportLink')).catch((err) => console.log(err)); }, []); const handleLegal = useCallback(() => { @@ -122,11 +116,11 @@ export const Settings: FC = () => { }, []); const handleNews = useCallback(() => { - Linking.openURL(getServerConfig('tonkeeperNewsUrl')).catch((err) => console.log(err)); + Linking.openURL(config.get('tonkeeperNewsUrl')).catch((err) => console.log(err)); }, []); const handleSupport = useCallback(() => { - Linking.openURL(getServerConfig('directSupportUrl')).catch((err) => console.log(err)); + Linking.openURL(config.get('directSupportUrl')).catch((err) => console.log(err)); }, []); const handleResetWallet = useCallback(() => { @@ -139,14 +133,15 @@ export const Settings: FC = () => { text: t('settings_reset_alert_button'), style: 'destructive', onPress: () => { - if (showNotifications) { - notifications.unsubscribe(); - } dispatch(walletActions.cleanWallet()); }, }, ]); - }, [dispatch, t]); + }, [dispatch]); + + const handleStopWatchWallet = useCallback(() => { + dispatch(walletActions.cleanWallet()); + }, [dispatch]); const handleSubscriptions = useCallback(() => { openSubscriptions(); @@ -156,24 +151,8 @@ export const Settings: FC = () => { openNotifications(); }, []); - const versions = useMemo(() => { - return Object.keys(SelectableVersionsConfig).filter((key) => { - if (key === SelectableVersions.V4R1) { - return showV4R1; - } - return true; - }) as SelectableVersion[]; - }, [showV4R1]); - const searchEngineVariants = Object.values(SearchEngine); - const handleChangeVersion = useCallback( - (version: SelectableVersion) => { - dispatch(walletActions.switchVersion(version)); - }, - [dispatch], - ); - const handleSwitchLanguage = useCallback(() => { Alert.alert(t('language.language_alert.title'), undefined, [ { @@ -194,15 +173,13 @@ export const Settings: FC = () => { }, []); const handleSecurity = useCallback(() => { - MainDB.isNewSecurityFlow().then((isNew) => { - if (isNew) { - openSecurity(); - } else { - openSecurityMigration(); - } - }); + openSecurity(); }, []); + const handleBackupSettings = useCallback(() => { + dispatch(walletActions.backupWallet()); + }, [dispatch]); + const handleAppearance = useCallback(() => { openAppearance(); }, []); @@ -226,13 +203,17 @@ export const Settings: FC = () => { style: 'destructive', onPress: () => { trackEvent('delete_wallet'); - notifications.unsubscribe(); openDeleteAccountDone(); }, }, ]); }, []); + const handleCustomizePress = useCallback( + () => nav.navigate(AppStackRouteNames.CustomizeWallet), + [nav], + ); + const notificationIndicator = React.useMemo(() => { if (notificationsBadge.isVisible) { return ( @@ -245,11 +226,24 @@ export const Settings: FC = () => { return null; }, [notificationsBadge.isVisible]); - const hasDiamods = useHasDiamondsOnBalance(); + const accountNfts = useNftsState((s) => s.accountNfts); + + const hasDiamods = useMemo(() => { + if (!wallet || wallet.isWatchOnly) { + return false; + } + + return Object.values(accountNfts).find((nft) => + checkIsTonDiamondsNFT(mapNewNftToOldNftData(nft, wallet.address.ton.friendly)), + ); + }, [wallet, accountNfts]); + const isAppearanceVisible = React.useMemo(() => { return hasDiamods && !flags.disable_apperance; }, [hasDiamods, flags.disable_apperance]); + const wallets = useWallets(); + return ( @@ -262,8 +256,21 @@ export const Settings: FC = () => { }} scrollEventThrottle={16} > + {wallet ? ( + <> + + } + /> + + + + ) : null} - {!!wallet && ( + {!!wallet && !wallet.isWatchOnly && ( { name={'ic-key-28'} /> } - title={t('settings_security')} - onPress={handleSecurity} + title={t('settings_backup_seed')} + onPress={handleBackupSettings} /> )} {shouldShowTokensButton && ( @@ -302,6 +309,20 @@ export const Settings: FC = () => { onPress={handleSubscriptions} /> )} + {!!wallet && showNotifications && ( + } + title={ + + + {t('settings_notifications')} + + {notificationIndicator} + + } + onPress={handleNotifications} + /> + )} {isAppearanceVisible && ( { onPress={handleAppearance} /> )} + {fiatCurrency.toUpperCase()} + } + title={t('settings_primary_currency')} + onPress={() => nav.navigate('ChooseCurrency')} + /> {isBatteryVisible && ( { - {!!wallet && showNotifications && ( + {!!wallet && tk.walletForUnlock && ( } - title={ - - - {t('settings_notifications')} - - {notificationIndicator} - + value={ + } - onPress={handleNotifications} + title={t('settings_security')} + onPress={handleSecurity} /> )} - {fiatCurrency.toUpperCase()} - } - title={t('settings_primary_currency')} - onPress={() => nav.navigate('ChooseCurrency')} - /> { } title={t('language.list_item.title')} /> - {!!wallet && ( - item} - width={220} - renderItem={(version) => ( - - - {SelectableVersionsConfig[version]?.label} - - - {Address.parse( - allTonAddesses[SelectableVersionsConfig[version]?.label], - { bounceable: !flags.address_style_nobounce }, - ).toShort()} - - - )} - > - - {SelectableVersionsConfig[version]?.label} - - } - title={t('settings_wallet_version')} - /> - - )} - {wallet && flags.address_style_settings ? ( + {wallet && !wallet.isWatchOnly && flags.address_style_settings ? ( @@ -471,7 +460,7 @@ export const Settings: FC = () => { } title={t('settings_rate')} /> - {!!wallet && ( + {!!wallet && !wallet.isWatchOnly && ( { {!!wallet && ( <> - - {t('settings_reset')} - + {wallet.isWatchOnly ? ( + + {t('stop_watch')} + + ) : ( + + {t('settings_reset')} + + )} diff --git a/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts b/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts index 7cc7aae59..768549066 100644 --- a/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts +++ b/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts @@ -1,8 +1,9 @@ import { RouteProp } from '@react-navigation/native'; -import { MainStackParamList } from '$navigation/MainStack'; - -import { MainStackRouteNames } from '$navigation'; +import { + CreateWalletStackParamList, + CreateWalletStackRouteNames, +} from '$navigation/CreateWalletStack/types'; export interface SetupBiometryProps { - route: RouteProp; + route: RouteProp; } diff --git a/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx b/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx index 4519417ff..ccff0bc86 100644 --- a/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx +++ b/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx @@ -3,25 +3,16 @@ import LottieView from 'lottie-react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as LocalAuthentication from 'expo-local-authentication'; import { useDispatch } from 'react-redux'; -import { useRoute } from '@react-navigation/native'; import { SetupBiometryProps } from './SetupBiometry.interface'; import * as S from './SetupBiometry.style'; import { Button, NavBar, Text } from '$uikit'; import { ns, platform } from '$utils'; -import { - openImportSetupNotifications, - openSetupNotifications, - openSetupWalletDone, - ResetPinStackRouteNames, - SetupWalletStackRouteNames, -} from '$navigation'; +import { openSetupNotifications, openSetupWalletDone } from '$navigation'; import { walletActions } from '$store/wallet'; import { t } from '@tonkeeper/shared/i18n'; -import { getPermission } from '$utils/messaging'; -import { Toast } from '$store'; -import { goBack, popToTop } from '$navigation/imperative'; import { Steezy } from '@tonkeeper/uikit'; +import { tk } from '$wallet'; const LottieFaceId = require('$assets/lottie/faceid.json'); const LottieTouchId = require('$assets/lottie/touchid.json'); @@ -29,7 +20,6 @@ const LottieTouchId = require('$assets/lottie/touchid.json'); export const SetupBiometry: FC = ({ route }) => { const { pin, biometryType } = route.params; - const routeNode = useRoute(); const dispatch = useDispatch(); const { bottom: bottomInset } = useSafeAreaInsets(); @@ -52,22 +42,12 @@ export const SetupBiometry: FC = ({ route }) => { walletActions.createWallet({ isBiometryEnabled, pin, - onDone: async () => { - if (routeNode.name === ResetPinStackRouteNames.SetupBiometry) { - popToTop(); - Toast.success(); - setTimeout(() => goBack(), 20); + onDone: async (identifiers) => { + const isNotificationsDenied = await tk.wallet.notifications.getIsDenied(); + if (isNotificationsDenied) { + openSetupWalletDone(identifiers); } else { - const hasNotificationPermission = await getPermission(); - if (hasNotificationPermission) { - openSetupWalletDone(); - } else { - if (routeNode.name === SetupWalletStackRouteNames.SetupBiometry) { - openSetupNotifications(); - } else { - openImportSetupNotifications(); - } - } + openSetupNotifications(identifiers); } }, onFail: () => { @@ -83,13 +63,13 @@ export const SetupBiometry: FC = ({ route }) => { return isTouchId ? t(`platform.${platform}.fingerprint_genitive`) : t(`platform.${platform}.face_recognition_genitive`); - }, [t, isTouchId]); + }, [isTouchId]); const biometryName = useMemo(() => { return isTouchId ? t(`platform.${platform}.fingerprint`) : t(`platform.${platform}.face_recognition`); - }, [t, isTouchId]); + }, [isTouchId]); const handleEnable = useCallback(() => { setLoading(true); diff --git a/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx b/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx index 4a33f8fe8..afd4f279a 100644 --- a/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx +++ b/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx @@ -1,47 +1,61 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Button, Icon, Screen, Spacer, Text } from '$uikit'; import * as S from '$core/SetupNotifications/SetupNotifications.style'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useDispatch } from 'react-redux'; import { t } from '@tonkeeper/shared/i18n'; import { openSetupWalletDone } from '$navigation'; import { ns } from '$utils'; import { debugLog } from '$utils/debugLog'; -import { useNotifications } from '$hooks/useNotifications'; -import { saveDontShowReminderNotifications } from '$utils/messaging'; import { Toast } from '$store'; +import { tk } from '$wallet'; +import { RouteProp } from '@react-navigation/native'; +import { + ImportWalletStackParamList, + ImportWalletStackRouteNames, +} from '$navigation/ImportWalletStack/types'; + +interface Props { + route: RouteProp; +} + +export const SetupNotifications: React.FC = (props) => { + const { identifiers } = props.route.params; -export const SetupNotifications: React.FC = () => { const [loading, setLoading] = React.useState(false); - const notifications = useNotifications(); const safeArea = useSafeAreaInsets(); - const dispatch = useDispatch(); - React.useEffect(() => { - saveDontShowReminderNotifications(); - }, []); + const handleDone = useCallback(() => { + openSetupWalletDone(identifiers); + }, [identifiers]); const handleEnableNotifications = React.useCallback(async () => { try { setLoading(true); - await notifications.subscribe(); - openSetupWalletDone(); + + if (identifiers.length > 1) { + await tk.enableNotificationsForAll(identifiers); + } else { + await tk.wallet.notifications.subscribe(); + } + + handleDone(); } catch (err) { setLoading(false); Toast.fail(err?.massage); debugLog('[SetupNotifications]:', err); } - }, []); + }, [handleDone, identifiers]); return ( - openSetupWalletDone()} + onPress={handleDone} > {t('later')} @@ -61,10 +75,7 @@ export const SetupNotifications: React.FC = () => { - diff --git a/packages/mobile/src/core/SetupWalletDone/SetupWalletDone.style.ts b/packages/mobile/src/core/SetupWalletDone/SetupWalletDone.style.ts deleted file mode 100644 index 9be058d9b..000000000 --- a/packages/mobile/src/core/SetupWalletDone/SetupWalletDone.style.ts +++ /dev/null @@ -1,23 +0,0 @@ -import LottieView from 'lottie-react-native'; -import styled from '$styled'; -import { deviceHeight, deviceWidth, ns } from '$utils'; - -const confettiWidth = deviceHeight * 0.5625; - -export const AnimationWrap = styled.View` - position: absolute; - z-index: 2; - bottom: 0; - left: ${(deviceWidth - confettiWidth) / 2}px; - width: ${confettiWidth}px; - height: ${deviceHeight}px; -`; - -export const LottieIcon = styled(LottieView)` - width: ${ns(148 + 12)}px; - height: ${ns(148 + 12)}px; -`; - -export const TitleWrapper = styled.View` - margin-top: ${ns(0)}px; -`; diff --git a/packages/mobile/src/core/SetupWalletDone/SetupWalletDone.tsx b/packages/mobile/src/core/SetupWalletDone/SetupWalletDone.tsx deleted file mode 100644 index 44de31d78..000000000 --- a/packages/mobile/src/core/SetupWalletDone/SetupWalletDone.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { FC, useCallback, useMemo, useRef } from 'react'; -import LottieView from 'lottie-react-native'; - -import { Icon, Text } from '$uikit'; -import * as CreateWalletStyle from '$core/CreateWallet/CreateWallet.style'; -import { useTheme } from '$hooks/useTheme'; -import { - SecurityMigrationStackRouteNames, - SetupWalletStackRouteNames, -} from '$navigation'; -import { getCurrentRoute, goBack, popToTop } from '$navigation/imperative'; -import * as S from './SetupWalletDone.style'; -import { ns, triggerNotificationSuccess } from '$utils'; -import { t } from '@tonkeeper/shared/i18n'; - -export const SetupWalletDone: FC = () => { - const theme = useTheme(); - - const confettiRef = useRef(null); - const checkIconRef = useRef(null); - - useMemo(() => { - const timer = setTimeout(() => { - confettiRef.current?.play(); - checkIconRef.current?.play(); - - triggerNotificationSuccess(); - }, 400); - - return () => clearTimeout(timer); - }, []); - - const handleAnimationEnd = useCallback(() => { - const timer = setTimeout(() => { - const isCreate = - getCurrentRoute()?.name === SetupWalletStackRouteNames.SetupWalletDone || - getCurrentRoute()?.name === SecurityMigrationStackRouteNames.SetupWalletDone; - popToTop(); - if (isCreate) { - setTimeout(() => goBack(), 10); - } - }, 300); - - return () => clearTimeout(timer); - }, []); - - return ( - <> - - - - - {t('check_words_success')} - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/Staking/Staking.tsx b/packages/mobile/src/core/Staking/Staking.tsx index c993554eb..6e9430815 100644 --- a/packages/mobile/src/core/Staking/Staking.tsx +++ b/packages/mobile/src/core/Staking/Staking.tsx @@ -2,28 +2,27 @@ import { useStakingRefreshControl } from '$hooks/useStakingRefreshControl'; import { useNavigation } from '@tonkeeper/router'; import { MainStackRouteNames, openDAppBrowser } from '$navigation'; import { StakingListCell } from '$shared/components'; -import { StakingProvider, useStakingStore } from '$store'; +import { useStakingUIStore } from '$store'; import { Button, Icon, ScrollHandler, Spacer, Text } from '$uikit'; import { List } from '$uikit/List/old/List'; -import { calculatePoolBalance, getImplementationIcon, getPoolIcon } from '$utils/staking'; +import { getImplementationIcon, getPoolIcon } from '$utils/staking'; import { formatter } from '$utils/formatter'; import BigNumber from 'bignumber.js'; import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { RefreshControl } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { shallow } from 'zustand/shallow'; import * as S from './Staking.style'; -import { jettonsBalancesSelector } from '$store/jettons'; -import { useSelector } from 'react-redux'; import { logEvent } from '@amplitude/analytics-browser'; import { t } from '@tonkeeper/shared/i18n'; import { Address } from '@tonkeeper/shared/Address'; import { PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; -import { walletSelector } from '$store/wallet'; -import { CryptoCurrencies, Decimals, getServerConfig } from '$shared/constants'; +import { CryptoCurrencies, Decimals } from '$shared/constants'; import { Flash } from '@tonkeeper/uikit'; import { Ton } from '$libs/Ton'; +import { useBalancesState, useJettons, useStakingState } from '@tonkeeper/shared/hooks'; +import { StakingManager, StakingProvider } from '$wallet/managers/StakingManager'; +import { config } from '$config'; interface Props {} @@ -32,15 +31,15 @@ export const Staking: FC = () => { const { bottom: bottomInset } = useSafeAreaInsets(); - const providers = useStakingStore((s) => s.providers, shallow); - const pools = useStakingStore((s) => s.pools, shallow); - const stakingInfo = useStakingStore((s) => s.stakingInfo, shallow); - const highestApyPool = useStakingStore((s) => s.highestApyPool, shallow); - const flashShownCount = useStakingStore((s) => s.stakingFlashShownCount); + const providers = useStakingState((s) => s.providers); + const pools = useStakingState((s) => s.pools); + const stakingInfo = useStakingState((s) => s.stakingInfo); + const highestApyPool = useStakingState((s) => s.highestApyPool); - const jettonBalances = useSelector(jettonsBalancesSelector); - const { balances } = useSelector(walletSelector); - const tonBalance = balances[CryptoCurrencies.Ton]; + const flashShownCount = useStakingUIStore((s) => s.stakingFlashShownCount); + + const { jettonBalances } = useJettons(); + const tonBalance = useBalancesState((s) => s.ton); const poolsList = useMemo(() => { return pools.map((pool) => { @@ -50,7 +49,7 @@ export const Staking: FC = () => { const balance = stakingJetton ? new BigNumber(stakingJetton.balance) - : calculatePoolBalance(pool, stakingInfo); + : StakingManager.calculatePoolBalance(pool, stakingInfo); const pendingWithdrawal = stakingInfo[pool.address]?.pending_withdraw; @@ -139,7 +138,7 @@ export const Staking: FC = () => { ); const handleLearnMorePress = useCallback(() => { - openDAppBrowser(getServerConfig('stakingInfoUrl')); + openDAppBrowser(config.get('stakingInfoUrl')); }, []); const otherPoolsEstimation = useMemo(() => { @@ -198,7 +197,7 @@ export const Staking: FC = () => { useEffect(() => { const timerId = setTimeout( - () => useStakingStore.getState().actions.increaseStakingFlashShownCount(), + () => useStakingUIStore.getState().actions.increaseStakingFlashShownCount(), 1000, ); diff --git a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx index f56cfac60..e2717c095 100644 --- a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx +++ b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx @@ -2,8 +2,10 @@ import { usePoolInfo } from '$hooks/usePoolInfo'; import { useStakingRefreshControl } from '$hooks/useStakingRefreshControl'; import { MainStackRouteNames, openDAppBrowser, openJetton } from '$navigation'; import { MainStackParamList } from '$navigation/MainStack'; -import { getServerConfig, KNOWN_STAKING_IMPLEMENTATIONS } from '$shared/constants'; -import { getStakingPoolByAddress, getStakingProviderById, useStakingStore } from '$store'; +import { + getStakingPoolByAddress, + getStakingProviderById, +} from '@tonkeeper/shared/utils/staking'; import { Button, Highlight, @@ -19,14 +21,11 @@ import { RouteProp } from '@react-navigation/native'; import React, { FC, useCallback, useMemo } from 'react'; import { RefreshControl } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; -import { shallow } from 'zustand/shallow'; import * as S from './StakingPoolDetails.style'; import { HideableAmount } from '$core/HideableAmount/HideableAmount'; import { t } from '@tonkeeper/shared/i18n'; import { useFlag } from '$utils/flags'; import { formatter } from '@tonkeeper/shared/formatter'; -import { fiatCurrencySelector } from '$store/main'; -import { useSelector } from 'react-redux'; import { IStakingLink, StakingLinkType } from './types'; import { Icon, List, Steezy } from '@tonkeeper/uikit'; import { getLinkIcon, getLinkTitle, getSocialLinkType } from './utils'; @@ -35,6 +34,10 @@ import { Linking } from 'react-native'; import { PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; import BigNumber from 'bignumber.js'; import { ListItemRate } from '../../tabs/Wallet/components/ListItemRate'; +import { useStakingState, useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; +import { StakingManager } from '$wallet/managers/StakingManager'; +import { config } from '$config'; +import { tk } from '$wallet'; interface Props { route: RouteProp; @@ -47,15 +50,21 @@ export const StakingPoolDetails: FC = (props) => { }, } = props; - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const tonstakersBeta = useFlag('tonstakers_beta'); - const pool = useStakingStore((s) => getStakingPoolByAddress(s, poolAddress), shallow); - const poolStakingInfo = useStakingStore((s) => s.stakingInfo[pool.address], shallow); - const provider = useStakingStore( + const pool = useStakingState( + (s) => getStakingPoolByAddress(s, poolAddress), + [poolAddress], + ); + const poolStakingInfo = useStakingState( + (s) => s.stakingInfo[pool.address], + [pool.address], + ); + const provider = useStakingState( (s) => getStakingProviderById(s, pool.implementation), - shallow, + [pool.implementation], ); const refreshControl = useStakingRefreshControl(); @@ -108,7 +117,7 @@ export const StakingPoolDetails: FC = (props) => { } list.push({ - url: getServerConfig('accountExplorer').replace('%s', pool.address), + url: config.get('accountExplorer', tk.wallet.isTestnet).replace('%s', pool.address), type: StakingLinkType.Explorer, icon: getLinkIcon(StakingLinkType.Explorer), }); @@ -139,7 +148,9 @@ export const StakingPoolDetails: FC = (props) => { openJetton(stakingJetton.jettonAddress); }, [stakingJetton]); - const isImplemeted = KNOWN_STAKING_IMPLEMENTATIONS.includes(pool.implementation); + const isImplemeted = StakingManager.KNOWN_STAKING_IMPLEMENTATIONS.includes( + pool.implementation, + ); const isLiquidTF = pool.implementation === PoolImplementationType.LiquidTF; @@ -206,6 +217,9 @@ export const StakingPoolDetails: FC = (props) => { [fiatCurrency, handlePressJetton, pool.name, stakingJetton, stakingJettonMetadata], ); + const wallet = useWallet(); + const isWatchOnly = wallet && wallet.isWatchOnly; + return ( = (props) => { - - - - - - + {!isWatchOnly ? ( + <> + + + + + + + + ) : null} {hasPendingDeposit ? ( <> diff --git a/packages/mobile/src/core/StakingPools/StakingPools.tsx b/packages/mobile/src/core/StakingPools/StakingPools.tsx index 19a9363e3..53792578b 100644 --- a/packages/mobile/src/core/StakingPools/StakingPools.tsx +++ b/packages/mobile/src/core/StakingPools/StakingPools.tsx @@ -7,24 +7,22 @@ import { StakingListCell } from '$shared/components'; import { getStakingPoolsByProvider, getStakingProviderById, - useStakingStore, -} from '$store'; +} from '@tonkeeper/shared/utils/staking'; import { ScrollHandler } from '$uikit'; import { List } from '$uikit/List/old/List'; -import { calculatePoolBalance, getPoolIcon } from '$utils/staking'; +import { getPoolIcon } from '$utils/staking'; import { RouteProp } from '@react-navigation/native'; import BigNumber from 'bignumber.js'; import React, { FC, useCallback, useMemo } from 'react'; import { RefreshControl } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { shallow } from 'zustand/shallow'; import * as S from './StakingPools.style'; import { logEvent } from '@amplitude/analytics-browser'; -import { useSelector } from 'react-redux'; -import { jettonsBalancesSelector } from '$store/jettons'; import { t } from '@tonkeeper/shared/i18n'; import { Address } from '@tonkeeper/shared/Address'; +import { useJettons, useStakingState } from '@tonkeeper/shared/hooks'; +import { StakingManager } from '$wallet/managers/StakingManager'; interface Props { route: RouteProp; @@ -37,11 +35,17 @@ export const StakingPools: FC = (props) => { }, } = props; - const provider = useStakingStore((s) => getStakingProviderById(s, providerId), shallow); - const pools = useStakingStore((s) => getStakingPoolsByProvider(s, providerId), shallow); - const stakingInfo = useStakingStore((s) => s.stakingInfo, shallow); + const provider = useStakingState( + (s) => getStakingProviderById(s, providerId), + [providerId], + ); + const pools = useStakingState( + (s) => getStakingPoolsByProvider(s, providerId), + [providerId], + ); + const stakingInfo = useStakingState((s) => s.stakingInfo); - const jettonBalances = useSelector(jettonsBalancesSelector); + const { jettonBalances } = useJettons(); const nav = useNavigation(); const { bottom: bottomInset } = useSafeAreaInsets(); @@ -56,7 +60,7 @@ export const StakingPools: FC = (props) => { const balance = stakingJetton ? new BigNumber(stakingJetton.balance) - : calculatePoolBalance(pool, stakingInfo); + : StakingManager.calculatePoolBalance(pool, stakingInfo); const pendingWithdrawal = stakingInfo[pool.address]?.pending_withdraw; diff --git a/packages/mobile/src/core/StakingSend/StakingSend.tsx b/packages/mobile/src/core/StakingSend/StakingSend.tsx index 639ab9ce2..44cbebd45 100644 --- a/packages/mobile/src/core/StakingSend/StakingSend.tsx +++ b/packages/mobile/src/core/StakingSend/StakingSend.tsx @@ -4,14 +4,14 @@ import { SendAmount, TokenType } from '$core/Send/Send.interface'; import { useFiatValue } from '$hooks/useFiatValue'; import { useInstance } from '$hooks/useInstance'; import { usePoolInfo } from '$hooks/usePoolInfo'; -import { useWallet } from '$hooks/useWallet'; import { Ton } from '$libs/Ton'; import { AppStackRouteNames } from '$navigation'; import { AppStackParamList } from '$navigation/AppStack'; import { StepView, StepViewItem, StepViewRef } from '$shared/components'; import { CryptoCurrencies, Decimals } from '$shared/constants'; -import { getStakingPoolByAddress, Toast, useStakingStore } from '$store'; -import { walletSelector } from '$store/wallet'; +import { Toast } from '$store'; +import { getStakingPoolByAddress } from '@tonkeeper/shared/utils/staking'; +import { walletWalletSelector } from '$store/wallet'; import { NavBar } from '$uikit'; import { calculateMessageTransferAmount, delay, parseLocaleNumber } from '$utils'; import { getTimeSec } from '$utils/getTimeSec'; @@ -21,7 +21,6 @@ import BigNumber from 'bignumber.js'; import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDerivedValue, useSharedValue } from 'react-native-reanimated'; import { useSelector } from 'react-redux'; -import { shallow } from 'zustand/shallow'; import { AmountStep, ConfirmStep } from './steps'; import { StakingSendSteps, StakingTransactionType } from './types'; import { getStakeSignRawMessage, getWithdrawalAlertFee, getWithdrawalFee } from './utils'; @@ -35,8 +34,9 @@ import { CanceledActionError } from '$core/Send/steps/ConfirmStep/ActionErrors'; import { t } from '@tonkeeper/shared/i18n'; import { MessageConsequences, PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; import { useCurrencyToSend } from '$hooks/useCurrencyToSend'; -import { tonapi } from '@tonkeeper/shared/tonkeeper'; import { SignRawMessage } from '$core/ModalContainer/NFTOperations/TXRequest.types'; +import { useStakingState, useWallet } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; interface Props { route: RouteProp; @@ -68,8 +68,14 @@ export const StakingSend: FC = (props) => { const unlockVault = useUnlockVault(); - const pool = useStakingStore((s) => getStakingPoolByAddress(s, poolAddress), shallow); - const poolStakingInfo = useStakingStore((s) => s.stakingInfo[pool.address], shallow); + const pool = useStakingState( + (s) => getStakingPoolByAddress(s, poolAddress), + [poolAddress], + ); + const poolStakingInfo = useStakingState( + (s) => s.stakingInfo[pool.address], + [pool.address], + ); const poolInfo = usePoolInfo(pool, poolStakingInfo); @@ -111,10 +117,10 @@ export const StakingSend: FC = (props) => { () => stepsScrollTop.value[currentStep.id] || 0, ); - const { address } = useSelector(walletSelector); - const walletAddress = address?.ton || ''; const wallet = useWallet(); - const operations = useInstance(() => new NFTOperations(wallet)); + const walletAddress = wallet.address.ton.raw; + const walletLegacy = useSelector(walletWalletSelector)!; + const operations = useInstance(() => new NFTOperations(walletLegacy)); const [amount, setAmount] = useState({ value: isWithdrawalConfrim @@ -216,7 +222,7 @@ export const StakingSend: FC = (props) => { actionRef.current = action; const boc = await action.getBoc(); - const response = await tonapi.wallet.emulateMessageToWallet({ boc }); + const response = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); setConsequences(response); diff --git a/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx b/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx index bc0f94613..5e99aaa0d 100644 --- a/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx +++ b/packages/mobile/src/core/StakingSend/steps/AmountStep/AmountStep.tsx @@ -5,10 +5,8 @@ import { Button, Spacer, Text } from '$uikit'; import React, { FC, memo, useEffect, useMemo, useRef } from 'react'; import * as S from './AmountStep.style'; import { parseLocaleNumber } from '$utils'; -import { useSelector } from 'react-redux'; import BigNumber from 'bignumber.js'; import { TextInput } from 'react-native-gesture-handler'; -import { walletWalletSelector } from '$store/wallet'; import { AmountInput, BottomButtonWrap, @@ -26,6 +24,7 @@ import { Ton } from '$libs/Ton'; import { stakingFormatter } from '$utils/formatter'; import { t } from '@tonkeeper/shared/i18n'; import { PoolInfo, PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; +import { useWallet } from '@tonkeeper/shared/hooks'; interface Props extends StepComponentProps { pool: PoolInfo; @@ -88,9 +87,9 @@ const AmountStepComponent: FC = (props) => { ? stakingBalance : walletBalance; - const wallet = useSelector(walletWalletSelector); + const wallet = useWallet(); - const isLockup = !!wallet?.ton.isLockup(); + const isLockup = wallet.isLockup; const { isReadyToContinue } = useMemo(() => { const bigNum = new BigNumber(parseLocaleNumber(amount.value)); diff --git a/packages/mobile/src/core/Subscriptions/Subscriptions.tsx b/packages/mobile/src/core/Subscriptions/Subscriptions.tsx index 46fab42b7..9d088c0a4 100644 --- a/packages/mobile/src/core/Subscriptions/Subscriptions.tsx +++ b/packages/mobile/src/core/Subscriptions/Subscriptions.tsx @@ -1,22 +1,18 @@ import React, { FC, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -const TonWeb = require('tonweb'); import { getUnixTime } from 'date-fns'; import * as S from './Subscriptions.style'; -import {Icon, NavBar, RoundedSectionList, ScrollHandler, Text} from '$uikit'; -import { subscriptionsSelector } from '$store/subscriptions'; +import { Icon, RoundedSectionList, ScrollHandler, Text } from '$uikit'; import { format, ns } from '$utils'; import { SubscriptionModel } from '$store/models'; -import { useTheme } from '$hooks/useTheme'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import {Ton} from "$libs/Ton"; +import { Ton } from '$libs/Ton'; import { t } from '@tonkeeper/shared/i18n'; import { openSubscription } from '$core/ModalContainer/CreateSubscription/CreateSubscription'; +import { useSubscriptions } from '@tonkeeper/shared/hooks/useSubscriptions'; export const Subscriptions: FC = () => { - const theme = useTheme(); - const { subscriptionsInfo } = useSelector(subscriptionsSelector); + const subscriptionsInfo = useSubscriptions((state) => state.subscriptions); const { bottom: bottomInset } = useSafeAreaInsets(); const sections = useMemo(() => { @@ -90,10 +86,7 @@ export const Subscriptions: FC = () => { - + ); }} diff --git a/packages/mobile/src/core/Swap/Swap.tsx b/packages/mobile/src/core/Swap/Swap.tsx index c2c4de137..94212c8e6 100644 --- a/packages/mobile/src/core/Swap/Swap.tsx +++ b/packages/mobile/src/core/Swap/Swap.tsx @@ -2,16 +2,16 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { StonfiInjectedObject } from './types'; import { openSignRawModal } from '$core/ModalContainer/NFTOperations/Modals/SignRawModal'; import { getTimeSec } from '$utils/getTimeSec'; -import { useSelector } from 'react-redux'; -import { walletAddressSelector } from '$store/wallet'; import { useNavigation } from '@tonkeeper/router'; import * as S from './Swap.style'; import { Icon } from '$uikit'; -import { getServerConfig } from '$shared/constants'; import { getDomainFromURL } from '$utils'; import { logEvent } from '@amplitude/analytics-browser'; import { checkIsTimeSynced } from '$navigation/hooks/useDeeplinkingResolvers'; import { useWebViewBridge } from '$hooks/jsBridge'; +import { useWallet } from '@tonkeeper/shared/hooks'; +import { config } from '$config'; +import { tk } from '$wallet'; interface Props { jettonAddress?: string; @@ -22,7 +22,7 @@ interface Props { export const Swap: FC = (props) => { const { jettonAddress } = props; - const baseUrl = getServerConfig('stonfiUrl'); + const baseUrl = config.get('stonfiUrl', tk.wallet.isTestnet); const url = useMemo(() => { const ft = props.ft ?? jettonAddress ?? 'TON'; @@ -37,13 +37,15 @@ export const Swap: FC = (props) => { return path; }, [baseUrl, jettonAddress, props.ft, props.tt]); - const address = useSelector(walletAddressSelector); + const wallet = useWallet(); + + const address = wallet?.address.ton.friendly; const nav = useNavigation(); const bridgeObject = useMemo( (): StonfiInjectedObject => ({ - address: address.ton, + address, close: () => nav.goBack(), sendTransaction: (request) => new Promise((resolve, reject) => { @@ -76,7 +78,7 @@ export const Swap: FC = (props) => { ); }), }), - [address.ton, nav], + [address, nav], ); const [ref, injectedJavaScriptBeforeContentLoaded, onMessage] = diff --git a/packages/mobile/src/core/TonConnect/TonConnectModal.tsx b/packages/mobile/src/core/TonConnect/TonConnectModal.tsx index b0b59f8af..95cc83106 100644 --- a/packages/mobile/src/core/TonConnect/TonConnectModal.tsx +++ b/packages/mobile/src/core/TonConnect/TonConnectModal.tsx @@ -2,11 +2,8 @@ import React, { useCallback } from 'react'; import axios from 'axios'; import queryString from 'query-string'; import TonWeb from 'tonweb'; -import { useSelector } from 'react-redux'; import { Linking, StyleSheet } from 'react-native'; import { useTheme } from '$hooks/useTheme'; -import { getServerConfig, SelectableVersionsConfig } from '$shared/constants'; -import { walletSelector } from '$store/wallet'; import { Button, Icon, List, Loader, Spacer, Text, TransitionOpacity } from '$uikit'; import { delay, @@ -32,13 +29,14 @@ import { store, Toast, useNotificationsStore } from '$store'; import { push } from '$navigation/imperative'; import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; import { SheetActions, useNavigation } from '@tonkeeper/router'; -import { mainSelector } from '$store/main'; import { createTonProofForTonkeeper } from '$utils/proof'; import { WalletApi, Configuration } from '@tonkeeper/core/src/legacy'; import * as SecureStore from 'expo-secure-store'; import { Address } from '@tonkeeper/core'; import { shouldShowNotifications } from '$store/zustand/notifications/selectors'; import { replaceString } from '@tonkeeper/shared/utils/replaceString'; +import { tk } from '$wallet'; +import { config } from '$config'; export const TonConnectModal = (props: TonConnectModalProps) => { const animation = useTonConnectAnimation(); @@ -48,8 +46,6 @@ export const TonConnectModal = (props: TonConnectModalProps) => { const showNotifications = useNotificationsStore(shouldShowNotifications); const [withNotifications, setWithNotifications] = React.useState(showNotifications); - const { version } = useSelector(walletSelector); - const { isTestnet } = useSelector(mainSelector); const maskedAddress = Address.toShort(animation.address); const handleSwitchNotifications = useCallback(() => { @@ -57,7 +53,7 @@ export const TonConnectModal = (props: TonConnectModalProps) => { setWithNotifications((prev) => !prev); }, []); - const closeModal = () => nav.goBack(); + const closeModal = useCallback(() => nav.goBack(), [nav]); const isTonapi = props.protocolVersion === 1 ? props?.hostname === 'tonapi.io' : false; @@ -110,7 +106,7 @@ export const TonConnectModal = (props: TonConnectModalProps) => { const vault = await unlockVault(); - const address = await vault.getTonAddress(isTestnet); + const address = await vault.getTonAddress(tk.wallet.isTestnet); const privateKey = await vault.getTonPrivateKey(); const walletSeed = TonWeb.utils.bytesToBase64(privateKey); @@ -179,9 +175,9 @@ export const TonConnectModal = (props: TonConnectModalProps) => { ); const walletApi = new WalletApi( new Configuration({ - basePath: getServerConfig('tonapiV2Endpoint'), + basePath: config.get('tonapiV2Endpoint', tk.wallet.isTestnet), headers: { - Authorization: `Bearer ${getServerConfig('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, }, }), ); @@ -221,15 +217,7 @@ export const TonConnectModal = (props: TonConnectModalProps) => { debugLog('[TonLogin]:', error); Toast.fail(message); } - }, [ - animation, - closeModal, - isTestnet, - props, - sendToCallbackUrl, - unlockVault, - withNotifications, - ]); + }, [animation, closeModal, props, sendToCallbackUrl, unlockVault, withNotifications]); const handleBackToService = React.useCallback(async () => { if (props.protocolVersion !== 1) { @@ -335,9 +323,7 @@ export const TonConnectModal = (props: TonConnectModalProps) => { {maskedAddress}{' '} - {SelectableVersionsConfig[version] - ? SelectableVersionsConfig[version].label - : null} + {tk.wallet.config.version} {isTonConnectV2 && showNotifications ? ( diff --git a/packages/mobile/src/core/TonConnect/useTonConnectAnimation.ts b/packages/mobile/src/core/TonConnect/useTonConnectAnimation.ts index 711fcb356..29854266d 100644 --- a/packages/mobile/src/core/TonConnect/useTonConnectAnimation.ts +++ b/packages/mobile/src/core/TonConnect/useTonConnectAnimation.ts @@ -2,11 +2,7 @@ import React from 'react'; import { delay } from '$utils'; import { useTickerAnimation } from './useTickerAnimation'; import { ADDRESS_CELL_WIDTH } from './TonConnect.style'; -import { useSelector } from 'react-redux'; -import { walletSelector } from '$store/wallet'; -import { CryptoCurrencies } from '$shared/constants'; -import { useFlags } from '$utils/flags'; -import { Address } from '@tonkeeper/core'; +import { tk } from '$wallet'; export enum States { INITIAL, @@ -19,15 +15,10 @@ export const ADDRESS_REPEAT_COUNT = 4; export const ADDRESS_TEXT_WIDTH = 800; export const useTonConnectAnimation = () => { - const flags = useFlags(['address_style_nobounce']); - const [state, setState] = React.useState(States.INITIAL); const ticker = useTickerAnimation(); - const { address: addresses } = useSelector(walletSelector); - const address = Address.parse(addresses[CryptoCurrencies.Ton], { - bounceable: !flags.address_style_nobounce, - }).toFriendly(); + const address = tk.wallet.address.ton.friendly; React.useEffect(() => { ticker.start({ diff --git a/packages/mobile/src/core/Wallet/ToncoinScreen.tsx b/packages/mobile/src/core/Wallet/ToncoinScreen.tsx index 39617715e..e1a4bec58 100644 --- a/packages/mobile/src/core/Wallet/ToncoinScreen.tsx +++ b/packages/mobile/src/core/Wallet/ToncoinScreen.tsx @@ -1,19 +1,14 @@ import React, { memo, useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import * as S from './Wallet.style'; import { useWalletInfo } from '$hooks/useWalletInfo'; import { Button, PopupMenu, PopupMenuItem } from '$uikit'; import { MainStackRouteNames, openDAppBrowser, openSend } from '$navigation'; import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; -import { walletActions, walletWalletSelector } from '$store/wallet'; +import { walletActions } from '$store/wallet'; import { Linking, View } from 'react-native'; import { delay, ns } from '$utils'; -import { - CryptoCurrencies, - CryptoCurrency, - Decimals, - getServerConfig, -} from '$shared/constants'; +import { CryptoCurrencies, CryptoCurrency, Decimals } from '$shared/constants'; import { i18n, t } from '@tonkeeper/shared/i18n'; import { useNavigation } from '@tonkeeper/router'; import { Chart } from '$shared/components/Chart/new/Chart'; @@ -25,11 +20,11 @@ import { Icon, Screen, TonIcon, IconButton, IconButtonList } from '@tonkeeper/ui import { ActivityList } from '@tonkeeper/shared/components'; import { useTonActivityList } from '@tonkeeper/shared/query/hooks/useTonActivityList'; -import { useWallet } from '../../tabs/useWallet'; import _ from 'lodash'; import { navigate } from '$navigation/imperative'; -import { fiatCurrencySelector } from '$store/main'; -import { FiatCurrencies } from '@tonkeeper/core'; +import { WalletCurrency } from '@tonkeeper/core'; +import { useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; +import { config } from '$config'; export const ToncoinScreen = memo(() => { const activityList = useTonActivityList(); @@ -37,11 +32,13 @@ export const ToncoinScreen = memo(() => { const handleOpenExplorer = useCallback(async () => { openDAppBrowser( - wallet.address.ton.raw - ? getServerConfig('accountExplorer').replace('%s', wallet.address.ton.raw) - : getServerConfig('explorerUrl'), + wallet + ? config + .get('accountExplorer', wallet.isTestnet) + .replace('%s', wallet.address.ton.raw) + : config.get('explorerUrl'), ); - }, [wallet.address.ton.raw]); + }, [wallet]); // Temp hack for slow navigation const [render, setRender] = useState(false); @@ -92,22 +89,20 @@ export const ToncoinScreen = memo(() => { }); const HeaderList = memo(() => { - const walletAddr = useWallet(); + const wallet = useWallet(); const flags = useFlags(['disable_swap']); - const fiatCurrency = useSelector(fiatCurrencySelector); - const shouldShowChart = fiatCurrency !== FiatCurrencies.Ton; - - const wallet = useSelector(walletWalletSelector); + const fiatCurrency = useWalletCurrency(); + const shouldShowChart = fiatCurrency !== WalletCurrency.TON; const dispatch = useDispatch(); const [lockupDeploy, setLockupDeploy] = useState('loading'); const nav = useNavigation(); useEffect(() => { - if (wallet && wallet.ton.isLockup()) { - wallet.ton - .getWalletInfo(walletAddr.address.ton.friendly) - .then((info: any) => { + if (wallet && wallet.isLockup) { + wallet + .getWalletInfo() + .then((info) => { setLockupDeploy( ['empty', 'uninit', 'nonexist'].includes(info.status) ? 'deploy' : 'deployed', ); @@ -135,7 +130,7 @@ const HeaderList = memo(() => { } }, []); - const { amount, tokenPrice } = useWalletInfo(CryptoCurrencies.Ton); + const { amount, tokenPrice } = useWalletInfo(); const handleReceive = useCallback(() => { if (!wallet) { @@ -143,7 +138,7 @@ const HeaderList = memo(() => { } nav.go('ReceiveModal'); - }, [wallet]); + }, [nav, wallet]); const handleSend = useCallback(() => { if (!wallet) { @@ -177,6 +172,8 @@ const HeaderList = memo(() => { ); }, [dispatch]); + const isWatchOnly = wallet && wallet.isWatchOnly; + return ( <> @@ -202,22 +199,26 @@ const HeaderList = memo(() => { - + {!isWatchOnly ? ( + + ) : null} - - {!flags.disable_swap && ( + {!isWatchOnly ? ( + + ) : null} + {!flags.disable_swap && !isWatchOnly && ( { )} - {wallet && wallet.ton.isLockup() && ( + {wallet && wallet.isLockup && ( + } + /> + + + {t('access_confirmation_title')} + + + + + + + + ); +}; diff --git a/packages/mobile/src/screens/MigrationPasscode/index.ts b/packages/mobile/src/screens/MigrationPasscode/index.ts new file mode 100644 index 000000000..af82262a4 --- /dev/null +++ b/packages/mobile/src/screens/MigrationPasscode/index.ts @@ -0,0 +1 @@ +export * from './MigrationPasscode'; diff --git a/packages/mobile/src/screens/StartScreen/StartScreen.tsx b/packages/mobile/src/screens/StartScreen/StartScreen.tsx new file mode 100644 index 000000000..d778594b4 --- /dev/null +++ b/packages/mobile/src/screens/StartScreen/StartScreen.tsx @@ -0,0 +1,312 @@ +import { Screen, View, Steezy, Text, Spacer, Button, Icon } from '@tonkeeper/uikit'; +import Svg, { Path, Defs, LinearGradient, Stop, G, ClipPath } from 'react-native-svg'; +import { useLogoAnimation } from './animations/useLogoAnimation'; +import { useWindowDimensions } from 'react-native'; +import Animated from 'react-native-reanimated'; +import { memo, useCallback, useEffect } from 'react'; +import { t } from '@tonkeeper/shared/i18n'; +import { MainStackRouteNames } from '$navigation'; +import { useNavigation } from '@tonkeeper/router'; +import { useDispatch } from 'react-redux'; +import { walletActions } from '$store/wallet'; + +export const StartScreen = memo(() => { + const { logoPosStyle, shapesOpacityStyle } = useLogoAnimation(); + const dimensions = useWindowDimensions(); + + const nav = useNavigation(); + const dispatch = useDispatch(); + + const origShapesWidth = 560; + const origShapesHeight = 494; + const origShapesScreenHeight = 844; + const ratioHeight = dimensions.height / origShapesScreenHeight; + const logoShapesPosX = origShapesWidth / 2 - dimensions.width / 2; + const logoShapesPosY = origShapesHeight / 2 - (origShapesHeight * ratioHeight) / 2; + + const handleCreatePress = useCallback(() => { + dispatch(walletActions.clearGeneratedVault()); + nav.navigate(MainStackRouteNames.CreateWalletStack); + }, [dispatch, nav]); + + return ( + + + + + + {/* */} + + + + + + + + + Tonkeeper + + + + {t('start_screen.caption')} + + + + + + - - )} ); }); - -const styles = Steezy.create({ - tonkensEdit: { - justifyContent: 'center', - alignItems: 'center', - marginBottom: 16, - }, -}); diff --git a/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx b/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx index b25a63d9f..b89bfd93e 100644 --- a/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx +++ b/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { Screen, @@ -11,29 +11,27 @@ import { } from '@tonkeeper/uikit'; import { Steezy } from '$styles'; import { RefreshControl } from 'react-native'; -import { useDispatch, useSelector } from 'react-redux'; import { openJetton, openTonInscription } from '$navigation'; -import { walletActions } from '$store/wallet'; import { Rate } from '../hooks/useBalance'; import { ListItemRate } from './ListItemRate'; import { TonIcon, TonIconProps } from '@tonkeeper/uikit'; import { CryptoCurrencies, LockupNames } from '$shared/constants'; import { NFTsList } from './NFTsList'; -import { TokenPrice, useTokenPrice } from '$hooks/useTokenPrice'; +import { TokenPrice } from '$hooks/useTokenPrice'; import { useTheme } from '$hooks/useTheme'; import { ListSeparator } from '$uikit/List/ListSeparator'; import { StakingWidget } from './StakingWidget'; import { HideableAmount } from '$core/HideableAmount/HideableAmount'; import { openWallet } from '$core/Wallet/ToncoinScreen'; import { TronBalance } from '@tonkeeper/core/src/TronAPI/TronAPIGenerated'; -import { fiatCurrencySelector } from '$store/main'; -import { FiatCurrencies } from '@tonkeeper/core'; +import { WalletCurrency } from '@tonkeeper/core'; import { useTonInscriptions } from '@tonkeeper/shared/query/hooks/useTonInscriptions'; import { formatter } from '@tonkeeper/shared/formatter'; import { Text } from '@tonkeeper/uikit'; import { JettonVerification } from '$store/models'; import { ListItemProps } from '$uikit/List/ListItem'; -import { config } from '@tonkeeper/shared/config'; +import { config } from '$config'; +import { useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; enum ContentType { Token, @@ -184,26 +182,15 @@ export const WalletContentList = memo( isRefreshing, isFocused, ListHeaderComponent, - tronBalances, }) => { const theme = useTheme(); - const dispatch = useDispatch(); - const fiatCurrency = useSelector(fiatCurrencySelector); - const shouldShowTonDiff = fiatCurrency !== FiatCurrencies.Ton; + const fiatCurrency = useWalletCurrency(); + const shouldShowTonDiff = fiatCurrency !== WalletCurrency.TON; const inscriptions = useTonInscriptions(); - const handleMigrate = useCallback( - (fromVersion: string) => () => { - dispatch( - walletActions.openMigration({ - isTransfer: true, - fromVersion, - }), - ); - }, - [dispatch], - ); + const wallet = useWallet(); + const isWatchOnly = wallet && wallet.isWatchOnly; const data = useMemo(() => { const content: Content[] = []; @@ -222,25 +209,9 @@ export const WalletContentList = memo( price: tonPrice.formatted.fiat ?? '-', trend: tonPrice.fiatDiff.trend, }, + isLast: isWatchOnly, }); - content.push( - ...balance.oldVersions.map((item) => ({ - type: ContentType.Token, - key: 'old_' + item.version, - onPress: handleMigrate(item.version), - title: t('wallet.old_wallet_title'), - tonIcon: { transparent: true }, - value: item.amount.formatted, - subvalue: item.amount.fiat, - rate: { - percent: shouldShowTonDiff ? tonPrice.fiatDiff.percent : '', - price: tonPrice.formatted.fiat ?? '-', - trend: tonPrice.fiatDiff.trend, - }, - })), - ); - if (balance.lockup.length > 0) { content.push( ...balance.lockup.map((item) => ({ @@ -287,10 +258,12 @@ export const WalletContentList = memo( // ); // } - content.push({ - key: 'staking', - type: ContentType.Staking, - }); + if (!isWatchOnly) { + content.push({ + key: 'staking', + type: ContentType.Staking, + }); + } content.push({ key: 'spacer_staking', @@ -374,7 +347,15 @@ export const WalletContentList = memo( }); return content; - }, [balance, handleMigrate, nfts, tokens.list, tonPrice, tronBalances]); + }, [ + balance, + shouldShowTonDiff, + tonPrice, + isWatchOnly, + tokens.list, + inscriptions.items, + nfts, + ]); const ListComponent = nfts ? Screen.FlashList : PagerView.FlatList; diff --git a/packages/mobile/src/tabs/Wallet/components/WalletSelector.tsx b/packages/mobile/src/tabs/Wallet/components/WalletSelector.tsx new file mode 100644 index 000000000..3f6322c14 --- /dev/null +++ b/packages/mobile/src/tabs/Wallet/components/WalletSelector.tsx @@ -0,0 +1,75 @@ +import { useWallet } from '@tonkeeper/shared/hooks'; +import { + Icon, + Spacer, + Steezy, + Text, + TouchableOpacity, + View, + deviceWidth, + getWalletColorHex, + isAndroid, +} from '@tonkeeper/uikit'; +import React, { FC, memo, useCallback } from 'react'; +import { Text as RNText } from 'react-native'; +import { useNavigation } from '@tonkeeper/router'; +import { useDispatch } from 'react-redux'; +import { walletActions } from '$store/wallet'; + +const WalletSelectorComponent: FC = () => { + const wallet = useWallet(); + const nav = useNavigation(); + const dispatch = useDispatch(); + + const handlePress = useCallback(() => { + dispatch(walletActions.clearGeneratedVault()); + nav.openModal('/switch-wallet'); + }, [dispatch, nav]); + + return ( + + + + {wallet.config.emoji} + + + + {wallet.config.name} + + + + + + + + ); +}; + +export const WalletSelector = memo(WalletSelectorComponent); + +const styles = Steezy.create({ + container: { alignItems: 'center' }, + selectorContainer: { + height: 40, + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 10, + paddingRight: 12, + borderRadius: 20, + }, + nameContainer: { + maxWidth: deviceWidth - 180, + }, + icon: { + opacity: 0.64, + }, + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git "a/packages/mobile/src/tabs/Wallet/components/\320\241onfirmRenewAllDomains.tsx" "b/packages/mobile/src/tabs/Wallet/components/\320\241onfirmRenewAllDomains.tsx" index 2b11d7b2e..c2ea4eb9c 100644 --- "a/packages/mobile/src/tabs/Wallet/components/\320\241onfirmRenewAllDomains.tsx" +++ "b/packages/mobile/src/tabs/Wallet/components/\320\241onfirmRenewAllDomains.tsx" @@ -9,7 +9,6 @@ import * as S from '../../../core/ModalContainer/NFTOperations/NFTOperations.sty import { useExpiringDomains } from '$store/zustand/domains/useExpiringDomains'; import { formatter } from '$utils/formatter'; import { useFiatValue } from '$hooks/useFiatValue'; -import { useWallet } from '$hooks/useWallet'; import { CryptoCurrencies, Decimals } from '$shared/constants'; import { copyText } from '$hooks/useCopyText'; import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; @@ -19,15 +18,15 @@ import { debugLog } from '$utils/debugLog'; import { Toast } from '$store/zustand/toast'; import { Ton } from '$libs/Ton'; import TonWeb from 'tonweb'; -import { Tonapi } from '$libs/Tonapi'; -import { useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; import { checkIsInsufficient, openInsufficientFundsModal, } from '$core/ModalContainer/InsufficientFunds/InsufficientFunds'; import BigNumber from 'bignumber.js'; -import { tk, tonapi } from '@tonkeeper/shared/tonkeeper'; +import { tk } from '$wallet'; import { Address } from '@tonkeeper/core'; +import { walletWalletSelector } from '$store/wallet'; enum States { INITIAL, @@ -43,8 +42,7 @@ export const СonfirmRenewAllDomains = memo((props) => { const remove = useExpiringDomains((state) => state.actions.remove); const unlock = useUnlockVault(); const [current, setCurrent] = useState(0); - const wallet = useWallet(); - const dispatch = useDispatch(); + const wallet = useSelector(walletWalletSelector)!; const [count] = useState(domains.length); const [amount] = useState(0.02 * count); @@ -111,7 +109,10 @@ export const СonfirmRenewAllDomains = memo((props) => { const queryMsg = await tx.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - await tonapi.blockchain.sendBlockchainMessage({ boc }, { format: 'text' }); + await tk.wallet.tonapi.blockchain.sendBlockchainMessage( + { boc }, + { format: 'text' }, + ); tk.wallet.activityList.reload(); await delay(15000); diff --git a/packages/mobile/src/tabs/Wallet/hooks/useBalance.ts b/packages/mobile/src/tabs/Wallet/hooks/useBalance.ts index 9c5ab4c7e..8dd815111 100644 --- a/packages/mobile/src/tabs/Wallet/hooks/useBalance.ts +++ b/packages/mobile/src/tabs/Wallet/hooks/useBalance.ts @@ -1,21 +1,18 @@ import { CryptoCurrencies, Decimals } from '$shared/constants'; -import { useSelector } from 'react-redux'; -import { fiatCurrencySelector } from '$store/main'; import { useGetTokenPrice, useTokenPrice } from '$hooks/useTokenPrice'; import { useCallback, useMemo } from 'react'; -import { - isLockupWalletSelector, - walletBalancesSelector, - walletOldBalancesSelector, - walletVersionSelector, -} from '$store/wallet'; import { Ton } from '$libs/Ton'; import BigNumber from 'bignumber.js'; import { formatter } from '$utils/formatter'; -import { getStakingJettons, useStakingStore } from '$store'; -import { shallow } from 'zustand/shallow'; -import { jettonsBalancesSelector } from '$store/jettons'; +import { getStakingJettons } from '@tonkeeper/shared/utils/staking'; import { Address } from '@tonkeeper/core'; +import { + useBalancesState, + useJettons, + useStakingState, + useWallet, + useWalletCurrency, +} from '@tonkeeper/shared/hooks'; export type Rate = { percent: string; @@ -26,7 +23,7 @@ export type Rate = { // TODO: rewrite const useAmountToFiat = () => { const tonPrice = useTokenPrice(CryptoCurrencies.Ton); - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const amountToFiat = useCallback( (amount: string, fiatAmountToSum?: number) => { @@ -46,9 +43,9 @@ const useAmountToFiat = () => { }; const useStakingBalance = () => { - const _stakingBalance = useStakingStore((s) => s.stakingBalance, shallow); - const stakingJettons = useStakingStore(getStakingJettons, shallow); - const jettonBalances = useSelector(jettonsBalancesSelector); + const _stakingBalance = useStakingState((s) => s.stakingBalance); + const stakingJettons = useStakingState(getStakingJettons); + const { jettonBalances } = useJettons(); const amountToFiat = useAmountToFiat(); const getTokenPrice = useGetTokenPrice(); @@ -79,81 +76,53 @@ const useStakingBalance = () => { }; export const useBalance = (tokensTotal: number) => { - const balances = useSelector(walletBalancesSelector); - const walletVersion = useSelector(walletVersionSelector); - const oldWalletBalances = useSelector(walletOldBalancesSelector); - const isLockup = useSelector(isLockupWalletSelector); + const balances = useBalancesState(); + const wallet = useWallet(); const amountToFiat = useAmountToFiat(); - const getTokenPrice = useGetTokenPrice(); const stakingBalance = useStakingBalance(); - const oldVersions = useMemo(() => { - if (isLockup) { - return []; - } - - return oldWalletBalances.reduce((acc, item) => { - if (walletVersion && walletVersion <= item.version) { - return acc; - } - - const balance = Ton.fromNano(item.balance); - - acc.push({ - version: item.version, - amount: { - value: balance, - formatted: formatter.format(balance), - fiat: amountToFiat(balance), - }, - }); - - return acc; - }, [] as any); - }, [isLockup, oldWalletBalances, walletVersion, amountToFiat]); - const lockup = useMemo(() => { - const lockupList: { type: CryptoCurrencies; amount: string }[] = []; - const restricted = balances[CryptoCurrencies.TonRestricted]; - const locked = balances[CryptoCurrencies.TonLocked]; - - if (restricted) { - lockupList.push({ - type: CryptoCurrencies.TonRestricted, - amount: restricted, - }); - } + const lockupList: { + type: CryptoCurrencies; + amount: { + nano: string; + fiat: string; + formatted: string; + }; + }[] = []; - if (locked) { - lockupList.push({ - type: CryptoCurrencies.TonLocked, - amount: locked, - }); + if (!wallet.isLockup) { + return lockupList; } - return lockupList.map((item) => { - const amount = balances[item.type]; + lockupList.push({ + type: CryptoCurrencies.TonRestricted, + amount: { + nano: balances.tonRestricted, + formatted: formatter.format(balances.tonRestricted), + fiat: amountToFiat(balances.tonRestricted), + }, + }); - return { - type: item.type, - amount: { - nano: item.amount, - formatted: formatter.format(amount), - fiat: amountToFiat(amount), - }, - }; + lockupList.push({ + type: CryptoCurrencies.TonLocked, + amount: { + nano: balances.tonLocked, + formatted: formatter.format(balances.tonLocked), + fiat: amountToFiat(balances.tonLocked), + }, }); - }, [balances, getTokenPrice]); - const ton = useMemo(() => { - const balance = balances[CryptoCurrencies.Ton] ?? '0'; + return lockupList; + }, [amountToFiat, balances, wallet.isLockup]); - const formatted = formatter.format(balance); + const ton = useMemo(() => { + const formatted = formatter.format(balances.ton); return { amount: { - nano: balance, - fiat: amountToFiat(balance), + nano: balances.ton, + fiat: amountToFiat(balances.ton), formatted, }, }; @@ -179,11 +148,10 @@ export const useBalance = (tokensTotal: number) => { return useMemo( () => ({ - oldVersions, lockup, total, ton, }), - [oldVersions, lockup, total, ton], + [lockup, total, ton], ); }; diff --git a/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts b/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts index 0fcc814ef..5aed6aa05 100644 --- a/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts +++ b/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts @@ -1,11 +1,9 @@ import { usePrevious } from '$hooks/usePrevious'; -import { isServerConfigLoaded } from '$shared/constants'; import { mainActions, mainSelector } from '$store/main'; -import { walletActions, walletWalletSelector } from '$store/wallet'; import { InternalNotificationProps } from '$uikit/InternalNotification/InternalNotification.interface'; import { useNetInfo } from '@react-native-community/netinfo'; import { MainDB } from '$database'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Linking } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import { t } from '@tonkeeper/shared/i18n'; @@ -13,27 +11,25 @@ import { useAddressUpdateStore } from '$store'; import { useFlag } from '$utils/flags'; import { useNavigation } from '@tonkeeper/router'; import { MainStackRouteNames } from '$navigation'; +import { config } from '$config'; +import { useWallet } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; export const useInternalNotifications = () => { const dispatch = useDispatch(); - const isConfigError = !isServerConfigLoaded(); const [isNoSignalDismissed, setNoSignalDismissed] = useState(false); const netInfo = useNetInfo(); const prevNetInfo = usePrevious(netInfo); - const wallet = useSelector(walletWalletSelector); + const wallet = useWallet(); const nav = useNavigation(); - const handleRefresh = useCallback(() => { - dispatch(walletActions.refreshBalancesPage(true)); - }, [dispatch]); - useEffect(() => { if (netInfo.isConnected && prevNetInfo.isConnected === false) { dispatch(mainActions.dismissBadHosts()); - handleRefresh(); + tk.wallets.forEach((item) => item.reload()); } - }, [netInfo.isConnected, prevNetInfo.isConnected, handleRefresh, dispatch]); + }, [netInfo.isConnected, prevNetInfo.isConnected, dispatch]); const { badHosts, @@ -50,7 +46,7 @@ export const useInternalNotifications = () => { const notifications = useMemo(() => { const result: InternalNotificationProps[] = []; - if (isConfigError) { + if (!config.isLoaded) { result.push({ title: t('notify_no_signal_title'), caption: t('notify_no_signal_caption'), @@ -140,7 +136,6 @@ export const useInternalNotifications = () => { return result; }, [ - isConfigError, netInfo.isConnected, badHosts, isBadHostsDismissed, diff --git a/packages/mobile/src/tabs/Wallet/hooks/useNFTs.ts b/packages/mobile/src/tabs/Wallet/hooks/useNFTs.ts deleted file mode 100644 index 322a3a104..000000000 --- a/packages/mobile/src/tabs/Wallet/hooks/useNFTs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { nftsSelector } from "$store/nfts"; -import { useSelector } from "react-redux"; - -export const useNFTs = () => { - const { myNfts } = useSelector(nftsSelector); - - const nfts = Object.values(myNfts).map((item) => { - return item; - }); - - return nfts; -}; \ No newline at end of file diff --git a/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts b/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts index e7c21ac56..f09e2e94d 100644 --- a/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts +++ b/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts @@ -38,7 +38,6 @@ export const useTonkens = (): { total: { fiat: number; }; - canEdit: boolean; } => { const { enabled: jettonBalances } = useJettonBalances(); const getTokenPrice = useGetTokenPrice(); diff --git a/packages/mobile/src/tabs/useWallet.ts b/packages/mobile/src/tabs/useWallet.ts deleted file mode 100644 index af783ba40..000000000 --- a/packages/mobile/src/tabs/useWallet.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { WalletAddress } from '@tonkeeper/core/src/Wallet'; -import { tk } from '@tonkeeper/shared/tonkeeper'; - -export const useWallet = (): { address: WalletAddress } => { - if (tk.wallet?.address) { - return { - address: tk.wallet.address, - }; - } - - return { - address: { - tron: '', - ton: { - friendly: '', - short: '', - raw: '', - }, - }, - }; -}; diff --git a/packages/mobile/src/tonconnect/config.ts b/packages/mobile/src/tonconnect/config.ts index 96d8b5c05..b37116efc 100644 --- a/packages/mobile/src/tonconnect/config.ts +++ b/packages/mobile/src/tonconnect/config.ts @@ -17,7 +17,7 @@ const getPlatform = (): DeviceInfo['platform'] => { export const tonConnectDeviceInfo: DeviceInfo = { platform: getPlatform(), appName: RNDeviceInfo.getApplicationName(), - appVersion: RNDeviceInfo.getReadableVersion(), + appVersion: RNDeviceInfo.getVersion(), maxProtocolVersion: CURRENT_PROTOCOL_VERSION, features: ['SendTransaction', { name: 'SendTransaction', maxMessages: 4 }], }; diff --git a/packages/mobile/src/tonconnect/hooks/useRemoteBridge.ts b/packages/mobile/src/tonconnect/hooks/useRemoteBridge.ts index 2c70f89ec..d1dbc017a 100644 --- a/packages/mobile/src/tonconnect/hooks/useRemoteBridge.ts +++ b/packages/mobile/src/tonconnect/hooks/useRemoteBridge.ts @@ -1,12 +1,14 @@ import { useAppState } from '$hooks/useAppState'; import { getAllConnections, useConnectedAppsStore } from '$store'; -import { walletSelector } from '$store/wallet'; import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; import { TonConnectRemoteBridge } from '../TonConnectRemoteBridge'; +import { useWallet } from '@tonkeeper/shared/hooks'; +import { Address } from '@tonkeeper/core'; export const useRemoteBridge = () => { - const { address } = useSelector(walletSelector); + const wallet = useWallet(); + + const address = Address.parse(wallet.address.ton.raw).toFriendly({ bounceable: true }); const appState = useAppState(); @@ -17,13 +19,13 @@ export const useRemoteBridge = () => { const initialConnections = getAllConnections( useConnectedAppsStore.getState(), - address.ton, + address, ); TonConnectRemoteBridge.open(initialConnections); const unsubscribe = useConnectedAppsStore.subscribe( - (s) => getAllConnections(s, address.ton), + (s) => getAllConnections(s, address), (connections) => { TonConnectRemoteBridge.open(connections); }, @@ -33,5 +35,5 @@ export const useRemoteBridge = () => { unsubscribe(); TonConnectRemoteBridge.close(); }; - }, [address.ton, appState]); + }, [address, appState]); }; diff --git a/packages/mobile/src/uikit/Checkbox/Checkbox.tsx b/packages/mobile/src/uikit/Checkbox/Checkbox.tsx index d3f0cc2a1..172e6ef73 100644 --- a/packages/mobile/src/uikit/Checkbox/Checkbox.tsx +++ b/packages/mobile/src/uikit/Checkbox/Checkbox.tsx @@ -102,5 +102,6 @@ const styles = Steezy.create(({ colors }) => ({ borderWidth: 2, alignItems: 'center', justifyContent: 'center', + margin: 3, }, })); diff --git a/packages/mobile/src/uikit/InlineKeyboard/InlineKeyboard.tsx b/packages/mobile/src/uikit/InlineKeyboard/InlineKeyboard.tsx index 661410128..8968bf64a 100644 --- a/packages/mobile/src/uikit/InlineKeyboard/InlineKeyboard.tsx +++ b/packages/mobile/src/uikit/InlineKeyboard/InlineKeyboard.tsx @@ -12,8 +12,6 @@ import { InlineKeyboardProps, KeyProps } from './InlineKeyboard.interface'; import * as S from './InlineKeyboard.style'; import { detectBiometryType, triggerSelection } from '$utils'; import { Icon } from '../Icon/Icon'; -import { useTheme } from '$hooks/useTheme'; -import { MainDB } from '$database'; const Key: FC = (props) => { const { onPress, children, disabled } = props; @@ -67,20 +65,14 @@ export const InlineKeyboard: FC = (props) => { biometryEnabled = false, onBiometryPress, } = props; - const theme = useTheme(); const [biometryType, setBiometryType] = useState(-1); useEffect(() => { if (biometryEnabled) { - Promise.all([ - MainDB.isBiometryEnabled(), - LocalAuthentication.supportedAuthenticationTypesAsync(), - ]).then(([isEnabled, types]) => { - if (isEnabled) { - const type = detectBiometryType(types); - if (type) { - setBiometryType(type); - } + LocalAuthentication.supportedAuthenticationTypesAsync().then((types) => { + const type = detectBiometryType(types); + if (type) { + setBiometryType(type); } }); } else { @@ -122,10 +114,7 @@ export const InlineKeyboard: FC = (props) => { if (biometryType === LocalAuthentication.AuthenticationType.FINGERPRINT) { biometryButton = ( - + ); } else if ( @@ -133,10 +122,7 @@ export const InlineKeyboard: FC = (props) => { ) { biometryButton = ( - + ); } @@ -148,16 +134,13 @@ export const InlineKeyboard: FC = (props) => { 0 - + , ); return result; - }, [biometryType, disabled, handleBackspace, onBiometryPress, handlePress, theme]); + }, [biometryType, disabled, handleBackspace, onBiometryPress, handlePress]); return {nums}; }; diff --git a/packages/mobile/src/uikit/List/ListItemWithCheckbox.tsx b/packages/mobile/src/uikit/List/ListItemWithCheckbox.tsx index 796d7abe2..8ba833584 100644 --- a/packages/mobile/src/uikit/List/ListItemWithCheckbox.tsx +++ b/packages/mobile/src/uikit/List/ListItemWithCheckbox.tsx @@ -19,13 +19,11 @@ export const ListItemWithCheckbox = memo((props) => { {...props} onPress={onPress || onChange} value={ - - - + } /> ); diff --git a/packages/mobile/src/uikit/NavBar/NavBar.interface.ts b/packages/mobile/src/uikit/NavBar/NavBar.interface.ts index 67e007248..b298ac435 100644 --- a/packages/mobile/src/uikit/NavBar/NavBar.interface.ts +++ b/packages/mobile/src/uikit/NavBar/NavBar.interface.ts @@ -4,7 +4,7 @@ import { ViewStyle } from 'react-native'; import { TextProps } from '../Text/Text'; export interface NavBarProps { - children: ReactNode; + children?: ReactNode; subtitle?: ReactNode; isModal?: boolean; title?: string | React.ReactNode; @@ -16,6 +16,7 @@ export interface NavBarProps { isClosedButton?: boolean; isBottomButton?: boolean; onBackPress?: () => void; + onClosePress?: () => void; onGoBack?: () => void; isTransparent?: boolean; isForceBackIcon?: boolean; diff --git a/packages/mobile/src/uikit/NavBar/NavBar.tsx b/packages/mobile/src/uikit/NavBar/NavBar.tsx index 0427c97d2..e2b62617b 100644 --- a/packages/mobile/src/uikit/NavBar/NavBar.tsx +++ b/packages/mobile/src/uikit/NavBar/NavBar.tsx @@ -39,6 +39,7 @@ export const NavBar: FC = (props) => { forceBigTitle = false, isClosedButton = false, onBackPress = undefined, + onClosePress = undefined, onGoBack = undefined, isTransparent = false, isForceBackIcon = false, @@ -98,7 +99,7 @@ export const NavBar: FC = (props) => { if (isClosedButton) { return ( - + diff --git a/packages/mobile/src/uikit/Toast/new/ToastComponent.tsx b/packages/mobile/src/uikit/Toast/new/ToastComponent.tsx index 4243152ae..b63f60fca 100644 --- a/packages/mobile/src/uikit/Toast/new/ToastComponent.tsx +++ b/packages/mobile/src/uikit/Toast/new/ToastComponent.tsx @@ -101,7 +101,11 @@ export const ToastComponent = memo(() => { styles.toast, animatedStyle, toast.size === 'small' && styles.toastSmall, - { backgroundColor: theme.colors.backgroundTertiary }, + { + backgroundColor: toast.warning + ? theme.colors.accentOrange + : theme.colors.backgroundTertiary, + }, ]} > {toast.isLoading && ( diff --git a/packages/mobile/src/uikit/TonDiamondIcon/TonDiamondIcon.tsx b/packages/mobile/src/uikit/TonDiamondIcon/TonDiamondIcon.tsx index 174a9fbc5..245b7190b 100644 --- a/packages/mobile/src/uikit/TonDiamondIcon/TonDiamondIcon.tsx +++ b/packages/mobile/src/uikit/TonDiamondIcon/TonDiamondIcon.tsx @@ -5,7 +5,6 @@ import { getDiamondSizeRatio, } from '$styled'; import React, { FC, memo } from 'react'; -import { ViewStyle } from 'react-native'; import { IconFromUri } from './IconFromUri'; import * as S from './TonDiamondIcon.style'; @@ -14,7 +13,7 @@ interface Props { nftIcon?: AccentNFTIcon; size: number; disabled?: boolean; - iconAnimatedStyle?: any;//AnimatedStyleProp; + iconAnimatedStyle?: any; //AnimatedStyleProp; rounded?: boolean; } diff --git a/packages/mobile/src/utils/flags.ts b/packages/mobile/src/utils/flags.ts index 402a419f8..dcec3e605 100644 --- a/packages/mobile/src/utils/flags.ts +++ b/packages/mobile/src/utils/flags.ts @@ -1,8 +1,8 @@ import React from 'react'; -import { getServerConfig } from '$shared/constants'; +import { config } from '$config'; export function getFlag(key?: string) { - const flags = getServerConfig('flags'); + const flags = config.get('flags'); if (key) { const flag = flags[key]; if (__DEV__ && flags[key] === undefined) { diff --git a/packages/mobile/src/utils/mapNewNftToOldNftData.ts b/packages/mobile/src/utils/mapNewNftToOldNftData.ts new file mode 100644 index 000000000..f5d6969c1 --- /dev/null +++ b/packages/mobile/src/utils/mapNewNftToOldNftData.ts @@ -0,0 +1,46 @@ +import { CryptoCurrencies } from '$shared/constants'; +import { NFTModel } from '$store/models'; +import { NftItem } from '@tonkeeper/core/src/TonAPI'; +import TonWeb from 'tonweb'; +import { getFlag } from './flags'; +import { Address } from '@tonkeeper/core'; + +export const mapNewNftToOldNftData = ( + nftItem: NftItem, + walletFriendlyAddress, +): NFTModel => { + const address = new TonWeb.utils.Address(nftItem.address).toString(true, true, true); + const ownerAddress = nftItem.owner?.address + ? Address.parse(nftItem.owner.address, { + bounceable: !getFlag('address_style_nobounce'), + }).toFriendly() + : ''; + const name = + typeof nftItem.metadata?.name === 'string' + ? nftItem.metadata.name.trim() + : nftItem.metadata?.name; + + const baseUrl = (nftItem.previews && + nftItem.previews.find((preview) => preview.resolution === '500x500')!.url)!; + + return { + ...nftItem, + ownerAddressToDisplay: nftItem.sale ? walletFriendlyAddress : undefined, + isApproved: !!nftItem.approved_by?.length ?? false, + internalId: `${CryptoCurrencies.Ton}_${address}`, + currency: CryptoCurrencies.Ton, + provider: 'TonProvider', + content: { + image: { + baseUrl, + }, + }, + description: nftItem.metadata?.description, + marketplaceURL: nftItem.metadata?.marketplace && nftItem.metadata?.external_url, + attributes: nftItem.metadata?.attributes, + address, + name, + ownerAddress, + collection: nftItem.collection, + }; +}; diff --git a/packages/mobile/src/utils/messaging.ts b/packages/mobile/src/utils/messaging.ts deleted file mode 100644 index b9c089d17..000000000 --- a/packages/mobile/src/utils/messaging.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { isAndroid } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { PermissionsAndroid, Platform } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import messaging from '@react-native-firebase/messaging'; -import { getTimeSec } from './getTimeSec'; -import _ from "lodash"; - -export async function getToken() { - return await messaging().getToken(); -} - -export async function getPermission() { - if (isAndroid && +Platform.Version >= 33) { - return await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); - } else { - const authStatus = await messaging().hasPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - return enabled; - } -} - -export async function requestUserPermissionAndGetToken() { - if (isAndroid && +Platform.Version >= 33) { - const status = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); - const enabled = status === PermissionsAndroid.RESULTS.GRANTED; - - if (!enabled) { - return false; - } - } else { - const hasPermission = await getPermission(); - - if (!hasPermission) { - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (!enabled) { - return false; - } - } - } - - return await getToken(); -} - -export enum SUBSCRIBE_STATUS { - SUBSCRIBED, - UNSUBSCRIBED, - NOT_SPECIFIED, -} - -let _subscribeStatus: SUBSCRIBE_STATUS = SUBSCRIBE_STATUS.NOT_SPECIFIED; - -export async function saveSubscribeStatus() { - try { - await AsyncStorage.setItem('isSubscribeNotifications', 'true'); - _subscribeStatus = SUBSCRIBE_STATUS.SUBSCRIBED; - } catch (err) { - _subscribeStatus = SUBSCRIBE_STATUS.NOT_SPECIFIED; - debugLog('[saveSubscribeStatus]', err); - } -} - -export async function removeSubscribeStatus() { - try { - await AsyncStorage.setItem('isSubscribeNotifications', 'false'); - _subscribeStatus = SUBSCRIBE_STATUS.UNSUBSCRIBED; - } catch (err) { - _subscribeStatus = SUBSCRIBE_STATUS.NOT_SPECIFIED; - debugLog('[removeSubscribeStatus]', err); - } -} - -export async function clearSubscribeStatus() { - try { - await AsyncStorage.removeItem('isSubscribeNotifications'); - _subscribeStatus = SUBSCRIBE_STATUS.NOT_SPECIFIED; - } catch (err) { - _subscribeStatus = SUBSCRIBE_STATUS.NOT_SPECIFIED; - debugLog('[removeSubscribeStatus]', err); - } -} - -export async function getSubscribeStatus() { - if (_subscribeStatus !== SUBSCRIBE_STATUS.NOT_SPECIFIED) { - return _subscribeStatus; - } - - try { - const status = await AsyncStorage.getItem('isSubscribeNotifications'); - if (_.isNil(status)) { - _subscribeStatus = SUBSCRIBE_STATUS.NOT_SPECIFIED; - } else if (status === 'true') { - _subscribeStatus = SUBSCRIBE_STATUS.SUBSCRIBED; - } else { - _subscribeStatus = SUBSCRIBE_STATUS.UNSUBSCRIBED; - } - return _subscribeStatus; - } catch (err) { - _subscribeStatus = SUBSCRIBE_STATUS.NOT_SPECIFIED; - return false; - } -} - -export async function saveReminderNotifications() { - try { - const twoWeeksInSec = 60 * 60 * 24 * 7 * 2; - const nextShowTime = getTimeSec() + twoWeeksInSec; - await AsyncStorage.setItem('ReminderNotificationsTimestamp', String(nextShowTime)); - } catch (err) {} -} - -export async function removeReminderNotifications() { - try { - await AsyncStorage.removeItem('ReminderNotificationsTimestamp'); - } catch (err) {} -} - -export async function shouldOpenReminderNotifications() { - try { - const status = await getSubscribeStatus(); - const timeToShow = await AsyncStorage.getItem('ReminderNotificationsTimestamp'); - - if (status === SUBSCRIBE_STATUS.NOT_SPECIFIED) { - if (!timeToShow) { - return true; - } - - return getTimeSec() > Number(timeToShow); - } - - return false; - } catch (err) { - console.error(err); - return false; - } -} - -export async function saveDontShowReminderNotifications() { - try { - await AsyncStorage.setItem('DontShowReminderNotifications', 'true'); - } catch (err) {} -} - -export async function shouldOpenReminderNotificationsAfterUpdate() { - try { - const hasPermission = await getPermission(); - const dontShow = await AsyncStorage.getItem('DontShowReminderNotifications'); - - return !hasPermission && !dontShow; - } catch (err) { - debugLog(err); - return false; - } -} diff --git a/packages/mobile/src/utils/nft.ts b/packages/mobile/src/utils/nft.ts index ce6700e0f..4e07c4bef 100644 --- a/packages/mobile/src/utils/nft.ts +++ b/packages/mobile/src/utils/nft.ts @@ -1,12 +1,7 @@ import { tonDiamondCollectionAddress, telegramNumbersAddress } from '$shared/constants'; import { getChainName } from '$shared/dynamicConfig'; -import { MarketplaceModel, NFTModel, TonDiamondMetadata } from '$store/models'; -import { myNftsSelector } from '$store/nfts'; -import { AccentKey } from '$styled'; -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; +import { NFTModel, TonDiamondMetadata } from '$store/models'; import TonWeb from 'tonweb'; -import { capitalizeFirstLetter } from './string'; const getTonDiamondsCollectionAddress = () => tonDiamondCollectionAddress[getChainName()]; const getTelegramNumbersCollectionAddress = () => telegramNumbersAddress[getChainName()]; @@ -39,34 +34,3 @@ export const checkIsTelegramNumbersNFT = (nft: NFTModel): boolean => { return collectionAddress === getTelegramNumbersCollectionAddress(); }; - -export const useHasDiamondsOnBalance = () => { - const myNfts = useSelector(myNftsSelector); - const diamond = useMemo(() => { - return Object.values(myNfts).find(checkIsTonDiamondsNFT); - }, [myNfts]); - - return !!diamond; -}; - -export const getDiamondsCollectionMarketUrl = ( - marketplace: MarketplaceModel, - accentKey: AccentKey, -) => { - const color = capitalizeFirstLetter(accentKey); - - const collectionAddress = getTonDiamondsCollectionAddress(); - - switch (marketplace.id) { - case 'getgems': - return `${ - marketplace.marketplace_url - }/collection/${collectionAddress}/?filter=${encodeURIComponent( - JSON.stringify({ attributes: { Color: [color] } }), - )}`; - case 'tonDiamonds': - return `${marketplace.marketplace_url}/collection/${collectionAddress}?traits[Color][0]=${color}`; - default: - return marketplace.marketplace_url; - } -}; diff --git a/packages/mobile/src/utils/proof.ts b/packages/mobile/src/utils/proof.ts index bc9d082a9..80e66e51f 100644 --- a/packages/mobile/src/utils/proof.ts +++ b/packages/mobile/src/utils/proof.ts @@ -5,8 +5,8 @@ import nacl from 'tweetnacl'; import naclUtils from 'tweetnacl-util'; const { createHash } = require('react-native-crypto'); import { ConnectApi, Configuration } from '@tonkeeper/core/src/legacy'; -import { getServerConfigSafe } from '$shared/constants'; import { Address } from '@tonkeeper/core'; +import { config } from '$config'; export interface TonProofArgs { address: string; @@ -27,9 +27,9 @@ export async function createTonProof({ const address = Address.parse(_addr).toRaw(); const connectApi = new ConnectApi( new Configuration({ - basePath: getServerConfigSafe('tonapiV2Endpoint'), + basePath: config.get('tonapiV2Endpoint'), headers: { - Authorization: `Bearer ${getServerConfigSafe('tonApiV2Key')}`, + Authorization: `Bearer ${config.get('tonApiV2Key')}`, }, }), ); diff --git a/packages/mobile/src/utils/proxyMedia.ts b/packages/mobile/src/utils/proxyMedia.ts index 71cfb6817..0c394091a 100644 --- a/packages/mobile/src/utils/proxyMedia.ts +++ b/packages/mobile/src/utils/proxyMedia.ts @@ -1,10 +1,9 @@ +import { config } from '$config'; import { ns } from '$utils/style'; -import { getServerConfig } from '$shared/constants'; const createHmac = require('create-hmac'); const urlSafeBase64 = (string) => { - // eslint-disable-next-line no-undef return Buffer.from(string) .toString('base64') .replace(/[=]/g, '') @@ -12,7 +11,6 @@ const urlSafeBase64 = (string) => { .replace(/\//g, '_'); }; -// eslint-disable-next-line no-undef const hexDecode = (hex) => Buffer.from(hex, 'hex'); const sign = (salt, target, secret) => { @@ -32,13 +30,13 @@ const EXTENTION = 'png'; Please provide width and height without additional normalizing */ export function proxyMedia(url: string, width: number = 300, height: number = 300) { - const KEY = getServerConfig('cachedMediaKey'); - const SALT = getServerConfig('cachedMediaSalt'); + const KEY = config.get('cachedMediaKey'); + const SALT = config.get('cachedMediaSalt'); const encoded_url = urlSafeBase64(url); const path = `/rs:${RESIZING_TYPE}:${Math.round(ns(width))}:${Math.round( ns(height), )}:${enlarge}/g:${GRAVITY}/${encoded_url}.${EXTENTION}`; const signature = sign(SALT, path, KEY); - return `${getServerConfig('cachedMediaEndpoint')}/${signature}${path}`; + return `${config.get('cachedMediaEndpoint')}/${signature}${path}`; } diff --git a/packages/mobile/src/utils/staking.ts b/packages/mobile/src/utils/staking.ts index d343b3ce4..b6ab6fc0f 100644 --- a/packages/mobile/src/utils/staking.ts +++ b/packages/mobile/src/utils/staking.ts @@ -9,10 +9,7 @@ import { whalesTeam2IconSource, whalesTeamIconSource, } from '@tonkeeper/uikit/assets/staking'; -import { Ton } from '$libs/Ton'; -import { StakingInfo } from '$store'; import { PoolInfo, PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; -import BigNumber from 'bignumber.js'; import { ImageRequireSource } from 'react-native'; export const getPoolIcon = (pool: PoolInfo): ImageRequireSource | null => { @@ -50,22 +47,3 @@ export const getImplementationIcon = (implementation: string) => { return liquidTfIconSource; } }; - -export const calculatePoolBalance = (pool: PoolInfo, stakingInfo: StakingInfo) => { - const amount = new BigNumber(Ton.fromNano(stakingInfo[pool.address]?.amount || '0')); - const pendingDeposit = new BigNumber( - Ton.fromNano(stakingInfo[pool.address]?.pending_deposit || '0'), - ); - const pendingWithdraw = new BigNumber( - Ton.fromNano(stakingInfo[pool.address]?.pending_withdraw || '0'), - ); - const readyWithdraw = new BigNumber( - Ton.fromNano(stakingInfo[pool.address]?.ready_withdraw || '0'), - ); - const balance = amount - .plus(pendingDeposit) - .plus(readyWithdraw) - .plus(pool.implementation === PoolImplementationType.LiquidTF ? pendingWithdraw : 0); - - return balance; -}; diff --git a/packages/mobile/src/utils/stats.ts b/packages/mobile/src/utils/stats.ts index 95518911c..76ba2d346 100644 --- a/packages/mobile/src/utils/stats.ts +++ b/packages/mobile/src/utils/stats.ts @@ -1,10 +1,10 @@ -import { getServerConfig } from '$shared/constants'; +import { config } from '$config'; import { init, logEvent } from '@amplitude/analytics-browser'; import AsyncStorage from '@react-native-async-storage/async-storage'; let TrakingEnabled = false; export function initStats() { - init(getServerConfig('amplitudeKey'), '-', { + init(config.get('amplitudeKey'), '-', { minIdLength: 1, deviceId: '-', trackingOptions: { @@ -16,9 +16,9 @@ export function initStats() { platform: true, adid: false, carrier: false, - } + }, }); - TrakingEnabled = true; + TrakingEnabled = true; } export function trackEvent(name: string, params: any = {}) { @@ -28,11 +28,10 @@ export function trackEvent(name: string, params: any = {}) { logEvent(name, params); } - export async function trackFirstLaunch() { const isFirstLaunch = !(await AsyncStorage.getItem('launched_before')); if (isFirstLaunch) { trackEvent('first_launch'); await AsyncStorage.setItem('launched_before', 'true'); } -} \ No newline at end of file +} diff --git a/packages/@core-js/src/Activity/ActivityList.ts b/packages/mobile/src/wallet/Activity/ActivityList.ts similarity index 91% rename from packages/@core-js/src/Activity/ActivityList.ts rename to packages/mobile/src/wallet/Activity/ActivityList.ts index de26e5df5..cf23c960b 100644 --- a/packages/@core-js/src/Activity/ActivityList.ts +++ b/packages/mobile/src/wallet/Activity/ActivityList.ts @@ -1,8 +1,7 @@ import { ActivitySection, ActionItem, ActivityModel } from '../models/ActivityModel'; -import { Storage } from '../declarations/Storage'; import { ActivityLoader } from './ActivityLoader'; -import { Logger } from '../utils/Logger'; -import { State } from '../utils/State'; +import { TonRawAddress } from '../WalletTypes'; +import { State, Storage, Logger } from '@tonkeeper/core'; type Cursors = { tron: number | null; @@ -32,10 +31,14 @@ export class ActivityList { error: null, }); - constructor(private activityLoader: ActivityLoader, private storage: Storage) { + constructor( + private tonRawAddress: TonRawAddress, + private activityLoader: ActivityLoader, + private storage: Storage, + ) { this.state.persist({ storage: this.storage, - key: 'ActivityList', + key: `${this.tonRawAddress}/ActivityList`, partialize: ({ sections }) => ({ sections: sections.map((section) => ({ ...section, @@ -140,8 +143,4 @@ export class ActivityList { private sortByTimestamp(items: ActionItem[]) { return items.sort((a, b) => b.event.timestamp - a.event.timestamp); } - - public preload() { - this.load(); - } } diff --git a/packages/@core-js/src/Activity/ActivityLoader.ts b/packages/mobile/src/wallet/Activity/ActivityLoader.ts similarity index 90% rename from packages/@core-js/src/Activity/ActivityLoader.ts rename to packages/mobile/src/wallet/Activity/ActivityLoader.ts index 29c6f7947..1278eeae7 100644 --- a/packages/@core-js/src/Activity/ActivityLoader.ts +++ b/packages/mobile/src/wallet/Activity/ActivityLoader.ts @@ -1,12 +1,12 @@ -import { AccountEvent, TonAPI } from '../TonAPI'; -import { WalletAddresses } from '../Wallet'; -import { TronAPI } from '../TronAPI'; +import { AccountEvent, TonAPI } from '@tonkeeper/core/src/TonAPI'; import { ActivityModel, ActionSource, ActionItem, ActionId, } from '../models/ActivityModel'; +import { TonRawAddress } from '../WalletTypes'; +import { TronAPI } from '@tonkeeper/core'; type LoadParams = { filter?: (events: TData[]) => TData[]; @@ -19,7 +19,7 @@ export class ActivityLoader { private tonActions = new Map(); constructor( - private addresses: WalletAddresses, + private tonRawAddress: TonRawAddress, private tonapi: TonAPI, private tronapi: TronAPI, ) {} @@ -28,7 +28,7 @@ export class ActivityLoader { const limit = params.limit ?? 50; const data = await this.tonapi.accounts.getAccountEvents({ before_lt: params.cursor ?? undefined, - accountId: this.addresses.ton, + accountId: this.tonRawAddress, subject_only: true, limit, }); @@ -36,7 +36,7 @@ export class ActivityLoader { const events = params.filter ? params.filter(data.events) : data.events; const actions = ActivityModel.createActions( { - ownerAddress: this.addresses.ton, + ownerAddress: this.tonRawAddress, source: ActionSource.Ton, events, }, @@ -80,14 +80,14 @@ export class ActivityLoader { const limit = params.limit ?? 50; const data = await this.tonapi.accounts.getAccountJettonHistoryById({ before_lt: params.cursor ?? undefined, - accountId: this.addresses.ton, + accountId: this.tonRawAddress, jettonId: params.jettonId, limit, }); const actions = ActivityModel.createActions( { - ownerAddress: this.addresses.ton, + ownerAddress: this.tonRawAddress, source: ActionSource.Ton, events: data.events, }, @@ -105,13 +105,13 @@ export class ActivityLoader { public async loadTonAction(actionId: ActionId) { const { eventId, actionIndex } = this.splitActionId(actionId); const event = await this.tonapi.accounts.getAccountEvent({ - accountId: this.addresses.ton, + accountId: this.tonRawAddress, eventId, }); if (event) { const action = ActivityModel.createAction({ - ownerAddress: this.addresses.ton, + ownerAddress: this.tonRawAddress, source: ActionSource.Ton, actionIndex, event, diff --git a/packages/@core-js/src/Activity/JettonActivityList.ts b/packages/mobile/src/wallet/Activity/JettonActivityList.ts similarity index 89% rename from packages/@core-js/src/Activity/JettonActivityList.ts rename to packages/mobile/src/wallet/Activity/JettonActivityList.ts index 0e8a06886..39254d750 100644 --- a/packages/@core-js/src/Activity/JettonActivityList.ts +++ b/packages/mobile/src/wallet/Activity/JettonActivityList.ts @@ -1,8 +1,9 @@ import { ActivityModel, ActivitySection } from '../models/ActivityModel'; -import { Storage } from '../declarations/Storage'; + import { ActivityLoader } from './ActivityLoader'; -import { Logger } from '../utils/Logger'; -import { State } from '../utils/State'; + +import { TonRawAddress } from '../WalletTypes'; +import { Logger, State, Storage } from '@tonkeeper/core'; type JettonActivityListState = { sections: { [key in string]: ActivitySection[] }; @@ -24,14 +25,18 @@ export class JettonActivityList { error: null, }); - constructor(private activityLoader: ActivityLoader, private storage: Storage) { + constructor( + private tonRawAddress: TonRawAddress, + private activityLoader: ActivityLoader, + private storage: Storage, + ) { this.state.persist({ partialize: ({ sections }) => ({ sections }), storage: this.storage, - key: 'JettonActivityList', + key: `${this.tonRawAddress}/JettonActivityList`, }); } - + public async load(jettonId: string, cursor?: number | null) { try { this.state.set({ isLoading: true, error: null }); diff --git a/packages/@core-js/src/Activity/TonActivityList.ts b/packages/mobile/src/wallet/Activity/TonActivityList.ts similarity index 89% rename from packages/@core-js/src/Activity/TonActivityList.ts rename to packages/mobile/src/wallet/Activity/TonActivityList.ts index c1bc41377..cd2cab314 100644 --- a/packages/@core-js/src/Activity/TonActivityList.ts +++ b/packages/mobile/src/wallet/Activity/TonActivityList.ts @@ -1,9 +1,9 @@ import { ActivityModel, ActivitySection } from '../models/ActivityModel'; -import { AccountEvent, ActionTypeEnum } from '../TonAPI'; -import { Storage } from '../declarations/Storage'; +import { Logger, State, Storage } from '@tonkeeper/core'; import { ActivityLoader } from './ActivityLoader'; -import { Logger } from '../utils/Logger'; -import { State } from '../utils/State'; + +import { TonRawAddress } from '../WalletTypes'; +import { AccountEvent, ActionTypeEnum } from '@tonkeeper/core/src/TonAPI'; type TonActivityListState = { sections: ActivitySection[]; @@ -25,11 +25,15 @@ export class TonActivityList { error: null, }); - constructor(private activityLoader: ActivityLoader, private storage: Storage) { + constructor( + private tonRawAddress: TonRawAddress, + private activityLoader: ActivityLoader, + private storage: Storage, + ) { this.state.persist({ partialize: ({ sections }) => ({ sections }), storage: this.storage, - key: 'TonActivityList', + key: `${this.tonRawAddress}/TonActivityList`, }); } diff --git a/packages/shared/modules/AppServerSentEvents.ts b/packages/mobile/src/wallet/AppServerSentEvents.ts similarity index 100% rename from packages/shared/modules/AppServerSentEvents.ts rename to packages/mobile/src/wallet/AppServerSentEvents.ts diff --git a/packages/mobile/src/wallet/AppVault.ts b/packages/mobile/src/wallet/AppVault.ts new file mode 100644 index 000000000..f7f01b4ca --- /dev/null +++ b/packages/mobile/src/wallet/AppVault.ts @@ -0,0 +1,230 @@ +import { Vault } from '@tonkeeper/core'; +import { Mnemonic } from '@tonkeeper/core/src/utils/mnemonic'; +import { Buffer } from 'buffer'; +import { generateSecureRandom } from 'react-native-securerandom'; +import scrypt from 'react-native-scrypt'; +import * as SecureStore from 'expo-secure-store'; + +const { nacl } = require('react-native-tweetnacl'); + +interface DecryptedData { + identifier: string; + mnemonic: string; +} + +type VaultState = Record; + +export class AppVault implements Vault { + constructor() {} + protected keychainService = 'TKProtected'; + protected walletsKey = 'wallets'; + protected biometryKey = 'biometry_passcode'; + + private decryptedVaultState: VaultState = {}; + + private async saveVaultState(passcode: string) { + const state = JSON.stringify(this.decryptedVaultState); + + const encrypted = await ScryptBox.encrypt(passcode, state); + + await SecureStore.setItemAsync(this.walletsKey, JSON.stringify(encrypted)); + } + + public async verifyPasscode(passcode: string) { + await this.getDecryptedVaultState(passcode); + } + + public async import(identifier: string, mnemonic: string, passcode: string) { + /** check passcode */ + if (Object.keys(this.decryptedVaultState).length > 0) { + await this.verifyPasscode(passcode); + } + + if (!(await Mnemonic.validateMnemonic(mnemonic.split(' ')))) { + throw new Error('Mnemonic phrase is incorrect'); + } + + const keyPair = await Mnemonic.mnemonicToKeyPair(mnemonic.split(' ')); + + this.decryptedVaultState[identifier] = { identifier, mnemonic }; + + await this.saveVaultState(passcode); + + return keyPair; + } + + public async remove(identifier: string, passcode: string) { + delete this.decryptedVaultState[identifier]; + + await this.saveVaultState(passcode); + } + + public async destroy() { + try { + this.decryptedVaultState = {}; + await SecureStore.deleteItemAsync(this.walletsKey); + await SecureStore.deleteItemAsync(this.biometryKey, { + keychainService: this.keychainService, + }); + } catch {} + } + + private async getDecryptedVaultState(passcode: string) { + const encryptedJson = await SecureStore.getItemAsync(this.walletsKey); + + if (!encryptedJson) { + throw new Error('Vault is empty'); + } + + const encrypted = JSON.parse(encryptedJson); + const stateJson = await ScryptBox.decrypt(passcode, encrypted); + + return JSON.parse(stateJson) as VaultState; + } + + public async exportWithPasscode(identifier: string, passcode: string) { + this.decryptedVaultState = await this.getDecryptedVaultState(passcode); + + const data = this.decryptedVaultState[identifier]; + + if (!data) { + throw new Error(`Mnemonic doesn't exist, identifier: ${identifier}`); + } + + return data.mnemonic; + } + + public async changePasscode(passcode: string, newPasscode: string) { + this.decryptedVaultState = await this.getDecryptedVaultState(passcode); + + await this.saveVaultState(newPasscode); + } + + public async exportWithBiometry(identifier: string) { + const passcode = await SecureStore.getItemAsync(this.biometryKey, { + keychainService: this.keychainService, + }); + + if (!passcode) { + throw new Error('Biometry data is not found'); + } + + return this.exportWithPasscode(identifier, passcode); + } + + public async setupBiometry(passcode: string) { + await SecureStore.setItemAsync(this.biometryKey, passcode, { + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + keychainService: this.keychainService, + requireAuthentication: true, + }); + } + + public async removeBiometry() { + await SecureStore.deleteItemAsync(this.biometryKey, { + keychainService: this.keychainService, + }); + } +} + +export const ScryptBox = { + async encrypt(passcode: string, value: string) { + // default parameters + const N = 16384; // 16K*128*8 = 16 Mb of memory + const r = 8; + const p = 1; + + const salt = Buffer.from(await generateSecureRandom(32)); + const enckey = await scrypt( + Buffer.from(passcode, 'utf8'), + salt, + N, + r, + p, + 32, + 'buffer', + ); + const nonce = salt.slice(0, 24); + const ct: Uint8Array = nacl.secretbox( + Uint8Array.from(Buffer.from(value, 'utf8')), + Uint8Array.from(nonce), + Uint8Array.from(enckey), + ); + + return { + kind: 'encrypted-scrypt-tweetnacl', + N: N, // scrypt "cost" parameter + r: r, // scrypt "block size" parameter + p: p, // scrypt "parallelization" parameter + salt: salt.toString('hex'), // hex-encoded nonce/salt + ct: Buffer.from(ct).toString('hex'), // hex-encoded ciphertext + }; + }, + // Attempts to decrypt the vault and returns `true` if succeeded. + async decrypt(password: string, state: any): Promise { + if (state.kind === 'encrypted-scrypt-tweetnacl') { + const salt = Buffer.from(state.salt, 'hex'); + const { N, r, p } = state; + const enckey = await scrypt( + Buffer.from(password, 'utf8'), + salt, + N, + r, + p, + 32, + 'buffer', + ); + const nonce = salt.slice(0, 24); + const ct = Buffer.from(state.ct, 'hex'); + const pt = nacl.secretbox.open(ct, Uint8Array.from(nonce), Uint8Array.from(enckey)); + if (pt) { + const phrase = Utf8ArrayToString(pt); + return phrase; + } else { + throw new Error('Invald Passcode'); + } + } else { + throw new Error('Unsupported encryption format ' + state.kind); + } + }, +}; + +function Utf8ArrayToString(array: Uint8Array): string { + let out = ''; + let len = array.length; + let i = 0; + let c: any = null; + while (i < len) { + c = array[i++]; + switch (c >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + // 0xxxxxxx + out += String.fromCharCode(c); + break; + case 12: + case 13: + // 110x xxxx 10xx xxxx + let char = array[i++]; + out += String.fromCharCode(((c & 0x1f) << 6) | (char & 0x3f)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + let a = array[i++]; + let b = array[i++]; + out += String.fromCharCode( + ((c & 0x0f) << 12) | ((a & 0x3f) << 6) | ((b & 0x3f) << 0), + ); + break; + } + } + return out; +} + +export const vault = new AppVault(); diff --git a/packages/mobile/src/wallet/Tonkeeper.ts b/packages/mobile/src/wallet/Tonkeeper.ts new file mode 100644 index 000000000..41d6b9047 --- /dev/null +++ b/packages/mobile/src/wallet/Tonkeeper.ts @@ -0,0 +1,466 @@ +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { Wallet } from '$wallet/Wallet'; +import { Address } from '@tonkeeper/core/src/formatters/Address'; +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { TonPriceManager } from './managers/TonPriceManager'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { + ImportWalletInfo, + WalletConfig, + WalletContractVersion, + WalletNetwork, + WalletStyleConfig, + WalletType, +} from './WalletTypes'; +import { createTonApiInstance } from './utils'; +import { Vault } from '@tonkeeper/core'; +import { v4 as uuidv4 } from 'uuid'; +import { Mnemonic } from '@tonkeeper/core/src/utils/mnemonic'; +import { DEFAULT_WALLET_STYLE_CONFIG } from './constants'; +import { t } from '@tonkeeper/shared/i18n'; +import { Buffer } from 'buffer'; +import nacl from 'tweetnacl'; + +class PermissionsManager { + public notifications = true; + public biometry = true; +} + +type TonkeeperOptions = { + storage: Storage; + vault: Vault; +}; + +export interface MultiWalletMigrationData { + pubkey: string; + keychainItemName: string; + lockupConfig?: { + wallet_type: WalletContractVersion.LockupV1; + workchain: number; + config_pubkey: string; + allowed_destinations: string[]; + }; +} + +export interface WalletsStoreState { + wallets: WalletConfig[]; + selectedIdentifier: string; + biometryEnabled: boolean; + isMigrated: boolean; +} + +export class Tonkeeper { + public permissions: PermissionsManager; + public wallets: Map = new Map(); + public tonPrice: TonPriceManager; + + public migrationData: MultiWalletMigrationData | null = null; + + public walletSubscribers = new Set<(wallet: Wallet) => void>(); + + private tonapi: { + mainnet: TonAPI; + testnet: TonAPI; + }; + + private vault: Vault; + + private storage: Storage; + + public walletsStore = new State({ + wallets: [], + selectedIdentifier: '', + biometryEnabled: false, + isMigrated: false, + }); + + constructor(options: TonkeeperOptions) { + this.storage = options.storage; + this.vault = options.vault; + this.tonapi = { + mainnet: createTonApiInstance(), + testnet: createTonApiInstance(true), + }; + + this.permissions = new PermissionsManager(); + this.tonPrice = new TonPriceManager(this.tonapi.mainnet, this.storage); + + this.walletsStore.persist({ + storage: this.storage, + key: 'walletsStore', + }); + } + + public get wallet() { + return this.wallets.get(this.walletsStore.data.selectedIdentifier)!; + } + + public get walletForUnlock() { + return Array.from(this.wallets.values()).find((wallet) => !wallet.isWatchOnly)!; + } + + public get biometryEnabled() { + return this.walletsStore.data.biometryEnabled; + } + + public async init() { + try { + await Promise.all([this.walletsStore.rehydrate(), this.tonPrice.rehydrate()]); + + this.tonPrice.load(); + + if (!this.walletsStore.data.isMigrated) { + this.migrationData = await this.getMigrationData(); + } + + await Promise.all( + this.walletsStore.data.wallets.map((walletConfig) => + this.createWalletInstance(walletConfig), + ), + ); + + this.emitChangeWallet(); + } catch (err) { + console.log('TK:init', err); + } + } + + private async getMigrationData(): Promise { + const keychainName = 'mainnet_default'; + + try { + const data = await this.storage.getItem(`${keychainName}_wallet`); + + if (!data) { + return null; + } + + const json = JSON.parse(data); + + return { + pubkey: json.vault.tonPubkey, + keychainItemName: json.vault.name, + lockupConfig: + json.vault.version === WalletContractVersion.LockupV1 + ? { + wallet_type: WalletContractVersion.LockupV1, + workchain: json.vault.workchain ?? 0, + config_pubkey: json.vault.configPubKey, + allowed_destinations: json.vault.allowedDestinations, + } + : undefined, + }; + } catch { + return null; + } + } + + public tronStorageKey = 'temp-tron-address'; + + public async load() { + try { + const tronAddress = await this.storage.getItem(this.tronStorageKey); + return { + tronAddress: tronAddress ? JSON.parse(tronAddress) : null, + }; + } catch (err) { + console.error('[tk:load]', err); + return { tronAddress: null, tonProof: null }; + } + } + + public async enableBiometry(passcode: string) { + await this.vault.setupBiometry(passcode); + + this.walletsStore.set({ biometryEnabled: true }); + } + + public async disableBiometry() { + await this.vault.removeBiometry(); + + this.walletsStore.set({ biometryEnabled: false }); + } + + public async importWallet( + mnemonic: string, + passcode: string, + versions: WalletContractVersion[], + walletConfig: Pick< + WalletConfig, + 'network' | 'workchain' | 'configPubKey' | 'allowedDestinations' + >, + ): Promise { + const newWallets: WalletConfig[] = []; + + let keyPair: nacl.SignKeyPair; + for (const version of versions) { + const identifier = uuidv4(); + + keyPair = await this.vault.import(identifier, mnemonic, passcode); + + newWallets.push({ + ...DEFAULT_WALLET_STYLE_CONFIG, + ...walletConfig, + name: versions.length > 1 ? `${t('wallet_title')} ${version}` : t('wallet_title'), + version, + type: WalletType.Regular, + pubkey: Buffer.from(keyPair.publicKey).toString('hex'), + identifier, + }); + } + const versionsOrder = Object.values(WalletContractVersion); + + const sortedWallets = newWallets.sort((a, b) => { + const indexA = versionsOrder.indexOf(a.version); + const indexB = versionsOrder.indexOf(b.version); + return indexA - indexB; + }); + + this.walletsStore.set(({ wallets }) => ({ wallets: [...wallets, ...sortedWallets] })); + const walletsInstances = await Promise.all( + sortedWallets.map((wallet) => this.createWalletInstance(wallet)), + ); + walletsInstances.map((instance) => instance.tonProof.obtainProof(keyPair)); + + this.setWallet(walletsInstances[0]); + + return walletsInstances.map((item) => item.identifier); + } + + public async getWalletsInfo(mnemonic: string, isTestnet: boolean) { + const keyPair = await Mnemonic.mnemonicToKeyPair(mnemonic.split(' ')); + + const pubkey = Buffer.from(keyPair.publicKey).toString('hex'); + + const tonapi = isTestnet ? this.tonapi.testnet : this.tonapi.mainnet; + + const [{ accounts }, addresses] = await Promise.all([ + tonapi.pubkeys.getWalletsByPublicKey(pubkey), + Address.fromPubkey(pubkey, isTestnet), + ]); + + if (!addresses) { + throw new Error("Can't parse pubkey"); + } + + const accountsJettons = await Promise.all( + accounts.map((account) => + tonapi.accounts.getAccountJettonsBalances({ accountId: account.address }), + ), + ); + + const versionByAddress = Object.keys(addresses).reduce( + (acc, version) => ({ ...acc, [addresses[version].raw]: version }), + {}, + ); + + const wallets = accounts.map( + (account, index): ImportWalletInfo => ({ + version: versionByAddress[account.address], + address: account.address, + balance: account.balance, + tokens: accountsJettons[index].balances.length > 0, + }), + ); + + if (!wallets.some((wallet) => wallet.version === WalletContractVersion.v4R2)) { + wallets.push({ + version: WalletContractVersion.v4R2, + address: addresses.v4R2.raw, + balance: 0, + tokens: false, + }); + } + + const versions = Object.values(WalletContractVersion); + + return wallets.sort((a, b) => { + const indexA = versions.indexOf(a.version); + const indexB = versions.indexOf(b.version); + return indexA - indexB; + }); + } + + public async addWatchOnlyWallet(address: string) { + const rawAddress = Address.parse(address).toRaw(); + const { public_key: pubkey } = await this.tonapi.mainnet.accounts.getAccountPublicKey( + rawAddress, + ); + + const addresses = await Address.fromPubkey(pubkey, false); + + if (!addresses) { + throw new Error("Can't parse pubkey"); + } + + const versionByAddress = Object.keys(addresses).reduce( + (acc, version) => ({ ...acc, [addresses[version].raw]: version }), + {}, + ); + + const workchain = Number(rawAddress.split(':')[0]); + + const version = versionByAddress[rawAddress] as WalletContractVersion; + + const config: WalletConfig = { + ...DEFAULT_WALLET_STYLE_CONFIG, + identifier: uuidv4(), + network: WalletNetwork.mainnet, + type: WalletType.WatchOnly, + pubkey, + workchain, + version, + }; + + this.walletsStore.set(({ wallets }) => ({ wallets: [...wallets, config] })); + const wallet = await this.createWalletInstance(config); + this.setWallet(wallet); + + return [wallet.identifier]; + } + + public async removeWallet(identifier: string) { + try { + const nextWallet = + this.wallets.get( + Array.from(this.wallets.keys()).find((item) => item !== identifier) ?? '', + ) ?? null; + + this.walletsStore.set(({ wallets }) => ({ + wallets: wallets.filter((w) => w.identifier !== identifier), + selectedIdentifier: nextWallet?.identifier ?? '', + })); + const wallet = this.wallets.get(identifier); + wallet?.notifications.unsubscribe().catch(null); + wallet?.destroy(); + this.wallets.delete(identifier); + + if (this.wallets.size === 0) { + this.walletsStore.set({ biometryEnabled: false }); + this.vault.destroy(); + } + + this.emitChangeWallet(); + } catch (e) { + console.log('removeWallet error', e); + } + } + + public async removeAllWallets() { + this.walletsStore.set({ + wallets: [], + selectedIdentifier: '', + biometryEnabled: false, + }); + this.wallets.forEach((wallet) => { + wallet.notifications.unsubscribe().catch(null); + wallet.destroy(); + }); + this.wallets.clear(); + this.vault.destroy(); + this.emitChangeWallet(); + } + + private async createWalletInstance(walletConfig: WalletConfig) { + const addresses = await Address.fromPubkey( + walletConfig.pubkey, + walletConfig.network === WalletNetwork.testnet, + walletConfig.version === WalletContractVersion.LockupV1 ? walletConfig : undefined, + ); + + const wallet = new Wallet(walletConfig, addresses!, this.storage, this.tonPrice); + + await wallet.rehydrate(); + wallet.preload(); + + this.wallets.set(wallet.identifier, wallet); + + return wallet; + } + + public async updateWallet( + config: Partial, + passedIdentifiers?: string[], + ) { + try { + if (!this.wallet) { + return; + } + + const identifiers = passedIdentifiers ?? [this.wallet.identifier]; + + const updatedWallets = this.walletsStore.data.wallets.map( + (wallet): WalletConfig => { + if (identifiers.includes(wallet.identifier)) { + return { + ...wallet, + ...config, + name: + identifiers.length > 1 + ? `${config.name} ${wallet.version}` + : config.name ?? wallet.name, + }; + } + return wallet; + }, + ); + + this.walletsStore.set({ wallets: updatedWallets }); + + identifiers.forEach((identifier) => { + const currentConfig = updatedWallets.find( + (item) => item.identifier === identifier, + ); + const wallet = this.wallets.get(identifier); + if (wallet && currentConfig) { + wallet.setConfig(currentConfig); + } + }); + + this.emitChangeWallet(); + } catch {} + } + + public onChangeWallet(subscriber: (wallet: Wallet) => void) { + this.walletSubscribers.add(subscriber); + return () => { + this.walletSubscribers.delete(subscriber); + }; + } + + private emitChangeWallet() { + this.walletSubscribers.forEach((subscriber) => subscriber(this.wallet)); + } + + public switchWallet(identifier: string) { + const wallet = this.wallets.get(identifier); + this.setWallet(wallet!); + } + + private setWallet(wallet: Wallet | null) { + this.walletsStore.set({ selectedIdentifier: wallet?.identifier ?? '' }); + this.emitChangeWallet(); + } + + public async enableNotificationsForAll(identifiers: string[]) { + await Promise.all( + identifiers.map(async (identifier) => { + const wallet = this.wallets.get(identifier); + if (wallet) { + await wallet.notifications.subscribe(); + } + }), + ); + } + + public getWalletByAddress(address: string) { + return Array.from(this.wallets.values()).find( + (wallet) => !wallet.isTestnet && Address.compare(wallet.address.ton.raw, address), + ); + } + + public setMigrated() { + console.log('migrated'); + this.walletsStore.set({ isMigrated: true }); + } +} diff --git a/packages/mobile/src/wallet/Wallet/Wallet.ts b/packages/mobile/src/wallet/Wallet/Wallet.ts new file mode 100644 index 000000000..4ea4ca779 --- /dev/null +++ b/packages/mobile/src/wallet/Wallet/Wallet.ts @@ -0,0 +1,126 @@ +import { AddressesByVersion } from '@tonkeeper/core/src/formatters/Address'; + +import { EventSourceListener } from '@tonkeeper/core/src/declarations/ServerSentEvents'; + +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { TonPriceManager } from '../managers/TonPriceManager'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { WalletConfig } from '../WalletTypes'; +import { WalletContent } from './WalletContent'; +import { AppState, AppStateStatus, NativeEventSubscription } from 'react-native'; + +export interface WalletStatusState { + isReloading: boolean; + isLoading: boolean; + updatedAt: number; +} + +export class Wallet extends WalletContent { + static readonly INITIAL_STATUS_STATE: WalletStatusState = { + isReloading: false, + isLoading: false, + updatedAt: Date.now(), + }; + + public listener: EventSourceListener | null = null; + private appStateListener: NativeEventSubscription | null = null; + private prevAppState: AppStateStatus = 'active'; + private lastTimeAppActive = Date.now(); + + public status = new State(Wallet.INITIAL_STATUS_STATE); + + constructor( + public config: WalletConfig, + public tonAllAddresses: AddressesByVersion, + protected storage: Storage, + protected tonPrice: TonPriceManager, + ) { + super(config, tonAllAddresses, storage, tonPrice); + + const tonRawAddress = this.address.ton.raw; + + this.status.persist({ + partialize: ({ updatedAt }) => ({ updatedAt }), + storage: this.storage, + key: `${tonRawAddress}/status`, + }); + + this.listenTransactions(); + this.listenAppState(); + } + + public async rehydrate() { + await super.rehydrate(); + + this.status.rehydrate(); + } + + public async preload() { + this.logger.info('preload wallet data'); + try { + this.status.set({ isLoading: true }); + await super.preload(); + this.status.set({ isLoading: false, updatedAt: Date.now() }); + } catch { + this.status.set({ isLoading: false }); + } + } + + public async reload() { + this.logger.info('reload wallet data'); + try { + this.status.set({ isReloading: true }); + this.tonPrice.load(); + await super.reload(); + this.status.set({ isReloading: false, updatedAt: Date.now() }); + } catch { + this.status.set({ isReloading: false }); + } + } + + private listenTransactions() { + this.listener = this.sse.listen('/v2/sse/accounts/transactions', { + accounts: this.address.ton.raw, + }); + this.listener.addEventListener('open', () => { + this.logger.info('start listen transactions'); + }); + this.listener.addEventListener('error', (err) => { + this.logger.error('error listen transactions', err); + }); + this.listener.addEventListener('message', () => { + this.logger.info('message receive'); + this.preload(); + }); + } + + private stopListenTransactions() { + this.listener?.close(); + this.logger.info('stop listen transactions'); + } + + private listenAppState() { + this.appStateListener = AppState.addEventListener('change', (nextAppState) => { + // close transactions listener if app was in background + if (nextAppState === 'background') { + this.lastTimeAppActive = Date.now(); + } + // reload data if app was in background more than 5 minutes + if (nextAppState === 'active' && this.prevAppState === 'background') { + if (Date.now() - this.lastTimeAppActive > 1000 * 60 * 5) { + this.preload(); + this.stopListenTransactions(); + this.listenTransactions(); + } + } + + this.prevAppState = nextAppState; + }); + } + + public destroy() { + this.tonProof.destroy(); + this.appStateListener?.remove(); + this.stopListenTransactions(); + } +} diff --git a/packages/mobile/src/wallet/Wallet/WalletBase.ts b/packages/mobile/src/wallet/Wallet/WalletBase.ts new file mode 100644 index 000000000..86bf11264 --- /dev/null +++ b/packages/mobile/src/wallet/Wallet/WalletBase.ts @@ -0,0 +1,112 @@ +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { + WalletAddress, + WalletConfig, + WalletContractVersion, + WalletNetwork, + WalletType, +} from '../WalletTypes'; +import { Address, AddressesByVersion } from '@tonkeeper/core/src/formatters/Address'; +import { + createBatteryApiInstance, + createSseInstance, + createTonApiInstance, + createTronApiInstance, +} from '../utils'; +import { BatteryAPI } from '@tonkeeper/core/src/BatteryAPI'; +import { + ContractService, + ServerSentEvents, + Storage, + TronAPI, + WalletVersion, +} from '@tonkeeper/core'; +import { signProofForTonkeeper } from '@tonkeeper/core/src/utils/tonProof'; +import { storeStateInit } from '@ton/ton'; +import nacl from 'tweetnacl'; +import { beginCell } from '@ton/core'; +import { TronService } from '@tonkeeper/core/src/TronService'; +import { NamespacedLogger, logger } from '$logger'; + +export class WalletBase { + public identifier: string; + public pubkey: string; + public address: WalletAddress; + + public tronService: TronService; + + public tonapi: TonAPI; + protected batteryapi: BatteryAPI; + protected tronapi: TronAPI; + protected sse: ServerSentEvents; + + private tonProofStorageKey: string; + + protected logger: NamespacedLogger; + + constructor( + public config: WalletConfig, + public tonAllAddresses: AddressesByVersion, + protected storage: Storage, + ) { + this.identifier = config.identifier; + this.pubkey = config.pubkey; + + const tonAddress = Address.parse(this.tonAllAddresses[config.version].raw, { + bounceable: false, + }).toAll({ + testOnly: config.network === WalletNetwork.testnet, + }); + + this.address = { + tron: { proxy: '', owner: '' }, + ton: tonAddress, + }; + + this.logger = logger.extend(`Wallet ${this.address.ton.short}`); + + this.tonapi = createTonApiInstance(this.isTestnet); + this.batteryapi = createBatteryApiInstance(this.isTestnet); + this.tronapi = createTronApiInstance(this.isTestnet); + this.sse = createSseInstance(this.isTestnet); + + this.tronService = new TronService(this.address, this.tronapi); + + this.tonProofStorageKey = `${this.address.ton.raw}/tonProof`; + } + + public setConfig(config: WalletConfig) { + this.config = config; + } + + public isV4() { + return this.config.version === WalletContractVersion.v4R2; + } + + public get isLockup() { + return this.config.version === WalletContractVersion.LockupV1; + } + + public get isTestnet() { + return this.config.network === WalletNetwork.testnet; + } + + public get isWatchOnly() { + return this.config.type === WalletType.WatchOnly; + } + + public getLockupConfig() { + return { + wallet_type: this.config.version, + workchain: this.config.workchain, + config_pubkey: this.config.configPubKey, + allowed_destinations: this.config.allowedDestinations, + }; + } + + public async getWalletInfo() { + return await this.tonapi.accounts.getAccount(this.address.ton.raw); + } + + protected async rehydrate() {} +} diff --git a/packages/mobile/src/wallet/Wallet/WalletContent.ts b/packages/mobile/src/wallet/Wallet/WalletContent.ts new file mode 100644 index 000000000..215fd532e --- /dev/null +++ b/packages/mobile/src/wallet/Wallet/WalletContent.ts @@ -0,0 +1,166 @@ +import { Address, AddressesByVersion } from '@tonkeeper/core/src/formatters/Address'; + +import { ActivityList } from '../Activity/ActivityList'; +import { NftsManager } from '../managers/NftsManager'; +import { SubscriptionsManager } from '../managers/SubscriptionsManager'; + +import { BalancesManager } from '../managers/BalancesManager'; +import { ActivityLoader } from '../Activity/ActivityLoader'; +import { TonActivityList } from '../Activity/TonActivityList'; +import { JettonActivityList } from '../Activity/JettonActivityList'; +import { TonInscriptions } from '../managers/TonInscriptions'; +import { JettonsManager } from '../managers/JettonsManager'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { TokenApprovalManager } from '../managers/TokenApprovalManager'; +import { TonPriceManager } from '../managers/TonPriceManager'; +import { StakingManager } from '../managers/StakingManager'; +import { WalletConfig } from '../WalletTypes'; +import { WalletBase } from './WalletBase'; +import BigNumber from 'bignumber.js'; +import { BatteryManager } from '../managers/BatteryManager'; +import { NotificationsManager } from '$wallet/managers/NotificationsManager'; +import { TonProofManager } from '$wallet/managers/TonProofManager'; + +export interface WalletStatusState { + isReloading: boolean; + isLoading: boolean; + updatedAt: number; +} + +export class WalletContent extends WalletBase { + public activityLoader: ActivityLoader; + public tonProof: TonProofManager; + public tokenApproval: TokenApprovalManager; + public balances: BalancesManager; + public nfts: NftsManager; + public jettons: JettonsManager; + public tonInscriptions: TonInscriptions; + public staking: StakingManager; + public subscriptions: SubscriptionsManager; + public battery: BatteryManager; + public notifications: NotificationsManager; + public activityList: ActivityList; + public tonActivityList: TonActivityList; + public jettonActivityList: JettonActivityList; + + constructor( + public config: WalletConfig, + public tonAllAddresses: AddressesByVersion, + protected storage: Storage, + protected tonPrice: TonPriceManager, + ) { + super(config, tonAllAddresses, storage); + + const tonRawAddress = this.address.ton.raw; + + this.activityLoader = new ActivityLoader(tonRawAddress, this.tonapi, this.tronapi); + + this.tonProof = new TonProofManager(this.identifier, this.tonapi); + this.tokenApproval = new TokenApprovalManager(tonRawAddress, this.storage); + this.balances = new BalancesManager( + tonRawAddress, + this.config, + this.tonapi, + this.storage, + ); + this.nfts = new NftsManager(tonRawAddress, this.tonapi, this.storage); + this.jettons = new JettonsManager( + tonRawAddress, + this.tonPrice, + this.tokenApproval, + this.tonapi, + this.storage, + ); + this.tonInscriptions = new TonInscriptions(tonRawAddress, this.tonapi, this.storage); + this.staking = new StakingManager( + tonRawAddress, + this.jettons, + this.tonapi, + this.storage, + ); + this.subscriptions = new SubscriptionsManager(tonRawAddress, this.storage); + this.battery = new BatteryManager(this.tonProof, this.batteryapi, this.storage); + this.notifications = new NotificationsManager( + tonRawAddress, + this.isTestnet, + this.storage, + this.logger, + ); + this.activityList = new ActivityList( + tonRawAddress, + this.activityLoader, + this.storage, + ); + this.tonActivityList = new TonActivityList( + tonRawAddress, + this.activityLoader, + this.storage, + ); + this.jettonActivityList = new JettonActivityList( + tonRawAddress, + this.activityLoader, + this.storage, + ); + } + + protected async rehydrate() { + await super.rehydrate(); + + this.tonProof.rehydrate(); + this.tokenApproval.rehydrate(); + this.balances.rehydrate(); + this.nfts.rehydrate(); + this.jettons.rehydrate(); + this.tonInscriptions.rehydrate(); + this.staking.rehydrate(); + this.subscriptions.rehydrate(); + this.battery.rehydrate(); + this.notifications.rehydrate(); + this.activityList.rehydrate(); + this.tonActivityList.rehydrate(); + this.jettonActivityList.rehydrate(); + } + + protected async preload() { + await Promise.all([ + this.balances.load(), + this.nfts.load(), + this.jettons.load(), + this.tonInscriptions.load(), + this.staking.load(), + this.subscriptions.load(), + this.battery.load(), + this.activityList.load(), + ]); + } + + public async reload() { + await Promise.all([ + this.balances.reload(), + this.nfts.reload(), + this.jettons.reload(), + this.tonInscriptions.load(), + this.staking.reload(), + this.subscriptions.reload(), + this.battery.load(), + this.activityList.reload(), + ]); + } + + public get totalFiat() { + const ton = new BigNumber(this.balances.state.data.ton).multipliedBy( + this.tonPrice.state.data.ton.fiat, + ); + const jettons = this.jettons.state.data.jettonBalances.reduce((total, jetton) => { + const rate = + this.jettons.state.data.jettonRates[Address.parse(jetton.jettonAddress).toRaw()]; + return rate + ? total.plus(new BigNumber(jetton.balance).multipliedBy(rate.fiat)) + : total; + }, new BigNumber(0)); + const staking = new BigNumber(this.staking.state.data.stakingBalance).multipliedBy( + this.tonPrice.state.data.ton.fiat, + ); + return ton.plus(jettons).plus(staking).toString(); + } +} diff --git a/packages/mobile/src/wallet/Wallet/index.ts b/packages/mobile/src/wallet/Wallet/index.ts new file mode 100644 index 000000000..7df19835e --- /dev/null +++ b/packages/mobile/src/wallet/Wallet/index.ts @@ -0,0 +1 @@ +export * from './Wallet'; diff --git a/packages/mobile/src/wallet/WalletTypes.ts b/packages/mobile/src/wallet/WalletTypes.ts new file mode 100644 index 000000000..d8c062978 --- /dev/null +++ b/packages/mobile/src/wallet/WalletTypes.ts @@ -0,0 +1,85 @@ +import { AddressFormats } from '@tonkeeper/core/src/formatters/Address'; +import { WalletCurrency } from '@tonkeeper/core/src/utils/AmountFormatter/FiatCurrencyConfig'; +import { WalletColor } from '@tonkeeper/uikit'; + +export type TonFriendlyAddress = string; +export type TonRawAddress = string; + +export type TonAddress = { + version: WalletContractVersion; + friendly: TonFriendlyAddress; + raw: TonRawAddress; +}; + +export enum WalletNetwork { + mainnet = -239, + testnet = -3, +} + +export enum WalletType { + Regular = 'Regular', + Lockup = 'Lockup', + WatchOnly = 'WatchOnly', +} + +export enum WalletContractVersion { + v4R2 = 'v4R2', + v4R1 = 'v4R1', + v3R2 = 'v3R2', + v3R1 = 'v3R1', + LockupV1 = 'lockup-0.1', +} + +export type TronAddresses = { + proxy: string; + owner: string; +}; + +export type WalletAddress = { + tron?: TronAddresses; + ton: AddressFormats; +}; + +export type StoreWalletInfo = { + pubkey: string; + currency: WalletCurrency; + network: WalletNetwork; + kind: WalletType; +}; + +export type TonWalletState = { + address: TonAddress; + allAddresses: { [key in WalletContractVersion]: TonAddress }; +}; + +export interface TokenRate { + fiat: number; + ton: number; + usd: number; + diff_24h: string; +} + +export interface WalletStyleConfig { + name: string; + color: WalletColor; + emoji: string; +} + +export interface WalletConfig extends WalletStyleConfig { + identifier: string; + pubkey: string; + network: WalletNetwork; + type: WalletType; + version: WalletContractVersion; + workchain: number; + /** lockup */ + allowedDestinations?: string; + configPubKey?: string; +} + +export interface ImportWalletInfo { + version: WalletContractVersion; + address: string; + balance: number; + tokens: boolean; +} diff --git a/packages/mobile/src/wallet/constants.ts b/packages/mobile/src/wallet/constants.ts new file mode 100644 index 000000000..9635631e3 --- /dev/null +++ b/packages/mobile/src/wallet/constants.ts @@ -0,0 +1,9 @@ +import { WalletColor } from '@tonkeeper/uikit/src/utils/walletColor'; +import { WalletStyleConfig } from './WalletTypes'; +import { t } from '@tonkeeper/shared/i18n'; + +export const DEFAULT_WALLET_STYLE_CONFIG: WalletStyleConfig = { + name: t('wallet_title'), + color: WalletColor.SteelGray, + emoji: '😀', +}; diff --git a/packages/mobile/src/wallet/index.ts b/packages/mobile/src/wallet/index.ts new file mode 100644 index 000000000..eb8cd03df --- /dev/null +++ b/packages/mobile/src/wallet/index.ts @@ -0,0 +1,12 @@ +import { AppStorage } from '@tonkeeper/shared/modules/AppStorage'; +import { Tonkeeper } from './Tonkeeper'; +import { vault } from './AppVault'; + +export const storage = new AppStorage(); + +export const tk = new Tonkeeper({ + storage, + vault, +}); + +export { vault }; diff --git a/packages/mobile/src/wallet/managers/BalancesManager.ts b/packages/mobile/src/wallet/managers/BalancesManager.ts new file mode 100644 index 000000000..d7295d4e4 --- /dev/null +++ b/packages/mobile/src/wallet/managers/BalancesManager.ts @@ -0,0 +1,131 @@ +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { + TonRawAddress, + WalletConfig, + WalletContractVersion, + WalletNetwork, +} from '../WalletTypes'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { AmountFormatter } from '@tonkeeper/core/src/utils/AmountFormatter'; +import { State } from '@tonkeeper/core/src/utils/State'; +import TonWeb from 'tonweb'; +import { config } from '$config'; +import BigNumber from 'bignumber.js'; +import { LockupWalletV1 } from 'tonweb/dist/types/contract/lockup/lockup-wallet-v1'; + +export interface BalancesState { + isReloading: boolean; + isLoading: boolean; + ton: string; + tonLocked: string; + tonRestricted: string; +} + +export class BalancesManager { + static readonly INITIAL_STATE: BalancesState = { + isReloading: false, + isLoading: false, + ton: '0', + tonLocked: '0', + tonRestricted: '0', + }; + + public state = new State(BalancesManager.INITIAL_STATE); + + constructor( + private tonRawAddress: TonRawAddress, + private walletConfig: WalletConfig, + private tonapi: TonAPI, + private storage: Storage, + ) { + this.state.persist({ + partialize: ({ ton, tonLocked, tonRestricted }) => ({ + ton, + tonLocked, + tonRestricted, + }), + storage: this.storage, + key: `${this.tonRawAddress}/balances`, + }); + } + + private get isLockup() { + return this.walletConfig.version === WalletContractVersion.LockupV1; + } + + public async getLockupBalances() { + try { + const isTestnet = this.walletConfig.network === WalletNetwork.testnet; + + const tonweb = new TonWeb( + new TonWeb.HttpProvider(config.get('tonEndpoint', isTestnet), { + apiKey: config.get('tonEndpointAPIKey', isTestnet), + }), + ); + + const tonPublicKey = Uint8Array.from(Buffer.from(this.walletConfig.pubkey, 'hex')); + + const tonWallet: LockupWalletV1 = new tonweb.lockupWallet.all[ + this.walletConfig.version + ](tonweb.provider, { + publicKey: tonPublicKey, + wc: this.walletConfig.workchain ?? 0, + config: { + wallet_type: this.walletConfig.version, + config_public_key: this.walletConfig.configPubKey, + allowed_destinations: this.walletConfig.allowedDestinations, + }, + }); + + const balances = await tonWallet.getBalances(); + const result = balances.map((item: number) => + AmountFormatter.fromNanoStatic(item.toString()), + ); + result[0] = new BigNumber(result[0]).minus(result[1]).minus(result[2]).toString(); + + return result; + } catch (e) { + if (e?.response?.status === 404) { + return ['0', '0', '0']; + } + + throw e; + } + } + + public async load() { + try { + this.state.set({ isLoading: true }); + + if (this.isLockup) { + const [ton, tonLocked, tonRestricted] = await this.getLockupBalances(); + + this.state.set({ isLoading: false, ton, tonLocked, tonRestricted }); + return; + } + + const account = await this.tonapi.accounts.getAccount(this.tonRawAddress); + + this.state.set({ + isLoading: false, + ton: AmountFormatter.fromNanoStatic(account.balance), + }); + } catch (e) { + this.state.set({ + isLoading: false, + }); + + throw e; + } + } + + public async reload() { + this.state.set({ isReloading: true }); + await this.load(); + this.state.set({ isReloading: false }); + } + + public async rehydrate() { + return this.state.rehydrate(); + } +} diff --git a/packages/@core-js/src/managers/BatteryManager.ts b/packages/mobile/src/wallet/managers/BatteryManager.ts similarity index 53% rename from packages/@core-js/src/managers/BatteryManager.ts rename to packages/mobile/src/wallet/managers/BatteryManager.ts index 2f8b9f8c8..24217cfc7 100644 --- a/packages/@core-js/src/managers/BatteryManager.ts +++ b/packages/mobile/src/wallet/managers/BatteryManager.ts @@ -1,24 +1,23 @@ -import { WalletContext, WalletIdentity } from '../Wallet'; -import { MessageConsequences } from '../TonAPI'; -import { Storage } from '../declarations/Storage'; -import { State } from '../utils/State'; +import { BatteryAPI } from '@tonkeeper/core/src/BatteryAPI'; +import { MessageConsequences } from '@tonkeeper/core/src/TonAPI'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { TonProofManager } from '$wallet/managers/TonProofManager'; export interface BatteryState { isLoading: boolean; balance?: string; } -export const batteryState = new State({ - isLoading: false, - balance: undefined, -}); - export class BatteryManager { - public state = batteryState; + public state = new State({ + isLoading: false, + balance: undefined, + }); constructor( - private ctx: WalletContext, - private identity: WalletIdentity, + private tonProof: TonProofManager, + private batteryapi: BatteryAPI, private storage: Storage, ) { this.state.persist({ @@ -30,10 +29,14 @@ export class BatteryManager { public async fetchBalance() { try { + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + this.state.set({ isLoading: true }); - const data = await this.ctx.batteryapi.getBalance({ + const data = await this.batteryapi.getBalance({ headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, }); this.state.set({ isLoading: false, balance: data.balance }); @@ -44,9 +47,13 @@ export class BatteryManager { public async getExcessesAccount() { try { - const data = await this.ctx.batteryapi.getConfig({ + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + const data = await this.batteryapi.getConfig({ headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, }); @@ -58,11 +65,15 @@ export class BatteryManager { public async applyPromo(promoCode: string) { try { - const data = await this.ctx.batteryapi.promoCode.promoCodeBatteryPurchase( + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + const data = await this.batteryapi.promoCode.promoCodeBatteryPurchase( { promo_code: promoCode }, { headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, }, ); @@ -79,11 +90,15 @@ export class BatteryManager { public async makeIosPurchase(transactions: { id: string }[]) { try { - const data = await this.ctx.batteryapi.ios.iosBatteryPurchase( + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + const data = await this.batteryapi.ios.iosBatteryPurchase( { transactions: transactions }, { headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, }, ); @@ -98,11 +113,15 @@ export class BatteryManager { public async makeAndroidPurchase(purchases: { token: string; product_id: string }[]) { try { - const data = await this.ctx.batteryapi.android.androidBatteryPurchase( + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + const data = await this.batteryapi.android.androidBatteryPurchase( { purchases }, { headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, }, ); @@ -117,11 +136,15 @@ export class BatteryManager { public async sendMessage(boc: string) { try { - await this.ctx.batteryapi.sendMessage( + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + await this.batteryapi.sendMessage( { boc }, { headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, format: 'text', }, @@ -135,11 +158,15 @@ export class BatteryManager { public async emulate(boc: string): Promise { try { - return await this.ctx.batteryapi.emulate.emulateMessageToWallet( + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + return await this.batteryapi.emulate.emulateMessageToWallet( { boc }, { headers: { - 'X-TonConnect-Auth': this.identity.tonProof, + 'X-TonConnect-Auth': this.tonProof.tonProofToken, }, }, ); @@ -148,6 +175,10 @@ export class BatteryManager { } } + public async load() { + return this.fetchBalance(); + } + public async rehydrate() { return this.state.rehydrate(); } diff --git a/packages/mobile/src/wallet/managers/JettonsManager.ts b/packages/mobile/src/wallet/managers/JettonsManager.ts new file mode 100644 index 000000000..501c39be3 --- /dev/null +++ b/packages/mobile/src/wallet/managers/JettonsManager.ts @@ -0,0 +1,159 @@ +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { TokenRate, TonRawAddress } from '../WalletTypes'; +import { Logger } from '@tonkeeper/core/src/utils/Logger'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { JettonBalanceModel } from '../models/JettonBalanceModel'; +import { Address } from '@tonkeeper/core/src/formatters/Address'; +import { TokenApprovalManager } from './TokenApprovalManager'; +import { TonPriceManager } from './TonPriceManager'; + +export type JettonsState = { + jettonBalances: JettonBalanceModel[]; + jettonRates: Record; + error?: string | null; + isReloading: boolean; + isLoading: boolean; +}; + +export class JettonsManager { + static readonly INITIAL_STATE: JettonsState = { + isReloading: false, + isLoading: false, + jettonBalances: [], + jettonRates: {}, + error: null, + }; + + public state = new State(JettonsManager.INITIAL_STATE); + + constructor( + private tonRawAddress: TonRawAddress, + private tonPrice: TonPriceManager, + private tokenApproval: TokenApprovalManager, + private tonapi: TonAPI, + private storage: Storage, + ) { + this.state.persist({ + partialize: ({ jettonBalances, jettonRates }) => ({ jettonBalances, jettonRates }), + storage: this.storage, + key: `${this.tonRawAddress}/jettons`, + }); + } + + public async loadRate(address: string) { + const jettonAddress = new Address(address).toRaw(); + const currency = this.tonPrice.state.data.currency.toUpperCase(); + + try { + const response = await this.tonapi.rates.getRates({ + tokens: jettonAddress, + currencies: ['TON', 'USD', currency].join(','), + }); + + const rate = response.rates[jettonAddress]; + + this.state.set({ + ...this.state.data.jettonRates, + [jettonAddress]: { + fiat: rate?.prices![currency], + usd: rate?.prices!.USD, + ton: rate?.prices!.TON, + diff_24h: rate?.diff_24h![currency], + }, + }); + } catch {} + } + + public async load() { + try { + this.state.set({ isLoading: true, error: null }); + + const currency = this.tonPrice.state.data.currency.toUpperCase(); + + const accountJettons = await this.tonapi.accounts.getAccountJettonsBalances({ + accountId: this.tonRawAddress, + currencies: ['TON', 'USD', currency].join(','), + }); + + const jettonBalances = accountJettons.balances + .filter((item) => { + if (item.balance === '0') { + return false; + } + + return true; + }) + .map((item) => { + return new JettonBalanceModel(item); + }); + + // Move Token to pending if name or symbol changed + this.state.data.jettonBalances.forEach((balance: JettonBalanceModel) => { + const newBalance = jettonBalances.find((b) => + Address.compare(b.jettonAddress, balance.jettonAddress), + ); + if ( + newBalance && + (balance.metadata.name !== newBalance.metadata.name || + balance.metadata.symbol !== newBalance.metadata.symbol) + ) { + this.tokenApproval.removeTokenStatus(balance.jettonAddress); + } + }); + + const jettonRates = accountJettons.balances.reduce( + (acc, item) => { + if (!item.price) { + return acc; + } + + return { + ...acc, + [item.jetton.address]: { + fiat: item.price?.prices![currency], + usd: item.price?.prices!.USD, + ton: item.price?.prices!.TON, + diff_24h: item.price?.diff_24h![currency], + }, + }; + }, + {}, + ); + + this.state.set({ + isLoading: false, + jettonBalances, + jettonRates: { ...this.state.data.jettonRates, ...jettonRates }, + }); + } catch (err) { + const message = `[JettonsManager]: ${Logger.getErrorMessage(err)}`; + console.log(message); + this.state.set({ + isLoading: false, + error: message, + }); + } + } + + public getLoadedJetton(jettonAddress: string) { + return this.state.data.jettonBalances.find((item) => + Address.compare(item.jettonAddress, jettonAddress), + ); + } + + public async rehydrate() { + return this.state.rehydrate(); + } + + public async reload() { + this.state.set({ isReloading: true }); + await this.load(); + this.state.set({ isReloading: false }); + } + + public reset() { + this.state.clear(); + this.state.clearPersist(); + } +} diff --git a/packages/mobile/src/wallet/managers/NftsManager.tsx b/packages/mobile/src/wallet/managers/NftsManager.tsx new file mode 100644 index 000000000..c94106936 --- /dev/null +++ b/packages/mobile/src/wallet/managers/NftsManager.tsx @@ -0,0 +1,207 @@ +import { CustomNftItem, NftImage } from '@tonkeeper/core/src/TonAPI/CustomNftItems'; +import { Address } from '@tonkeeper/core/src/formatters/Address'; +import { NftItem, TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { TonRawAddress } from '$wallet/WalletTypes'; + +export type NftsState = { + nfts: Record; + selectedDiamond: NftItem | null; + accountNfts: Record; + isReloading: boolean; + isLoading: boolean; +}; + +export class NftsManager { + static readonly INITIAL_STATE: NftsState = { + nfts: {}, + selectedDiamond: null, + accountNfts: {}, + isReloading: false, + isLoading: false, + }; + + public state = new State(NftsManager.INITIAL_STATE); + + constructor( + private tonRawAddress: TonRawAddress, + private tonapi: TonAPI, + private storage: Storage, + ) { + this.state.persist({ + partialize: ({ accountNfts, selectedDiamond }) => ({ + accountNfts, + selectedDiamond, + }), + storage: this.storage, + key: `${this.tonRawAddress}/nfts`, + }); + } + + public setSelectedDiamond(nftAddress: string | null) { + this.state.set(({ accountNfts }) => ({ + selectedDiamond: nftAddress ? accountNfts[Address.parse(nftAddress).toRaw()] : null, + })); + } + + public async load() { + try { + this.state.set({ isLoading: true }); + + const response = await this.tonapi.accounts.getAccountNftItems({ + accountId: this.tonRawAddress, + indirect_ownership: true, + }); + + const accountNfts = response.nft_items.reduce>( + (acc, item) => { + if (item.metadata?.render_type !== 'hidden') { + acc[item.address] = item; + } + return acc; + }, + {}, + ); + + this.state.set({ accountNfts }); + + if (this.state.data.selectedDiamond) { + this.setSelectedDiamond(this.state.data.selectedDiamond.address); + } + } catch { + } finally { + this.state.set({ isLoading: false }); + } + } + + public reset() { + this.state.set(NftsManager.INITIAL_STATE); + } + + public async rehydrate() { + return this.state.rehydrate(); + } + + public async reload() { + this.state.set({ isReloading: true }); + await this.load(); + this.state.set({ isReloading: false }); + } + + public updateNftOwner(nftAddress: string, ownerAddress: string) { + this.state.set((state) => { + if (state.nfts[nftAddress]?.owner) { + state.nfts[nftAddress].owner!.address = ownerAddress; + } + + return state; + }); + } + + public getCachedByAddress(nftAddress: string, existingNftItem?: NftItem) { + if (existingNftItem) { + return this.makeCustomNftItem(existingNftItem); + } + + const address = new Address(nftAddress); + + const nftItem = + this.state.data.accountNfts[address.toRaw()] ?? + this.state.data.nfts[address.toRaw()]; + if (nftItem) { + return this.makeCustomNftItem(nftItem); + } + + return null; + } + + public async fetchByAddress(nftAddress: string) { + const nftItem = await this.tonapi.nfts.getNftItemByAddress(nftAddress); + + if (nftItem) { + this.state.set({ nfts: { ...this.state.data.nfts, [nftItem.address]: nftItem } }); + + const customNftItem = this.makeCustomNftItem(nftItem); + + return customNftItem; + } + + throw new Error('No nftItem'); + } + + public makeCustomNftItem(nftItem: NftItem) { + const image = (nftItem.previews ?? []).reduce( + (acc, image) => { + if (image.resolution === '5x5') { + acc.preview = image.url; + } + + if (image.resolution === '100x100') { + acc.small = image.url; + } + + if (image.resolution === '500x500') { + acc.medium = image.url; + } + + if (image.resolution === '1500x1500') { + acc.large = image.url; + } + + return acc; + }, + { + preview: null, + small: null, + medium: null, + large: null, + }, + ); + + const isDomain = !!nftItem.dns; + const isUsername = isTelegramUsername(nftItem.dns); + + const customNftItem: CustomNftItem = { + ...nftItem, + name: nftItem.metadata.name, + isUsername, + isDomain, + image, + }; + + if (customNftItem.metadata) { + customNftItem.marketplaceURL = nftItem.metadata.external_url; + } + + // Custom collection name + if (isDomain && customNftItem.collection) { + customNftItem.collection.name = 'TON DNS'; + } + + // Custom nft name + if (isDomain) { + customNftItem.name = modifyNftName(nftItem.dns)!; + } else if (!customNftItem.name) { + customNftItem.name = Address.toShort(nftItem.address); + } + + return customNftItem; + } +} + +export const domainToUsername = (name?: string) => { + return name ? '@' + name.replace('.t.me', '') : ''; +}; + +export const isTelegramUsername = (name?: string) => { + return name?.endsWith('.t.me') || false; +}; + +export const modifyNftName = (name?: string) => { + if (isTelegramUsername(name)) { + return domainToUsername(name); + } + + return name; +}; diff --git a/packages/mobile/src/wallet/managers/NotificationsManager.ts b/packages/mobile/src/wallet/managers/NotificationsManager.ts new file mode 100644 index 000000000..e04ec74c0 --- /dev/null +++ b/packages/mobile/src/wallet/managers/NotificationsManager.ts @@ -0,0 +1,153 @@ +import DeviceInfo from 'react-native-device-info'; +import { config } from '$config'; +import { TonRawAddress } from '../WalletTypes'; +import { Address, State, Storage, network } from '@tonkeeper/core'; +import { i18n } from '@tonkeeper/shared/i18n'; +import { isAndroid } from '$utils'; +import { PermissionsAndroid, Platform } from 'react-native'; +import messaging from '@react-native-firebase/messaging'; +import { NamespacedLogger } from '$logger'; + +export interface NotificationsState { + isSubscribed: boolean; +} + +export class NotificationsManager { + static readonly INITIAL_STATE: NotificationsState = { + isSubscribed: false, + }; + + public state = new State(NotificationsManager.INITIAL_STATE); + + constructor( + private tonRawAddress: TonRawAddress, + private isTestnet: boolean, + private storage: Storage, + private logger: NamespacedLogger, + ) { + this.state.persist({ + partialize: ({ isSubscribed }) => ({ isSubscribed }), + storage: this.storage, + key: `${this.tonRawAddress}/notifications`, + }); + } + + public async rehydrate() { + return this.state.rehydrate(); + } + + public async subscribe() { + this.logger.info('NotificationsManager.subscribe call'); + + const token = await this.requestUserPermissionAndGetToken(); + + if (!token) { + return false; + } + + const endpoint = `${config.get( + 'tonapiIOEndpoint', + this.isTestnet, + )}/v1/internal/pushes/plain/subscribe`; + const deviceId = DeviceInfo.getUniqueId(); + + await network.post(endpoint, { + params: { + locale: i18n.locale, + device: deviceId, + accounts: [ + { address: Address.parse(this.tonRawAddress).toFriendly({ bounceable: true }) }, + ], + token, + }, + }); + + this.state.set({ isSubscribed: true }); + + this.logger.info('NotificationsManager.subscribe done'); + + return true; + } + + public async unsubscribe() { + this.logger.info('NotificationsManager.unsubscribe call'); + + if (!this.state.data.isSubscribed) { + return false; + } + + const deviceId = DeviceInfo.getUniqueId(); + const endpoint = `${config.get('tonapiIOEndpoint', this.isTestnet)}/unsubscribe`; + + await network.post(endpoint, { + params: { + device: deviceId, + accounts: [ + { address: Address.parse(this.tonRawAddress).toFriendly({ bounceable: true }) }, + ], + }, + }); + + this.state.set({ isSubscribed: false }); + + this.logger.info('NotificationsManager.unsubscribe done'); + + return true; + } + + public async getIsDenied() { + try { + const authStatus = await messaging().hasPermission(); + return authStatus === messaging.AuthorizationStatus.DENIED; + } catch { + return false; + } + } + + private async getPermission() { + if (isAndroid && +Platform.Version >= 33) { + return await PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, + ); + } else { + const authStatus = await messaging().hasPermission(); + const enabled = + authStatus === messaging.AuthorizationStatus.AUTHORIZED || + authStatus === messaging.AuthorizationStatus.PROVISIONAL; + + return enabled; + } + } + + private async getToken() { + return await messaging().getToken(); + } + + private async requestUserPermissionAndGetToken() { + if (isAndroid && +Platform.Version >= 33) { + const status = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, + ); + const enabled = status === PermissionsAndroid.RESULTS.GRANTED; + + if (!enabled) { + return false; + } + } else { + const hasPermission = await this.getPermission(); + + if (!hasPermission) { + const authStatus = await messaging().requestPermission(); + const enabled = + authStatus === messaging.AuthorizationStatus.AUTHORIZED || + authStatus === messaging.AuthorizationStatus.PROVISIONAL; + + if (!enabled) { + return false; + } + } + } + + return await this.getToken(); + } +} diff --git a/packages/mobile/src/wallet/managers/StakingManager.ts b/packages/mobile/src/wallet/managers/StakingManager.ts new file mode 100644 index 000000000..105926afd --- /dev/null +++ b/packages/mobile/src/wallet/managers/StakingManager.ts @@ -0,0 +1,293 @@ +import { + AccountStakingInfo, + PoolImplementationType, + PoolInfo, + TonAPI, +} from '@tonkeeper/core/src/TonAPI'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { TonRawAddress } from '../WalletTypes'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { JettonMetadata } from '../models/JettonBalanceModel'; +import { JettonsManager } from './JettonsManager'; +import BigNumber from 'bignumber.js'; +import { AmountFormatter } from '@tonkeeper/core/src/utils/AmountFormatter'; +import { isEqual } from 'lodash'; + +export interface StakingProvider { + id: PoolImplementationType; + name: string; + description: string; + url: string; + maxApy: number; + minStake: number; + socials: string[]; +} + +export type StakingInfo = Record; + +export enum StakingApiStatus { + BackgroundFetching = 'BackgroundFetching', + Refreshing = 'Refreshing', + Idle = 'Idle', +} + +export type StakingState = { + status: StakingApiStatus; + pools: PoolInfo[]; + highestApyPool: PoolInfo | null; + providers: StakingProvider[]; + stakingInfo: StakingInfo; + stakingJettons: Record; + stakingJettonsUpdatedAt: number; + stakingBalance: string; +}; + +export class StakingManager { + static readonly KNOWN_STAKING_IMPLEMENTATIONS = [ + PoolImplementationType.Whales, + PoolImplementationType.Tf, + PoolImplementationType.LiquidTF, + ]; + + static readonly INITIAL_STATE: StakingState = { + status: StakingApiStatus.Idle, + stakingInfo: {}, + stakingJettons: {}, + stakingJettonsUpdatedAt: 0, + pools: [], + providers: [], + highestApyPool: null, + stakingBalance: '0', + }; + + static calculatePoolBalance(pool: PoolInfo, stakingInfo: StakingInfo) { + const amount = new BigNumber( + AmountFormatter.fromNanoStatic(stakingInfo[pool.address]?.amount || '0'), + ); + const pendingDeposit = new BigNumber( + AmountFormatter.fromNanoStatic(stakingInfo[pool.address]?.pending_deposit || '0'), + ); + const pendingWithdraw = new BigNumber( + AmountFormatter.fromNanoStatic(stakingInfo[pool.address]?.pending_withdraw || '0'), + ); + const readyWithdraw = new BigNumber( + AmountFormatter.fromNanoStatic(stakingInfo[pool.address]?.ready_withdraw || '0'), + ); + const balance = amount + .plus(pendingDeposit) + .plus(readyWithdraw) + .plus( + pool.implementation === PoolImplementationType.LiquidTF ? pendingWithdraw : 0, + ); + + return balance; + } + + public state = new State(StakingManager.INITIAL_STATE); + + constructor( + private tonRawAddress: TonRawAddress, + private jettons: JettonsManager, + private tonapi: TonAPI, + private storage: Storage, + ) { + this.state.persist({ + partialize: ({ status: _status, ...state }) => state, + storage: this.storage, + key: `${this.tonRawAddress}/staking`, + }); + } + + public async load(silent?: boolean, updateIfBalanceSame = true) { + const { status } = this.state.data; + + if (status !== StakingApiStatus.Idle) { + if (status === StakingApiStatus.BackgroundFetching && !silent) { + this.state.set({ status: StakingApiStatus.Refreshing }); + } + + return; + } + + try { + this.state.set({ + status: silent + ? StakingApiStatus.BackgroundFetching + : StakingApiStatus.Refreshing, + }); + + const [poolsResponse, nominatorsResponse] = await Promise.allSettled([ + this.tonapi.staking.getStakingPools({ + available_for: this.tonRawAddress, + include_unverified: false, + }), + this.tonapi.staking.getAccountNominatorsPools(this.tonRawAddress), + ]); + + let pools = this.state.data.pools; + + let nextState: Partial = {}; + + // const tonstakersEnabled = !getFlag('disable_tonstakers'); + + if (poolsResponse.status === 'fulfilled') { + const { implementations } = poolsResponse.value; + + pools = poolsResponse.value.pools + // .filter( + // (pool) => + // tonstakersEnabled || + // pool.implementation !== PoolImplementationType.LiquidTF, + // ) + .map((pool) => { + if (pool.implementation !== PoolImplementationType.Whales) { + return pool; + } + + const cycle_start = pool.cycle_end > 0 ? pool.cycle_end - 36 * 3600 : 0; + + return { ...pool, cycle_start }; + }) + .sort((a, b) => { + if (a.name.includes('Tonkeeper') && !b.name.includes('Tonkeeper')) { + return -1; + } + + if (b.name.includes('Tonkeeper') && !a.name.includes('Tonkeeper')) { + return 1; + } + + if (a.name.includes('Tonkeeper') && b.name.includes('Tonkeeper')) { + return a.name.includes('#1') ? -1 : 1; + } + + if (a.apy === b.apy) { + return a.cycle_start > b.cycle_start ? 1 : -1; + } + + return a.apy > b.apy ? 1 : -1; + }); + + const providers = (Object.keys(implementations) as PoolImplementationType[]) + .filter((id) => pools.some((pool) => pool.implementation === id)) + .sort((a, b) => { + const indexA = StakingManager.KNOWN_STAKING_IMPLEMENTATIONS.indexOf(a); + const indexB = StakingManager.KNOWN_STAKING_IMPLEMENTATIONS.indexOf(b); + + if (indexA === -1 && indexB === -1) { + return 0; + } + + if (indexA === -1) { + return 1; + } + + if (indexB === -1) { + return -1; + } + + return indexA > indexB ? 1 : -1; + }) + .map((id): StakingProvider => { + const implementationPools = pools.filter( + (pool) => pool.implementation === id, + ); + const maxApy = Math.max(...implementationPools.map((pool) => pool.apy)); + const minStake = Math.min( + ...implementationPools.map((pool) => pool.min_stake), + ); + + return { id, maxApy, minStake, ...implementations[id] }; + }); + + const highestApyPool = pools.reduce((acc, cur) => { + if (!acc) { + return cur; + } + + return cur.apy > acc.apy ? cur : acc; + }, null); + + await Promise.all( + pools + .filter((pool) => pool.liquid_jetton_master) + .map((pool) => { + return (async () => { + if (this.state.data.stakingJettonsUpdatedAt + 3600 * 1000 > Date.now()) { + return; + } + + const [jettonInfo] = await Promise.all([ + this.tonapi.jettons.getJettonInfo(pool.liquid_jetton_master!), + this.jettons.loadRate(pool.liquid_jetton_master!), + ]); + + this.state.set((state) => ({ + stakingJettons: { + ...state.stakingJettons, + [pool.liquid_jetton_master!]: { + ...jettonInfo.metadata, + decimals: Number(jettonInfo.metadata.decimals), + }, + }, + })); + })(); + }), + ); + + this.state.set({ stakingJettonsUpdatedAt: Date.now() }); + + nextState = { + ...nextState, + pools: pools.sort((a, b) => b.apy - a.apy), + providers, + highestApyPool, + }; + } + + if (nominatorsResponse.status === 'fulfilled') { + const stakingInfo = nominatorsResponse.value.pools.reduce( + (acc, cur) => ({ ...acc, [cur.pool]: cur }), + {}, + ); + + const stakingBalance = pools.reduce((total, pool) => { + return total.plus(StakingManager.calculatePoolBalance(pool, stakingInfo)); + }, new BigNumber('0')); + + nextState = { + ...nextState, + stakingInfo, + stakingBalance: stakingBalance.toString(), + }; + } + + if ( + updateIfBalanceSame || + !isEqual(nextState.stakingInfo, this.state.data.stakingInfo) + ) { + this.state.set({ ...nextState }); + } + } catch (e) { + console.log('fetchPools error', e); + } finally { + this.state.set({ status: StakingApiStatus.Idle }); + } + } + + public async reload() { + await this.load(); + } + + public reset() { + this.state.set({ + stakingInfo: {}, + stakingBalance: '0', + status: StakingApiStatus.Idle, + }); + } + + public async rehydrate() { + return this.state.rehydrate(); + } +} diff --git a/packages/mobile/src/wallet/managers/SubscriptionsManager.ts b/packages/mobile/src/wallet/managers/SubscriptionsManager.ts new file mode 100644 index 000000000..98569b906 --- /dev/null +++ b/packages/mobile/src/wallet/managers/SubscriptionsManager.ts @@ -0,0 +1,80 @@ +import { network } from '@tonkeeper/core/src/utils/network'; +import { TonRawAddress } from '../WalletTypes'; +import { State, Storage } from '@tonkeeper/core'; +import { config } from '$config'; + +export interface Subscription { + id?: string; + productName: string; + channelTgId: string; + amountNano: string; + intervalSec: number; + address: string; + status: string; + merchantName: string; + merchantPhoto: string; + returnUrl: string; + subscriptionId: number; + subscriptionAddress: string; + isActive?: boolean; + chargedAt: number; + fee: string; + userReturnUrl: string; +} + +export type Subscriptions = { [k: string]: Subscription }; + +export interface SubscriptionsState { + subscriptions: Subscriptions; + isLoading: boolean; +} + +export interface SubscriptionsResponse { + data: Subscriptions; +} + +export class SubscriptionsManager { + constructor(private tonRawAddress: TonRawAddress, private storage: Storage) { + this.state.persist({ + partialize: ({ subscriptions }) => ({ + subscriptions, + }), + storage: this.storage, + key: `${this.tonRawAddress}/subscriptions`, + }); + } + + static readonly INITIAL_STATE: SubscriptionsState = { + subscriptions: {}, + isLoading: false, + }; + + public state = new State(SubscriptionsManager.INITIAL_STATE); + + public async load() { + try { + this.state.set({ isLoading: true }); + const { data: subscriptions } = await network.get( + `${config.get('subscriptionsHost')}/v1/subscriptions`, + { + params: { address: this.tonRawAddress }, + }, + ); + this.state.set({ isLoading: false, subscriptions: subscriptions.data }); + } catch { + this.state.set({ isLoading: false }); + } + } + + public async reload() { + await this.load(); + } + + public reset() { + this.state.set(SubscriptionsManager.INITIAL_STATE); + } + + public async rehydrate() { + return this.state.rehydrate(); + } +} diff --git a/packages/mobile/src/wallet/managers/TokenApprovalManager.ts b/packages/mobile/src/wallet/managers/TokenApprovalManager.ts new file mode 100644 index 000000000..c91539089 --- /dev/null +++ b/packages/mobile/src/wallet/managers/TokenApprovalManager.ts @@ -0,0 +1,111 @@ +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { TonRawAddress } from '../WalletTypes'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { Address } from '@tonkeeper/core/src/formatters/Address'; + +export enum TokenApprovalStatus { + Approved = 'approved', + Declined = 'declined', +} + +export enum TokenApprovalType { + Collection = 'collection', + Token = 'token', +} + +export interface ApprovalStatus { + current: TokenApprovalStatus; + type: TokenApprovalType; + updated_at: number; + approved_meta_revision: number; +} + +export type TokenApprovalState = { + tokens: Record; + hasWatchedCollectiblesTab: boolean; +}; + +export class TokenApprovalManager { + static readonly INITIAL_STATE: TokenApprovalState = { + tokens: {}, + hasWatchedCollectiblesTab: false, + }; + + public state = new State(TokenApprovalManager.INITIAL_STATE); + + constructor(private tonRawAddress: TonRawAddress, private storage: Storage) { + this.state.persist({ + storage: this.storage, + key: `${this.tonRawAddress}/tokenApproval`, + }); + this.migrate(); + } + + removeTokenStatus(address: string) { + const { tokens } = this.state.data; + const rawAddress = Address.parse(address).toRaw(); + if (tokens[rawAddress]) { + delete tokens[rawAddress]; + this.state.set({ tokens }); + } + } + + setHasWatchedCollectiblesTab(hasWatchedCollectiblesTab: boolean) { + this.state.set({ hasWatchedCollectiblesTab }); + } + + updateTokenStatus( + address: string, + status: TokenApprovalStatus, + type: TokenApprovalType, + ) { + const { tokens } = this.state.data; + const rawAddress = Address.parse(address).toRaw(); + const token = { ...tokens[rawAddress] }; + + if (token) { + token.current = status; + token.updated_at = Date.now(); + + this.state.set({ tokens: { ...tokens, [rawAddress]: token } }); + } else { + this.state.set({ + tokens: { + ...tokens, + [rawAddress]: { + type, + current: status, + updated_at: Date.now(), + approved_meta_revision: 0, + }, + }, + }); + } + } + + public async rehydrate() { + return this.state.rehydrate(); + } + + public reset() { + this.state.clear(); + this.state.clearPersist(); + } + + private async migrate() { + try { + const data = await this.storage.getItem('tokenApproval'); + + if (!data) { + return; + } + + const state = JSON.parse(data).state; + + if (state) { + this.storage.removeItem('tokenApproval'); + this.state.set(state); + } + } catch {} + } +} diff --git a/packages/@core-js/src/managers/TonInscriptions.ts b/packages/mobile/src/wallet/managers/TonInscriptions.ts similarity index 64% rename from packages/@core-js/src/managers/TonInscriptions.ts rename to packages/mobile/src/wallet/managers/TonInscriptions.ts index e1dd3a23a..a4e110d44 100644 --- a/packages/@core-js/src/managers/TonInscriptions.ts +++ b/packages/mobile/src/wallet/managers/TonInscriptions.ts @@ -1,6 +1,6 @@ -import { InscriptionBalance, TonAPI } from '../TonAPI'; -import { Storage } from '../declarations/Storage'; -import { State } from '../utils/State'; +import { InscriptionBalance, TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { State } from '@tonkeeper/core/src/utils/State'; type TonInscriptionsState = { items: InscriptionBalance[]; @@ -14,22 +14,26 @@ export class TonInscriptions { }); constructor( - private tonAddress: string, + private tonRawAddress: string, private tonapi: TonAPI, private storage: Storage, ) { this.state.persist({ partialize: ({ items }) => ({ items }), storage: this.storage, - key: 'inscriptions', + key: `${this.tonRawAddress}/inscriptions`, }); } - public async getInscriptions() { + public async rehydrate() { + return this.state.rehydrate(); + } + + public async load() { try { this.state.set({ isLoading: true }); const data = await this.tonapi.experimental.getAccountInscriptions({ - accountId: this.tonAddress, + accountId: this.tonRawAddress, }); this.state.set({ items: data.inscriptions.filter((inscription) => inscription.balance !== '0'), @@ -38,8 +42,4 @@ export class TonInscriptions { this.state.set({ isLoading: false }); } } - - public async preload() { - this.getInscriptions(); - } } diff --git a/packages/mobile/src/wallet/managers/TonPriceManager.ts b/packages/mobile/src/wallet/managers/TonPriceManager.ts new file mode 100644 index 000000000..a0bfd05d4 --- /dev/null +++ b/packages/mobile/src/wallet/managers/TonPriceManager.ts @@ -0,0 +1,83 @@ +import { WalletCurrency } from '@tonkeeper/core/src/utils/AmountFormatter/FiatCurrencyConfig'; +import { Storage } from '@tonkeeper/core/src/declarations/Storage'; +import { State } from '@tonkeeper/core/src/utils/State'; +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { TokenRate } from '../WalletTypes'; +import { NamespacedLogger, logger } from '$logger'; + +export type PricesState = { + ton: TokenRate; + updatedAt: number; + currency: WalletCurrency; +}; + +export class TonPriceManager { + public state = new State({ + ton: { + fiat: 0, + usd: 0, + ton: 0, + diff_24h: '', + }, + updatedAt: Date.now(), + currency: WalletCurrency.USD, + }); + + private logger: NamespacedLogger; + + constructor(private tonapi: TonAPI, private storage: Storage) { + this.state.persist({ + storage: this.storage, + key: 'ton_price', + }); + + this.logger = logger.extend('TonPriceManager'); + + this.migrate(); + } + + setFiatCurrency(currency: WalletCurrency) { + this.logger.info('Setting fiat currency', currency); + this.state.clear(); + this.state.clearPersist(); + this.state.set({ currency }); + } + + public async load() { + this.logger.info('Loading TON price'); + try { + const currency = this.state.data.currency.toUpperCase(); + const token = 'TON'; + const data = await this.tonapi.rates.getRates({ + currencies: ['TON', 'USD', currency].join(','), + tokens: token, + }); + + this.state.set({ + ton: { + fiat: data.rates[token].prices![currency], + usd: data.rates[token].prices!.USD, + ton: data.rates[token].prices!.TON, + diff_24h: data.rates[token].diff_24h![currency], + }, + updatedAt: Date.now(), + }); + } catch {} + } + + private async migrate() { + try { + const key = 'mainnet_default_primary_currency'; + const currency = (await this.storage.getItem(key)) as WalletCurrency; + + if (currency) { + this.storage.removeItem(key); + this.state.set({ currency }); + } + } catch {} + } + + public async rehydrate() { + return this.state.rehydrate(); + } +} diff --git a/packages/mobile/src/wallet/managers/TonProofManager.ts b/packages/mobile/src/wallet/managers/TonProofManager.ts new file mode 100644 index 000000000..c49ffbc06 --- /dev/null +++ b/packages/mobile/src/wallet/managers/TonProofManager.ts @@ -0,0 +1,47 @@ +import nacl from 'tweetnacl'; +import * as SecureStore from 'expo-secure-store'; +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { ContractService, WalletVersion } from '@tonkeeper/core'; +import { Buffer } from 'buffer'; +import { beginCell } from '@ton/core'; +import { storeStateInit } from '@ton/ton'; +import { signProofForTonkeeper } from '@tonkeeper/core/src/utils/tonProof'; + +export class TonProofManager { + public tonProofToken: string | null = null; + + constructor(public identifier: string, public tonapi: TonAPI) {} + + public async obtainProof(keyPair: nacl.SignKeyPair) { + const contract = ContractService.getWalletContract( + WalletVersion.v4R2, + Buffer.from(keyPair.publicKey), + 0, + ); + const stateInitCell = beginCell().store(storeStateInit(contract.init)).endCell(); + const rawAddress = contract.address.toRawString(); + + try { + const { payload } = await this.tonapi.tonconnect.getTonConnectPayload(); + const proof = await signProofForTonkeeper( + rawAddress, + keyPair.secretKey, + payload, + stateInitCell.toBoc({ idx: false }).toString('base64'), + ); + const { token } = await this.tonapi.wallet.tonConnectProof(proof); + + await SecureStore.setItemAsync(`proof-${this.identifier}`, token); + } catch (err) { + return null; + } + } + + public async rehydrate() { + this.tonProofToken = await SecureStore.getItemAsync(`proof-${this.identifier}`); + } + + public destroy() { + SecureStore.deleteItemAsync(`proof-${this.identifier}`); + } +} diff --git a/packages/@core-js/src/models/ActivityModel/ActivityModel.ts b/packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts similarity index 95% rename from packages/@core-js/src/models/ActivityModel/ActivityModel.ts rename to packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts index 6b2b16000..d37629b8f 100644 --- a/packages/@core-js/src/models/ActivityModel/ActivityModel.ts +++ b/packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts @@ -1,8 +1,4 @@ -import { AccountEvent, ActionStatusEnum } from '../../TonAPI'; import { differenceInCalendarMonths, format } from 'date-fns'; -import { toLowerCaseFirstLetter } from '../../utils/strings'; -import { TronEvent } from '../../TronAPI/TronAPIGenerated'; -import { Address } from '../../formatters/Address'; import { nanoid } from 'nanoid/non-secure'; import { AnyActionTypePayload, @@ -15,6 +11,10 @@ import { ActionItem, AnyActionItem, } from './ActivityModelTypes'; +import { AccountEvent, ActionStatusEnum } from '@tonkeeper/core/src/TonAPI'; +import { toLowerCaseFirstLetter } from '@tonkeeper/uikit'; +import { Address } from '@tonkeeper/core'; +import { TronEvent } from '@tonkeeper/core/src/TronAPI/TronAPIGenerated'; type CreateActionOptions = { source: ActionSource; @@ -114,6 +114,7 @@ export class ActivityModel { payload: action.payload, action_id: nanoid(), type: (action as any).type, + initialActionType: (action as any).type, isFirst: false, isLast: false, destination, @@ -200,7 +201,7 @@ export class ActivityModel { static defineActionDestination( ownerAddress: string, actionType: ActionType, - payload: AnyActionPayload, + payload?: AnyActionPayload, ): ActionDestination { if ( actionType === ActionType.WithdrawStake || diff --git a/packages/@core-js/src/models/ActivityModel/ActivityModelTypes.ts b/packages/mobile/src/wallet/models/ActivityModel/ActivityModelTypes.ts similarity index 96% rename from packages/@core-js/src/models/ActivityModel/ActivityModelTypes.ts rename to packages/mobile/src/wallet/models/ActivityModel/ActivityModelTypes.ts index 0e92bd842..69d1bc9e7 100644 --- a/packages/@core-js/src/models/ActivityModel/ActivityModelTypes.ts +++ b/packages/mobile/src/wallet/models/ActivityModel/ActivityModelTypes.ts @@ -1,26 +1,29 @@ -import { ReceiveTRC20Action, SendTRC20Action } from '../../TronAPI/TronAPIGenerated'; import { AccountEvent, - ActionSimplePreview, - ActionStatusEnum, - AuctionBidAction, - ContractDeployAction, - DepositStakeAction, - ElectionsDepositStakeAction, - ElectionsRecoverStakeAction, - JettonBurnAction, - JettonMintAction, - JettonSwapAction, + TonTransferAction, JettonTransferAction, NftItemTransferAction, - NftPurchaseAction, - SmartContractAction, + ContractDeployAction, SubscriptionAction, - TonTransferAction, UnSubscriptionAction, + AuctionBidAction, + NftPurchaseAction, + SmartContractAction, + JettonSwapAction, + JettonBurnAction, + JettonMintAction, + DepositStakeAction, WithdrawStakeAction, WithdrawStakeRequestAction, -} from '../../TonAPI/TonAPIGenerated'; + ElectionsRecoverStakeAction, + ElectionsDepositStakeAction, + ActionStatusEnum, + ActionSimplePreview, +} from '@tonkeeper/core/src/TonAPI'; +import { + SendTRC20Action, + ReceiveTRC20Action, +} from '@tonkeeper/core/src/TronAPI/TronAPIGenerated'; export type GroupKey = string; export type ActionId = string; diff --git a/packages/@core-js/src/models/ActivityModel/index.ts b/packages/mobile/src/wallet/models/ActivityModel/index.ts similarity index 100% rename from packages/@core-js/src/models/ActivityModel/index.ts rename to packages/mobile/src/wallet/models/ActivityModel/index.ts diff --git a/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts b/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts new file mode 100644 index 000000000..3574dde41 --- /dev/null +++ b/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts @@ -0,0 +1,23 @@ +import { Address, AmountFormatter } from '@tonkeeper/core'; +import { JettonBalance } from '@tonkeeper/core/src/TonAPI'; +import { JettonMetadata, JettonVerification } from './types'; + +export class JettonBalanceModel { + metadata: JettonMetadata; + balance: string; + jettonAddress: string; + walletAddress: string; + verification: JettonVerification; + + constructor(jettonBalance: JettonBalance) { + this.metadata = jettonBalance.jetton; + this.balance = AmountFormatter.fromNanoStatic( + jettonBalance.balance, + jettonBalance.jetton.decimals, + ); + this.jettonAddress = new Address(jettonBalance.jetton.address).toFriendly(); + this.walletAddress = new Address(jettonBalance.wallet_address.address).toFriendly(); + this.verification = jettonBalance.jetton + .verification as unknown as JettonVerification; + } +} diff --git a/packages/mobile/src/wallet/models/JettonBalanceModel/index.ts b/packages/mobile/src/wallet/models/JettonBalanceModel/index.ts new file mode 100644 index 000000000..bbf7fb9b8 --- /dev/null +++ b/packages/mobile/src/wallet/models/JettonBalanceModel/index.ts @@ -0,0 +1,2 @@ +export * from './JettonBalanceModel'; +export * from './types'; diff --git a/packages/mobile/src/wallet/models/JettonBalanceModel/types.ts b/packages/mobile/src/wallet/models/JettonBalanceModel/types.ts new file mode 100644 index 000000000..6f8e6bed1 --- /dev/null +++ b/packages/mobile/src/wallet/models/JettonBalanceModel/types.ts @@ -0,0 +1,15 @@ +export interface JettonMetadata { + address: string; + decimals: number; + symbol?: string; + image_data?: string; + image?: string; + description?: string; + name?: string; +} + +export enum JettonVerification { + WHITELIST = 'whitelist', + NONE = 'none', + BLACKLIST = 'blacklist', +} diff --git a/packages/mobile/src/wallet/utils.ts b/packages/mobile/src/wallet/utils.ts new file mode 100644 index 000000000..d23c3a3ab --- /dev/null +++ b/packages/mobile/src/wallet/utils.ts @@ -0,0 +1,49 @@ +import { TronAPI } from '@tonkeeper/core'; +import { BatteryAPI } from '@tonkeeper/core/src/BatteryAPI'; +import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { config } from '$config'; +import { AppServerSentEvents } from './AppServerSentEvents'; +import { i18n } from '@tonkeeper/shared/i18n'; + +export const createTonApiInstance = (isTestnet = false) => { + return new TonAPI({ + baseHeaders: () => ({ + Authorization: `Bearer ${config.get('tonApiV2Key', isTestnet)}`, + }), + baseUrl: () => config.get('tonapiIOEndpoint', isTestnet), + }); +}; + +export const createBatteryApiInstance = (isTestnet = false) => { + return new BatteryAPI({ + baseUrl: () => { + if (isTestnet) { + return config.get('batteryTestnetHost'); + } + + return config.get('batteryHost'); + }, + baseHeaders: { + 'Accept-Language': i18n.locale, + }, + }); +}; + +export const createTronApiInstance = (isTestnet = false) => { + return new TronAPI({ + baseUrl: () => { + if (isTestnet) { + return config.get('tronapiTestnetHost'); + } + + return config.get('tronapiHost'); + }, + }); +}; + +export const createSseInstance = (isTestnet = false) => { + return new AppServerSentEvents({ + baseUrl: () => config.get('tonapiIOEndpoint', isTestnet), + token: () => config.get('tonApiV2Key', isTestnet), + }); +}; diff --git a/packages/mobile/tsconfig.json b/packages/mobile/tsconfig.json index df2712410..c57152f08 100644 --- a/packages/mobile/tsconfig.json +++ b/packages/mobile/tsconfig.json @@ -21,7 +21,8 @@ "$shared": ["shared"], "$assets": ["assets"], "$navigation": ["navigation"], - "$services": ["services"], + "$wallet": ["wallet"], + "$logger": ["logger"], "$translation": ["translation"], "$tonconnect": ["tonconnect"], "$api/*": ["api/*"], @@ -37,13 +38,15 @@ "$shared/*": ["shared/*"], "$assets/*": ["assets/*"], "$navigation/*": ["navigation/*"], - "$services/*": ["services/*"], + "$wallet/*": ["wallet/*"], + "$logger/*": ["logger/*"], "$translation/*": ["translation/"], "$blockchain": ["blockchain/"], "$database": ["database/"], - "$tonconnect/*": ["tonconnect/*"] + "$tonconnect/*": ["tonconnect/*"], + "$config": ["config/"] } }, - "include": ["src"], + "include": ["src", "src/wallet/managers", "src/wallet/Wallet"], "exclude": ["node_modules"] } \ No newline at end of file diff --git a/packages/router/src/SheetsProvider.tsx b/packages/router/src/SheetsProvider.tsx index 99ba25e9c..69c1261f3 100644 --- a/packages/router/src/SheetsProvider.tsx +++ b/packages/router/src/SheetsProvider.tsx @@ -14,8 +14,8 @@ export type SheetParams = Record; export enum SheetActions { REPLACE = 'REPLACE', - ADD = 'ADD' -}; + ADD = 'ADD', +} export type SheetInitialState = 'opened' | 'closed'; @@ -26,39 +26,42 @@ export type SheetStackParams = { component: React.ComponentType; }; -export type SheetStack = SheetStackParams & { +export type SheetStack = SheetStackParams & { id: string; initialState: SheetInitialState; }; export type SheetStackList = SheetStack[]; -export type SheetContextValue = - & BottomSheetRefs - & SheetMeasurements - & { +export type SheetContextValue = BottomSheetRefs & + SheetMeasurements & { + id: string; + initialState: SheetInitialState; + delegateMethods: (methods: SheetMethods) => void; + removeFromStack: () => void; + close: () => void; + onClose?: (() => void) | null; + }; + +export type SheetStackDispatchAction = + | { + type: 'ADD'; id: string; - initialState: SheetInitialState; - delegateMethods: (methods: SheetMethods) => void; - removeFromStack: () => void; - close: () => void; + path: string; + params: SheetParams; + component: React.ComponentType; + initialState?: SheetInitialState; + } + | { + type: 'REMOVE'; + id: string; + } + | { + type: 'REMOVE_ALL'; }; -export type SheetStackDispatchAction = { - type: 'ADD'; - id: string; - path: string; - params: SheetParams; - component: React.ComponentType; - initialState?: SheetInitialState; -} | { - type: 'REMOVE' - id: string; -} | { - type: 'REMOVE_ALL' -}; - -const SheetDispatchContext = React.createContext | null>(null); +const SheetDispatchContext = + React.createContext | null>(null); const SheetContext = React.createContext(null); function SheetReducer(stack: SheetStackList, action: SheetStackDispatchAction) { @@ -66,17 +69,17 @@ function SheetReducer(stack: SheetStackList, action: SheetStackDispatchAction) { case 'ADD': return [ ...stack, - { + { initialState: action.initialState ?? 'opened', path: action.path, params: action.params, component: action.component, id: action.id, - } + }, ]; case 'REMOVE': return stack.filter((item) => item.id !== action.id); - case 'REMOVE_ALL': + case 'REMOVE_ALL': return []; } } @@ -84,9 +87,12 @@ function SheetReducer(stack: SheetStackList, action: SheetStackDispatchAction) { const useSheetRefs = () => { const refs = React.useRef(new Map()); - const setRef = React.useCallback((id: string) => (el: SheetInternalRef) => { - refs.current.set(id, el); - }, []); + const setRef = React.useCallback( + (id: string) => (el: SheetInternalRef) => { + refs.current.set(id, el); + }, + [], + ); const removeRef = React.useCallback((id: string) => { refs.current.delete(id); @@ -95,7 +101,7 @@ const useSheetRefs = () => { const getRef = React.useCallback((id: string) => { return refs.current.get(id); }, []); - + const getLastRef = React.useCallback(() => { return Array.from(refs.current)[refs.current.size - 1]?.[1]; }, []); @@ -106,85 +112,89 @@ const useSheetRefs = () => { setRef, getRef, }; -} +}; type SheetsProviderParams = { component: React.ComponentType; $$action: SheetActions; params: Record; - path: string; + path: string; }; export type SheetsProviderRef = { replaceStack: (stack: SheetStackParams) => void; addStack: (stack: SheetStackParams) => void; -} +}; -export const SheetsProvider = React.memo(React.forwardRef((props, ref) => { - const nav = useNavigation(); - const sheetsRegistry = useSheetRefs(); - const [stack, dispatch] = React.useReducer(SheetReducer, []); - - const removeFromStack = React.useCallback((id: string, noGoBack?: boolean) => { - if (stack.length > 1) { - dispatch({ type: 'REMOVE', id }); - sheetsRegistry.removeRef(id); - } else if (!noGoBack) { - nav.goBack(); - } - }, [stack.length]); +export const SheetsProvider = React.memo( + React.forwardRef((props, ref) => { + const nav = useNavigation(); + const sheetsRegistry = useSheetRefs(); + const [stack, dispatch] = React.useReducer(SheetReducer, []); + const removeFromStack = React.useCallback( + (id: string, noGoBack?: boolean) => { + if (noGoBack) { + dispatch({ type: 'REMOVE', id }); + sheetsRegistry.removeRef(id); + } else { + nav.goBack(); + } + }, + [stack], + ); - const replaceStack = React.useCallback(async (stack: SheetStackParams) => { - const modalId = addStack({ ...stack, initialState: 'closed' }); - const lastSheet = sheetsRegistry.getLastRef(); + const replaceStack = React.useCallback(async (stack: SheetStackParams) => { + const modalId = addStack({ ...stack, initialState: 'closed' }); + const lastSheet = sheetsRegistry.getLastRef(); - if (lastSheet) { - if (!isAndroid) { - Keyboard.dismiss(); + if (lastSheet) { + if (!isAndroid) { + Keyboard.dismiss(); + } + lastSheet.close(() => { + sheetsRegistry.getRef(modalId)?.present(); + }); } - lastSheet.close(() => { - sheetsRegistry.getRef(modalId)?.present(); - }) - } - }, []); - - const addStack = (stack: SheetStackParams) => { - const id = nanoid(); - dispatch({ - initialState: stack.initialState, - component: stack.component, - params: stack.params, - path: stack.path, - type: 'ADD', - id - }); - - return id; - }; + }, []); + + const addStack = (stack: SheetStackParams) => { + const id = nanoid(); + dispatch({ + initialState: stack.initialState, + component: stack.component, + params: stack.params, + path: stack.path, + type: 'ADD', + id, + }); + + return id; + }; - React.useImperativeHandle(ref, () => ({ - replaceStack, - addStack - })); - - return ( - - {stack.map((item) => ( - - ))} - - ); -})); + React.useImperativeHandle(ref, () => ({ + replaceStack, + addStack, + })); + + return ( + + {stack.map((item) => ( + + ))} + + ); + }), +); interface SheetInternalProps { removeFromStack: (id: string, noGoBack?: boolean) => void; - item: SheetStack; -}; + item: SheetStack; +} type SheetInternalRef = { close: (onClosed?: () => void) => void; @@ -194,75 +204,77 @@ type SheetInternalRef = { type SheetMethods = { present: () => void; close: () => void; -} +}; -const SheetInternal = React.forwardRef< - SheetInternalRef, - SheetInternalProps ->((props, ref) => { - const closedSheetCallback = React.useRef<(() => void) | null>(null); - const delegatedMethods = React.useRef(null); - const measurements = useSheetMeasurements(); - - const delegateMethods = (methods: SheetMethods) => { - delegatedMethods.current = methods; - }; +const SheetInternal = React.forwardRef( + (props, ref) => { + const closedSheetCallback = React.useRef<(() => void) | null>(null); + const delegatedMethods = React.useRef(null); + const measurements = useSheetMeasurements(); - const removeFromStack = () => { - if (closedSheetCallback.current) { - closedSheetCallback.current(); - props.removeFromStack(props.item.id, true); - } else { - props.removeFromStack(props.item.id); - } - }; + const delegateMethods = (methods: SheetMethods) => { + delegatedMethods.current = methods; + }; - const close = () => { - if (!isAndroid) { - Keyboard.dismiss(); - } + const removeFromStack = () => { + if (closedSheetCallback.current) { + closedSheetCallback.current(); + props.removeFromStack(props.item.id, true); + } else { + props.removeFromStack(props.item.id); + } + }; - if (delegatedMethods.current) { - delegatedMethods.current.close(); - } else { - removeFromStack(); - } - }; + const close = () => { + if (!isAndroid) { + Keyboard.dismiss(); + } - React.useImperativeHandle(ref, () => ({ - present: () => { - delegatedMethods.current?.present(); - }, - close: (onClosed) => { if (delegatedMethods.current) { - if (onClosed) { - closedSheetCallback.current = onClosed; - } - delegatedMethods.current.close(); + } else { + removeFromStack(); } - }, - })) - - const value: SheetContextValue = { - ...measurements, - initialState: props.item.initialState, - id: props.item.id, - delegateMethods, - removeFromStack, - close, - }; + }; - const SheetComponent = props.item.component; - - return ( - - - - - - ); -}); + React.useImperativeHandle( + ref, + () => ({ + present: () => { + delegatedMethods.current?.present(); + }, + close: (onClosed) => { + if (onClosed) { + closedSheetCallback.current = onClosed; + } + + close(); + }, + }), + [close], + ); + + const value: SheetContextValue = { + ...measurements, + initialState: props.item.initialState, + id: props.item.id, + delegateMethods, + removeFromStack, + close, + onClose: closedSheetCallback.current, + }; + + const SheetComponent = props.item.component; + + return ( + + + + + + ); + }, +); export const useSheetInternal = () => { const sheet = React.useContext(SheetContext); @@ -277,4 +289,4 @@ export const useSheetInternal = () => { export const useCloseModal = () => { const sheet = React.useContext(SheetContext); return sheet?.close; -}; \ No newline at end of file +}; diff --git a/packages/shared/Address.ts b/packages/shared/Address.ts index 7d30f7af1..0eace9fd9 100644 --- a/packages/shared/Address.ts +++ b/packages/shared/Address.ts @@ -1,7 +1,7 @@ -import { WalletNetwork } from '@tonkeeper/core/src/Wallet'; import { AddressFormatter } from '@tonkeeper/core/src/formatters/Address'; -import { tk } from './tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { WalletNetwork } from '@tonkeeper/mobile/src/wallet/WalletTypes'; export const Address = new AddressFormatter({ - testOnly: () => tk.wallet?.identity.network === WalletNetwork.testnet, + testOnly: () => tk.wallet?.config.network === WalletNetwork.testnet, }); diff --git a/packages/shared/components/ActivityList/ActionListItem.tsx b/packages/shared/components/ActivityList/ActionListItem.tsx index 2e4c3ccd5..38aa814c9 100644 --- a/packages/shared/components/ActivityList/ActionListItem.tsx +++ b/packages/shared/components/ActivityList/ActionListItem.tsx @@ -18,17 +18,17 @@ import { memo, useCallback, useMemo } from 'react'; import { ImageRequireSource } from 'react-native'; import { Address } from '../../Address'; import { t } from '../../i18n'; + +import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; +import { useFlags } from '@tonkeeper/mobile/src/utils/flags'; +import { config } from '@tonkeeper/mobile/src/config'; +import { AmountFormatter } from '@tonkeeper/core'; import { - ActionSource, ActionType, - AmountFormatter, AnyActionItem, isJettonTransferAction, -} from '@tonkeeper/core'; - -import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; -import { useFlags } from '@tonkeeper/mobile/src/utils/flags'; -import { config } from '../../config'; + ActionSource, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; export interface ActionListItemProps { onPress?: () => void; diff --git a/packages/shared/components/ActivityList/ActionListItemByType.tsx b/packages/shared/components/ActivityList/ActionListItemByType.tsx index 797c945bf..802815a97 100644 --- a/packages/shared/components/ActivityList/ActionListItemByType.tsx +++ b/packages/shared/components/ActivityList/ActionListItemByType.tsx @@ -2,9 +2,8 @@ import { UnSubscribeActionListItem } from './items/UnSubscribeActionListItem'; import { JettonSwapActionListItem } from './items/JettonSwapActionListItem'; import { SubscribeActionListItem } from './items/SubscribeActionListItem'; import { ListItemContainer, ListItemContentText } from '@tonkeeper/uikit'; -import { modifyNftName } from '@tonkeeper/core/src/managers/NftsManager'; import { ActionListItem, ActionListItemProps } from './ActionListItem'; -import { ActionType, Address, AnyActionItem } from '@tonkeeper/core'; +import { Address } from '@tonkeeper/core'; import { NftPreviewContent } from './NftPreviewContent'; import { t } from '../../i18n'; import { memo } from 'react'; @@ -12,6 +11,11 @@ import { memo } from 'react'; import { getImplementationIcon } from '@tonkeeper/mobile/src/utils/staking'; import { excludeUndefinedValues } from '@tonkeeper/core/src/utils/common'; import { ListItemEncryptedComment } from '@tonkeeper/uikit/src/components/List/ListItemEncryptedComment'; +import { modifyNftName } from '@tonkeeper/mobile/src/wallet/managers/NftsManager'; +import { + ActionType, + AnyActionItem, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; export const ActionListItemByType = memo((props) => { const { action } = props; diff --git a/packages/shared/components/ActivityList/ActivityList.tsx b/packages/shared/components/ActivityList/ActivityList.tsx index b13e5e2ad..e8b050609 100644 --- a/packages/shared/components/ActivityList/ActivityList.tsx +++ b/packages/shared/components/ActivityList/ActivityList.tsx @@ -1,6 +1,5 @@ import { DefaultSectionT, SectionListData, StyleSheet, View } from 'react-native'; import { formatTransactionsGroupDate } from '../../utils/date'; -import { ActivitySection } from '@tonkeeper/core'; import { renderActionItem } from './ActionListItemByType'; import { memo, useMemo } from 'react'; import { @@ -13,6 +12,7 @@ import { Button, copyText, } from '@tonkeeper/uikit'; +import { ActivitySection } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; type ListComponentType = React.ComponentType | React.ReactElement | null | undefined; diff --git a/packages/shared/components/ActivityList/findSenderAccount.ts b/packages/shared/components/ActivityList/findSenderAccount.ts index addb34904..f5bdc939d 100644 --- a/packages/shared/components/ActivityList/findSenderAccount.ts +++ b/packages/shared/components/ActivityList/findSenderAccount.ts @@ -1,8 +1,9 @@ -import { AnyActionItem } from '@tonkeeper/core'; +import { AnyActionItem } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; export function findSenderAccount(action: AnyActionItem) { if (action.payload && ('sender' in action.payload || 'recipient' in action.payload)) { - const senderAccount = action.destination === 'in' ? action.payload?.sender : action.payload?.recipient; + const senderAccount = + action.destination === 'in' ? action.payload?.sender : action.payload?.recipient; if (senderAccount) { return senderAccount; } diff --git a/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx b/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx index d2cf7486c..6e8eed3df 100644 --- a/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx +++ b/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx @@ -1,14 +1,13 @@ import { ActionListItem, ActionListItemProps } from '../ActionListItem'; -import { Address, ActionType, AmountFormatter } from '@tonkeeper/core'; +import { Address, AmountFormatter } from '@tonkeeper/core'; import { ActionStatusEnum } from '@tonkeeper/core/src/TonAPI'; import { formatTransactionTime } from '../../../utils/date'; import { View, StyleSheet } from 'react-native'; import { Text } from '@tonkeeper/uikit'; import { memo, useMemo } from 'react'; import { t } from '../../../i18n'; - import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; -import { getFlag } from '@tonkeeper/mobile/src/utils/flags'; +import { ActionType } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; type JettonSwapActionListItemProps = ActionListItemProps; diff --git a/packages/shared/components/ActivityList/items/SubscribeActionListItem.tsx b/packages/shared/components/ActivityList/items/SubscribeActionListItem.tsx index 1895b7881..8106b3bbc 100644 --- a/packages/shared/components/ActivityList/items/SubscribeActionListItem.tsx +++ b/packages/shared/components/ActivityList/items/SubscribeActionListItem.tsx @@ -1,9 +1,10 @@ import { ActionListItem, ActionListItemProps } from '../ActionListItem'; import { useSubscription } from '../../../query/hooks/useSubscription'; -import { ActionType } from '@tonkeeper/core'; + import { StyleSheet } from 'react-native'; import { t } from '../../../i18n'; import { memo } from 'react'; +import { ActionType } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; type SubscribeActionListItemProps = ActionListItemProps; diff --git a/packages/shared/components/ActivityList/items/UnSubscribeActionListItem.tsx b/packages/shared/components/ActivityList/items/UnSubscribeActionListItem.tsx index 931f603cd..a5d873d91 100644 --- a/packages/shared/components/ActivityList/items/UnSubscribeActionListItem.tsx +++ b/packages/shared/components/ActivityList/items/UnSubscribeActionListItem.tsx @@ -1,8 +1,9 @@ import { ActionListItem, ActionListItemProps } from '../ActionListItem'; import { useSubscription } from '../../../query/hooks/useSubscription'; -import { ActionType } from '@tonkeeper/core'; + import { t } from '../../../i18n'; import { memo } from 'react'; +import { ActionType } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; type UnSubscribeActionListItemProps = ActionListItemProps; diff --git a/packages/shared/components/BatteryIcon/BatteryIcon.tsx b/packages/shared/components/BatteryIcon/BatteryIcon.tsx index e11a77423..052931752 100644 --- a/packages/shared/components/BatteryIcon/BatteryIcon.tsx +++ b/packages/shared/components/BatteryIcon/BatteryIcon.tsx @@ -3,7 +3,7 @@ import { useBatteryBalance } from '../../query/hooks/useBatteryBalance'; import { Icon, IconNames, TouchableOpacity } from '@tonkeeper/uikit'; import { BatteryState, getBatteryState } from '../../utils/battery'; import { openRefillBatteryModal } from '../../modals/RefillBatteryModal'; -import { config } from '../../config'; +import { config } from '@tonkeeper/mobile/src/config'; const iconNames: { [key: string]: IconNames } = { [BatteryState.Empty]: 'ic-empty-battery-28', diff --git a/packages/shared/components/EncryptedComment/EncryptedComment.tsx b/packages/shared/components/EncryptedComment/EncryptedComment.tsx index 132bf0381..c640c3a7d 100644 --- a/packages/shared/components/EncryptedComment/EncryptedComment.tsx +++ b/packages/shared/components/EncryptedComment/EncryptedComment.tsx @@ -17,6 +17,7 @@ import { import { SpoilerViewMock } from './components/SpoilerViewMock'; import { useCopyText } from '@tonkeeper/mobile/src/hooks/useCopyText'; import { openEncryptedCommentModalIfNeeded } from '../../modals/EncryptedCommentModal'; +import { tk } from '@tonkeeper/mobile/src/wallet'; export enum EncryptedCommentLayout { LIST_ITEM, @@ -86,6 +87,9 @@ const EncryptedCommentComponent: React.FC = (props) => { ); const handleDecryptComment = useCallback(() => { + if (tk.wallet.isWatchOnly) { + return; + } openEncryptedCommentModalIfNeeded(() => decryptComment(props.actionId, props.encryptedComment, props.sender.address), ); diff --git a/packages/shared/components/RefillBattery/RefillBattery.tsx b/packages/shared/components/RefillBattery/RefillBattery.tsx index 1a2445bdf..6251637b1 100644 --- a/packages/shared/components/RefillBattery/RefillBattery.tsx +++ b/packages/shared/components/RefillBattery/RefillBattery.tsx @@ -10,7 +10,7 @@ import { Icon, IconNames, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; import { navigation, SheetActions } from '@tonkeeper/router'; import { RefillBatteryIAP } from './RefillBatteryIAP'; import { t } from '@tonkeeper/shared/i18n'; -import { config } from '../../config'; +import { config } from '@tonkeeper/mobile/src/config'; import { RechargeByPromoButton } from './RechargeByPromoButton'; const iconNames: { [key: string]: IconNames } = { diff --git a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx index 33206ea07..58f0b81ad 100644 --- a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx +++ b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx @@ -1,7 +1,7 @@ import { Button, List, Spacer, Steezy, Text, Toast, View } from '@tonkeeper/uikit'; import { memo, useCallback, useEffect, useState } from 'react'; import { t } from '@tonkeeper/shared/i18n'; -import { tk } from '../../tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; import { Platform } from 'react-native'; import { goBack } from '@tonkeeper/mobile/src/navigation/imperative'; import { useIAP } from 'react-native-iap'; diff --git a/packages/shared/components/WalletListItem/WalletListItem.tsx b/packages/shared/components/WalletListItem/WalletListItem.tsx new file mode 100644 index 000000000..7ff2a6984 --- /dev/null +++ b/packages/shared/components/WalletListItem/WalletListItem.tsx @@ -0,0 +1,75 @@ +import { Tag } from '@tonkeeper/mobile/src/uikit'; +import { Wallet } from '@tonkeeper/mobile/src/wallet/Wallet'; +import { + List, + Steezy, + Text, + View, + deviceWidth, + getWalletColorHex, + isAndroid, +} from '@tonkeeper/uikit'; +import { ListItemProps } from '@tonkeeper/uikit/src/components/List/ListItem'; +import { FC, memo } from 'react'; +import { Text as RNText } from 'react-native'; +import { t } from '../../i18n'; + +interface Props extends ListItemProps { + wallet: Wallet; +} + +const WalletListItemComponent: FC = (props) => { + const { wallet, ...listItemProps } = props; + + const titleWithTag = wallet.isTestnet || wallet.isWatchOnly; + + return ( + + + {wallet.config.name} + + {wallet.isTestnet ? Testnet : null} + {wallet.isWatchOnly ? {t('watch_only')} : null} + + } + leftContent={ + + {wallet.config.emoji} + + } + {...listItemProps} + /> + ); +}; + +export const WalletListItem = memo(WalletListItemComponent); + +const styles = Steezy.create(({ colors }) => ({ + iconContainer: { + width: 44, + height: 44, + borderRadius: 44 / 2, + backgroundColor: colors.backgroundHighlighted, + alignItems: 'center', + justifyContent: 'center', + }, + titleContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + titleWithTag: { + maxWidth: deviceWidth - 240, + }, + emoji: { + fontSize: isAndroid ? 21 : 24, + marginTop: isAndroid ? -2 : 2, + }, +})); diff --git a/packages/shared/components/index.ts b/packages/shared/components/index.ts index f39443e82..5f6db974d 100644 --- a/packages/shared/components/index.ts +++ b/packages/shared/components/index.ts @@ -2,3 +2,4 @@ export * from './ActivityList'; export * from './PasscodeInput'; export * from './EncryptedComment'; export * from './CountryButton'; +export * from './WalletListItem/WalletListItem'; diff --git a/packages/shared/config.ts b/packages/shared/config.ts deleted file mode 100644 index 590d16d61..000000000 --- a/packages/shared/config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AppStorage } from './modules/AppStorage'; -import DeviceInfo from 'react-native-device-info'; -import { AppConfig } from './modules/AppConfig'; -import { Platform } from 'react-native'; - -export type AppConfigVars = { - // tonapiTestnetHost: string; - // tonapiProdHost: string; - // tonapiDevHost: string; - // tonapiAuthToken: string; - tonapiIOEndpoint: string; - tonapiV2Endpoint: string; - tonApiV2Key: string; - tonApiKey: string; - transactionExplorer: string; - tonapiTestnetHost: string; - tronapiHost: string; - tronapiTestnetHost: string; - batteryHost: string; - batteryTestnetHost: string; - batteryMeanFees: string; - disable_battery: boolean; - disable_battery_iap_module: boolean; - disable_battery_send: boolean; - disable_show_unverified_token: boolean; -}; - -const defaultConfig: Partial = { - tonapiTestnetHost: 'https://testnet.tonapi.io', - tonapiIOEndpoint: 'https://keeper.tonapi.io', - tonapiV2Endpoint: 'https://tonapi.io', - tronapiHost: 'https://tron.tonkeeper.com', - tronapiTestnetHost: 'https://testnet-tron.tonkeeper.com', - batteryHost: 'https://battery.tonkeeper.com', - batteryTestnetHost: 'https://testnet-battery.tonkeeper.com', - batteryMeanFees: '0.08', - disable_battery: true, - disable_battery_iap_module: true, - disable_battery_send: true, - disable_show_unverified_token: false, -}; - -export const config = new AppConfig({ - storage: new AppStorage(), - defaultConfig, - request: { - host: 'https://boot.tonkeeper.com/keys', - params: () => ({ - build: DeviceInfo.getReadableVersion(), - platform: Platform.OS, - }), - }, -}); diff --git a/packages/shared/hooks/index.ts b/packages/shared/hooks/index.ts new file mode 100644 index 000000000..44f69fb0d --- /dev/null +++ b/packages/shared/hooks/index.ts @@ -0,0 +1,11 @@ +export * from './useJettons'; +export * from './useTokenApproval'; +export * from './useRates'; +export * from './useWalletCurrency'; +export * from './useStakingState'; +export * from './useWallet'; +export * from './useWalletStatus'; +export * from './useWallets'; +export * from './useNftsState'; +export * from './useBalancesState'; +export * from './useBiometrySettings'; diff --git a/packages/shared/hooks/useBalancesState.ts b/packages/shared/hooks/useBalancesState.ts new file mode 100644 index 000000000..3ff3926db --- /dev/null +++ b/packages/shared/hooks/useBalancesState.ts @@ -0,0 +1,19 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { DependencyList, useRef } from 'react'; +import { State } from '@tonkeeper/core'; +import { useWallet } from './useWallet'; +import { + BalancesState, + BalancesManager, +} from '@tonkeeper/mobile/src/wallet/managers/BalancesManager'; + +export function useBalancesState( + selector?: ExternalStateSelector, + deps?: DependencyList, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(BalancesManager.INITIAL_STATE)).current; + + return useExternalState(wallet?.balances.state ?? initialState, selector, deps); +} diff --git a/packages/shared/hooks/useBiometrySettings.ts b/packages/shared/hooks/useBiometrySettings.ts new file mode 100644 index 000000000..42423e4d8 --- /dev/null +++ b/packages/shared/hooks/useBiometrySettings.ts @@ -0,0 +1,15 @@ +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { useExternalState } from './useExternalState'; + +export const useBiometrySettings = () => { + const biometryEnabled = useExternalState( + tk.walletsStore, + (state) => state.biometryEnabled, + ); + + return { + biometryEnabled, + enableBiometry: (passcode: string) => tk.enableBiometry(passcode), + disableBiometry: () => tk.disableBiometry(), + }; +}; diff --git a/packages/shared/hooks/useExternalState.ts b/packages/shared/hooks/useExternalState.ts index 800e02709..2783f846e 100644 --- a/packages/shared/hooks/useExternalState.ts +++ b/packages/shared/hooks/useExternalState.ts @@ -1,7 +1,14 @@ -import { useCallback, useRef, useSyncExternalStore } from 'react'; +import { + DependencyList, + useCallback, + useMemo, + useRef, + useSyncExternalStore, +} from 'react'; import { DefaultStateData, State } from '@tonkeeper/core'; +import memoize from 'lodash/memoize'; -type ExternalStateSelector = ( +export type ExternalStateSelector = ( state: TStateData, ) => TSelectedData; @@ -12,22 +19,28 @@ export function useExternalState< TSelected = TStateData, >( state: State, - selector: ExternalStateSelector = defaultStateSelector, + selectorFn: ExternalStateSelector = defaultStateSelector, + deps?: DependencyList, ): TSelected { + const selector = useCallback(memoize(selectorFn), deps ?? []); + let currentData = useRef(selector(state.getSnapshot())); - const getSnapshot = () => selector(state.getSnapshot()); + const getSnapshot = useCallback(() => selector(state.getSnapshot()), [selector, state]); return useSyncExternalStore( - useCallback((cb) => { - return state.subscribe((data) => { - const nextState = selector(data); - if (currentData.current !== nextState) { - currentData.current = nextState; - cb(); - } - }); - }, []), + useCallback( + (cb) => { + return state.subscribe((data) => { + const nextState = selector(data); + if (currentData.current !== nextState) { + currentData.current = nextState; + cb(); + } + }); + }, + [selector, state], + ), getSnapshot, getSnapshot, ); diff --git a/packages/shared/hooks/useJettons.ts b/packages/shared/hooks/useJettons.ts new file mode 100644 index 000000000..eaf9c71b2 --- /dev/null +++ b/packages/shared/hooks/useJettons.ts @@ -0,0 +1,18 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { State } from '@tonkeeper/core'; +import { useRef } from 'react'; +import { useWallet } from './useWallet'; +import { + JettonsState, + JettonsManager, +} from '@tonkeeper/mobile/src/wallet/managers/JettonsManager'; + +export function useJettons( + selector?: ExternalStateSelector, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(JettonsManager.INITIAL_STATE)).current; + + return useExternalState(wallet?.jettons.state ?? initialState, selector); +} diff --git a/packages/shared/hooks/useNftsState.ts b/packages/shared/hooks/useNftsState.ts new file mode 100644 index 000000000..165b2580f --- /dev/null +++ b/packages/shared/hooks/useNftsState.ts @@ -0,0 +1,19 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { DependencyList, useRef } from 'react'; +import { State } from '@tonkeeper/core'; +import { useWallet } from './useWallet'; +import { + NftsState, + NftsManager, +} from '@tonkeeper/mobile/src/wallet/managers/NftsManager'; + +export function useNftsState( + selector?: ExternalStateSelector, + deps?: DependencyList, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(NftsManager.INITIAL_STATE)).current; + + return useExternalState(wallet?.nfts.state ?? initialState, selector, deps); +} diff --git a/packages/shared/hooks/useRates.ts b/packages/shared/hooks/useRates.ts new file mode 100644 index 000000000..e94b49ecf --- /dev/null +++ b/packages/shared/hooks/useRates.ts @@ -0,0 +1,11 @@ +import { useExternalState } from './useExternalState'; +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { useJettons } from './useJettons'; +import { TokenRate } from '@tonkeeper/mobile/src/wallet/WalletTypes'; + +export function useRates(): Record { + const ton = useExternalState(tk.tonPrice.state, (s) => s.ton); + const jettonsRates = useJettons((s) => s.jettonRates); + + return { ton, ...jettonsRates }; +} diff --git a/packages/shared/hooks/useStakingState.ts b/packages/shared/hooks/useStakingState.ts new file mode 100644 index 000000000..45941dd6c --- /dev/null +++ b/packages/shared/hooks/useStakingState.ts @@ -0,0 +1,19 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { DependencyList, useRef } from 'react'; +import { State } from '@tonkeeper/core'; +import { useWallet } from './useWallet'; +import { + StakingState, + StakingManager, +} from '@tonkeeper/mobile/src/wallet/managers/StakingManager'; + +export function useStakingState( + selector?: ExternalStateSelector, + deps?: DependencyList, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(StakingManager.INITIAL_STATE)).current; + + return useExternalState(wallet?.staking.state ?? initialState, selector, deps); +} diff --git a/packages/shared/hooks/useSubscriptions.ts b/packages/shared/hooks/useSubscriptions.ts new file mode 100644 index 000000000..a4ae168fb --- /dev/null +++ b/packages/shared/hooks/useSubscriptions.ts @@ -0,0 +1,18 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { State } from '@tonkeeper/core'; +import { useRef } from 'react'; +import { useWallet } from './useWallet'; +import { + SubscriptionsState, + SubscriptionsManager, +} from '@tonkeeper/mobile/src/wallet/managers/SubscriptionsManager'; + +export function useSubscriptions( + selector?: ExternalStateSelector, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(SubscriptionsManager.INITIAL_STATE)).current; + + return useExternalState(wallet?.subscriptions.state ?? initialState, selector); +} diff --git a/packages/shared/hooks/useTokenApproval.ts b/packages/shared/hooks/useTokenApproval.ts new file mode 100644 index 000000000..5c8bb2c1c --- /dev/null +++ b/packages/shared/hooks/useTokenApproval.ts @@ -0,0 +1,18 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { State } from '@tonkeeper/core'; +import { useRef } from 'react'; +import { useWallet } from './useWallet'; +import { + TokenApprovalState, + TokenApprovalManager, +} from '@tonkeeper/mobile/src/wallet/managers/TokenApprovalManager'; + +export function useTokenApproval( + selector?: ExternalStateSelector, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(TokenApprovalManager.INITIAL_STATE)).current; + + return useExternalState(wallet?.tokenApproval.state ?? initialState, selector); +} diff --git a/packages/shared/hooks/useWallet.ts b/packages/shared/hooks/useWallet.ts new file mode 100644 index 000000000..1f7f3765b --- /dev/null +++ b/packages/shared/hooks/useWallet.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { WalletContext } from '@tonkeeper/mobile/src/context'; + +export const useWallet = () => { + return useContext(WalletContext).wallet; +}; diff --git a/packages/shared/hooks/useWalletCurrency.ts b/packages/shared/hooks/useWalletCurrency.ts new file mode 100644 index 000000000..5fab2b75a --- /dev/null +++ b/packages/shared/hooks/useWalletCurrency.ts @@ -0,0 +1,6 @@ +import { useExternalState } from './useExternalState'; +import { tk } from '@tonkeeper/mobile/src/wallet'; + +export function useWalletCurrency() { + return useExternalState(tk.tonPrice.state, (s) => s.currency); +} diff --git a/packages/shared/hooks/useWalletStatus.ts b/packages/shared/hooks/useWalletStatus.ts new file mode 100644 index 000000000..d8646ab14 --- /dev/null +++ b/packages/shared/hooks/useWalletStatus.ts @@ -0,0 +1,15 @@ +import { ExternalStateSelector, useExternalState } from './useExternalState'; +import { State } from '@tonkeeper/core'; +import { useRef } from 'react'; +import { useWallet } from './useWallet'; +import { Wallet, WalletStatusState } from '@tonkeeper/mobile/src/wallet/Wallet'; + +export function useWalletStatus( + selector?: ExternalStateSelector, +): T { + const wallet = useWallet(); + + const initialState = useRef(new State(Wallet.INITIAL_STATUS_STATE)).current; + + return useExternalState(wallet?.status ?? initialState, selector); +} diff --git a/packages/shared/hooks/useWallets.ts b/packages/shared/hooks/useWallets.ts new file mode 100644 index 000000000..721f2d09e --- /dev/null +++ b/packages/shared/hooks/useWallets.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from 'react'; +import { tk } from '@tonkeeper/mobile/src/wallet'; + +export const useWallets = () => { + const [wallets, setWallet] = useState(Array.from(tk.wallets.values())); + + useEffect(() => { + const unsubscribe = tk.onChangeWallet(() => { + setWallet(Array.from(tk.wallets.values())); + }); + + return unsubscribe; + }, []); + + return wallets; +}; diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 6ec31979f..cd7ce1ebb 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -1,129 +1,128 @@ { - "about_ton" : "TON is a fully decentralized layer-1 blockchain designed by Telegram to onboard billions of users. It boasts ultra-fast transactions, tiny fees, easy-to-use apps, and is environmentally friendly.", - "access_confirmation_logout" : "Log out", - "access_confirmation_reset" : "Reset", - "access_confirmation_title" : "Enter passcode", - "access_confirmation_update_biometry" : "Enter passcode to use biometric auth data", - "access_denied" : "Access denied", - "account_deleted" : "Account deleted", - "activityActionModal" : { - "bid" : "Bid", - "burned" : "Burned", - "call_contract" : "Call contract", - "deposit" : "Stake", - "purchase" : "Purchase", - "received" : "Received", - "sent" : "Sent", - "swapped" : "Swapped", - "time_on" : "on %{time}", - "withdraw" : "Unstake", - "withdrawal_request" : "Unstake Request" + "about_ton": "TON is a fully decentralized layer-1 blockchain designed by Telegram to onboard billions of users. It boasts ultra-fast transactions, tiny fees, easy-to-use apps, and is environmentally friendly.", + "access_confirmation_logout": "Log out", + "access_confirmation_reset": "Reset", + "access_confirmation_title": "Enter passcode", + "access_confirmation_update_biometry": "Enter passcode to use biometric auth data", + "access_denied": "Access denied", + "account_deleted": "Account deleted", + "activityActionModal": { + "bid": "Bid", + "burned": "Burned", + "call_contract": "Call contract", + "deposit": "Stake", + "purchase": "Purchase", + "received": "Received", + "sent": "Sent", + "swapped": "Swapped", + "time_on": "on %{time}", + "withdraw": "Unstake", + "withdrawal_request": "Unstake Request" }, - "activity" : { - "buy_toncoin_btn" : "Buy Toncoin", - "empty_transaction_caption" : "Make your first transaction!", - "empty_transaction_title" : "Your history will be shown here", - "failed_transaction" : "Failed", - "receive_btn" : "Receive", - "received" : "Received", - "screen_title" : "History", - "sent" : "Sent" + "activity": { + "buy_toncoin_btn": "Buy Toncoin", + "empty_transaction_caption": "Make your first transaction!", + "empty_transaction_title": "Your history will be shown here", + "failed_transaction": "Failed", + "receive_btn": "Receive", + "received": "Received", + "screen_title": "History", + "sent": "Sent" }, - "add_edit_favorite" : { - "address_label" : "Address", - "add_title" : "New favorite", - "delete" : "Delete", - "edit_title" : "Edit favorite", - "name_placeholder" : "Name", - "save" : "Save" + "add_edit_favorite": { + "address_label": "Address", + "add_title": "New favorite", + "delete": "Delete", + "edit_title": "Edit favorite", + "name_placeholder": "Name", + "save": "Save" }, - "add_other_coins" : "Add other cryptos", - "address_copied" : "Address copied", - "address_update" : { - "first_option" : "Most likely the destination wallet is already actively used and you won’t notice any difference.", - "learn_more" : "Learn more", - "new_style" : "New address", - "notification_desc_did_change" : "On October 5 your wallet address did update to the UQ format which is just better. The old address will also work. You don’t need to do anything.", - "notification_desc_will_change" : "On October 5 your wallet address will update to the UQ format which is just better. The old address will also work. You don’t need to do anything.", - "old_style" : "Old address", - "post_dates" : "\nOctober 5, 2023: all addresses switch to the UQ format in Tonkeeper.\n\nJanuary 1, 2024: Tonkeeper stops checking the contract status and uses the “bounceable” flag in the address. Coins sent to non-published contracts with an EQ address will bounce back to the sender.", - "post_rest" : "There are two address styles on the TON blockchain and until now only one was used for apps and wallets.\n\nThe EQ format is best for smart contracts that process incoming funds. If a smart contract isn’t published yet — i.e., the code isn’t on the blockchain — then the TONs sent to that address will bounce back to the sender. And this is a safety feature: if there is any error, TONs bounce back.\n\nThe UQ format is best for wallets. Coins never bounce because wallets are designed to simply store funds. Each wallet starts as a plain address without a code on the blockchain. So, it would make no sense for coins to bounce back.\n\nThis year we are switching wallets to the more suitable UQ format. And if you continue sending funds to an old EQ address someone gave you, there will be two options:\n", - "post_top" : "By the end of this year the entire TON network will display wallet addresses differently. The new address will start with UQ instead of EQ. And the last four letters will change too. The old address will also work and direct to the same wallet. It doesn't effect the safety of funds stored in your wallet anyhow.", - "second_option" : "Less likely that the wallet has never been used for payments. And Tonkeeper won’t send coins to that address next year. You will have to ask the recipient for a new UQ address.", - "title" : "Address Update", - "why_change" : "Why change?", - "your_wallet" : "Your wallet" + "add_other_coins": "Add other cryptos", + "address_copied": "Address copied", + "address_update": { + "first_option": "Most likely the destination wallet is already actively used and you won’t notice any difference.", + "learn_more": "Learn more", + "new_style": "New address", + "notification_desc_did_change": "On October 5 your wallet address did update to the UQ format which is just better. The old address will also work. You don’t need to do anything.", + "notification_desc_will_change": "On October 5 your wallet address will update to the UQ format which is just better. The old address will also work. You don’t need to do anything.", + "old_style": "Old address", + "post_dates": "\nOctober 5, 2023: all addresses switch to the UQ format in Tonkeeper.\n\nJanuary 1, 2024: Tonkeeper stops checking the contract status and uses the “bounceable” flag in the address. Coins sent to non-published contracts with an EQ address will bounce back to the sender.", + "post_rest": "There are two address styles on the TON blockchain and until now only one was used for apps and wallets.\n\nThe EQ format is best for smart contracts that process incoming funds. If a smart contract isn’t published yet — i.e., the code isn’t on the blockchain — then the TONs sent to that address will bounce back to the sender. And this is a safety feature: if there is any error, TONs bounce back.\n\nThe UQ format is best for wallets. Coins never bounce because wallets are designed to simply store funds. Each wallet starts as a plain address without a code on the blockchain. So, it would make no sense for coins to bounce back.\n\nThis year we are switching wallets to the more suitable UQ format. And if you continue sending funds to an old EQ address someone gave you, there will be two options:\n", + "post_top": "By the end of this year the entire TON network will display wallet addresses differently. The new address will start with UQ instead of EQ. And the last four letters will change too. The old address will also work and direct to the same wallet. It doesn't effect the safety of funds stored in your wallet anyhow.", + "second_option": "Less likely that the wallet has never been used for payments. And Tonkeeper won’t send coins to that address next year. You will have to ask the recipient for a new UQ address.", + "title": "Address Update", + "why_change": "Why change?", + "your_wallet": "Your wallet" }, - "all_regions" : "All Regions", - "appearance_accent_name" : { - "andromeda" : "Andromeda", - "arctic" : "Arctic", - "azure" : "Azure", - "coral" : "Coral", - "cosmos" : "Cosmos", - "default" : "Tonkeeper", - "flamingo" : "Flamingo", - "fluid" : "Fluid", - "galaxy" : "Galaxy", - "iris" : "Iris", - "marine" : "Marine", - "ocean" : "Ocean", - "sky" : "Sky" + "appearance_accent_name": { + "andromeda": "Andromeda", + "arctic": "Arctic", + "azure": "Azure", + "coral": "Coral", + "cosmos": "Cosmos", + "default": "Tonkeeper", + "flamingo": "Flamingo", + "fluid": "Fluid", + "galaxy": "Galaxy", + "iris": "Iris", + "marine": "Marine", + "ocean": "Ocean", + "sky": "Sky" }, - "appearance_confirm" : "Set", - "appearance_description" : "TON Diamonds NFT will make your wallet more colorful and unique.", - "appearance_title" : "Theme", - "app_name" : "Tonkeeper", - "approval" : { + "appearance_confirm": "Set", + "appearance_description": "TON Diamonds NFT will make your wallet more colorful and unique.", + "appearance_title": "Theme", + "app_name": "Tonkeeper", + "approval": { "unverified_token": "Unverified Token", - "accept" : "Accept", - "accepted" : "Visible", - "accepted_at_collection" : "Accepted on %{date}", - "accepted_at_token" : "Accepted on %{date}", - "accepted_collection" : "Accepted collection", - "accepted_token" : "Accepted token", - "approve_all" : "Accept all", - "approve_collection_many" : "Approve incoming tokens from \"%{collection}\" collection", - "approve_collection_one" : "Approve incoming token from \"%{collection}\" collection", - "approve_many" : "Approve %{count} incoming tokens", - "approve_token" : "Approve incoming token \"%{name}\"", - "approve_two_collections" : "Approve incoming tokens from \"%{collection1}\" \"%{collection2}\" collections", - "approve_two_tokens" : "Approve incoming tokens \"%{name1}\" and \"%{name2}\"", - "blacklisted_collection" : "Blacklisted collection", - "blacklisted_token" : "Blacklisted token", - "decline" : "Decline", - "declined" : "Hidden", - "declined_at_collection" : "Declined on %{date}", - "declined_at_token" : "Declined on %{date}", - "details_collection" : "Collection details", - "details_token" : "Token details", - "id_collection" : "Collection ID", - "id_token" : "Token ID", - "manage_tokens" : "Manage tokens", - "move_to_accepted" : "Move to Accepted", - "move_to_accepted_collection" : "Show collection in wallet", - "move_to_accepted_token" : "Show token in wallet", - "move_to_declined" : "Move to Declined", - "move_to_declined_collection" : "Hide collection from wallet", - "move_to_declined_token" : "Hide token from wallet", - "name" : "Name", - "pending" : "Pending", - "show_all" : "Show all", - "single_token" : "Single token", - "token_copied" : "Token ID copied", - "token_count" : { - "one" : "%{count} token", - "other" : "%{count} tokens" + "accept": "Accept", + "accepted": "Visible", + "accepted_at_collection": "Accepted on %{date}", + "accepted_at_token": "Accepted on %{date}", + "accepted_collection": "Accepted collection", + "accepted_token": "Accepted token", + "approve_all": "Accept all", + "approve_collection_many": "Approve incoming tokens from \"%{collection}\" collection", + "approve_collection_one": "Approve incoming token from \"%{collection}\" collection", + "approve_many": "Approve %{count} incoming tokens", + "approve_token": "Approve incoming token \"%{name}\"", + "approve_two_collections": "Approve incoming tokens from \"%{collection1}\" \"%{collection2}\" collections", + "approve_two_tokens": "Approve incoming tokens \"%{name1}\" and \"%{name2}\"", + "blacklisted_collection": "Blacklisted collection", + "blacklisted_token": "Blacklisted token", + "decline": "Decline", + "declined": "Hidden", + "declined_at_collection": "Declined on %{date}", + "declined_at_token": "Declined on %{date}", + "details_collection": "Collection details", + "details_token": "Token details", + "id_collection": "Collection ID", + "id_token": "Token ID", + "manage_tokens": "Manage tokens", + "move_to_accepted": "Move to Accepted", + "move_to_accepted_collection": "Show collection in wallet", + "move_to_accepted_token": "Show token in wallet", + "move_to_declined": "Move to Declined", + "move_to_declined_collection": "Hide collection from wallet", + "move_to_declined_token": "Hide token from wallet", + "name": "Name", + "pending": "Pending", + "show_all": "Show all", + "single_token": "Single token", + "token_copied": "Token ID copied", + "token_count": { + "one": "%{count} token", + "other": "%{count} tokens" }, - "verify_collection" : "Verify collection", - "verify_description_collection" : "These tokens are from unknown issuer. To detect counterfeits, verify the collection ID with the issuer's official source. You can change token visibility later in settings.", - "verify_description_token" : "This token is from unknown issuer. To detect counterfeits, verify the token ID with the issuer's official source. You can change token visibility later in settings.", - "verify_token" : "Verify token", - "whitelisted_collection" : "Whitelisted collection", - "whitelisted_token" : "Whitelisted token" + "verify_collection": "Verify collection", + "verify_description_collection": "These tokens are from unknown issuer. To detect counterfeits, verify the collection ID with the issuer's official source. You can change token visibility later in settings.", + "verify_description_token": "This token is from unknown issuer. To detect counterfeits, verify the token ID with the issuer's official source. You can change token visibility later in settings.", + "verify_token": "Verify token", + "whitelisted_collection": "Whitelisted collection", + "whitelisted_token": "Whitelisted token" }, - "auth_failed" : "Authentication failed", - "balances_setup_wallet" : "Set up wallet", + "auth_failed": "Authentication failed", + "balances_setup_wallet": "Set up wallet", "battery": { "screen_title": "Battery", "settings": "Battery", @@ -159,893 +158,877 @@ "refilled": "Your battery is charged" } }, - "browser" : { - "about_dapps_caption" : "Explore apps and services where you can use Tonkeeper for sign-in and payments.", - "about_dapps_learn_more" : "Learn more", - "about_dapps_title" : "Use Tonkeeper with all TON apps and services", - "actions" : { - "copy_link" : "Copy link", - "disconnect" : "Disconnect", - "mute" : "Mute", - "refresh" : "Refresh", - "share" : "Share" + "browser": { + "about_dapps_caption": "Explore apps and services where you can use Tonkeeper for sign-in and payments.", + "about_dapps_learn_more": "Learn more", + "about_dapps_title": "Use Tonkeeper with all TON apps and services", + "actions": { + "copy_link": "Copy link", + "disconnect": "Disconnect", + "mute": "Mute", + "refresh": "Refresh", + "share": "Share" }, - "apps_all" : "All", - "connected" : "Connected", - "connected_empty_text" : "Explore apps and services in Tonkeeper browser.", - "connected_empty_title" : "Connected apps will be shown here", - "connected_title" : "Connected", - "empty_search" : "Your search returned no results", - "explore" : "Explore", - "explore_all" : "Explore all", - "more_description" : "Markets, exchanges and more", - "more_title" : "Explore all services", - "open_link" : "Open link", - "popular_title" : "Popular", - "remove_alert" : { - "approve_button" : "Remove", - "title" : "Remove “%{name}”?" + "apps_all": "All", + "connected": "Connected", + "connected_empty_text": "Explore apps and services in Tonkeeper browser.", + "connected_empty_title": "Connected apps will be shown here", + "connected_title": "Connected", + "empty_search": "Your search returned no results", + "explore": "Explore", + "explore_all": "Explore all", + "more_description": "Markets, exchanges and more", + "more_title": "Explore all services", + "open_link": "Open link", + "popular_title": "Popular", + "remove_alert": { + "approve_button": "Remove", + "title": "Remove “%{name}”?" }, "explore": "Explore", "connected": "Connected", "connected_empty_title": "Connected apps will be shown here", "connected_empty_text": "Explore apps and services in Tonkeeper browser.", "apps_all": "All", - "search_label" : "Search or enter address", - "start_typing" : "Enter an address or search the web", - "title" : "Browser", - "web_search_title" : "%{searchEngine} Search" + "search_label": "Search or enter address", + "start_typing": "Enter an address or search the web", + "title": "Browser", + "web_search_title": "%{searchEngine} Search" }, - "cancel" : "Cancel", - "chart" : { - "check_connection" : "Please check your connection and try again.", - "no_internet" : "No internet connection", - "periods" : { - "1D" : "D", - "1H" : "H", - "1M" : "M", - "1Y" : "Y", - "6M" : "6M", - "7D" : "W" + "cancel": "Cancel", + "chart": { + "check_connection": "Please check your connection and try again.", + "no_internet": "No internet connection", + "periods": { + "1D": "D", + "1H": "H", + "1M": "M", + "1Y": "Y", + "6M": "6M", + "7D": "W" }, - "price" : "Price" + "price": "Price" }, - "check_words_caption" : "To check whether you’ve written down your recovery phrase correctly, please enter the %{wordNum1}th, %{wordNum2}th, and  %{wordNum3}st words.", - "check_words_success" : "Congratulations! You’ve set up your wallet", - "check_words_title" : "So, let’s check", - "choose_country" : { - "auto" : "Auto", - "cancel" : "Cancel", - "empty_placeholder" : "Your search returned no results", - "search" : "Search", - "title" : "Choose your country", + "check_words_caption": "To check whether you’ve written down your recovery phrase correctly, please enter the %{wordNum1}th, %{wordNum2}th, and  %{wordNum3}st words.", + "check_words_success": "Congratulations! You’ve set up your wallet", + "check_words_title": "So, let’s check", + "choose_country": { + "auto": "Auto", + "cancel": "Cancel", + "empty_placeholder": "Your search returned no results", + "search": "Search", + "title": "Choose your country", "auto": "Auto" }, - "choose_currency" : { - "currencies" : { + "choose_currency": { + "currencies": { + "TON": "Toncoin", + "AED": "United Arab Emirates Dirham", + "BDT": "Bangladeshi Taka", + "BRL": "Brazilian Real", + "BYN": "Belarusian Ruble", + "CAD": "Canadian Dollar", + "CHF": "Swiss Franc", + "CNY": "China Yuan", + "EUR": "Euro", + "GBP": "Great Britain Pound", + "GEL": "Georgian Lari", + "IDR": "Indonesian Rupiah", + "ILS": "Israeli Shekel", + "INR": "Indian Rupee", + "IRR": "Iranian Rial", + "JPY": "Japanese Yen", + "KRW": "South Korean Won", + "KZT": "Kazakhstani Tenge", + "NGN": "Nigerian Naira", + "RUB": "Russian Ruble", + "THB": "Thai Baht", "TON": "Toncoin", - "AED" : "United Arab Emirates Dirham", - "BDT" : "Bangladeshi Taka", - "BRL" : "Brazilian Real", - "BYN" : "Belarusian Ruble", - "CAD" : "Canadian Dollar", - "CHF" : "Swiss Franc", - "CNY" : "China Yuan", - "EUR" : "Euro", - "GBP" : "Great Britain Pound", - "GEL" : "Georgian Lari", - "IDR" : "Indonesian Rupiah", - "ILS" : "Israeli Shekel", - "INR" : "Indian Rupee", - "IRR" : "Iranian Rial", - "JPY" : "Japanese Yen", - "KRW" : "South Korean Won", - "KZT" : "Kazakhstani Tenge", - "NGN" : "Nigerian Naira", - "RUB" : "Russian Ruble", - "THB" : "Thai Baht", - "TON" : "Toncoin", - "TRY" : "Turkish Lira", - "UAH" : "Ukrainian hryvnia", - "USD" : "United States Dollar", - "UZS" : "Uzbekistani sum", - "VND" : "Vietnamese Dong" + "TRY": "Turkish Lira", + "UAH": "Ukrainian hryvnia", + "USD": "United States Dollar", + "UZS": "Uzbekistani sum", + "VND": "Vietnamese Dong" }, - "header_title" : "Primary currency" + "header_title": "Primary currency" }, - "confirm" : "Confirm", - "confirm_renew_all_domains_title" : "Confirm action", - "confirm_sending_amount" : "Amount", - "confirm_sending_fee" : "Fee", - "confirm_sending_inactive_warn_about" : "What you should do", - "confirm_sending_inactive_warn_description" : "Do not proceed if you expect blockchain magic to happen. It won't.", - "confirm_sending_inactive_warn_title" : "Inactive contract", - "confirm_sending_liquid_warn_description" : "You will no longer receive rewards from the sended Staked TON. The new owner will begin to receive rewards.", - "confirm_sending_liquid_warn_title" : "Note", - "confirm_sending_message" : "Comment", - "confirm_sending_method_title" : "Send your funds to %{name}?", - "confirm_sending_recipient" : "Recipient", - "confirm_sending_recipient_address" : "Recipient address", - "confirm_sending_sent_caption_btc" : "Your transaction is sent to the network and will be processed within an hour.", - "confirm_sending_sent_caption_ton" : "Your transaction is sent to the network and will be processed in a few seconds.", - "confirm_sending_submit" : "Confirm and Send", - "confirm_sending_title" : "Confirm sending", - "confirmSendModal" : { + "confirm": "Confirm", + "confirm_renew_all_domains_title": "Confirm action", + "confirm_sending_amount": "Amount", + "confirm_sending_fee": "Fee", + "confirm_sending_inactive_warn_about": "What you should do", + "confirm_sending_inactive_warn_description": "Do not proceed if you expect blockchain magic to happen. It won't.", + "confirm_sending_inactive_warn_title": "Inactive contract", + "confirm_sending_liquid_warn_description": "You will no longer receive rewards from the sended Staked TON. The new owner will begin to receive rewards.", + "confirm_sending_liquid_warn_title": "Note", + "confirm_sending_message": "Comment", + "confirm_sending_method_title": "Send your funds to %{name}?", + "confirm_sending_recipient": "Recipient", + "confirm_sending_recipient_address": "Recipient address", + "confirm_sending_sent_caption_btc": "Your transaction is sent to the network and will be processed within an hour.", + "confirm_sending_sent_caption_ton": "Your transaction is sent to the network and will be processed in a few seconds.", + "confirm_sending_submit": "Confirm and Send", + "confirm_sending_title": "Confirm sending", + "confirmSendModal": { "refund": "Refund", - "network_fee" : "Network fee", + "network_fee": "Network fee", "will_be_paid_with_battery": "Will be paid with Battery", - "title" : "Confirm action", - "to_your_address" : "To your address", - "transaction_type" : { - "burn" : "Burn", - "receive" : "Receive", - "send" : "Send" + "title": "Confirm action", + "to_your_address": "To your address", + "transaction_type": { + "burn": "Burn", + "receive": "Receive", + "send": "Send" } }, - "continue" : "Continue", - "copied" : "Copied", - "copy_error_log" : "Copy error log", - "create_pin_current_title" : "Enter current passcode", - "create_pin_new_title" : "Create new passcode", - "create_pin_repeat_title" : "Re-enter passcode", - "create_wallet_caption" : "We strongly recommend you write down the recovery phrase because it’s the only way to have access to and recover your wallet in case of losing your device. Do not send it to yourself via email or take a screenshot. It’s safer when kept offline.", - "create_wallet_continue_button" : "Continue", - "create_wallet_generated" : "Your wallet has\njust been created!", - "create_wallet_generating" : "Generating wallet...", - "create_wallet_title" : "Grab a pen and a piece of paper", - "decryption_error" : "Decryption error", - "deploy_contract_button" : "Confirm and deploy", - "deploy_contract_title" : "Deploy contract", - "disable_nft_marketplace_banner_description" : "Collect and exchange.", - "dns_addresses" : { - "few" : "%{count} addresses", - "many" : "%{count} addresses", - "one" : "%{count} address", - "other" : "%{count} addresses" + "continue": "Continue", + "copied": "Copied", + "copy_error_log": "Copy error log", + "create_pin_current_title": "Enter current passcode", + "create_pin_new_title": "Create new passcode", + "create_pin_repeat_title": "Re-enter passcode", + "create_wallet_caption": "We strongly recommend you write down the recovery phrase because it’s the only way to have access to and recover your wallet in case of losing your device. Do not send it to yourself via email or take a screenshot. It’s safer when kept offline.", + "create_wallet_continue_button": "Continue", + "create_wallet_generated": "Your wallet has\njust been created!", + "create_wallet_generating": "Generating wallet...", + "create_wallet_title": "Grab a pen and a piece of paper", + "decryption_error": "Decryption error", + "deploy_contract_button": "Confirm and deploy", + "deploy_contract_title": "Deploy contract", + "disable_nft_marketplace_banner_description": "Collect and exchange.", + "dns_addresses": { + "few": "%{count} addresses", + "many": "%{count} addresses", + "one": "%{count} address", + "other": "%{count} addresses" }, - "dns_address_linked" : "Address linked", - "dns_address_unlinked" : "Address unlinked", - "dns_alert_expiring_many" : { - "few" : "You have %{count} expiring domains. Renew all until %{untilDate}.", - "many" : "You have %{count} expiring domains. Renew all until %{untilDate}.", - "one" : "You have %{count} expiring domain. Renew all until %{untilDate}.", - "other" : "You have %{count} expiring domains. Renew all until %{untilDate}." + "dns_address_linked": "Address linked", + "dns_address_unlinked": "Address unlinked", + "dns_alert_expiring_many": { + "few": "You have %{count} expiring domains. Renew all until %{untilDate}.", + "many": "You have %{count} expiring domains. Renew all until %{untilDate}.", + "one": "You have %{count} expiring domain. Renew all until %{untilDate}.", + "other": "You have %{count} expiring domains. Renew all until %{untilDate}." }, - "dns_alert_expiring_one" : "%{domain} expires in %{count} days. Renew it until %{untilDate}.", - "dns_current_address" : "Your current address", - "dns_expiration_date" : "Expiration date", - "dns_link_title" : "Confirm transaction", - "dns_on_sale_text" : "Domain is on sale at the marketplace now. For transfer, you should remove it from sale first.", - "dns_renew_all_until_btn" : "Renew all until %{untilDate}", - "dns_renew_in_progress_btn" : "Domain renew in progress…", - "dns_renew_toast_success" : "Domain renewed", - "dns_renew_until_btn" : "Renew until %{untilDate}", - "dns_renew_valid_caption" : { - "one" : "Expires in %{count} day", - "other" : "Expires in %{count} days" + "dns_alert_expiring_one": "%{domain} expires in %{count} days. Renew it until %{untilDate}.", + "dns_current_address": "Your current address", + "dns_expiration_date": "Expiration date", + "dns_link_title": "Confirm transaction", + "dns_on_sale_text": "Domain is on sale at the marketplace now. For transfer, you should remove it from sale first.", + "dns_renew_all_until_btn": "Renew all until %{untilDate}", + "dns_renew_in_progress_btn": "Domain renew in progress…", + "dns_renew_toast_success": "Domain renewed", + "dns_renew_until_btn": "Renew until %{untilDate}", + "dns_renew_valid_caption": { + "one": "Expires in %{count} day", + "other": "Expires in %{count} days" }, - "dns_replace_button" : "Replace", - "dns_replace_description" : "Add wallet address that domain {{domain}} will link to.", - "dns_replace_save" : "Save", - "dns_unlink_title" : "Confirm unlink", - "dns_wallet_address" : "Wallet address", - "domains_renewed" : "Domains renewed", - "edit_coins_add" : "Add", - "edit_coins_added" : "Added", - "edit_coins_added_toast" : "Added", - "edit_coins_hide" : "Hide", - "edit_coins_title" : "Add crypto", - "error_network" : "Network error", - "error_occurred" : "An error occurred", - "exchange_method_dont_show_again" : "Do not show again", - "exchange_method_open_warning" : "You are opening an external app not operated by Tonkeeper.", - "exchange_modal" : { - "hide" : "Hide", - "other_ways_to_buy" : "Other ways to buy", - "show_all" : "Show all", + "dns_replace_button": "Replace", + "dns_replace_description": "Add wallet address that domain {{domain}} will link to.", + "dns_replace_save": "Save", + "dns_unlink_title": "Confirm unlink", + "dns_wallet_address": "Wallet address", + "domains_renewed": "Domains renewed", + "edit_coins_add": "Add", + "edit_coins_added": "Added", + "edit_coins_added_toast": "Added", + "edit_coins_hide": "Hide", + "edit_coins_title": "Add crypto", + "error_network": "Network error", + "error_occurred": "An error occurred", + "exchange_method_dont_show_again": "Do not show again", + "exchange_method_open_warning": "You are opening an external app not operated by Tonkeeper.", + "exchange_modal": { + "hide": "Hide", + "other_ways_to_buy": "Other ways to buy", + "show_all": "Show all", "buy": "Buy", "sell": "Sell" }, - "exchange" : { - "not_exists" : "Invalid exchange ID" + "exchange": { + "not_exists": "Invalid exchange ID" }, - "exchange_other_ways" : "Other ways to buy or sell TON", - "exchange_telegram_bot" : "TELEGRAM BOT", - "exchange_title" : "Buy TON", - "expiring_domains" : "Expiring domains", - "form_optional_indicator" : "Optional", - "import_wallet_caption" : "To restore access to your wallet, enter the 24 secret recovery words given to you when you created your wallet.", - "import_wallet_reset_caption" : "Please restore access to your wallet\n8by entering 24 secret words you wrote down then creating the wallet.", - "import_wallet_title" : "Enter your\nrecovery phrase", - "import_wallet_wrong_words_err" : "Incorrect phrase", - "info_about_inactive_back" : "Back", - "info_about_inactive_desc1" : "Tonkeeper does not know if this address is a wallet or a smart contract.", - "info_about_inactive_desc2" : "If you wish to simply deposit money to a wallet — you may proceed.", - "info_about_inactive_desc3_1" : "If you expect an automatic action from a smart contract,", - "info_about_inactive_desc3_2" : "— your transfer may get stuck on that address.", - "info_about_inactive_desc3_bold" : " DO NOT proceed ", - "info_about_inactive_title" : "Inactive contract", - "intro_continue_btn" : "Get started", - "intro_item1_caption" : "Thanks to the unique architecture of The Open Network, TON transactions are settled in seconds.", - "intro_item1_title" : "World-class speed", - "intro_item2_caption" : "Tonkeeper stores your cryptographic keys on your device. All trades are executed via decentralized protocols so that your crypto never ends up in the hands of centralized exchanges.", - "intro_item2_title" : "End-to-end security", - "intro_item3_caption" : "-", - "intro_item3_title" : "-", - "intro_title" : "Welcome\nto ", - "jetton_id" : "Token ID: %{jettonAddress}", - "jetton_id_copied" : "Token ID copied", - "jetton_name" : "%{name} Token", - "jetton_open_explorer" : "View details", - "jetton_price" : "Price:", - "jettons_list_title" : "Tokens", - "jettons_manage_tokens" : "Manage tokens", - "jettons_show_jettons" : "Show tokens in wallet", - "jetton_token" : "Token", - "language" : { - "language_alert" : { - "cancel" : "Cancel", - "open" : "Settings", - "title" : "To change the language of the app, go to device Settings" + "exchange_other_ways": "Other ways to buy or sell TON", + "exchange_telegram_bot": "TELEGRAM BOT", + "exchange_title": "Buy TON", + "expiring_domains": "Expiring domains", + "form_optional_indicator": "Optional", + "import_wallet_caption": "To restore access to your wallet, enter the 24 secret recovery words given to you when you created your wallet.", + "import_wallet_reset_caption": "Please restore access to your wallet\n8by entering 24 secret words you wrote down then creating the wallet.", + "import_wallet_title": "Enter your\nrecovery phrase", + "import_wallet_wrong_words_err": "Incorrect phrase", + "info_about_inactive_back": "Back", + "info_about_inactive_desc1": "Tonkeeper does not know if this address is a wallet or a smart contract.", + "info_about_inactive_desc2": "If you wish to simply deposit money to a wallet — you may proceed.", + "info_about_inactive_desc3_1": "If you expect an automatic action from a smart contract,", + "info_about_inactive_desc3_2": "— your transfer may get stuck on that address.", + "info_about_inactive_desc3_bold": " DO NOT proceed ", + "info_about_inactive_title": "Inactive contract", + "intro_continue_btn": "Get started", + "intro_item1_caption": "Thanks to the unique architecture of The Open Network, TON transactions are settled in seconds.", + "intro_item1_title": "World-class speed", + "intro_item2_caption": "Tonkeeper stores your cryptographic keys on your device. All trades are executed via decentralized protocols so that your crypto never ends up in the hands of centralized exchanges.", + "intro_item2_title": "End-to-end security", + "intro_item3_caption": "-", + "intro_item3_title": "-", + "intro_title": "Welcome\nto ", + "jetton_id": "Token ID: %{jettonAddress}", + "jetton_id_copied": "Token ID copied", + "jetton_name": "%{name} Token", + "jetton_open_explorer": "View details", + "jetton_price": "Price:", + "jettons_list_title": "Tokens", + "jettons_manage_tokens": "Manage tokens", + "jettons_show_jettons": "Show tokens in wallet", + "jetton_token": "Token", + "language": { + "language_alert": { + "cancel": "Cancel", + "open": "Settings", + "title": "To change the language of the app, go to device Settings" }, - "list_item" : { - "title" : "Language", - "value" : "English" + "list_item": { + "title": "Language", + "value": "English" } }, - "later" : "Later", - "legal_font_license" : "Montserrat font", - "legal_header_title" : "Legal", - "legal_licenses_title" : "Licenses", - "legal_privacy" : "Privacy policy", - "legal_terms" : "Terms of service", - "link_copied" : "Link copied", - "loading" : "Loading", - "manage_other_coins" : "Manage my crypto", - "migration_cancel_btn" : "Upgrade later", - "migration_caption" : "Tonkeeper introduces new wallet format that supports subscription payments. Your balance will be transferred to a new address. Your recovery phrase will remain the same.", - "migration_failed" : "Migration failed. Unable to transfer your balance.", - "migration_fee_info" : "Network fee ≈%{tonFee} TON (%{fiatFee})", - "migration_in_progress" : "Migration to Wallet v4 in progress", - "migration_migrate_btn" : "Upgrade wallet", - "migration_new_wallet" : "New address", - "migration_old_wallet" : "Old address", - "migration_title" : "Upgrade your wallet", - "nft_about_collection" : "About collection", - "nft_about_dns" : "TON DNS is a service that allows users to assign a human-readable name to crypto wallets, smart contracts, and websites. \n\nWith TON DNS, access to decentralized services is analogous to access to websites on the internet.", - "nft_browse_markets" : "Browse Markets", - "nft_chain" : "Chain", - "nft_change_owner_title" : "Change collection owner", - "nft_change_theme" : "Change theme", - "nft_collection" : "Collection", - "nft_collection_name" : "Collection name", - "nft_confirm_operation" : "Сonfirm", - "nft_contract_address" : "Contract address", - "nft_deploy_collection_title" : "Create NFT Collection", - "nft_details" : "Details", - "nft_diamonds_description" : "TON Diamonds possess the power to change the theme in your Tonkeeper wallet and match it to the color of your NFT.", - "nft_features" : "Features", - "nft_fee" : "Fee", - "nft_fee_and_royalties" : "Fees & royalties", - "nft_hide_details" : "Hide details", - "nft_item_deploy_title" : "Mint NFT", - "nft_item_name" : "NFT name", - "nft_link_domain_button" : "Link domain", - "nft_link_domain_caption" : "After you link the domain you will be able to transfer it and use as an alias for your address.", - "nft_link_domain_mismatch_warn" : "The domain is not linked to your current address. Be careful with transactions to this domain.", - "nft_link_username_button" : "Link name", - "nft_link_username_caption" : "After you link the name you will be able to transfer it and use as an alias for your address.", - "nft_link_username_mismatch_warn" : "The name is not linked to your current address. Be careful with transactions to this name.", - "nft_marketplace_address" : "Marketplace", - "nft_marketplace_banner_description" : "Buy, sell, collect and exchange.", - "nft_marketplace_banner_title" : "Your NFT tokens will be stored here", - "nft_marketplaces" : "Discover", - "nft_marketplaces_title" : "NFT Markets", - "nft_metadata" : "Metadata", - "nft_more" : "More", - "nft_new_owner_address" : "New owner address", - "nft_on_sale" : "On sale", - "nft_on_sale_text" : "NFT is on sale at the marketplace now. For transfer, you should remove it from sale first.", - "nft_open_in_marketplace" : "View on NFT Market", - "nft_operations_expired" : "Request timed out, please try again", - "nft_operation_success" : "Done", - "nft_owner_address" : "Owner", - "nft_price" : "Price", - "nft_proceeds" : "Your proceeds", - "nft_properties" : "Properties", - "nft_royalty" : "Royalty", - "nft_royalty_address" : "Royalty address", - "nft_sale_cancel_title" : "Remove from sale", - "nft_sale_place_title" : "Sell NFT", - "nft_show_details" : "Show details", - "nft_single_nft" : "Single NFT", - "nft_standard" : "Token Standard", - "nft_title" : "NFTs", - "nft_token_id" : "Token ID", - "nft_transaction_head_placeholder" : "NFT", - "nft_transfer_comment" : "Comment", - "nft_transfer_description" : "The NFT will be sent to this address. Be careful when sending an NFT to another user.", - "nft_transfer_dns" : "Transfer", - "nft_transfer_nft" : "Transfer", - "nft_transfer_recipient" : "Recipient", - "nft_transfer_title" : "Transfer NFT", - "nft_unlink_domain_button" : "Linked with {{address}}", - "nft_unnamed_collection" : "Unnamed collection", - "nft_view_in_explorer" : "View in explorer", - "nokyc" : "no KYC", - "notifications" : { - "alert" : { - "cancel" : "Cancel", - "description" : "Link address from the notification doesn't match with app address.", - "open" : "Open anyway", - "title" : "Are you sure you want to open an external link?" + "later": "Later", + "legal_font_license": "Montserrat font", + "legal_header_title": "Legal", + "legal_licenses_title": "Licenses", + "legal_privacy": "Privacy policy", + "legal_terms": "Terms of service", + "link_copied": "Link copied", + "loading": "Loading", + "manage_other_coins": "Manage my crypto", + "nft_about_collection": "About collection", + "nft_about_dns": "TON DNS is a service that allows users to assign a human-readable name to crypto wallets, smart contracts, and websites. \n\nWith TON DNS, access to decentralized services is analogous to access to websites on the internet.", + "nft_browse_markets": "Browse Markets", + "nft_chain": "Chain", + "nft_change_owner_title": "Change collection owner", + "nft_change_theme": "Change theme", + "nft_collection": "Collection", + "nft_collection_name": "Collection name", + "nft_confirm_operation": "Сonfirm", + "nft_contract_address": "Contract address", + "nft_deploy_collection_title": "Create NFT Collection", + "nft_details": "Details", + "nft_diamonds_description": "TON Diamonds possess the power to change the theme in your Tonkeeper wallet and match it to the color of your NFT.", + "nft_features": "Features", + "nft_fee": "Fee", + "nft_fee_and_royalties": "Fees & royalties", + "nft_hide_details": "Hide details", + "nft_item_deploy_title": "Mint NFT", + "nft_item_name": "NFT name", + "nft_link_domain_button": "Link domain", + "nft_link_domain_caption": "After you link the domain you will be able to transfer it and use as an alias for your address.", + "nft_link_domain_mismatch_warn": "The domain is not linked to your current address. Be careful with transactions to this domain.", + "nft_link_username_button": "Link name", + "nft_link_username_caption": "After you link the name you will be able to transfer it and use as an alias for your address.", + "nft_link_username_mismatch_warn": "The name is not linked to your current address. Be careful with transactions to this name.", + "nft_marketplace_address": "Marketplace", + "nft_marketplace_banner_description": "Buy, sell, collect and exchange.", + "nft_marketplace_banner_title": "Your NFT tokens will be stored here", + "nft_marketplaces": "Discover", + "nft_marketplaces_title": "NFT Markets", + "nft_metadata": "Metadata", + "nft_more": "More", + "nft_new_owner_address": "New owner address", + "nft_on_sale": "On sale", + "nft_on_sale_text": "NFT is on sale at the marketplace now. For transfer, you should remove it from sale first.", + "nft_open_in_marketplace": "View on NFT Market", + "nft_operations_expired": "Request timed out, please try again", + "nft_operation_success": "Done", + "nft_owner_address": "Owner", + "nft_price": "Price", + "nft_proceeds": "Your proceeds", + "nft_properties": "Properties", + "nft_royalty": "Royalty", + "nft_royalty_address": "Royalty address", + "nft_sale_cancel_title": "Remove from sale", + "nft_sale_place_title": "Sell NFT", + "nft_show_details": "Show details", + "nft_single_nft": "Single NFT", + "nft_standard": "Token Standard", + "nft_title": "NFTs", + "nft_token_id": "Token ID", + "nft_transaction_head_placeholder": "NFT", + "nft_transfer_comment": "Comment", + "nft_transfer_description": "The NFT will be sent to this address. Be careful when sending an NFT to another user.", + "nft_transfer_dns": "Transfer", + "nft_transfer_nft": "Transfer", + "nft_transfer_recipient": "Recipient", + "nft_transfer_title": "Transfer NFT", + "nft_unlink_domain_button": "Linked with {{address}}", + "nft_unnamed_collection": "Unnamed collection", + "nft_view_in_explorer": "View in explorer", + "notifications": { + "alert": { + "cancel": "Cancel", + "description": "Link address from the notification doesn't match with app address.", + "open": "Open anyway", + "title": "Are you sure you want to open an external link?" }, - "allow_notifications" : "Enable notifications", - "apps" : "Apps", - "apps_description" : "Notifications from connected apps in your activity", - "disconnect_app" : "Disconnect %{app_name}", - "disconnected_app" : "Disconnected app", - "earlier" : "Earlier", - "from_connected" : "From connected apps", - "muted" : "Notifications have been muted", - "mute_notifications" : "Mute notifications", - "notifications" : "Notifications", - "placeholder" : { - "description" : "Explore apps and services in Tonkeeper browser.", - "title" : "Notifications will be shown here" + "allow_notifications": "Enable notifications", + "apps": "Apps", + "apps_description": "Notifications from connected apps in your activity", + "disconnect_app": "Disconnect %{app_name}", + "disconnected_app": "Disconnected app", + "earlier": "Earlier", + "from_connected": "From connected apps", + "muted": "Notifications have been muted", + "mute_notifications": "Mute notifications", + "notifications": "Notifications", + "placeholder": { + "description": "Explore apps and services in Tonkeeper browser.", + "title": "Notifications will be shown here" }, - "report" : "Report" + "report": "Report" }, - "notifications_disabled_action" : "Open Settings", - "notifications_disabled_description" : "You turned off notifications in your phone’s settings. To activate notifications, go to Settings on this device.", - "notifications_disabled_title" : "Notifications are disabled", - "notifications_not_supported" : "Notifications are not supported on your device", - "notifications_switch_title" : "Push notifications", - "notifications_title" : "Notifications", - "notification_switch_description" : "Get notifications when you receive TON, tokens and NFTs. Notifications from connected apps.", - "notify_connection_err_caption" : "%{host} is not responding. Please try again later.", - "notify_connection_err_caption_few" : "%{hosts} and %{lastHost} are not responding. Please try again later.", - "notify_connection_err_title" : "Could not connect to server", - "notify_incorrect_time_err_caption" : "In device settings, enable automatic time and date. When time isn't set automatically, it may affect fund transfers.", - "notify_incorrect_time_err_title" : "Time and date are incorrect", - "notify_no_signal_caption" : "Please check your internet connection.", - "notify_no_signal_title" : "No signal", - "passcode_changed" : "Passcode changed", - "paste" : "Paste", - "pin_enter_faceid_err" : " Biometrics check failed", - "pin_enter_skip_faceid_err" : "Biometrics is required", - "platform" : { - "android" : { - "capitalized_face_recognition" : "Face recognition", - "capitalized_fingerprint" : "Fingerprint", - "face_recognition" : "face recognition", - "face_recognition_genitive" : "face recognition", - "fingerprint" : "fingerprint", - "fingerprint_genitive" : "fingerprint" + "notifications_disabled_action": "Open Settings", + "notifications_disabled_description": "You turned off notifications in your phone’s settings. To activate notifications, go to Settings on this device.", + "notifications_disabled_title": "Notifications are disabled", + "notifications_not_supported": "Notifications are not supported on your device", + "notifications_switch_title": "Push notifications", + "notifications_title": "Notifications", + "notification_switch_description": "Get notifications when you receive TON, tokens and NFTs. Notifications from connected apps.", + "notify_connection_err_caption": "%{host} is not responding. Please try again later.", + "notify_connection_err_caption_few": "%{hosts} and %{lastHost} are not responding. Please try again later.", + "notify_connection_err_title": "Could not connect to server", + "notify_incorrect_time_err_caption": "In device settings, enable automatic time and date. When time isn't set automatically, it may affect fund transfers.", + "notify_incorrect_time_err_title": "Time and date are incorrect", + "notify_no_signal_caption": "Please check your internet connection.", + "notify_no_signal_title": "No signal", + "passcode_changed": "Passcode changed", + "paste": "Paste", + "pin_enter_faceid_err": " Biometrics check failed", + "pin_enter_skip_faceid_err": "Biometrics is required", + "platform": { + "android": { + "capitalized_face_recognition": "Face recognition", + "capitalized_fingerprint": "Fingerprint", + "face_recognition": "face recognition", + "face_recognition_genitive": "face recognition", + "fingerprint": "fingerprint", + "fingerprint_genitive": "fingerprint" }, - "ios" : { - "capitalized_face_recognition" : "Face ID", - "capitalized_fingerprint" : "Touch ID", - "face_recognition" : "Face ID", - "face_recognition_genitive" : "Face ID", - "fingerprint" : "Touch ID", - "fingerprint_genitive" : "Touch ID" + "ios": { + "capitalized_face_recognition": "Face ID", + "capitalized_fingerprint": "Touch ID", + "face_recognition": "Face ID", + "face_recognition_genitive": "Face ID", + "fingerprint": "Touch ID", + "fingerprint_genitive": "Touch ID" } }, - "programmable_nfts" : { - "alert" : { - "cancel" : "Cancel", - "description" : "Visit this external link only if you trust the author of the collection.\n\n{{uri}}", - "open" : "Open anyway", - "title" : "Are you sure you want to open an external link?" + "programmable_nfts": { + "alert": { + "cancel": "Cancel", + "description": "Visit this external link only if you trust the author of the collection.\n\n{{uri}}", + "open": "Open anyway", + "title": "Are you sure you want to open an external link?" } }, - "receive_address_title" : "Or use wallet address", - "receive_copy" : "Copy", - "receiveModal" : { - "copy" : "Copy", - "receive" : "Receive", - "receive_description" : "Send only %{tokenName} and tokens in TON network to this address, or you might lose your funds.", - "receive_title" : "Receive %{tokenName}", - "receive_ton" : "Send only Toncoin TON and tokens in TON network to this address, or you might lose your funds." + "receive_address_title": "Or use wallet address", + "receive_copy": "Copy", + "receiveModal": { + "copy": "Copy", + "receive": "Receive", + "receive_description": "Send only %{tokenName} and tokens in TON network to this address, or you might lose your funds.", + "receive_title": "Receive %{tokenName}", + "receive_ton": "Send only Toncoin TON and tokens in TON network to this address, or you might lose your funds." }, - "receive_qr_title" : "Show QR code to receive", - "receive_received_title" : "You received\n%{amount} %{currency}", - "receive_share" : "Share", - "receive_title" : "Receive %{currency}", - "receive_ton_and_jettons" : "Receive TON and other tokens", - "refresh_app" : "Restart", - "region_nokyc" : "Neutral Waters", - "reminder_notifications_caption" : "Get notifications when you receive TON, tokens and NFTs.", - "reminder_notifications_enable_button" : "Enable notifications", - "reminder_notifications_later_button" : "Later", - "reminder_notifications_title" : "Get instant notifications", - "renew_in_progress" : "Renew in progress…", - "renew_progress_of" : "%{current} of %{count}", - "require_create_wallet_modal_caption" : "You need a connected wallet to use\nTonkeeper. Either create a new wallet or import an existing one.", - "require_create_wallet_modal_create_new" : "Create new wallet", - "require_create_wallet_modal_import" : "Import existing wallet", - "require_create_wallet_modal_title" : "Let’s set up your wallet", - "scan_qr_open_settings" : "Open settings", - "scan_qr_permission_error" : "Allow camera access to scan QR codes", - "scan_qr_title" : "Scan QR code", - "secret_words_caption" : "Write down these 24 words in the order given below and store them in a secret, safe place.", - "secret_words_title" : "Your recovery phrase", - "security_change_passcode" : "Change passcode", - "security_migration_caption" : "Now your balance and any operations are secured by a passcode. You can also speed up with biometric check.", - "security_migration_skip_button" : "Do not update now", - "security_migration_submit_button" : "Update security settings", - "security_migration_title" : "Update wallet security", - "security_reset_passcode" : "Reset passcode", - "security_title" : "Security", - "security_use_biometry_switch" : "Use %{biometryType}", - "security_use_biometry_tip" : "You can always unlock your wallet with a passcode.", - "send_address_placeholder" : "Address or name", - "send_all_warning_title" : "Are you sure you want to send all your balance?", - "send_build_tx_error" : "Your transaction failed", - "send_comment_label" : "Add a comment", - "send_fee_estimation_error" : "Failed to calculate fee", - "send_get_wallet_info_error" : "Failed to get wallet info", - "send_insufficient_funds" : "Insufficient funds", - "send_invalid_recipient_caption" : "The domain does not exist or the wallet address is not linked to it", - "send_invalid_recipient_title" : "Invalid recipient", - "send_lockup_warning_caption" : "Please check that you are sending to the allowed address.\n\nFee will be deducted even if transaction fails.", - "send_lockup_warning_submit_button" : "Send", - "send_lockup_warning_title" : "Amount exceeds liquid balance", - "send_publish_tx_error" : "Failed to send transaction", - "send_screen_steps" : { - "address" : { - "delete_alert_text" : "Are you sure want to delete «%{name}» from your favorites?", - "placeholder" : "Wallet address or domain", - "recent_label" : "Recent", - "suggest_actions" : { - "add" : "Add to favorites", - "delete" : "Delete", - "edit" : "Edit favorite", - "hide" : "Hide" + "receive_qr_title": "Show QR code to receive", + "receive_received_title": "You received\n%{amount} %{currency}", + "receive_share": "Share", + "receive_title": "Receive %{currency}", + "receive_ton_and_jettons": "Receive TON and other tokens", + "refresh_app": "Restart", + "reminder_notifications_caption": "Get notifications when you receive TON, tokens and NFTs.", + "reminder_notifications_enable_button": "Enable notifications", + "reminder_notifications_later_button": "Later", + "reminder_notifications_title": "Get instant notifications", + "renew_in_progress": "Renew in progress…", + "renew_progress_of": "%{current} of %{count}", + "require_create_wallet_modal_caption": "You need a connected wallet to use\nTonkeeper. Either create a new wallet or import an existing one.", + "require_create_wallet_modal_create_new": "Create new wallet", + "require_create_wallet_modal_import": "Import existing wallet", + "require_create_wallet_modal_title": "Let’s set up your wallet", + "scan_qr_open_settings": "Open settings", + "scan_qr_permission_error": "Allow camera access to scan QR codes", + "scan_qr_title": "Scan QR code", + "secret_words_caption": "Write down these 24 words in the order given below and store them in a secret, safe place.", + "secret_words_title": "Your recovery phrase", + "security_change_passcode": "Change passcode", + "security_reset_passcode": "Reset passcode", + "security_title": "Security", + "security_use_biometry_switch": "Use %{biometryType}", + "security_use_biometry_tip": "You can always unlock your wallet with a passcode.", + "send_address_placeholder": "Address or name", + "send_all_warning_title": "Are you sure you want to send all your balance?", + "send_build_tx_error": "Your transaction failed", + "send_comment_label": "Add a comment", + "send_fee_estimation_error": "Failed to calculate fee", + "send_get_wallet_info_error": "Failed to get wallet info", + "send_insufficient_funds": "Insufficient funds", + "send_invalid_recipient_caption": "The domain does not exist or the wallet address is not linked to it", + "send_invalid_recipient_title": "Invalid recipient", + "send_lockup_warning_caption": "Please check that you are sending to the allowed address.\n\nFee will be deducted even if transaction fails.", + "send_lockup_warning_submit_button": "Send", + "send_lockup_warning_title": "Amount exceeds liquid balance", + "send_publish_tx_error": "Failed to send transaction", + "send_screen_steps": { + "address": { + "delete_alert_text": "Are you sure want to delete «%{name}» from your favorites?", + "placeholder": "Wallet address or domain", + "recent_label": "Recent", + "suggest_actions": { + "add": "Add to favorites", + "delete": "Delete", + "edit": "Edit favorite", + "hide": "Hide" }, - "suggests_label" : "Favorites and recent", - "title" : "Recipient" + "suggests_label": "Favorites and recent", + "title": "Recipient" }, - "amount" : { - "insufficient_balance" : "Insufficient balance", - "less_than_min" : "Minimum %{minAmount} TON", - "liquid_jetton_note" : "Sending tsTON liquidity token", - "max" : "MAX", - "recipient_label" : "To:", - "remaining" : "Available: %{amount}", - "title" : "Amount" + "amount": { + "insufficient_balance": "Insufficient balance", + "less_than_min": "Minimum %{minAmount} TON", + "liquid_jetton_note": "Sending tsTON liquidity token", + "max": "MAX", + "recipient_label": "To:", + "remaining": "Available: %{amount}", + "title": "Amount" }, - "comfirm" : { - "action" : "%{coin} transfer", - "comment_decrypt" : "Decrypt", - "comment_description" : "Will be visible to everyone.", - "comment_description_encrypted" : "Will be visible only to recipient and you.", - "comment_encrypt" : "Encrypt comment", - "comment_label_encrypted" : "Encrypted comment", - "comment_label" : "Comment", - "comment_label_required" : "Required comment", - "comment_required_text" : "You must include the note from the exchange for transfer. Without it your funds will be lost. ", - "details_label" : "Details", - "details_max_balance_label" : "Sending max. balance of %{currency}", - "title" : "Confirm action", + "comfirm": { + "action": "%{coin} transfer", + "comment_decrypt": "Decrypt", + "comment_description": "Will be visible to everyone.", + "comment_description_encrypted": "Will be visible only to recipient and you.", + "comment_encrypt": "Encrypt comment", + "comment_label_encrypted": "Encrypted comment", + "comment_label": "Comment", + "comment_label_required": "Required comment", + "comment_required_text": "You must include the note from the exchange for transfer. Without it your funds will be lost. ", + "details_label": "Details", + "details_max_balance_label": "Sending max. balance of %{currency}", + "title": "Confirm action", "will_be_paid_with_battery": "Will be paid with Battery" }, - "done" : { - "add_favorite" : "Save address to favorites", - "address" : "Address: %{address}", - "comment" : "Comment: %{comment}", - "description" : "Your transaction has been sent to the network and will be processed in a few seconds.", - "done_label" : "Done", - "favorite_saved" : "Saved to favorites", - "fee" : "Fee: %{fee}", - "title" : "%{currency} sent!", - "to" : "To: %{name}" + "done": { + "add_favorite": "Save address to favorites", + "address": "Address: %{address}", + "comment": "Comment: %{comment}", + "description": "Your transaction has been sent to the network and will be processed in a few seconds.", + "done_label": "Done", + "favorite_saved": "Saved to favorites", + "fee": "Fee: %{fee}", + "title": "%{currency} sent!", + "to": "To: %{name}" } }, - "send_sending_failed" : "Sending failed", - "send_sending_wrong_time_description" : "Turn on automatic time and date in your device settings. Then retry your transfer.", - "send_sending_wrong_time_title" : "Error occurred", - "send_title" : "Send %{currency}", - "settings_appearance" : "Theme", - "settings_backup_seed" : "Show recovery phrase", - "settings_contact_support" : "Contact us", - "settings_delete_account" : "Delete account", - "settings_delete_alert_button" : "Delete account and data", - "settings_delete_alert_caption" : "This action will delete your account and all data from this application.", - "settings_delete_alert_title" : "Are you sure you want to delete your account?", - "settings_jettons_list" : "Tokens", - "settings_legal_documents" : "Legal", + "send_sending_failed": "Sending failed", + "send_sending_wrong_time_description": "Turn on automatic time and date in your device settings. Then retry your transfer.", + "send_sending_wrong_time_title": "Error occurred", + "send_title": "Send %{currency}", + "settings_appearance": "Theme", + "settings_backup_seed": "Recovery Phrase", + "settings_contact_support": "Contact us", + "settings_delete_account": "Delete account", + "settings_delete_alert_button": "Delete account and data", + "settings_delete_alert_caption": "This action will delete your account and all data from this application.", + "settings_delete_alert_title": "Are you sure you want to delete your account?", + "settings_jettons_list": "Tokens", + "settings_legal_documents": "Legal", "language": { "list_item": { "title": "Language", "value": "English" }, - "language_alert" : { + "language_alert": { "title": "To change the language of the app, go to device Settings", "cancel": "Cancel", "open": "Settings" } }, - "settings_network_alert_title" : "Select network", - "settings_news" : "Tonkeeper news", - "settings_notifications" : "Notifications", - "settings_primary_currency" : "Currency", - "settings_rate" : "Rate Tonkeeper", - "settings_recovery_phrase" : "Recovery phrase", - "settings_reset" : "Log out", - "settings_reset_alert_button" : "Log out", - "settings_reset_alert_caption" : "This will erase keys to the wallet. Make sure you have backed up your secret recovery phrase.", - "settings_reset_alert_title" : "Log out?", - "settings_search_engine" : "Search", - "settings_security" : "Security", - "settings_subscriptions" : "Subscriptions", - "settings_support" : "Support", - "settings_title" : "Settings", - "settings_to_mainnet" : "Switch to Mainnet", - "settings_to_testnet" : "Switch to Testnet", - "settings_version" : "Version", - "settings_wallet_version" : "Active address", - "setup_biometry_caption" : "%{biometryType} allows you to open your wallet faster without having\nto enter your password.", - "setup_biometry_enable_button" : "Enable %{biometryType}", - "setup_biometry_title" : "Quick sign-in with %{biometryType}", - "setup_notifications_caption" : "Get notifications when you \n receive TON, tokens and NFTs.", - "setup_notifications_enable_button" : "Enable notifications", - "setup_notifications_title" : "Get instant notifications", - "skip" : "Skip", - "spam_action" : "Spam", - "staking" : { - "active" : "Active", - "confirm" : { - "address" : { - "label" : "Recipient address" + "settings_network_alert_title": "Select network", + "settings_news": "Tonkeeper news", + "settings_notifications": "Notifications", + "settings_primary_currency": "Currency", + "settings_rate": "Rate Tonkeeper", + "settings_recovery_phrase": "Recovery phrase", + "settings_reset": "Log out", + "settings_reset_alert_button": "Log out", + "settings_reset_alert_caption": "This will erase keys to all wallets. Make sure you have backed up your secret recovery phrase.", + "settings_reset_alert_title": "Log out?", + "settings_search_engine": "Search", + "settings_security": "Security", + "settings_subscriptions": "Subscriptions", + "settings_support": "Support", + "settings_title": "Settings", + "settings_to_mainnet": "Switch to Mainnet", + "settings_to_testnet": "Switch to Testnet", + "settings_version": "Version", + "setup_biometry_caption": "%{biometryType} allows you to open your wallet faster without having\nto enter your password.", + "setup_biometry_enable_button": "Enable %{biometryType}", + "setup_biometry_title": "Quick sign-in with %{biometryType}", + "setup_notifications_caption": "Get notifications when you \n receive TON, tokens and NFTs.", + "setup_notifications_enable_button": "Enable notifications", + "setup_notifications_title": "Get instant notifications", + "skip": "Skip", + "spam_action": "Spam", + "staking": { + "active": "Active", + "confirm": { + "address": { + "label": "Recipient address" }, - "amount" : { - "label" : "Amount", - "value" : "%{value} TON" + "amount": { + "label": "Amount", + "value": "%{value} TON" }, - "fee" : { - "label" : "Fee", - "value" : "≈ %{value} TON" + "fee": { + "label": "Fee", + "value": "≈ %{value} TON" }, - "recipient" : { - "label" : "Recipient" + "recipient": { + "label": "Recipient" }, - "withdraw_amount" : { - "label" : "Unstake amount" + "withdraw_amount": { + "label": "Unstake amount" } }, - "confirm_deposit" : "Confirm and Stake", - "confirm_unstake" : "Confirm and Unstake", - "deposit" : "Stake", - "desc_large" : "Join staking and get rewards. Staking helps to maintain the TON network.", - "details" : { - "about_pool" : "Details", - "apy" : { - "highest_tag" : "MAX", - "label" : "APY", - "value" : "≈ %{value}%" + "confirm_deposit": "Confirm and Stake", + "confirm_unstake": "Confirm and Unstake", + "deposit": "Stake", + "desc_large": "Join staking and get rewards. Staking helps to maintain the TON network.", + "details": { + "about_pool": "Details", + "apy": { + "highest_tag": "MAX", + "label": "APY", + "value": "≈ %{value}%" }, - "balance" : "Staking balance", - "cooldown" : { - "active" : "Active", - "desc" : "Two-hour period applied at the start of each staking cycle to improve the process of withdrawals and deposits between cycles", - "title" : "Cooldown period" + "balance": "Staking balance", + "cooldown": { + "active": "Active", + "desc": "Two-hour period applied at the start of each staking cycle to improve the process of withdrawals and deposits between cycles", + "title": "Cooldown period" }, - "frequency" : { - "label" : "Reward frequency", - "value" : "Every %{count} hours" + "frequency": { + "label": "Reward frequency", + "value": "Every %{count} hours" }, - "links_title" : "Links", - "liquidity_token" : { - "label" : "%{token} liquidity token", - "value" : "Learn more" + "links_title": "Links", + "liquidity_token": { + "label": "%{token} liquidity token", + "value": "Learn more" }, - "min_deposit" : { - "label" : "Minimal deposit", - "value" : "%{value} TON" + "min_deposit": { + "label": "Minimal deposit", + "value": "%{value} TON" }, - "next_cycle" : { - "in" : "in", + "next_cycle": { + "in": "in", "message": "Unstake request will be processed after the end of the validation cycle in %{value}" }, - "note" : "Staking is based on smart contracts by third parties. Tonkeeper is not responsible for staking experience.", - "pendingDeposit" : "Pending Stake", - "pendingWithdraw" : "Pending Unstake", - "pendingWithdrawDesc" : "after the end of the cycle", - "pool_address" : { - "label" : "Pool address" + "note": "Staking is based on smart contracts by third parties. Tonkeeper is not responsible for staking experience.", + "pendingDeposit": "Pending Stake", + "pendingWithdraw": "Pending Unstake", + "pendingWithdrawDesc": "after the end of the cycle", + "pool_address": { + "label": "Pool address" }, - "readyWithdraw" : "Unstake ready", - "socials" : { - "telegram" : "Community", - "twitter" : "Twitter" + "readyWithdraw": "Unstake ready", + "socials": { + "telegram": "Community", + "twitter": "Twitter" }, - "tap_to_collect" : "Tap to collect" + "tap_to_collect": "Tap to collect" }, - "estimated_profit" : "%{amount} TON – annual profit\nif you stake TON today", - "estimated_profit_compare" : "More profitable by %{amount} TON per year than your current staking", - "get_withdrawal" : "Get withdrawal", - "highest_apy" : "MAX APY", - "jetton_note" : "When you stake TON in a %{poolName} pool, you receive a token called %{token} that represents your share in the pool. As the pool accumulates profits, your %{token} represents larger amount of TON.", - "learn_more" : "Learn more", - "message" : { - "pendingDeposit" : "%{amount} TON staked\n", - "pendingWithdraw" : "%{amount} TON unstaked\n", - "pendingWithdrawLiquid" : "%{amount} TON unstaked after the end of the cycle", - "readyWithdraw" : "%{amount} TON ready.\nTap to collect" + "estimated_profit": "%{amount} TON – annual profit\nif you stake TON today", + "estimated_profit_compare": "More profitable by %{amount} TON per year than your current staking", + "get_withdrawal": "Get withdrawal", + "highest_apy": "MAX APY", + "jetton_note": "When you stake TON in a %{poolName} pool, you receive a token called %{token} that represents your share in the pool. As the pool accumulates profits, your %{token} represents larger amount of TON.", + "learn_more": "Learn more", + "message": { + "pendingDeposit": "%{amount} TON staked\n", + "pendingWithdraw": "%{amount} TON unstaked\n", + "pendingWithdrawLiquid": "%{amount} TON unstaked after the end of the cycle", + "readyWithdraw": "%{amount} TON ready.\nTap to collect" }, - "no_funds" : "No funds available for unstake", - "not_exists" : "Invalid pool address", - "other" : "Other", - "rewards" : { - "after_top_up" : "After stake", - "current" : "Current", - "title" : "Your APY", - "value" : "≈ %{value} TON" + "no_funds": "No funds available for unstake", + "not_exists": "Invalid pool address", + "other": "Other", + "rewards": { + "after_top_up": "After stake", + "current": "Current", + "title": "Your APY", + "value": "≈ %{value} TON" }, - "send_staked_ton" : "Staked TON", - "staked" : "Staked", - "staked_ton" : "Staked TON", - "staking_desc" : "Minimum deposit %{minStake} TON.\nEarn up to %{maxApy}%.", - "staking_pool_desc" : "APY ≈ %{apy}%", - "title" : "Staking", - "title_large" : "TON Staking", - "top_up" : "Stake", - "transaction" : "Confirm action", - "warning" : { - "about" : "About %{name}", - "beta_desc" : "We are not responsible for the stability and staking experience. Use at your own risk.", - "desc" : "Staking based on third-party smart contracts. We are not responsible for their work.", - "title" : "Warning" + "send_staked_ton": "Staked TON", + "staked": "Staked", + "staked_ton": "Staked TON", + "staking_desc": "Minimum deposit %{minStake} TON.\nEarn up to %{maxApy}%.", + "staking_pool_desc": "APY ≈ %{apy}%", + "title": "Staking", + "title_large": "TON Staking", + "top_up": "Stake", + "transaction": "Confirm action", + "warning": { + "about": "About %{name}", + "beta_desc": "We are not responsible for the stability and staking experience. Use at your own risk.", + "desc": "Staking based on third-party smart contracts. We are not responsible for their work.", + "title": "Warning" }, - "widget_desc" : "APY up to %{apy}%", - "widget_staking_options" : "Staking options", - "widget_title" : "Stake TON", - "withdraw" : "Unstake", - "withdrawal_fee_warning" : { - "continue" : "Continue anyway", - "message" : "Please leave at least {{amount}} TON on your balance.", - "title" : "You will have not enough funds for withdraw" + "widget_desc": "APY up to %{apy}%", + "widget_staking_options": "Staking options", + "widget_title": "Stake TON", + "withdraw": "Unstake", + "withdrawal_fee_warning": { + "continue": "Continue anyway", + "message": "Please leave at least {{amount}} TON on your balance.", + "title": "You will have not enough funds for withdraw" }, - "withdrawal_request" : "Unstake" + "withdrawal_request": "Unstake" }, - "subscription_back_to_merchant_button" : "Back", - "subscription_back_to_merchant_caption" : "The transaction is being processed. Your subscription will be active soon.", - "subscription_back_to_merchant_name" : "Back to %{merchantName}", - "subscription_back_to_merchant_title" : "Back to channel?", - "subscription_cancel" : "Cancel subscription", - "subscription_cancel_alert_cancel_btn" : "Not now", - "subscription_cancel_alert_caption" : "If you cancel now, you can still access your subscription until %{nextBill}", - "subscription_cancel_alert_submit_btn" : "Yes, cancel", - "subscription_cancel_alert_title" : "Cancel subscription?", - "subscription_expiring" : "Expiring", - "subscription_fee" : "Fee", - "subscription_next_bill" : "Next bill", - "subscription_open_merchant" : "Open in Telegram", - "subscription_period" : "Interval", - "subscription_period_custom" : "Every %{period}", - "subscription_period_day" : "Daily", - "subscription_period_half_year" : "Half-yearly", - "subscription_period_hour" : "Hourly", - "subscription_period_month" : "Monthly", - "subscription_period_quarter" : "Quarterly", - "subscription_period_week" : "Weekly", - "subscription_period_weeks" : "Every %{count} weeks", - "subscription_period_year" : "Yearly", - "subscription_price" : "Price", - "subscription_sent" : "Transaction sent", - "subscriptions_item_caption" : "%{price} TON, next bill on %{nextBill}", - "subscriptions_item_caption_expired" : "Expired %{date}", - "subscriptions_item_caption_expiring" : "%{price} TON, expiring %{date}", - "subscriptions_section_active" : "Active", - "subscriptions_section_expired" : "Expired", - "subscription_started" : "Subscription started", - "subscriptions_title" : "Subscriptions", - "subscription_subscribe" : "Subscribe", - "subscription_title" : "Subscription", - "subscription_unsubscribe" : "Unsubscribe", - "success" : "Success!", - "swap_title" : "Swap", - "tab_browser" : "Browser", - "tab_nft" : "NFTs", - "tab_settings" : "Settings", - "tab_swap" : "Swap", - "tab_wallet" : "Wallet", - "today" : "Today", - "ton_login_back_to_button" : "Back to %{name}", - "ton_login_caption" : "%{name} is requesting access to your wallet address ", - "ton_login_connect_button" : "Connect wallet", - "ton_login_notice" : "Be sure to check the service address before connecting the wallet.", - "ton_login_success" : "Done", - "ton_login_title" : "Connect to %domain?", - "transaction_bid_collection_name" : "Issuer", - "transaction_bid_date" : "Bid %{date}", - "transaction_bid_dns" : "Name", - "transaction_buy_date" : "Purchased on %{date}", - "transaction_buy_status_failed" : "Failed", - "transaction_buy_status_pending" : "Pending", - "transaction_buy_status_success" : "Success", - "transaction_confirmations" : "Confirmations", - "transaction_confirm_bid" : "Confirm bid", - "transaction_contract_deploy_date" : "%{date}", - "transaction_copy_caution" : "Be careful with external links. Never give your secret phrase to third-party resources — you can lose all your funds.\n\n- - -\n\n", - "transactionDetails" : { - "address" : "Address", - "bid_collection_name" : "Issuer", - "bid_name" : "Name", - "comment" : "Comment", - "description" : "Description", - "operation" : "Operation", - "payload" : "Payload", - "recipient" : "Recipient", - "recipient_address" : "Recipient address", - "sender" : "Sender", - "sender_address" : "Sender address", - "spam" : "SPAM", - "subscription_merchant_label" : "Merchant", - "subscription_product_label" : "Subscription", - "transaction" : "Transaction", - "unsubscription_title" : "Unsubscribed", - "withdraw_amount" : "Unstake amount" + "subscription_back_to_merchant_button": "Back", + "subscription_back_to_merchant_caption": "The transaction is being processed. Your subscription will be active soon.", + "subscription_back_to_merchant_name": "Back to %{merchantName}", + "subscription_back_to_merchant_title": "Back to channel?", + "subscription_cancel": "Cancel subscription", + "subscription_cancel_alert_cancel_btn": "Not now", + "subscription_cancel_alert_caption": "If you cancel now, you can still access your subscription until %{nextBill}", + "subscription_cancel_alert_submit_btn": "Yes, cancel", + "subscription_cancel_alert_title": "Cancel subscription?", + "subscription_expiring": "Expiring", + "subscription_fee": "Fee", + "subscription_next_bill": "Next bill", + "subscription_open_merchant": "Open in Telegram", + "subscription_period": "Interval", + "subscription_period_custom": "Every %{period}", + "subscription_period_day": "Daily", + "subscription_period_half_year": "Half-yearly", + "subscription_period_hour": "Hourly", + "subscription_period_month": "Monthly", + "subscription_period_quarter": "Quarterly", + "subscription_period_week": "Weekly", + "subscription_period_weeks": "Every %{count} weeks", + "subscription_period_year": "Yearly", + "subscription_price": "Price", + "subscription_sent": "Transaction sent", + "subscriptions_item_caption": "%{price} TON, next bill on %{nextBill}", + "subscriptions_item_caption_expired": "Expired %{date}", + "subscriptions_item_caption_expiring": "%{price} TON, expiring %{date}", + "subscriptions_section_active": "Active", + "subscriptions_section_expired": "Expired", + "subscription_started": "Subscription started", + "subscriptions_title": "Subscriptions", + "subscription_subscribe": "Subscribe", + "subscription_title": "Subscription", + "subscription_unsubscribe": "Unsubscribe", + "success": "Success!", + "swap_title": "Swap", + "tab_browser": "Browser", + "tab_nft": "NFTs", + "tab_settings": "Settings", + "tab_swap": "Swap", + "tab_wallet": "Wallet", + "today": "Today", + "ton_login_back_to_button": "Back to %{name}", + "ton_login_caption": "%{name} is requesting access to your wallet address ", + "ton_login_connect_button": "Connect wallet", + "ton_login_notice": "Be sure to check the service address before connecting the wallet.", + "ton_login_success": "Done", + "ton_login_title": "Connect to %domain?", + "transaction_bid_collection_name": "Issuer", + "transaction_bid_date": "Bid %{date}", + "transaction_bid_dns": "Name", + "transaction_buy_date": "Purchased on %{date}", + "transaction_buy_status_failed": "Failed", + "transaction_buy_status_pending": "Pending", + "transaction_buy_status_success": "Success", + "transaction_confirmations": "Confirmations", + "transaction_confirm_bid": "Confirm bid", + "transaction_contract_deploy_date": "%{date}", + "transaction_copy_caution": "Be careful with external links. Never give your secret phrase to third-party resources — you can lose all your funds.\n\n- - -\n\n", + "transactionDetails": { + "address": "Address", + "bid_collection_name": "Issuer", + "bid_name": "Name", + "comment": "Comment", + "description": "Description", + "operation": "Operation", + "payload": "Payload", + "recipient": "Recipient", + "recipient_address": "Recipient address", + "sender": "Sender", + "sender_address": "Sender address", + "spam": "SPAM", + "subscription_merchant_label": "Merchant", + "subscription_product_label": "Subscription", + "transaction": "Transaction", + "unsubscription_title": "Unsubscribed", + "withdraw_amount": "Unstake amount" }, - "transaction_exchange_from_currency" : "From", - "transaction_fee" : "Fee", - "transaction_hash" : "Transaction", - "transaction_merchant" : "Merchant", - "transaction_message" : "Message", - "transaction_purchase_id" : "Purchase ID", - "transaction_receive_date" : "Received on %{date}", - "transaction_recipient" : "Recipient", - "transaction_recipient_address" : "Recipient address", - "transaction_refund" : "Refund", - "transactions" : { - "bid" : "Bid", - "burned" : "Burned", - "contract_deploy" : "Contract Deploy", - "deposit" : "Stake", - "failed" : "Failed", - "nft_purchase" : "NFT Purchase", - "smartcontract_exec" : "Call contract", - "spam" : "Spam", - "subscription" : "Subscribed", - "swap" : "Swap", - "unknown" : "Unknown", - "unknown_description" : "Something happened but we couldn't recognize", - "unsubscription" : "Unsubscribed", - "wallet_initialized" : "Wallet initialized", - "withdraw" : "Unstake", - "withdrawal_request" : "Unstake Request" + "transaction_exchange_from_currency": "From", + "transaction_fee": "Fee", + "transaction_hash": "Transaction", + "transaction_merchant": "Merchant", + "transaction_message": "Message", + "transaction_purchase_id": "Purchase ID", + "transaction_receive_date": "Received on %{date}", + "transaction_recipient": "Recipient", + "transaction_recipient_address": "Recipient address", + "transaction_refund": "Refund", + "transactions": { + "bid": "Bid", + "burned": "Burned", + "contract_deploy": "Contract Deploy", + "deposit": "Stake", + "failed": "Failed", + "nft_purchase": "NFT Purchase", + "smartcontract_exec": "Call contract", + "spam": "Spam", + "subscription": "Subscribed", + "swap": "Swap", + "unknown": "Unknown", + "unknown_description": "Something happened but we couldn't recognize", + "unsubscription": "Unsubscribed", + "wallet_initialized": "Wallet initialized", + "withdraw": "Unstake", + "withdrawal_request": "Unstake Request" }, - "transaction_sender" : "Sender", - "transaction_sender_address" : "Sender address", - "transaction_send_more_button" : "Send more to this recipient", - "transaction_sent_date" : "Sent on %{date}", - "transaction_show_subscription_button" : "View subscription", - "transaction_status" : "Status", - "transaction_subscription" : "Subscription", - "transaction_subscription_date" : "Charged on %{date}", - "transaction_transfer_name" : "Transfer name", - "transaction_type_bid" : "Bid", - "transaction_type_bounced" : "Bounced", - "transaction_type_buy" : "Purchased", - "transaction_type_contract_deploy" : "Contract Deploy", - "transaction_type_from" : "From", - "transaction_type_new_subscriber" : "New subscriber", - "transaction_type_pending" : "Pending", - "transaction_type_receive" : "Received", - "transaction_type_sent" : "Sent", - "transaction_type_subscriber_lost" : "Subscriber lost", - "transaction_type_subscription" : "Subscribed", - "transaction_type_to" : "To", - "transaction_type_unsubscription" : "Unsubscribed", - "transaction_type_wallet_initialized" : "Wallet initialized", - "transaction_unsubscription" : "Unsubscription", - "transaction_unsubscription_date" : "%{date}", - "transaction_view_in_explorer" : "View in explorer", - "transaction_wallet_initialized_date" : "%{date}", - "transaction_your_bid" : "Your bid", - "transfer_deeplink_address_error" : "Incorrect recipient address", - "transfer_deeplink_unknown_token" : "Unknown token", + "transaction_sender": "Sender", + "transaction_sender_address": "Sender address", + "transaction_send_more_button": "Send more to this recipient", + "transaction_sent_date": "Sent on %{date}", + "transaction_show_subscription_button": "View subscription", + "transaction_status": "Status", + "transaction_subscription": "Subscription", + "transaction_subscription_date": "Charged on %{date}", + "transaction_transfer_name": "Transfer name", + "transaction_type_bid": "Bid", + "transaction_type_bounced": "Bounced", + "transaction_type_buy": "Purchased", + "transaction_type_contract_deploy": "Contract Deploy", + "transaction_type_from": "From", + "transaction_type_new_subscriber": "New subscriber", + "transaction_type_pending": "Pending", + "transaction_type_receive": "Received", + "transaction_type_sent": "Sent", + "transaction_type_subscriber_lost": "Subscriber lost", + "transaction_type_subscription": "Subscribed", + "transaction_type_to": "To", + "transaction_type_unsubscription": "Unsubscribed", + "transaction_type_wallet_initialized": "Wallet initialized", + "transaction_unsubscription": "Unsubscription", + "transaction_unsubscription_date": "%{date}", + "transaction_view_in_explorer": "View in explorer", + "transaction_wallet_initialized_date": "%{date}", + "transaction_your_bid": "Your bid", + "transfer_deeplink_address_error": "Incorrect recipient address", + "transfer_deeplink_unknown_token": "Unknown token", "transfer_deeplink_wrong_params": "Wrong parameters", - "transfer_deeplink_amount_error" : "Incorrect amount request", - "transfer_deeplink_expired_error" : "Expired link", - "transfer_deeplink_nft_address_error" : "Incorrect NFT address", - "transfer_from_old_wallet_btn" : "Transfer", - "transfer_from_old_wallet_caption" : "Tonkeeper will transfer all coins from your old address to your current address.", - "transfer_from_old_wallet_in_progress" : "Transfer in progress", - "transfer_from_old_wallet_title" : "Transfer to current address", - "txActions" : { - "amount" : "Amount", - "fee" : "Fee", - "refund" : "Refund", - "signRaw" : { - "addressMismatch" : { - "wrongVersion" : { - "close" : "Cancel", - "description" : "Switch your active address to %{version} to confirm the action.", - "switch" : "Switch and continue", - "title" : "Action for another address of your wallet" + "transfer_deeplink_amount_error": "Incorrect amount request", + "transfer_deeplink_expired_error": "Expired link", + "transfer_deeplink_nft_address_error": "Incorrect NFT address", + "transfer_from_old_wallet_btn": "Transfer", + "transfer_from_old_wallet_caption": "Tonkeeper will transfer all coins from your old address to your current address.", + "transfer_from_old_wallet_in_progress": "Transfer in progress", + "transfer_from_old_wallet_title": "Transfer to current address", + "txActions": { + "amount": "Amount", + "fee": "Fee", + "refund": "Refund", + "signRaw": { + "addressMismatch": { + "wrongVersion": { + "close": "Cancel", + "description": "Switch your active address to %{version} to confirm the action.", + "switch": "Switch and continue", + "title": "Action for another address of your wallet" }, - "wrongWallet" : { - "close" : "OK", - "description" : "Log in to another wallet %{address} and try again.", - "title" : "Action for another wallet" + "wrongWallet": { + "close": "OK", + "description": "Log in to another wallet %{address} and try again.", + "title": "Action for another wallet" } }, - "comment" : "Comment", - "insufficientFunds" : { + "comment": "Comment", + "insufficientFunds": { "rechargeBattery": "Recharge Battery", - "rechargeWallet" : "Buy %{currency}", - "stakingFee" : "%{count} TON needed for transaction. Estimated fee %{fee} TON will be deducted, the rest will be refunded.", - "stakingDeposit" : "Minimum balance for participate:\n%{amount} %{currency}\n", - "title" : "Insufficient funds", - "toBePaid" : "To be paid: %{amount} %{currency}\n", - "withFees" : "+ blockchain fees.\n", - "yourBalance" : "Your balance: %{balance} %{currency}." + "rechargeWallet": "Buy %{currency}", + "stakingFee": "%{count} TON needed for transaction. Estimated fee %{fee} TON will be deducted, the rest will be refunded.", + "stakingDeposit": "Minimum balance for participate:\n%{amount} %{currency}\n", + "title": "Insufficient funds", + "toBePaid": "To be paid: %{amount} %{currency}\n", + "withFees": "+ blockchain fees.\n", + "yourBalance": "Your balance: %{balance} %{currency}." }, - "recipient" : "Recipient", - "title" : "Confirm transaction", - "totalFee" : "Total fee", - "totalRefund" : "Total refund", - "types" : { - "contractDeploy" : "Contract Deploy", - "jettonTransfer" : "Token Transfer", - "nftItemTransfer" : "NFT Transfer", - "subscribe" : "Subscription", - "tonTransfer" : "TON Transfer", - "unknownTransaction" : "Unknown transaction", - "unSubscribe" : "Unsubscription" + "recipient": "Recipient", + "title": "Confirm transaction", + "totalFee": "Total fee", + "totalRefund": "Total refund", + "types": { + "contractDeploy": "Contract Deploy", + "jettonTransfer": "Token Transfer", + "nftItemTransfer": "NFT Transfer", + "subscribe": "Subscription", + "tonTransfer": "TON Transfer", + "unknownTransaction": "Unknown transaction", + "unSubscribe": "Unsubscription" }, - "warning_caption" : "Tonkeeper cannot fully verify the result of this transaction. Please make sure you trust the recipient.", - "warning_title" : "Warning", - "wrongTime" : { - "button" : "Open Settings", - "description" : "Seems like your device clock is not in sync with the network. Open device settings and enable automatic time and date.", - "title" : "The clocks are not synchronized" + "warning_caption": "Tonkeeper cannot fully verify the result of this transaction. Please make sure you trust the recipient.", + "warning_title": "Warning", + "wrongTime": { + "button": "Open Settings", + "description": "Seems like your device clock is not in sync with the network. Open device settings and enable automatic time and date.", + "title": "The clocks are not synchronized" } } }, - "update" : { - "description" : "A new version of Tonkeeper is available. You can download it now.", - "download" : "Download", - "downloading" : "Downloading… {{progress}}%", - "mb" : "{{size}} MB", - "remindLater" : "Remind me later", - "retry" : "Download error. Tap to retry.", - "tap" : "Tap to update", - "title" : "Update Tonkeeper", - "version" : "Version {{version}}" + "update": { + "description": "A new version of Tonkeeper is available. You can download it now.", + "download": "Download", + "downloading": "Downloading… {{progress}}%", + "mb": "{{size}} MB", + "remindLater": "Remind me later", + "retry": "Download error. Tap to retry.", + "tap": "Tap to update", + "title": "Update Tonkeeper", + "version": "Version {{version}}" }, - "username_issued_by_telegram" : "Issued by Telegram. ", - "username_manage_name_button" : "Manage name", - "wallet_about" : "About", - "wallet_buy" : "Buy", - "wallet" : { - "buy_btn" : "Buy & Sell", - "collectibles_tab_lable" : "Collectibles", - "edit_tokens_btn" : "Edit", - "nft_tab_lable" : "Collectibles", - "old_wallets_title" : "Old wallets", - "old_wallet_title" : "Old wallet", - "receive_btn" : "Receive", - "screen_title" : "Wallet", - "send_btn" : "Send", - "swap_btn" : "Swap", - "tonkens_tab_lable" : "Tokens", - "updated_at" : "Updated on %{value}" + "username_issued_by_telegram": "Issued by Telegram. ", + "username_manage_name_button": "Manage name", + "wallet_about": "About", + "wallet_buy": "Buy", + "wallet": { + "buy_btn": "Buy & Sell", + "collectibles_tab_lable": "Collectibles", + "edit_tokens_btn": "Edit", + "nft_tab_lable": "Collectibles", + "old_wallets_title": "Old wallets", + "old_wallet_title": "Old wallet", + "receive_btn": "Receive", + "screen_title": "Wallet", + "send_btn": "Send", + "swap_btn": "Swap", + "tonkens_tab_lable": "Tokens", + "updated_at": "Updated on %{value}" }, - "wallet_chat" : "Chat", - "wallet_community" : "Community", - "wallet_hours_symbol" : "h", - "wallet_old_balance" : "Old wallet:", - "wallet_receive" : "Receive", - "wallet_sell" : "Sell", - "wallet_send" : "Send", - "wallet_source_code" : "Source code", - "wallet_swap" : "Swap", - "wallet_title" : "Wallet", - "yesterday" : "Yesterday", + "wallet_chat": "Chat", + "wallet_community": "Community", + "wallet_hours_symbol": "h", + "wallet_old_balance": "Old wallet:", + "wallet_receive": "Receive", + "wallet_sell": "Sell", + "wallet_send": "Send", + "wallet_source_code": "Source code", + "wallet_swap": "Swap", + "wallet_title": "Wallet", + "yesterday": "Yesterday", "all_regions": "All Regions", "region_nokyc": "Neutral Waters", "nokyc": "no KYC", @@ -1055,7 +1038,7 @@ "fee": { "label": "Fee", "refund_label": "Refund", - "value" : "≈ %{value} TON" + "value": "≈ %{value} TON" } } }, @@ -1079,5 +1062,52 @@ "p4": "Used for scam. Token's name or image can lead users into deception." }, "button": "OK" - } + }, + "wallets": "Wallets", + "add_wallet": "Add wallet", + "add_wallet_modal": { + "title": "Add Wallet", + "subtitle": "Create a new wallet or add an existing one.", + "create": { + "title": "New Wallet", + "subtitle": "Create new wallet" + }, + "import": { + "title": "Import Wallet", + "subtitle": "Import wallet with a 24 secret recovery words" + }, + "watch_only": { + "title": "Watch Account", + "subtitle": "For monitor wallet activity without recovery phrase" + }, + "testnet": { + "title": "Testnet Account", + "subtitle": "Import wallet with a 24 secret recovery words to Testnet" + } + }, + "add_watch_only": { + "title": "Watch Account", + "subtitle": "Monitor wallet activity without recovery phrase. You will be notified of any transactions from this wallet.", + "wallet_not_found": "Wallet address not found" + }, + "watch_only": "Watch only", + "stop_watch": "Delete Watch Account", + "customize": "Customize", + "customize_modal": { + "title": "Customize your Wallet", + "subtitle": "Wallet name and icon are stored locally on your device.", + "wallet_name": "Wallet Name", + "save": "Save" + }, + "start_screen": { + "caption": "Create a new wallet or add \nan existing one", + "create_wallet_button": "Create New Wallet", + "import_wallet_button": "Import Existing Wallet" + }, + "choose_wallets": { + "title": "Choose Wallets", + "subtitle": "Choose wallets you want to add.", + "tokens": "tokens" + }, + "old_wallet_error": "Use wallet with v4R2 version" } diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 5e21c99c0..8a16a9768 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -622,7 +622,7 @@ "send_sending_wrong_time_title" : "Произошла ошибка", "send_title" : "Отправить %{currency}", "settings_appearance" : "Тема", - "settings_backup_seed" : "Показать секретный ключ", + "settings_backup_seed" : "Секретный ключ", "settings_contact_support" : "Написать команде", "settings_delete_account" : "Удалить аккаунт", "settings_delete_alert_button" : "Удалить аккаунт и данные", @@ -1126,5 +1126,52 @@ "p4": "Используется в мошенничестве. Название токена или изображение может вводить пользователей в заблуждение." }, "button": "Понятно" - } + }, + "wallets": "Кошельки", + "add_wallet": "Добавить кошелёк", + "add_wallet_modal": { + "title": "Добавить кошелёк", + "subtitle": "Создайте новый кошелёк или добавьте существующий.", + "create": { + "title": "Новый кошелёк", + "subtitle": "Создать новый кошелёк" + }, + "import": { + "title": "Существующий кошелёк", + "subtitle": "Добавить кошелёк с помощью секретного ключа из 24 слов" + }, + "watch_only": { + "title": "Аккаунт для просмотра", + "subtitle": "Для отслеживания активности кошелька без ввода секретного ключа" + }, + "testnet": { + "title": "Аккаунт в Testnet", + "subtitle": "Добавить кошелёк с помощью секретного ключа из 24 слов" + } + }, + "add_watch_only": { + "title": "Аккаунт для просмотра", + "subtitle": "Отслеживайте активность, получайте уведомления о транзакциях этого кошелька без ввода секретного ключа.", + "wallet_not_found": "Адрес кошелька не найден" + }, + "watch_only": "Только просмотр", + "stop_watch": "Удалить аккаунт", + "customize": "Кастомизировать", + "customize_modal": { + "title": "Кастомизируйте свой кошелёк", + "subtitle": "Имя и иконка кошелька хранятся локально на вашем устройстве.", + "wallet_name": "Имя кошелька", + "save": "Сохранить" + }, + "start_screen": { + "caption": "Создайте новый или подключите существующий кошелёк", + "create_wallet_button": "Создать новый кошелёк", + "import_wallet_button": "Подключить существующий" + }, + "choose_wallets": { + "title": "Выберите кошельки", + "subtitle": "Выберите кошельки, которые вы хотите добавить.", + "tokens": "токены" + }, + "old_wallet_error": "Используйте кошелёк версии v4R2" } diff --git a/packages/shared/modals/ActivityActionModal/ActionModalContent.tsx b/packages/shared/modals/ActivityActionModal/ActionModalContent.tsx index 9072de57d..2fc7b903c 100644 --- a/packages/shared/modals/ActivityActionModal/ActionModalContent.tsx +++ b/packages/shared/modals/ActivityActionModal/ActionModalContent.tsx @@ -7,30 +7,29 @@ import { SText as Text, View, } from '@tonkeeper/uikit'; -import { - ActionAmountType, - ActionType, - AmountFormatter, - AnyActionItem, - isJettonTransferAction, -} from '@tonkeeper/core'; import { formatTransactionDetailsTime } from '../../utils/date'; import { ActionStatusEnum, JettonVerificationType } from '@tonkeeper/core/src/TonAPI'; import React, { memo, useCallback, useMemo } from 'react'; import { formatter } from '../../formatter'; -import { config } from '../../config'; +import { config } from '@tonkeeper/mobile/src/config'; import { t } from '../../i18n'; +import { useWalletCurrency } from '../../hooks'; // TODO: move to manager import { useGetTokenPrice } from '@tonkeeper/mobile/src/hooks/useTokenPrice'; // TODO: move to shared -import { fiatCurrencySelector } from '@tonkeeper/mobile/src/store/main'; -import { useSelector } from 'react-redux'; import { ExtraListItem } from './components/ExtraListItem'; import { Linking } from 'react-native'; import { Address } from '../../Address'; import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; +import { + ActionAmountType, + ActionType, + AnyActionItem, + isJettonTransferAction, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; +import { AmountFormatter } from '@tonkeeper/core'; interface ActionModalContentProps { children?: React.ReactNode; @@ -93,7 +92,7 @@ export const ActionModalContent = memo((props) => { return time; }, [action.event.timestamp, action.destination, label]); - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const getTokenPrice = useGetTokenPrice(); const amount = useMemo(() => { diff --git a/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx b/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx index e96147d49..051410498 100644 --- a/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx +++ b/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx @@ -1,9 +1,13 @@ -import { ActionItem, ActionSource, AnyActionItem } from '@tonkeeper/core'; import { SheetActions, navigation } from '@tonkeeper/router'; import { renderActionModalContent } from './renderActionModalContent'; import { Modal, Toast } from '@tonkeeper/uikit'; -import { tk } from '../../tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; import { memo } from 'react'; +import { + ActionItem, + ActionSource, + AnyActionItem, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; type ActivityActionModalProps = { action: AnyActionItem; diff --git a/packages/shared/modals/ActivityActionModal/RechargeByPromoModal.tsx b/packages/shared/modals/ActivityActionModal/RechargeByPromoModal.tsx index bc0d5cd66..11241c130 100644 --- a/packages/shared/modals/ActivityActionModal/RechargeByPromoModal.tsx +++ b/packages/shared/modals/ActivityActionModal/RechargeByPromoModal.tsx @@ -11,7 +11,7 @@ import { } from '@tonkeeper/uikit'; import { navigation, SheetActions, useNavigation } from '@tonkeeper/router'; import { t } from '@tonkeeper/shared/i18n'; -import { tk } from '../../tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; export const RechargeByPromoModal = memo(() => { const [code, setCode] = useState(''); diff --git a/packages/shared/modals/ActivityActionModal/components/AddressListItem.tsx b/packages/shared/modals/ActivityActionModal/components/AddressListItem.tsx index 9e6f59653..e9360ac95 100644 --- a/packages/shared/modals/ActivityActionModal/components/AddressListItem.tsx +++ b/packages/shared/modals/ActivityActionModal/components/AddressListItem.tsx @@ -1,9 +1,9 @@ import { List, ListSeparator, Text, copyText } from '@tonkeeper/uikit'; import { AccountAddress } from '@tonkeeper/core/src/TonAPI'; -import { ActionDestination } from '@tonkeeper/core'; import { Address } from '@tonkeeper/shared/Address'; import { t } from '../../../i18n'; import { memo } from 'react'; +import { ActionDestination } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; interface AddressListItemProps { destination?: ActionDestination; diff --git a/packages/shared/modals/ActivityActionModal/components/ExtraListItem.tsx b/packages/shared/modals/ActivityActionModal/components/ExtraListItem.tsx index 256a3fc43..268ff730f 100644 --- a/packages/shared/modals/ActivityActionModal/components/ExtraListItem.tsx +++ b/packages/shared/modals/ActivityActionModal/components/ExtraListItem.tsx @@ -6,16 +6,14 @@ import { t } from '../../../i18n'; // TODO: move to manager import { useTokenPrice } from '@tonkeeper/mobile/src/hooks/useTokenPrice'; -// TODO: move to shared -import { fiatCurrencySelector } from '@tonkeeper/mobile/src/store/main'; -import { useSelector } from 'react-redux'; +import { useWalletCurrency } from '../../../hooks'; interface ExtraListItemProps { extra?: number; } export const ExtraListItem = memo((props) => { - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const tokenPrice = useTokenPrice('ton'); const extra = useMemo(() => { diff --git a/packages/shared/modals/ActivityActionModal/content/AuctionBidActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/AuctionBidActionContent.tsx index 8e50a7937..4ff92ddb0 100644 --- a/packages/shared/modals/ActivityActionModal/content/AuctionBidActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/AuctionBidActionContent.tsx @@ -1,13 +1,16 @@ import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; import { copyText, List, ListItem } from '@tonkeeper/uikit'; import { memo, useMemo } from 'react'; import { t } from '../../../i18n'; import { isTelegramUsername, domainToUsername, -} from '@tonkeeper/core/src/managers/NftsManager'; +} from '@tonkeeper/mobile/src/wallet/managers/NftsManager'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; interface AuctionBidActionContentProps { action: ActionItem; diff --git a/packages/shared/modals/ActivityActionModal/content/ContractDeployActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/ContractDeployActionContent.tsx index d80df1752..89537ab1d 100644 --- a/packages/shared/modals/ActivityActionModal/content/ContractDeployActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/ContractDeployActionContent.tsx @@ -1,8 +1,11 @@ import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; import { Text } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; interface ContractDeployActionContentProps { action: ActionItem; diff --git a/packages/shared/modals/ActivityActionModal/content/DepositStakeActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/DepositStakeActionContent.tsx index d0f30d086..319aebdc4 100644 --- a/packages/shared/modals/ActivityActionModal/content/DepositStakeActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/DepositStakeActionContent.tsx @@ -1,7 +1,10 @@ import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { List } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/JettonBurnActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/JettonBurnActionContent.tsx index 074da57cb..b23dc2915 100644 --- a/packages/shared/modals/ActivityActionModal/content/JettonBurnActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/JettonBurnActionContent.tsx @@ -1,6 +1,9 @@ import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { FastImage, List, Steezy } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/JettonMintActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/JettonMintActionContent.tsx index 49485ef11..98590d31b 100644 --- a/packages/shared/modals/ActivityActionModal/content/JettonMintActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/JettonMintActionContent.tsx @@ -1,7 +1,10 @@ import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { FastImage, List, Steezy } from '@tonkeeper/uikit'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx index ed81fb855..4dd21d54a 100644 --- a/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx @@ -1,18 +1,21 @@ import { TonIconBackgroundColor } from '@tonkeeper/uikit/src/components/TonIcon'; import { Steezy, View, SText as Text, Picture, TonIcon, List } from '@tonkeeper/uikit'; import { useGetTokenPrice } from '@tonkeeper/mobile/src/hooks/useTokenPrice'; -import { ActionItem, ActionType, AmountFormatter } from '@tonkeeper/core'; -import { fiatCurrencySelector } from '@tonkeeper/mobile/src/store/main'; +import { AmountFormatter } from '@tonkeeper/core'; import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; import { formatter } from '../../../formatter'; import { Address } from '../../../Address'; -import { useSelector } from 'react-redux'; import { memo, useMemo } from 'react'; import { t } from '../../../i18n'; import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; +import { useWalletCurrency } from '../../../hooks'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; interface JettonSwapActionContentProps { action: ActionItem; @@ -23,7 +26,7 @@ export const JettonSwapActionContent = memo((props const { payload } = action; const { format, formatNano } = useHideableFormatter(); - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const getTokenPrice = useGetTokenPrice(); const amountInFiat = useMemo(() => { diff --git a/packages/shared/modals/ActivityActionModal/content/JettonTransferActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/JettonTransferActionContent.tsx index 36857f616..4b5d75dbb 100644 --- a/packages/shared/modals/ActivityActionModal/content/JettonTransferActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/JettonTransferActionContent.tsx @@ -9,12 +9,15 @@ import { import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { t } from '../../../i18n'; import { memo } from 'react'; import { JettonVerificationType } from '@tonkeeper/core/src/TonAPI'; import { EncryptedComment, EncryptedCommentLayout } from '../../../components'; -import { config } from '../../../config'; +import { config } from '@tonkeeper/mobile/src/config'; import { openUnverifiedTokenDetailsModal } from '../../UnverifiedTokenDetailsModal'; interface JettonTransferContentProps { diff --git a/packages/shared/modals/ActivityActionModal/content/NftItemTransferActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/NftItemTransferActionContent.tsx index a49f0e199..cc845cabe 100644 --- a/packages/shared/modals/ActivityActionModal/content/NftItemTransferActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/NftItemTransferActionContent.tsx @@ -3,7 +3,10 @@ import { NftItemPayload } from '../components/NftItemPayload'; import { ActionStatusEnum } from '@tonkeeper/core/src/TonAPI'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { copyText, List, Text } from '@tonkeeper/uikit'; import { memo } from 'react'; import { t } from '../../../i18n'; diff --git a/packages/shared/modals/ActivityActionModal/content/NftPurchaseActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/NftPurchaseActionContent.tsx index f13fe757c..bc91da222 100644 --- a/packages/shared/modals/ActivityActionModal/content/NftPurchaseActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/NftPurchaseActionContent.tsx @@ -3,7 +3,10 @@ import { NftItemPayload } from '../components/NftItemPayload'; import { ActionStatusEnum } from '@tonkeeper/core/src/TonAPI'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { List, Text } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/SmartContractActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/SmartContractActionContent.tsx index d7079ca3c..cfe8688ee 100644 --- a/packages/shared/modals/ActivityActionModal/content/SmartContractActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/SmartContractActionContent.tsx @@ -1,7 +1,10 @@ import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { List, copyText } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/SubscribeActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/SubscribeActionContent.tsx index 32a7c0501..47fefe7ba 100644 --- a/packages/shared/modals/ActivityActionModal/content/SubscribeActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/SubscribeActionContent.tsx @@ -1,7 +1,10 @@ import { useSubscription } from '../../../query/hooks/useSubscription'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { Button, List, Steezy, Text, View, copyText } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/TonTransferActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/TonTransferActionContent.tsx index 72557305d..d3b8a78cc 100644 --- a/packages/shared/modals/ActivityActionModal/content/TonTransferActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/TonTransferActionContent.tsx @@ -1,10 +1,13 @@ import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { List, TonIcon, copyText } from '@tonkeeper/uikit'; -import { ActionItem, ActionType } from '@tonkeeper/core'; import { ActionModalContent } from '../ActionModalContent'; import { t } from '../../../i18n'; import { EncryptedComment, EncryptedCommentLayout } from '../../../components'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; interface TonTransferActionContentProps { action: ActionItem; diff --git a/packages/shared/modals/ActivityActionModal/content/UnSubscribeActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/UnSubscribeActionContent.tsx index 7b9ca00a7..ab9f80347 100644 --- a/packages/shared/modals/ActivityActionModal/content/UnSubscribeActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/UnSubscribeActionContent.tsx @@ -1,7 +1,10 @@ import { useSubscription } from '../../../query/hooks/useSubscription'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { Button, List, Steezy, Text, View, copyText } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; @@ -49,7 +52,7 @@ export const UnSubscribeActionContent = memo((pro const styles = Steezy.create({ buttonContainer: { - marginHorizontal: 16, + marginHorizontal: 16, marginBottom: 16, - } -}); \ No newline at end of file + }, +}); diff --git a/packages/shared/modals/ActivityActionModal/content/WithdrawStakeActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/WithdrawStakeActionContent.tsx index 39c2769ae..7d3e77d43 100644 --- a/packages/shared/modals/ActivityActionModal/content/WithdrawStakeActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/WithdrawStakeActionContent.tsx @@ -1,7 +1,10 @@ import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { List } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; diff --git a/packages/shared/modals/ActivityActionModal/content/WithdrawStakeRequestActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/WithdrawStakeRequestActionContent.tsx index f788df081..2e53fc5ed 100644 --- a/packages/shared/modals/ActivityActionModal/content/WithdrawStakeRequestActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/WithdrawStakeRequestActionContent.tsx @@ -1,16 +1,18 @@ import { AddressListItem } from '../components/AddressListItem'; import { ExtraListItem } from '../components/ExtraListItem'; import { ActionModalContent } from '../ActionModalContent'; -import { ActionItem, ActionType } from '@tonkeeper/core'; +import { + ActionItem, + ActionType, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; import { List, ListItem, copyText } from '@tonkeeper/uikit'; import { t } from '../../../i18n'; import { memo } from 'react'; import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; import { StakingIcon } from '../components/StakingIcon'; -import { fiatCurrencySelector } from '@tonkeeper/mobile/src/store/main'; import { useTokenPrice } from '@tonkeeper/mobile/src/hooks/useTokenPrice'; -import { useSelector } from 'react-redux'; import { formatter } from '../../../formatter'; +import { useWalletCurrency } from '../../../hooks'; interface WithdrawStakeRequestActionContentProps { action: ActionItem; @@ -22,7 +24,7 @@ export const WithdrawStakeRequestActionContent = const { formatNano, format } = useHideableFormatter(); - const fiatCurrency = useSelector(fiatCurrencySelector); + const fiatCurrency = useWalletCurrency(); const tokenPrice = useTokenPrice( 'ton', formatter.fromNano(action.amount?.value ?? '0'), diff --git a/packages/shared/modals/ActivityActionModal/renderActionModalContent.tsx b/packages/shared/modals/ActivityActionModal/renderActionModalContent.tsx index b45f7d8f4..581333998 100644 --- a/packages/shared/modals/ActivityActionModal/renderActionModalContent.tsx +++ b/packages/shared/modals/ActivityActionModal/renderActionModalContent.tsx @@ -1,5 +1,4 @@ import { TonTransferActionContent } from './content/TonTransferActionContent'; -import { ActionType, AnyActionItem } from '@tonkeeper/core'; import { ActionModalContent } from './ActionModalContent'; import { JettonTransferActionContent } from './content/JettonTransferActionContent'; @@ -16,6 +15,10 @@ import { DepositStakeActionContent } from './content/DepositStakeActionContent'; import { WithdrawStakeActionContent } from './content/WithdrawStakeActionContent'; import { WithdrawStakeRequestActionContent } from './content/WithdrawStakeRequestActionContent'; import { SubscribeActionContent } from './content/SubscribeActionContent'; +import { + ActionType, + AnyActionItem, +} from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; export function renderActionModalContent(action: AnyActionItem) { switch (action.type) { diff --git a/packages/shared/modals/AddWalletModal.tsx b/packages/shared/modals/AddWalletModal.tsx index 4b9bf71eb..93cdc38c5 100644 --- a/packages/shared/modals/AddWalletModal.tsx +++ b/packages/shared/modals/AddWalletModal.tsx @@ -1,65 +1,93 @@ import { useNavigation } from '@tonkeeper/router'; import { Icon, List, Modal, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; import { memo } from 'react'; +import { t } from '../i18n'; +import { DevFeature, useDevFeaturesToggle } from '@tonkeeper/mobile/src/store'; interface AddWalletModalProps {} export const AddWalletModal = memo((props) => { const nav = useNavigation(); + const { devFeatures } = useDevFeaturesToggle(); + return ( - Add another wallet + {t('add_wallet_modal.title')} - Create a new wallet or add an existing one + {t('add_wallet_modal.subtitle')} { + nav.goBack(); + nav.navigate('CreateWalletStack'); + }} leftContentStyle={styles.iconContainer} - leftContent={} - title="Create new wallet" - subtitle="By using your recovery phrase" - subtitleNumberOfLines={2} + leftContent={} + title={t('add_wallet_modal.create.title')} + subtitle={t('add_wallet_modal.create.subtitle')} + subtitleNumberOfLines={3} chevron /> { nav.goBack(); - setTimeout(() => { - nav.navigate('/import'); - }, 600); + nav.navigate('ImportWalletStack'); }} leftContentStyle={styles.iconContainer} leftContent={} - title="With recovery phrase" - subtitle="Import wallet with a 24 secret recovery words" - subtitleNumberOfLines={2} + title={t('add_wallet_modal.import.title')} + subtitle={t('add_wallet_modal.import.subtitle')} + subtitleNumberOfLines={3} chevron /> { + nav.goBack(); + nav.navigate('AddWatchOnlyStack'); + }} leftContentStyle={styles.iconContainer} - leftContent={} - title="Add watch address" - subtitle="For monitor wallet activity without recovery phrase" - subtitleNumberOfLines={2} + leftContent={} + title={t('add_wallet_modal.watch_only.title')} + subtitle={t('add_wallet_modal.watch_only.subtitle')} + subtitleNumberOfLines={3} chevron /> + {devFeatures[DevFeature.ShowTestnet] ? ( + + { + nav.goBack(); + setTimeout(() => { + nav.navigate('ImportWalletStack', { + screen: 'ImportWallet', + params: { testnet: true }, + }); + }, 700); + }} + leftContentStyle={styles.iconContainer} + leftContent={} + title={t('add_wallet_modal.testnet.title')} + subtitle={t('add_wallet_modal.testnet.subtitle')} + subtitleNumberOfLines={3} + chevron + /> + + ) : null} diff --git a/packages/shared/modals/ReceiveInscriptionModal.tsx b/packages/shared/modals/ReceiveInscriptionModal.tsx index 58a4ea4a5..f9dd7118e 100644 --- a/packages/shared/modals/ReceiveInscriptionModal.tsx +++ b/packages/shared/modals/ReceiveInscriptionModal.tsx @@ -4,7 +4,7 @@ import { navigation } from '@tonkeeper/router'; import { memo, useMemo } from 'react'; import { t } from '../i18n'; -import { tk } from '../tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; import { useTonInscription } from '../query/hooks/useTonInscription'; import { AppStackRouteNames } from '@tonkeeper/mobile/src/navigation'; diff --git a/packages/shared/modals/ReceiveJettonModal.tsx b/packages/shared/modals/ReceiveJettonModal.tsx index dbcd06745..6e5050876 100644 --- a/packages/shared/modals/ReceiveJettonModal.tsx +++ b/packages/shared/modals/ReceiveJettonModal.tsx @@ -4,9 +4,9 @@ import { navigation } from '@tonkeeper/router'; import { memo, useMemo } from 'react'; import { t } from '../i18n'; -import { jettonsSelector } from '@tonkeeper/mobile/src/store/jettons'; -import { useSelector } from 'react-redux'; -import { tk } from '../tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { useJettons } from '../hooks'; +import { Address } from '../Address'; interface ReceiveJettonModalProps { jettonAddress: string; @@ -15,10 +15,11 @@ interface ReceiveJettonModalProps { export const ReceiveJettonModal = memo((props) => { const { jettonAddress } = props; - // TODO: Replace with new jetton manager - const { jettonBalances } = useSelector(jettonsSelector); + const { jettonBalances } = useJettons(); const jetton = useMemo(() => { - return jettonBalances.find((item) => item.jettonAddress === jettonAddress)!; + return jettonBalances.find((item) => + Address.compare(item.jettonAddress, jettonAddress), + )!; }, []); const link = useMemo(() => { diff --git a/packages/shared/modals/ReceiveModal.tsx b/packages/shared/modals/ReceiveModal.tsx index 91a8dac26..32802bf5f 100644 --- a/packages/shared/modals/ReceiveModal.tsx +++ b/packages/shared/modals/ReceiveModal.tsx @@ -2,7 +2,7 @@ import { TransitionOpacity, SegmentedControl, TonIcon, Modal } from '@tonkeeper/ import { ReceiveTokenContent } from '../components/ReceiveTokenContent'; import { memo, useCallback, useMemo, useState } from 'react'; import { StyleSheet } from 'react-native'; -import { tk } from '../tonkeeper'; +import { tk } from '@tonkeeper/mobile/src/wallet'; import { t } from '../i18n'; enum Segments { diff --git a/packages/shared/modals/SwitchWalletModal.tsx b/packages/shared/modals/SwitchWalletModal.tsx index b4fe7544a..a54f0e40e 100644 --- a/packages/shared/modals/SwitchWalletModal.tsx +++ b/packages/shared/modals/SwitchWalletModal.tsx @@ -1,76 +1,59 @@ -import { useSwitchWallet, useWallet } from '@tonkeeper/core'; import { useNavigation } from '@tonkeeper/router'; -import { Button, Icon, List, Modal, Steezy, View } from '@tonkeeper/uikit'; -import { memo, useCallback } from 'react'; - - -const useWallets = () => { - return [ - { - identity: '123', - name: 'Wallet', - }, - { - identity: '333', - name: 'Main', - }, - ]; -}; +import { Button, Icon, List, Modal, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; +import { memo } from 'react'; +import { useWallet, useWalletCurrency, useWallets } from '../hooks'; +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { t } from '../i18n'; +import { formatter } from '../formatter'; +import { Tag } from '@tonkeeper/mobile/src/uikit'; +import { WalletListItem } from '../components'; export const SwitchWalletModal = memo(() => { const nav = useNavigation(); const currentWallet = useWallet(); - const switchWallet = useSwitchWallet(); const wallets = useWallets(); - - const handleSwitchWallet = useCallback( - (identity: string) => () => { - // tk.switchWalle(identity); - - const wallet = wallets.find((i) => i.identity === identity); - switchWallet(wallet); - }, - [], - ); + const currency = useWalletCurrency(); return ( - - - - {wallets.map((wallet) => ( - - - - ) - } + + + + + {wallets.map((wallet) => ( + { + tk.switchWallet(wallet.identifier); + nav.goBack(); + }} + subtitle={formatter.format(wallet.totalFiat, { currency })} + rightContent={ + currentWallet.identifier === wallet.identifier && ( + + + + ) + } + /> + ))} + + + diff --git a/packages/mobile/src/core/DevMenu/DevComponents/DevDeeplinking.tsx b/packages/mobile/src/core/DevMenu/DevComponents/DevDeeplinking.tsx index b30ac687a..e6bfa32c8 100644 --- a/packages/mobile/src/core/DevMenu/DevComponents/DevDeeplinking.tsx +++ b/packages/mobile/src/core/DevMenu/DevComponents/DevDeeplinking.tsx @@ -3,11 +3,63 @@ import { AttachScreenButton } from '$navigation/AttachScreen'; import { Button, DevSeparator, Screen, Text } from '$uikit'; import { useDeeplinking } from '$libs/deeplinking'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { getTimeSec } from '$utils/getTimeSec'; +import { Base64 } from '$utils'; + +const getExpiresSec = () => { + return getTimeSec() + 10 * 60; +}; export const DevDeeplinking: React.FC = () => { const deeplinking = useDeeplinking(); const { bottom: paddingBottom } = useSafeAreaInsets(); + const handleTwoTransfers = () => { + const data = Base64.encodeObj({ + version: '0', + body: { + type: 'sign-raw-payload', + default: { + source: 'EQD2NmD_lH5f5u1Kj3KfGyTvhZSX0Eg6qp2a5IQUKXxOG21n', + valid_until: getExpiresSec(), + messages: [ + { + address: 'EQD2NmD_lH5f5u1Kj3KfGyTvhZSX0Eg6qp2a5IQUKXxOG21n', + amount: '100000000', + }, + { + address: 'EQD2NmD_lH5f5u1Kj3KfGyTvhZSX0Eg6qp2a5IQUKXxOG21n', + amount: '10000000', + payload: 'te6ccsEBAQEADgAAABgAAAAAQ29tbWVudCE07Pl9', + }, + ], + }, + params: { + source: 'EQD2NmD_lH5f5u1Kj3KfGyTvhZSX0Eg6qp2a5IQUKXxOG21n', + valid_until: getExpiresSec(), + messages: [ + { + address: 'EQD2NmD_lH5f5u1Kj3KfGyTvhZSX0Eg6qp2a5IQUKXxOG21n', + amount: '100000000', + }, + { + address: 'EQD2NmD_lH5f5u1Kj3KfGyTvhZSX0Eg6qp2a5IQUKXxOG21n', + amount: '10000000', + payload: 'te6ccsEBAQEADgAAABgAAAAAQ29tbWVudCE07Pl9', + }, + ], + }, + response_options: { + callback_url: 'https://txrequest.testtonlogin.xyz/api/complete', + return_url: 'https://txrequest.testtonlogin.xyz/api/complete', + broadcast: true, + }, + expires_sec: getExpiresSec(), + }, + }); + deeplinking.resolve(`https://app.tonkeeper.com/v1/txrequest-inline/${data}`); + }; + return ( } /> @@ -37,15 +89,7 @@ export const DevDeeplinking: React.FC = () => { - + @@ -183,7 +227,7 @@ export const DevDeeplinking: React.FC = () => { ) : null} diff --git a/packages/mobile/src/core/NFT/RenewDomainButton.tsx b/packages/mobile/src/core/NFT/RenewDomainButton.tsx index a8a5420ef..9b15e3815 100644 --- a/packages/mobile/src/core/NFT/RenewDomainButton.tsx +++ b/packages/mobile/src/core/NFT/RenewDomainButton.tsx @@ -15,6 +15,7 @@ import { openAddressMismatchModal } from '$core/ModalContainer/AddressMismatch/A import { Base64 } from '$utils'; import { Address } from '@tonkeeper/core'; import { useWallet } from '@tonkeeper/shared/hooks'; +import { tk } from '$wallet'; export type RenewDomainButtonRef = { renewUpdated: () => void; @@ -54,7 +55,7 @@ export const RenewDomainButton = forwardRef { return hasDiamods && !flags.disable_apperance; }, [hasDiamods, flags.disable_apperance]); - const wallets = useWallets(); - return ( diff --git a/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx b/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx index ccff0bc86..6f2c17259 100644 --- a/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx +++ b/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx @@ -36,8 +36,15 @@ export const SetupBiometry: FC = ({ route }) => { return () => clearTimeout(timer); }, []); + const [isCreatingWallet, setCreatingWallet] = useState(false); + const doCreateWallet = useCallback( (isBiometryEnabled: boolean) => () => { + if (isCreatingWallet) { + return; + } + + setCreatingWallet(true); dispatch( walletActions.createWallet({ isBiometryEnabled, @@ -49,14 +56,16 @@ export const SetupBiometry: FC = ({ route }) => { } else { openSetupNotifications(identifiers); } + setCreatingWallet(false); }, onFail: () => { + setCreatingWallet(false); setLoading(false); }, }), ); }, - [dispatch, pin], + [isCreatingWallet, dispatch, pin], ); const biometryNameGenitive = useMemo(() => { @@ -85,6 +94,7 @@ export const SetupBiometry: FC = ({ route }) => { mode="secondary" style={{ marginRight: ns(16) }} onPress={doCreateWallet(false)} + disabled={isCreatingWallet} > {t('later')} @@ -113,7 +123,11 @@ export const SetupBiometry: FC = ({ route }) => { - + } + /> + + + + + {t('setup_biometry_title', { biometryType: biometryNameGenitive })} + + + + {t('setup_biometry_caption', { + biometryType: isTouchId + ? t(`platform.${platform}.capitalized_fingerprint`) + : t(`platform.${platform}.capitalized_face_recognition`), + })} + + + + + + + + + ); +}; + +const styles = Steezy.create({ + lottieIcon: { + width: 160, + height: 160, + }, +}); diff --git a/packages/mobile/src/screens/ChangePinBiometry/SetupBiometry.style.ts b/packages/mobile/src/screens/ChangePinBiometry/SetupBiometry.style.ts new file mode 100644 index 000000000..8ce8da7b5 --- /dev/null +++ b/packages/mobile/src/screens/ChangePinBiometry/SetupBiometry.style.ts @@ -0,0 +1,28 @@ +import styled from '$styled'; +import { nfs, ns } from '$utils'; + +export const Wrap = styled.View` + flex: 1; + padding: ${ns(32)}px; + padding-top: 0; +`; + +export const Content = styled.View` + flex: 1; + align-items: center; + justify-content: center; +`; + +export const IconWrap = styled.View` + width: ${ns(160)}px; + height: ${ns(160)}px; +`; + +export const CaptionWrapper = styled.View` + margin-top: ${ns(4)}px; +`; + +export const Footer = styled.View` + flex: 0 0 auto; + padding-top: ${ns(16)}px; +`; diff --git a/packages/mobile/src/screens/ChangePinBiometry/index.ts b/packages/mobile/src/screens/ChangePinBiometry/index.ts new file mode 100644 index 000000000..1bf89232a --- /dev/null +++ b/packages/mobile/src/screens/ChangePinBiometry/index.ts @@ -0,0 +1 @@ +export * from './ChangePinBiometry'; diff --git a/packages/mobile/src/screens/ResetPin/ResetPin.tsx b/packages/mobile/src/screens/ResetPin/ResetPin.tsx index 2e9a53c49..477fe8b57 100644 --- a/packages/mobile/src/screens/ResetPin/ResetPin.tsx +++ b/packages/mobile/src/screens/ResetPin/ResetPin.tsx @@ -7,10 +7,13 @@ import { useParams } from '@tonkeeper/router/src/imperative'; import { useBiometrySettings } from '@tonkeeper/shared/hooks'; import { vault } from '$wallet'; import { useNavigation } from '@tonkeeper/router'; +import * as LocalAuthentication from 'expo-local-authentication'; +import { detectBiometryType } from '$utils'; +import { MainStackRouteNames } from '$navigation'; export const ResetPin: FC = () => { const { passcode: oldPasscode } = useParams<{ passcode: string }>(); - const { biometryEnabled, enableBiometry, disableBiometry } = useBiometrySettings(); + const { biometryEnabled, disableBiometry } = useBiometrySettings(); const nav = useNavigation(); const handlePinCreated = useCallback( @@ -22,22 +25,23 @@ export const ResetPin: FC = () => { try { await vault.changePasscode(oldPasscode, passcode); + Toast.success(t('passcode_changed')); + if (biometryEnabled) { await disableBiometry(); + const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); + const biometryType = detectBiometryType(types); - try { - await enableBiometry(passcode); - } catch {} + nav.replace(MainStackRouteNames.ChangePinBiometry, { biometryType, passcode }); + } else { + nav.goBack(); } - - Toast.success(t('passcode_changed')); - nav.goBack(); } catch (e) { Toast.fail(e.message); nav.goBack(); } }, - [biometryEnabled, disableBiometry, enableBiometry, nav, oldPasscode], + [biometryEnabled, disableBiometry, nav, oldPasscode], ); return ( diff --git a/packages/mobile/src/screens/index.ts b/packages/mobile/src/screens/index.ts index 7ac38cd50..c1385695f 100644 --- a/packages/mobile/src/screens/index.ts +++ b/packages/mobile/src/screens/index.ts @@ -5,3 +5,4 @@ export * from './HoldersWebView'; export * from './MigrationStartScreen'; export * from './MigrationCreatePasscode'; export * from './ResetPin'; +export * from './ChangePinBiometry'; diff --git a/packages/mobile/src/wallet/Tonkeeper.ts b/packages/mobile/src/wallet/Tonkeeper.ts index 1eb8220bc..e69e1f5e6 100644 --- a/packages/mobile/src/wallet/Tonkeeper.ts +++ b/packages/mobile/src/wallet/Tonkeeper.ts @@ -22,6 +22,7 @@ import nacl from 'tweetnacl'; import * as LocalAuthentication from 'expo-local-authentication'; import { detectBiometryType } from '$utils'; import { AccountsStream } from './streaming'; +import { InteractionManager } from 'react-native'; type TonkeeperOptions = { storage: Storage; @@ -47,6 +48,7 @@ export interface WalletsStoreState { wallets: WalletConfig[]; selectedIdentifier: string; biometryEnabled: boolean; + lockEnabled: boolean; isMigrated: boolean; } @@ -76,6 +78,7 @@ export class Tonkeeper { wallets: [], selectedIdentifier: '', biometryEnabled: false, + lockEnabled: false, isMigrated: false, }); @@ -111,6 +114,10 @@ export class Tonkeeper { return this.walletsStore.data.biometryEnabled; } + public get lockEnabled() { + return this.walletsStore.data.lockEnabled; + } + public async init() { try { await Promise.all([this.walletsStore.rehydrate(), this.tonPrice.rehydrate()]); @@ -171,20 +178,6 @@ export class Tonkeeper { } } - public async enableBiometry(passcode: string) { - await this.vault.setupBiometry(passcode); - - this.walletsStore.set({ biometryEnabled: true }); - } - - public async disableBiometry() { - try { - await this.vault.removeBiometry(); - } catch {} - - this.walletsStore.set({ biometryEnabled: false }); - } - private getNewWalletName() { const regex = new RegExp(`${DEFAULT_WALLET_STYLE_CONFIG.name} (\\d+)`); const lastNumber = [...this.wallets.values()].reduce((maxNumber, wallet) => { @@ -402,7 +395,10 @@ export class Tonkeeper { ); await wallet.rehydrate(); - wallet.preload(); + + InteractionManager.runAfterInteractions(() => { + wallet.preload(); + }); this.wallets.set(wallet.identifier, wallet); @@ -495,4 +491,26 @@ export class Tonkeeper { console.log('migrated'); this.walletsStore.set({ isMigrated: true }); } + + public async enableBiometry(passcode: string) { + await this.vault.setupBiometry(passcode); + + this.walletsStore.set({ biometryEnabled: true }); + } + + public async disableBiometry() { + try { + await this.vault.removeBiometry(); + } catch {} + + this.walletsStore.set({ biometryEnabled: false }); + } + + public async enableLock() { + this.walletsStore.set({ lockEnabled: true }); + } + + public async disableLock() { + this.walletsStore.set({ lockEnabled: false }); + } } diff --git a/packages/shared/hooks/index.ts b/packages/shared/hooks/index.ts index 44f69fb0d..c6939ee2b 100644 --- a/packages/shared/hooks/index.ts +++ b/packages/shared/hooks/index.ts @@ -9,3 +9,4 @@ export * from './useWallets'; export * from './useNftsState'; export * from './useBalancesState'; export * from './useBiometrySettings'; +export * from './useLockSettings'; diff --git a/packages/shared/hooks/useLockSettings.ts b/packages/shared/hooks/useLockSettings.ts new file mode 100644 index 000000000..c7ba40979 --- /dev/null +++ b/packages/shared/hooks/useLockSettings.ts @@ -0,0 +1,11 @@ +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { useExternalState } from './useExternalState'; + +export const useLockSettings = () => { + const lockEnabled = useExternalState(tk.walletsStore, (state) => state.lockEnabled); + + return { + lockEnabled, + toggleLock: () => (tk.lockEnabled ? tk.disableLock() : tk.enableLock()), + }; +}; diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 692428078..1059aa748 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -563,7 +563,9 @@ "security_reset_passcode": "Reset passcode", "security_title": "Security", "security_use_biometry_switch": "Use %{biometryType}", + "security_lock_screen_switch": "Lock Screen", "security_use_biometry_tip": "You can always unlock your wallet with a passcode.", + "security_lock_screen_tip": "Require passcode to view wallet contents.", "send_address_placeholder": "Address or name", "send_all_warning_title": "Are you sure you want to send all your balance?", "send_build_tx_error": "Your transaction failed", diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 4053982ef..0af8513d5 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -555,7 +555,9 @@ "security_reset_passcode" : "Сбросить пин-код", "security_title" : "Безопасность", "security_use_biometry_switch" : "Использовать %{biometryType}", + "security_lock_screen_switch": "Экран блокировки", "security_use_biometry_tip" : "Вы всегда можете разблокировать кошелёк с помощью пин-кода", + "security_lock_screen_tip": "Запрос пароля для просмотра кошелька.", "send_address_placeholder" : "Адрес или имя", "send_all_warning_title" : "Вы уверены, что хотите отправить весь свой баланс?", "send_build_tx_error" : "Ошибка создания транзакции", From ec16f446fb95e07c2585c56d4f2c82cd7b98d6b1 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Mon, 19 Feb 2024 18:42:33 +0300 Subject: [PATCH 33/61] fix(mobile): Bottomsheet header (#729) --- .../containers/Modal/SheetModal/SheetModalHeader.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx b/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx index ba33e0a05..8fe64b23d 100644 --- a/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx +++ b/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx @@ -89,13 +89,7 @@ export const SheetModalHeader = memo((props) => { )} {hasTitle && ( - + {typeof title === 'string' ? ( Date: Mon, 19 Feb 2024 23:00:21 +0600 Subject: [PATCH 34/61] fix(mobile): multiwallet bug fixes (#730) --- packages/mobile/src/hooks/useImportWallet.ts | 1 - packages/mobile/src/wallet/Tonkeeper.ts | 2 +- .../Modal/SheetModal/SheetModalHeader.tsx | 17 ++++++----------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/mobile/src/hooks/useImportWallet.ts b/packages/mobile/src/hooks/useImportWallet.ts index eb54c0964..839df9086 100644 --- a/packages/mobile/src/hooks/useImportWallet.ts +++ b/packages/mobile/src/hooks/useImportWallet.ts @@ -39,7 +39,6 @@ export const useImportWallet = () => { isTestnet, onDone: async (identifiers) => { tk.setMigrated(); - tk.enableLock(); dispatch(walletActions.clearGeneratedVault()); diff --git a/packages/mobile/src/wallet/Tonkeeper.ts b/packages/mobile/src/wallet/Tonkeeper.ts index e69e1f5e6..05eb02f1c 100644 --- a/packages/mobile/src/wallet/Tonkeeper.ts +++ b/packages/mobile/src/wallet/Tonkeeper.ts @@ -78,7 +78,7 @@ export class Tonkeeper { wallets: [], selectedIdentifier: '', biometryEnabled: false, - lockEnabled: false, + lockEnabled: true, isMigrated: false, }); diff --git a/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx b/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx index 8fe64b23d..f575261dc 100644 --- a/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx +++ b/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx @@ -1,10 +1,10 @@ -import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { LayoutChangeEvent, StyleSheet, TouchableOpacity, View } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import { useSheetInternal } from '@tonkeeper/router'; import { IconNames } from '../../../components/Icon'; import { Text } from '../../../components/Text'; import { Icon } from '../../../components/Icon'; -import { memo, useLayoutEffect } from 'react'; +import { memo, useCallback, useLayoutEffect } from 'react'; import { useTheme } from '../../../styles'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; @@ -45,15 +45,9 @@ export const SheetModalHeader = memo((props) => { [hasTitle], ); - useLayoutEffect(() => { - if (hasTitle) { - measureHeader({ - nativeEvent: { - layout: { height: 64 }, - }, - }); - } - }, [hasTitle]); + const handleLayout = useCallback((event: LayoutChangeEvent) => { + measureHeader(event); + }, []); return ( ((props) => { borderAnimatedStyle, !hasTitle && styles.absolute, ]} + onLayout={handleLayout} > {gradient && ( Date: Tue, 20 Feb 2024 00:53:33 +0600 Subject: [PATCH 35/61] fix(mobile): modal header (#731) --- .../Modal/SheetModal/SheetModalHeader.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx b/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx index f575261dc..62014ddae 100644 --- a/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx +++ b/packages/uikit/src/containers/Modal/SheetModal/SheetModalHeader.tsx @@ -1,10 +1,10 @@ -import { LayoutChangeEvent, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import { useSheetInternal } from '@tonkeeper/router'; import { IconNames } from '../../../components/Icon'; import { Text } from '../../../components/Text'; import { Icon } from '../../../components/Icon'; -import { memo, useCallback, useLayoutEffect } from 'react'; +import { memo, useLayoutEffect } from 'react'; import { useTheme } from '../../../styles'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; @@ -45,9 +45,24 @@ export const SheetModalHeader = memo((props) => { [hasTitle], ); - const handleLayout = useCallback((event: LayoutChangeEvent) => { - measureHeader(event); - }, []); + useLayoutEffect(() => { + if (hasSubtitle) { + measureHeader({ + nativeEvent: { + layout: { height: 84 }, + }, + }); + + return; + } + if (hasTitle) { + measureHeader({ + nativeEvent: { + layout: { height: 64 }, + }, + }); + } + }, [hasTitle]); return ( ((props) => { borderAnimatedStyle, !hasTitle && styles.absolute, ]} - onLayout={handleLayout} > {gradient && ( Date: Tue, 20 Feb 2024 18:01:38 +0300 Subject: [PATCH 36/61] feature(mobile): In-app language selector (#734) * feature(mobile): In-app language selector * fix(mobile): Remove locale names from translations * Update SelectLanguage.tsx --- .../components/Language/SelectLanguage.tsx | 53 +++++++++++++++++++ .../mobile/src/core/Settings/Settings.tsx | 14 +++-- .../SettingsStack/SettingsStack.interface.ts | 1 + .../SettingsStack/SettingsStack.tsx | 2 + packages/mobile/src/navigation/helper.ts | 4 ++ .../mobile/src/navigation/navigationNames.ts | 1 + .../store/zustand/selectedLanguage/types.ts | 6 +++ .../useSelectedLanguageStore.ts | 31 +++++++++++ packages/shared/i18n/i18n.ts | 23 ++++---- .../shared/i18n/locales/tonkeeper/en.json | 28 ++++------ .../shared/i18n/locales/tonkeeper/ru-RU.json | 29 ++++------ .../shared/i18n/locales/tonkeeper/tr-TR.json | 10 ---- .../i18n/locales/tonkeeper/zh-Hans-CN.json | 10 ---- packages/shared/i18n/translations.ts | 12 +++++ 14 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 packages/mobile/src/components/Language/SelectLanguage.tsx create mode 100644 packages/mobile/src/store/zustand/selectedLanguage/types.ts create mode 100644 packages/mobile/src/store/zustand/selectedLanguage/useSelectedLanguageStore.ts diff --git a/packages/mobile/src/components/Language/SelectLanguage.tsx b/packages/mobile/src/components/Language/SelectLanguage.tsx new file mode 100644 index 000000000..e8222edb7 --- /dev/null +++ b/packages/mobile/src/components/Language/SelectLanguage.tsx @@ -0,0 +1,53 @@ +import React, { memo, useCallback } from 'react'; +import { List, Screen } from '@tonkeeper/uikit'; +import { useSelectedLanguageStore } from '$store/zustand/selectedLanguage/useSelectedLanguageStore'; +import RNRestart from 'react-native-restart'; +import { + detectLocale, + nativeLocaleNames, + SupportedLocales, +} from '@tonkeeper/shared/i18n/translations'; +import { Icon } from '$uikit'; +import { t } from '@tonkeeper/shared/i18n'; + +export const SelectLanguage = memo(() => { + const setSelectedLanguage = useSelectedLanguageStore( + (state) => state.actions.setSelectedLanguage, + ); + const selectedLanguage = useSelectedLanguageStore((state) => state.selectedLanguage); + + const handleSwitchLanguage = useCallback( + (languageKey: string) => () => { + setSelectedLanguage(languageKey); + RNRestart.restart(); + }, + [setSelectedLanguage], + ); + + return ( + + + + + {['system', ...SupportedLocales].map((locale) => ( + + ) + } + /> + ))} + + + + ); +}); diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index 642cdef35..bd1afc902 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import Rate, { AndroidMarket } from 'react-native-rate'; -import { Alert, Linking, View } from 'react-native'; +import { Alert, Linking, Platform, View } from 'react-native'; import DeviceInfo from 'react-native-device-info'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import Animated from 'react-native-reanimated'; @@ -23,6 +23,7 @@ import { openNotifications, openRefillBattery, openSecurity, + openSelectLanguage, openSubscriptions, } from '$navigation'; import { walletActions } from '$store/wallet'; @@ -40,7 +41,7 @@ import { useFlags } from '$utils/flags'; import { SearchEngine, useBrowserStore, useNotificationsStore } from '$store'; import AnimatedLottieView from 'lottie-react-native'; import { Steezy } from '$styles'; -import { t } from '@tonkeeper/shared/i18n'; +import { i18n, t } from '@tonkeeper/shared/i18n'; import { trackEvent } from '$utils/stats'; import { openAppearance } from '$core/ModalContainer/AppearanceModal'; import { config } from '$config'; @@ -50,6 +51,7 @@ import { tk } from '$wallet'; import { mapNewNftToOldNftData } from '$utils/mapNewNftToOldNftData'; import { WalletListItem } from '@tonkeeper/shared/components'; import { useSubscriptions } from '@tonkeeper/shared/hooks/useSubscriptions'; +import { nativeLocaleNames } from '@tonkeeper/shared/i18n/translations'; export const Settings: FC = () => { const animationRef = useRef(null); @@ -150,6 +152,10 @@ export const Settings: FC = () => { const searchEngineVariants = Object.values(SearchEngine); const handleSwitchLanguage = useCallback(() => { + if (Platform.OS === 'android' && Platform.Version < 33) { + return openSelectLanguage(); + } + Alert.alert(t('language.language_alert.title'), undefined, [ { text: t('language.language_alert.cancel'), @@ -400,10 +406,10 @@ export const Settings: FC = () => { onPress={handleSwitchLanguage} value={ - {t('language.list_item.value')} + {nativeLocaleNames[i18n.locale]} } - title={t('language.list_item.title')} + title={t('language.title')} /> {wallet && !wallet.isWatchOnly && flags.address_style_settings ? ( (); @@ -32,6 +33,7 @@ export const SettingsStack: FC = () => { + void; + }; +} diff --git a/packages/mobile/src/store/zustand/selectedLanguage/useSelectedLanguageStore.ts b/packages/mobile/src/store/zustand/selectedLanguage/useSelectedLanguageStore.ts new file mode 100644 index 000000000..185b3175d --- /dev/null +++ b/packages/mobile/src/store/zustand/selectedLanguage/useSelectedLanguageStore.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand'; +import { ISelectedLanguageStore } from '$store/zustand/selectedLanguage/types'; +import { createJSONStorage, persist } from 'zustand/esm/middleware'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const initialState: Omit = { + selectedLanguage: 'system', +}; + +/* + In-app language selector for old Androids that not support per-app preferences (< Android 13) + Learn more: https://developer.android.com/guide/topics/resources/app-languages + */ +export const useSelectedLanguageStore = create( + persist( + (set) => ({ + ...initialState, + actions: { + setSelectedLanguage: async (language) => { + set({ selectedLanguage: language }); + }, + }, + }), + { + name: 'in-app-language', + storage: createJSONStorage(() => AsyncStorage), + partialize: ({ selectedLanguage }) => + ({ selectedLanguage } as ISelectedLanguageStore), + }, + ), +); diff --git a/packages/shared/i18n/i18n.ts b/packages/shared/i18n/i18n.ts index 6f3576868..056d4f799 100644 --- a/packages/shared/i18n/i18n.ts +++ b/packages/shared/i18n/i18n.ts @@ -1,21 +1,17 @@ -import { findBestAvailableLanguage } from 'react-native-localize'; -import { SupportedLocales, translations } from './translations'; +import { detectLocale, SupportedLocales, translations } from './translations'; import { pluralizeForRussian } from './pluralization'; import { I18nManager } from 'react-native'; import { I18n } from 'i18n-js'; - -const detectLocale = (supportesLocales: string[], defaultLocale: string) => { - const localize = findBestAvailableLanguage(supportesLocales); - return localize?.languageTag ?? defaultLocale; -}; +import { useSelectedLanguageStore } from '@tonkeeper/mobile/src/store/zustand/selectedLanguage/useSelectedLanguageStore'; const getI18n = () => { I18nManager.forceRTL(false); I18nManager.allowRTL(false); const i18n = new I18n(translations); - - i18n.locale = detectLocale(SupportedLocales, 'en'); + const selectedLocale = useSelectedLanguageStore.getState().selectedLanguage; + i18n.locale = + selectedLocale === 'system' ? detectLocale(SupportedLocales, 'en') : selectedLocale; i18n.enableFallback = true; i18n.pluralization.register('ru', pluralizeForRussian); @@ -23,10 +19,15 @@ const getI18n = () => { return i18n; }; +(useSelectedLanguageStore.persist.rehydrate() as unknown as Promise).then(() => { + const selectedLocale = useSelectedLanguageStore.getState().selectedLanguage; + if (selectedLocale !== 'system') { + i18n.locale = selectedLocale; + } +}); + export const i18n = getI18n(); export const getLocale = () => { return i18n.locale; }; - - diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 1059aa748..f7bec7a69 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -381,17 +381,6 @@ "jettons_manage_tokens": "Manage tokens", "jettons_show_jettons": "Show tokens in wallet", "jetton_token": "Token", - "language": { - "language_alert": { - "cancel": "Cancel", - "open": "Settings", - "title": "To change the language of the app, go to device Settings" - }, - "list_item": { - "title": "Language", - "value": "English" - } - }, "later": "Later", "legal_font_license": "Montserrat font", "legal_header_title": "Legal", @@ -644,20 +633,25 @@ "settings_jettons_list": "Tokens", "settings_legal_documents": "Legal", "language": { - "list_item": { - "title": "Language", - "value": "English" - }, + "title": "Language", "language_alert": { - "title": "To change the language of the app, go to device Settings", "cancel": "Cancel", - "open": "Settings" + "open": "Settings", + "title": "To change the language of the app, go to device Settings" + }, + "languagesList": { + "system": "System", + "ru": "Russian", + "en": "English", + "tr": "Turkish", + "zh-Hans": "Chinese" } }, "settings_network_alert_title": "Select network", "settings_news": "Tonkeeper news", "settings_notifications": "Notifications", "settings_primary_currency": "Currency", + "settings_bank_card": "Bank card", "settings_rate": "Rate Tonkeeper", "settings_recovery_phrase": "Recovery phrase", "settings_reset": "Sign Out", diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 0af8513d5..e6a2d2231 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -358,17 +358,6 @@ "jettons_list_title" : "Токены", "jettons_manage_tokens" : "Настроить токены", "jetton_token" : "Токен", - "language" : { - "language_alert" : { - "cancel" : "Отмена", - "open" : "Настройки", - "title" : "Чтобы изменить язык приложения, перейдите в настройки устройства" - }, - "list_item" : { - "title" : "Язык", - "value" : "Русский" - } - }, "later" : "Позже", "legal_font_license" : "Шрифт Montserrat", "legal_header_title" : "Документы", @@ -635,14 +624,18 @@ "settings_jettons_list" : "Токены", "settings_legal_documents" : "Юридические документы", "language": { - "list_item": { - "title": "Язык", - "value": "Русский" - }, + "title" : "Язык", "language_alert" : { - "title": "Чтобы изменить язык приложения, перейдите в настройки устройства", - "cancel": "Отмена", - "open": "Настройки" + "cancel" : "Отмена", + "open" : "Настройки", + "title" : "Чтобы изменить язык приложения, перейдите в настройки устройства" + }, + "languagesList": { + "system": "Системный", + "ru": "Русский", + "en": "Английский", + "tr": "Турецкий", + "zh-Hans": "Китайский" } }, "settings_network_alert_title" : "Выберите сеть", diff --git a/packages/shared/i18n/locales/tonkeeper/tr-TR.json b/packages/shared/i18n/locales/tonkeeper/tr-TR.json index e5d731850..727dc3c32 100644 --- a/packages/shared/i18n/locales/tonkeeper/tr-TR.json +++ b/packages/shared/i18n/locales/tonkeeper/tr-TR.json @@ -284,11 +284,6 @@ "jettons_manage_tokens" : "Token'ları yönetin", "jettons_show_jettons" : "Cüzdandaki token'ları göster", "jetton_token" : "Token", - "language" : { - "list_item" : { - "value" : "Türkçe" - } - }, "later" : "Daha sonra", "legal_font_license" : "Montserrat yazı tipi", "legal_header_title" : "Hukuki açıklama", @@ -544,11 +539,6 @@ "settings_delete_alert_title" : "Hesabınızı silmek istediğinize emin misiniz?", "settings_jettons_list" : "Token'lar", "settings_legal_documents" : "Hukuki açıklama", - "language": { - "list_item": { - "value": "Türkçe" - } - }, "settings_network_alert_title" : "Ağ seçin", "settings_news" : "Tonkeeper Haberleri", "settings_notifications" : "Bildirimler", diff --git a/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json b/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json index 9e2596eef..4a51ac9de 100644 --- a/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json +++ b/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json @@ -262,11 +262,6 @@ "jettons_manage_tokens" : "管理代币", "jettons_show_jettons" : "在钱包中显示代币", "jetton_token" : "代币", - "language" : { - "list_item" : { - "value" : "中文" - } - }, "later" : "稍后", "legal_font_license" : "Montserrat字体", "legal_header_title" : "法律", @@ -508,11 +503,6 @@ "settings_delete_alert_title" : "您确定要删除您的账户吗?", "settings_jettons_list" : "代币", "settings_legal_documents" : "法律文件", - "language": { - "list_item": { - "value": "中文" - } - }, "settings_network_alert_title" : "选择网络", "settings_news" : "Tonkeeper新闻", "settings_news_url" : "https://t.me/tonkeeper", diff --git a/packages/shared/i18n/translations.ts b/packages/shared/i18n/translations.ts index 8d8c7badc..ec6228b95 100644 --- a/packages/shared/i18n/translations.ts +++ b/packages/shared/i18n/translations.ts @@ -2,7 +2,19 @@ import en from './locales/tonkeeper/en.json'; import ru from './locales/tonkeeper/ru-RU.json'; import tr from './locales/tonkeeper/tr-TR.json'; import zhHans from './locales/tonkeeper/zh-Hans-CN.json'; +import { findBestAvailableLanguage } from 'react-native-localize'; + +export const detectLocale = (supportedLocales: string[], defaultLocale: string) => { + const localize = findBestAvailableLanguage(supportedLocales); + return localize?.languageTag ?? defaultLocale; +}; export const translations = { ru, en, tr, 'zh-Hans': zhHans }; export const SupportedLocales = Object.keys(translations); +export const nativeLocaleNames = { + ru: 'Русский', + en: 'English', + tr: 'Türkçe', + 'zh-Hans': '简体中文', +}; From 6abc7c589189e04482f675bf5d4e8cf0c16cfadf Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Tue, 20 Feb 2024 18:02:48 +0300 Subject: [PATCH 37/61] feature(mobile): Jetton and NFT transfer on-chain analytics (#726) * Jetton and NFT transfer on-chain analytics * fix(mobile): Analytics for staking --- .../@core-js/src/service/contractService.ts | 22 +++++++++++++++++-- packages/mobile/src/blockchain/wallet.ts | 1 - packages/mobile/src/core/NFTSend/NFTSend.tsx | 2 -- packages/mobile/src/core/StakingSend/utils.ts | 13 +++++------ .../hooks/useDeeplinkingResolvers.ts | 1 - 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/@core-js/src/service/contractService.ts b/packages/@core-js/src/service/contractService.ts index 7a0d480c6..79f3cd3e8 100644 --- a/packages/@core-js/src/service/contractService.ts +++ b/packages/@core-js/src/service/contractService.ts @@ -6,6 +6,7 @@ import { LockupContractV1AdditionalParams, } from '../legacy'; import { WalletContractV3R1, WalletContractV3R2, WalletContractV4 } from '@ton/ton'; +import nacl from 'tweetnacl'; export enum WalletVersion { v3R1 = 0, @@ -37,6 +38,7 @@ export interface CreateNftTransferBodyParams { /* Address of new owner's address */ newOwnerAddress: AnyAddress; forwardBody?: Cell | string; + /* Query id. Defaults to Tonkeeper signature query id with 32 random bits */ queryId?: number; } @@ -47,6 +49,7 @@ export interface CreateJettonTransferBodyParams { receiverAddress: AnyAddress; jettonAmount: number | bigint; forwardBody?: Cell | string; + /* Query id. Defaults to Tonkeeper signature query id with 32 random bits */ queryId?: number; } @@ -71,6 +74,15 @@ export class ContractService { } } + public static getWalletQueryId() { + const tonkeeperSignature = (0x546de4ef).toString(16); + const value = Buffer.concat([ + Buffer.from(tonkeeperSignature, 'hex'), + nacl.randomBytes(4), + ]); + return BigInt('0x' + value.toString('hex')); + } + static prepareForwardBody(body?: Cell | string) { return typeof body === 'string' ? comment(body) : body; } @@ -78,7 +90,10 @@ export class ContractService { static createNftTransferBody(createNftTransferBodyParams: CreateNftTransferBodyParams) { return beginCell() .storeUint(0x5fcc3d14, 32) - .storeUint(createNftTransferBodyParams.queryId || 0, 64) + .storeUint( + createNftTransferBodyParams.queryId || ContractService.getWalletQueryId(), + 64, + ) .storeAddress(tonAddress(createNftTransferBodyParams.newOwnerAddress)) .storeAddress(tonAddress(createNftTransferBodyParams.excessesAddress)) .storeBit(false) @@ -92,7 +107,10 @@ export class ContractService { ) { return beginCell() .storeUint(0xf8a7ea5, 32) // request_transfer op - .storeUint(createJettonTransferBodyParams.queryId || 0, 64) + .storeUint( + createJettonTransferBodyParams.queryId || ContractService.getWalletQueryId(), + 64, + ) .storeCoins(createJettonTransferBodyParams.jettonAmount) .storeAddress(tonAddress(createJettonTransferBodyParams.receiverAddress)) .storeAddress(tonAddress(createJettonTransferBodyParams.excessesAddress)) diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index a80591025..1d1c046bf 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -380,7 +380,6 @@ export class TonWallet { bounce: true, value: jettonTransferAmount, body: ContractService.createJettonTransferBody({ - queryId: Date.now(), jettonAmount, receiverAddress: recipient.address, excessesAddress: excessesAccount ?? tk.wallet.address.ton.raw, diff --git a/packages/mobile/src/core/NFTSend/NFTSend.tsx b/packages/mobile/src/core/NFTSend/NFTSend.tsx index b12678d73..3c6046185 100644 --- a/packages/mobile/src/core/NFTSend/NFTSend.tsx +++ b/packages/mobile/src/core/NFTSend/NFTSend.tsx @@ -135,7 +135,6 @@ export const NFTSend: FC = (props) => { to: nftAddress, value: ONE_TON, body: ContractService.createNftTransferBody({ - queryId: Date.now(), newOwnerAddress: recipient!.address, excessesAddress: wallet.address.ton.raw, forwardBody: commentValue, @@ -279,7 +278,6 @@ export const NFTSend: FC = (props) => { to: nftAddress, value: totalAmount, body: ContractService.createNftTransferBody({ - queryId: Date.now(), newOwnerAddress: recipient!.address, excessesAddress: excessesAccount || wallet.address.ton.raw, forwardBody: commentValue, diff --git a/packages/mobile/src/core/StakingSend/utils.ts b/packages/mobile/src/core/StakingSend/utils.ts index f6b148145..468be4726 100644 --- a/packages/mobile/src/core/StakingSend/utils.ts +++ b/packages/mobile/src/core/StakingSend/utils.ts @@ -8,17 +8,14 @@ import { JettonBalanceModel } from '$store/models'; import BigNumber from 'bignumber.js'; import { Address } from '@tonkeeper/shared/Address'; import { PoolInfo, PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; +import { ContractService } from '@tonkeeper/core'; const { Cell } = TonWeb.boc; -export function getRandomQueryId() { - return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); -} - export const createWhalesAddStakeCommand = async () => { const command = new Cell(); command.bits.writeUint(2077040623, 32); - command.bits.writeUint(getRandomQueryId(), 64); // Query ID + command.bits.writeUint(ContractService.getWalletQueryId(), 64); // Query ID command.bits.writeCoins(100000); // Gas return encodeBytes(await command.toBoc()); @@ -27,7 +24,7 @@ export const createWhalesAddStakeCommand = async () => { export const createWhalesWithdrawStakeCell = async (amount: BN) => { const command = new Cell(); command.bits.writeUint(3665837821, 32); - command.bits.writeUint(getRandomQueryId(), 64); // Query ID + command.bits.writeUint(ContractService.getWalletQueryId(), 64); // Query ID command.bits.writeCoins(100000); // Gas command.bits.writeCoins(amount); // Amount @@ -37,7 +34,7 @@ export const createWhalesWithdrawStakeCell = async (amount: BN) => { export const createLiquidTfAddStakeCommand = async () => { const command = new Cell(); command.bits.writeUint(0x47d54391, 32); - command.bits.writeUint(getRandomQueryId(), 64); // Query ID + command.bits.writeUint(ContractService.getWalletQueryId(), 64); // Query ID command.bits.writeUint(0x000000000005b7ce, 64); // App ID return encodeBytes(await command.toBoc()); @@ -50,7 +47,7 @@ export const createLiquidTfWithdrawStakeCell = async (amount: BN, address: strin const payload = new Cell(); payload.bits.writeUint(0x595f07bc, 32); - payload.bits.writeUint(getRandomQueryId(), 64); // Query ID + payload.bits.writeUint(ContractService.getWalletQueryId(), 64); // Query ID payload.bits.writeCoins(amount); // Amount payload.bits.writeAddress(Address.parse(address).toTonWeb()); payload.bits.writeBit(1); diff --git a/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts b/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts index e0c18dc61..b13a5a1cf 100644 --- a/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts +++ b/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts @@ -475,7 +475,6 @@ export function useDeeplinkingResolvers() { amount: AmountFormatter.toNano(1), address: query.nft, payload: ContractService.createNftTransferBody({ - queryId: Date.now(), newOwnerAddress: address, excessesAddress: excessesAccount || tk.wallet.address.ton.raw, }) From bba696da10961f27fd966651b03be8ad2e833252 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Tue, 20 Feb 2024 18:03:02 +0300 Subject: [PATCH 38/61] feature(mobile): Add hideable inscriptions (#733) * feature(mobile): Add hideable inscriptions * fix(mobile): Show zero balance for inscriptions * fix(mobile): Add leftContentGradient to Tabs Header --- .../src/core/ManageTokens/ManageTokens.tsx | 225 +++++---------- .../ManageTokens/hooks/useInscriptionData.tsx | 127 +++++++++ .../core/ManageTokens/hooks/useJettonData.tsx | 7 +- .../core/ManageTokens/hooks/useNftData.tsx | 7 +- .../ApproveToken/ApproveToken.tsx | 36 ++- .../src/hooks/useInscriptionBalances.ts | 33 +++ .../src/hooks/useShouldShowTokensButton.ts | 11 +- .../src/store/zustand/tokenApproval/types.ts | 1 + .../mobile/src/tabs/Wallet/WalletScreen.tsx | 5 +- .../components/Tabs/ScrollableTabsBar.tsx | 261 ++++++++++++++++++ .../tabs/Wallet/components/Tabs/TabsBar.tsx | 3 +- .../Wallet/components/Tabs/TabsHeader.tsx | 31 ++- .../src/tabs/Wallet/components/Tabs/index.ts | 8 +- .../Wallet/components/WalletContentList.tsx | 14 +- .../wallet/managers/TokenApprovalManager.ts | 10 +- .../shared/i18n/locales/tonkeeper/it.json | 3 +- .../shared/i18n/locales/tonkeeper/ru-RU.json | 1 + 17 files changed, 587 insertions(+), 196 deletions(-) create mode 100644 packages/mobile/src/core/ManageTokens/hooks/useInscriptionData.tsx create mode 100644 packages/mobile/src/hooks/useInscriptionBalances.ts create mode 100644 packages/mobile/src/tabs/Wallet/components/Tabs/ScrollableTabsBar.tsx diff --git a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx index 0a69352ca..e22df6099 100644 --- a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx +++ b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx @@ -1,40 +1,25 @@ -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; -import { Icon, Screen, Spacer, SText, View, List, Button } from '$uikit'; +import { Screen, Spacer, SText, View, List, Button } from '$uikit'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { JettonBalanceModel } from '$store/models'; import { Tabs } from '../../tabs/Wallet/components/Tabs'; import { Steezy } from '$styles'; import { FlashList } from '@shopify/flash-list'; import { t } from '@tonkeeper/shared/i18n'; import { ListSeparator } from '$uikit/List/ListSeparator'; import { StyleSheet } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; import { ContentType, Content } from '$core/ManageTokens/ManageTokens.types'; import { useJettonData } from '$core/ManageTokens/hooks/useJettonData'; import { useNftData } from '$core/ManageTokens/hooks/useNftData'; -import { ScaleDecorator } from '$uikit/DraggableFlashList'; -import { NestableDraggableFlatList } from '$uikit/DraggableFlashList/components/NestableDraggableFlatList'; -import { NestableScrollContainer } from '$uikit/DraggableFlashList/components/NestableScrollContainer'; -import { Haptics } from '$utils'; import Animated, { useAnimatedScrollHandler, useSharedValue, } from 'react-native-reanimated'; import { useParams } from '$navigation/imperative'; -import { Address } from '@tonkeeper/shared/Address'; -import { useTokenApproval } from '@tonkeeper/shared/hooks'; -import { tk } from '$wallet'; +import { useInscriptionData } from '$core/ManageTokens/hooks/useInscriptionData'; const AnimatedFlashList = Animated.createAnimatedComponent(FlashList); -export function reorderJettons(newOrder: JettonBalanceModel[]) { - return newOrder.map((jettonBalance) => { - const rawAddress = Address.parse(jettonBalance.jettonAddress).toRaw(); - return rawAddress; - }); -} - const FLashListItem = ({ item, renderDragButton, @@ -88,36 +73,13 @@ const FLashListItem = ({ } }; -const DraggableFLashListItem = ({ item, drag, isActive }: { item: Content }) => { - const handleDrag = useCallback(() => { - drag?.(); - Haptics.impactMedium(); - }, [drag]); - - const renderDragButton = useCallback(() => { - return ( - - - - ); - }, [handleDrag, isActive]); - - return ( - - - - ); -}; - export const ManageTokens: FC = () => { const params = useParams<{ initialTab?: string }>(); const { bottom: bottomInset } = useSafeAreaInsets(); const [tab, setTab] = useState(params?.initialTab || 'tokens'); const jettonData = useJettonData(); const nftData = useNftData(); - const hasWatchedCollectiblesTab = useTokenApproval( - (state) => state.hasWatchedCollectiblesTab, - ); + const inscriptionData = useInscriptionData(); const scrollY = useSharedValue(0); const scrollHandler = useAnimatedScrollHandler({ onScroll: (event) => { @@ -125,134 +87,80 @@ export const ManageTokens: FC = () => { }, }); - const withCollectibleDot = React.useMemo(() => { - return !hasWatchedCollectiblesTab; - }, [hasWatchedCollectiblesTab]); + const tabsContent = useMemo(() => { + return [ + { + label: t('wallet.tonkens_tab_lable'), + id: 'tokens', + items: jettonData, + }, + { + label: t('wallet.nft_tab_lable'), + id: 'collectibles', + items: nftData, + }, + { + label: t('wallet.inscriptions_tab_label'), + id: 'inscriptions', + items: inscriptionData, + }, + ].filter((content) => content.items.length); + }, [inscriptionData, jettonData, nftData]); - const renderJettonList = useCallback(() => { - return ( - - ); - // TODO: draggable flashlist - return ( - - {jettonData.pending.length > 0 && ( - <> - - {t('approval.pending')} - - item?.id} - contentContainerStyle={StyleSheet.flatten([styles.flashList.static])} - data={jettonData.pending} - renderItem={DraggableFLashListItem} - /> - - - )} - {jettonData.enabled.length > 0 && ( - <> - - {t('approval.accepted')} - - item?.id} - contentContainerStyle={StyleSheet.flatten([styles.flashList.static])} - data={jettonData.enabled} - renderItem={DraggableFLashListItem} - /> - - - )} - {jettonData.disabled.length > 0 && ( - <> - - {t('approval.declined')} - - item?.id} - contentContainerStyle={StyleSheet.flatten([styles.flashList.static])} - data={jettonData.disabled} - renderItem={DraggableFLashListItem} - /> - - - )} - - ); - }, [bottomInset, jettonData, scrollHandler]); + const renderList = useCallback( + (data) => { + return ( + + ); + }, + [bottomInset, scrollHandler], + ); const renderTabs = useCallback(() => { return ( - - - + + { - setTab(value); - if (value === 'collectibles') { - tk.wallet.tokenApproval.setHasWatchedCollectiblesTab(true); - } - }} + onChange={({ value }) => setTab(value)} value={tab} - items={[ - { label: t('wallet.tonkens_tab_lable'), value: 'tokens' }, - { - label: t('wallet.nft_tab_lable'), - value: 'collectibles', - withDot: withCollectibleDot, - }, - ]} + items={tabsContent.map((content) => ({ + label: content.label, + value: content.id, + }))} /> - {renderJettonList()} - - item?.id} - estimatedItemSize={76} - contentContainerStyle={StyleSheet.flatten([ - styles.flashList.static, - { paddingBottom: bottomInset }, - ])} - onScroll={scrollHandler} - scrollEventThrottle={16} - data={nftData} - renderItem={FLashListItem} - /> - + {tabsContent.map((content, idx) => ( + + {renderList(content.items)} + + ))} ); - }, [ - bottomInset, - nftData, - renderJettonList, - scrollHandler, - scrollY, - tab, - withCollectibleDot, - ]); + }, [renderList, scrollY, tab, tabsContent]); - if (nftData.length && jettonData.length) { + if (tabsContent.length > 1) { return renderTabs(); } else { return ( @@ -266,7 +174,7 @@ export const ManageTokens: FC = () => { styles.flashList.static, { paddingBottom: bottomInset }, ])} - data={nftData.length ? nftData : jettonData} + data={tabsContent[0].items} /> ); @@ -278,6 +186,9 @@ const styles = Steezy.create(({ safeArea, corners, colors }) => ({ position: 'relative', paddingTop: safeArea.top, }, + flex: { + flex: 1, + }, flashList: { paddingHorizontal: 16, }, @@ -308,4 +219,10 @@ const styles = Steezy.create(({ safeArea, corners, colors }) => ({ alignItems: 'center', justifyContent: 'space-between', }, + tabsContainer: { paddingBottom: 16 }, + tabsItem: { paddingTop: 16, paddingBottom: 8 }, + tabsIndicator: { bottom: 0 }, + contentContainer: { + paddingLeft: 65, + }, })); diff --git a/packages/mobile/src/core/ManageTokens/hooks/useInscriptionData.tsx b/packages/mobile/src/core/ManageTokens/hooks/useInscriptionData.tsx new file mode 100644 index 000000000..ca0e8afef --- /dev/null +++ b/packages/mobile/src/core/ManageTokens/hooks/useInscriptionData.tsx @@ -0,0 +1,127 @@ +import React, { useMemo, useState } from 'react'; +import { t } from '@tonkeeper/shared/i18n'; +import { formatter } from '$utils/formatter'; +import { openApproveTokenModal } from '$core/ModalContainer/ApproveToken/ApproveToken'; +import { tk } from '$wallet'; +import { + TokenApprovalStatus, + TokenApprovalType, +} from '$store/zustand/tokenApproval/types'; +import { ListButton } from '$uikit'; +import { CellItem, Content, ContentType } from '$core/ManageTokens/ManageTokens.types'; +import { InscriptionBalance } from '@tonkeeper/core/src/TonAPI'; +import { useInscriptionBalances } from '$hooks/useInscriptionBalances'; + +const baseInscriptionCellData = (inscription: InscriptionBalance) => ({ + type: ContentType.Cell, + id: `${inscription.ticker}_${inscription.type}`, + title: inscription.ticker, + subtitle: formatter.format( + formatter.fromNano(inscription.balance, inscription.decimals), + { + currency: inscription.ticker, + currencySeparator: 'wide', + }, + ), + onPress: () => + openApproveTokenModal({ + type: TokenApprovalType.Inscription, + tokenIdentifier: `${inscription.ticker}_${inscription.type}`, + name: inscription.ticker, + }), +}); + +export function useInscriptionData() { + const [isExtendedEnabled, setIsExtendedEnabled] = useState(false); + const [isExtendedDisabled, setIsExtendedDisabled] = useState(false); + const { enabled, disabled } = useInscriptionBalances(); + return useMemo(() => { + const content: Content[] = []; + + if (enabled.length) { + content.push({ + id: 'enabled_title', + type: ContentType.Title, + title: t('approval.accepted'), + }); + content.push( + ...enabled.slice(0, !isExtendedEnabled ? 4 : undefined).map( + (inscriptionBalance, index, array) => + ({ + ...baseInscriptionCellData(inscriptionBalance), + isFirst: index === 0, + leftContent: ( + + tk.wallet.tokenApproval.updateTokenStatus( + `${inscriptionBalance.ticker}_${inscriptionBalance.type}`, + TokenApprovalStatus.Declined, + TokenApprovalType.Inscription, + ) + } + /> + ), + isLast: index === array.length - 1, + } as CellItem), + ), + ); + if (!isExtendedEnabled && enabled.length > 4) { + content.push({ + id: 'show_accepted_inscriptionss', + onPress: () => setIsExtendedEnabled(true), + type: ContentType.ShowAllButton, + }); + } + content.push({ + id: 'accepted_spacer', + type: ContentType.Spacer, + bottom: 16, + }); + } + + if (disabled.length) { + content.push({ + id: 'disabled_title', + type: ContentType.Title, + title: t('approval.declined'), + }); + content.push( + ...disabled.slice(0, !isExtendedDisabled ? 4 : undefined).map( + (inscriptionBalance, index, array) => + ({ + ...baseInscriptionCellData(inscriptionBalance), + isFirst: index === 0, + isLast: index === array.length - 1, + leftContent: ( + + tk.wallet.tokenApproval.updateTokenStatus( + `${inscriptionBalance.ticker}_${inscriptionBalance.type}`, + TokenApprovalStatus.Approved, + TokenApprovalType.Inscription, + ) + } + /> + ), + } as CellItem), + ), + ); + if (!isExtendedDisabled && disabled.length > 4) { + content.push({ + id: 'show_disabled_inscriptions', + onPress: () => setIsExtendedDisabled(true), + type: ContentType.ShowAllButton, + }); + } + content.push({ + id: 'disabled_spacer', + type: ContentType.Spacer, + bottom: 16, + }); + } + + return content; + }, [disabled, enabled, isExtendedDisabled, isExtendedEnabled]); +} diff --git a/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx b/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx index 6f7076297..13b32744f 100644 --- a/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx +++ b/packages/mobile/src/core/ManageTokens/hooks/useJettonData.tsx @@ -13,6 +13,7 @@ import { useJettonBalances } from '$hooks/useJettonBalances'; import { config } from '$config'; import { JettonVerification } from '$store/models'; import { Text } from '@tonkeeper/uikit'; +import { Address } from '@tonkeeper/core'; const baseJettonCellData = (jettonBalance) => ({ type: ContentType.Cell, @@ -34,7 +35,7 @@ const baseJettonCellData = (jettonBalance) => ({ onPress: () => openApproveTokenModal({ type: TokenApprovalType.Token, - tokenAddress: jettonBalance.jettonAddress, + tokenIdentifier: Address.parse(jettonBalance.jettonAddress).toRaw(), verification: jettonBalance.verification, image: jettonBalance.metadata?.image, name: jettonBalance.metadata?.name, @@ -66,7 +67,7 @@ export function useJettonData() { type="remove" onPress={() => tk.wallet.tokenApproval.updateTokenStatus( - jettonBalance.jettonAddress, + Address.parse(jettonBalance.jettonAddress).toRaw(), TokenApprovalStatus.Declined, TokenApprovalType.Token, ) @@ -112,7 +113,7 @@ export function useJettonData() { type="add" onPress={() => tk.wallet.tokenApproval.updateTokenStatus( - jettonBalance.jettonAddress, + Address.parse(jettonBalance.jettonAddress).toRaw(), TokenApprovalStatus.Approved, TokenApprovalType.Token, ) diff --git a/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx b/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx index 36a3fd9a8..abaa5ea98 100644 --- a/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx +++ b/packages/mobile/src/core/ManageTokens/hooks/useNftData.tsx @@ -13,6 +13,7 @@ import { TokenApprovalType, TokenApprovalStatus, } from '$wallet/managers/TokenApprovalManager'; +import { Address } from '@tonkeeper/core'; const baseNftCellData = (nft: NFTModel) => ({ type: ContentType.Cell, @@ -33,7 +34,7 @@ const baseNftCellData = (nft: NFTModel) => ({ verification: nft.isApproved ? JettonVerification.WHITELIST : JettonVerification.NONE, - tokenAddress: nft.collection?.address || nft.address, + tokenIdentifier: Address.parse(nft.collection?.address || nft.address).toRaw(), image: nft.content.image.baseUrl, name: nft.collection?.name, }), @@ -84,7 +85,7 @@ export function useNftData() { type="remove" onPress={() => tk.wallet.tokenApproval.updateTokenStatus( - nft.collection?.address || nft.address, + Address.parse(nft.collection?.address || nft.address).toRaw(), TokenApprovalStatus.Declined, nft.collection?.address ? TokenApprovalType.Collection @@ -135,7 +136,7 @@ export function useNftData() { type="add" onPress={() => tk.wallet.tokenApproval.updateTokenStatus( - nft.collection?.address || nft.address, + Address.parse(nft.collection?.address || nft.address).toRaw(), TokenApprovalStatus.Approved, nft.collection?.address ? TokenApprovalType.Collection diff --git a/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx b/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx index f162c0470..151928b71 100644 --- a/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx +++ b/packages/mobile/src/core/ModalContainer/ApproveToken/ApproveToken.tsx @@ -2,7 +2,7 @@ import { Modal } from '@tonkeeper/uikit'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import React, { memo, useCallback, useMemo } from 'react'; import { JettonVerification } from '$store/models'; -import { Button, Icon, Spacer, View, List } from '$uikit'; +import { Button, Icon, List, Spacer, View } from '$uikit'; import { Steezy } from '$styles'; import { t } from '@tonkeeper/shared/i18n'; import { triggerImpactLight } from '$utils'; @@ -12,20 +12,20 @@ import Clipboard from '@react-native-community/clipboard'; import { TranslateOptions } from 'i18n-js'; import { push } from '$navigation/imperative'; -import { Address } from '@tonkeeper/core'; import { useTokenApproval } from '@tonkeeper/shared/hooks'; import { tk } from '$wallet'; import { - TokenApprovalType, TokenApprovalStatus, + TokenApprovalType, } from '$wallet/managers/TokenApprovalManager'; +import { Address } from '@tonkeeper/core'; export enum ImageType { ROUND = 'round', SQUARE = 'square', } export interface ApproveTokenModalParams { - tokenAddress: string; + tokenIdentifier: string; type: TokenApprovalType; verification?: JettonVerification; imageType?: ImageType; @@ -35,27 +35,26 @@ export interface ApproveTokenModalParams { export const ApproveToken = memo((props: ApproveTokenModalParams) => { const nav = useNavigation(); const currentStatus = useTokenApproval((state) => { - const rawAddress = Address.parse(props.tokenAddress).toRaw(); - return state.tokens[rawAddress]; + return state.tokens[props.tokenIdentifier]; }); const handleUpdateStatus = useCallback( (approvalStatus: TokenApprovalStatus) => () => { tk.wallet.tokenApproval.updateTokenStatus( - props.tokenAddress, + props.tokenIdentifier, approvalStatus, props.type, ); nav.goBack(); }, - [nav, props.tokenAddress, props.type], + [nav, props.tokenIdentifier, props.type], ); const handleCopyAddress = useCallback(() => { - Clipboard.setString(props.tokenAddress); + Clipboard.setString(props.tokenIdentifier); triggerImpactLight(); Toast.show(t('approval.token_copied')); - }, [props.tokenAddress]); + }, [props.tokenIdentifier]); const modalState = useMemo(() => { if ( @@ -73,10 +72,13 @@ export const ApproveToken = memo((props: ApproveTokenModalParams) => { }, [currentStatus, props.verification]); const translationPrefix = useMemo(() => { - if (props.type === TokenApprovalType.Token) { - return 'token'; - } else { - return 'collection'; + switch (props.type) { + case TokenApprovalType.Token: + return 'token'; + case TokenApprovalType.Collection: + return 'collection'; + case TokenApprovalType.Inscription: + return 'token'; } }, [props.type]); @@ -134,7 +136,11 @@ export const ApproveToken = memo((props: ApproveTokenModalParams) => { diff --git a/packages/mobile/src/hooks/useInscriptionBalances.ts b/packages/mobile/src/hooks/useInscriptionBalances.ts new file mode 100644 index 000000000..286de1328 --- /dev/null +++ b/packages/mobile/src/hooks/useInscriptionBalances.ts @@ -0,0 +1,33 @@ +import { useTonInscriptions } from '@tonkeeper/shared/query/hooks/useTonInscriptions'; +import { useTokenApproval } from '@tonkeeper/shared/hooks'; +import { InscriptionBalance } from '@tonkeeper/core/src/TonAPI'; +import { useMemo } from 'react'; +import { TokenApprovalStatus } from '$wallet/managers/TokenApprovalManager'; + +export interface IBalances { + enabled: InscriptionBalance[]; + disabled: InscriptionBalance[]; +} + +export function useInscriptionBalances() { + const inscriptions = useTonInscriptions(); + const approvalStatuses = useTokenApproval(); + + return useMemo(() => { + const balances: IBalances = { + enabled: [], + disabled: [], + }; + inscriptions.items.forEach((inscription) => { + if ( + approvalStatuses.tokens[`${inscription.ticker}_${inscription.type}`]?.current === + TokenApprovalStatus.Declined + ) { + balances.disabled.push(inscription); + } else { + balances.enabled.push(inscription); + } + }); + return balances; + }, [approvalStatuses.tokens, inscriptions.items]); +} diff --git a/packages/mobile/src/hooks/useShouldShowTokensButton.ts b/packages/mobile/src/hooks/useShouldShowTokensButton.ts index ca7c03f52..1107da9af 100644 --- a/packages/mobile/src/hooks/useShouldShowTokensButton.ts +++ b/packages/mobile/src/hooks/useShouldShowTokensButton.ts @@ -1,11 +1,14 @@ import { useJettons, useNftsState } from '@tonkeeper/shared/hooks'; +import { useTonInscriptions } from '@tonkeeper/shared/query/hooks/useTonInscriptions'; export const useShouldShowTokensButton = () => { const { jettonBalances } = useJettons(); - - const hasJettons = jettonBalances.length > 0; + const inscriptions = useTonInscriptions(); const { accountNfts } = useNftsState(); - const hasNfts = Object.keys(accountNfts).length > 0; - return hasJettons || hasNfts; + return Boolean( + inscriptions.items?.length || + jettonBalances.length > 0 || + Object.keys(accountNfts).length > 0, + ); }; diff --git a/packages/mobile/src/store/zustand/tokenApproval/types.ts b/packages/mobile/src/store/zustand/tokenApproval/types.ts index c27b0b566..2c87d4d44 100644 --- a/packages/mobile/src/store/zustand/tokenApproval/types.ts +++ b/packages/mobile/src/store/zustand/tokenApproval/types.ts @@ -6,6 +6,7 @@ export enum TokenApprovalStatus { export enum TokenApprovalType { Collection = 'collection', Token = 'token', + Inscription = 'inscription', } export interface ApprovalStatus { diff --git a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx index 04b00dfc0..13ed7b6db 100644 --- a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx +++ b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx @@ -49,7 +49,7 @@ import { useNetInfo } from '@react-native-community/netinfo'; import { format } from 'date-fns'; import { getLocale } from '$utils/date'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { useWallet, useWalletStatus } from '@tonkeeper/shared/hooks'; +import { useWallet, useWalletCurrency, useWalletStatus } from '@tonkeeper/shared/hooks'; import { WalletSelector } from './components/WalletSelector'; export const WalletScreen = memo(({ navigation }) => { @@ -65,6 +65,7 @@ export const WalletScreen = memo(({ navigation }) => { useUpdatesStore((state) => state.update.state) !== UpdateState.NOT_STARTED; const balance = useBalance(tokens.total.fiat); const tonPrice = useTokenPrice(CryptoCurrencies.Ton); + const currency = useWalletCurrency(); const { isReloading: isRefreshing, updatedAt: walletUpdatedAt } = useWalletStatus(); @@ -326,6 +327,7 @@ export const WalletScreen = memo(({ navigation }) => { { ) : ( void; + itemStyle?: StyleProp; + indicatorStyle?: StyleProp; + containerStyle?: StyleProp; + value: string; + indent?: boolean; + sticky?: any; + scrollY?: Animated.SharedValue; + children?: React.ReactNode; + contentContainerStyle?: StyleProp; +} + +const INDICATOR_WIDTH = ns(24); + +export const ScrollableTabsBarComponent = (props: ScrollableTabsBarProps) => { + const { value, indent = true } = props; + const scrollRef = useAnimatedRef(); + const { setActiveIndex, pageOffset, scrollY, headerHeight, isScrollInMomentum } = + useTabCtx(); + const theme = useTheme(); + + const [tabsLayouts, setTabsBarLayouts] = useState<{ [key: string]: LayoutRectangle }>( + {}, + ); + + const roundedPageOffset = useDerivedValue(() => Math.round(pageOffset?.value)); + + useAnimatedReaction( + () => roundedPageOffset?.value, + (cur, prev) => { + if (cur !== prev) { + const layouts = Object.values(tabsLayouts); + if (!layouts.length) { + return; + } + const nearestLayoutIndex = Math.min( + layouts.length - 1, + Math.max(0, Math.round(cur)), + ); + const initialX = layouts[0].x; + scrollTo(scrollRef, layouts[nearestLayoutIndex].x - initialX, 0, true); + } + }, + [tabsLayouts], + ); + + const handleLayout = useCallback((index: number, event: LayoutChangeEvent) => { + const layout = event?.nativeEvent?.layout; + + if (layout) { + setTabsBarLayouts((s) => ({ ...s, [`${index}`]: layout })); + } + }, []); + + const indicatorRange = useMemo(() => { + return Object.values(tabsLayouts).map((item) => { + return item.x + (item.width / 2 - INDICATOR_WIDTH / 2); + }); + }, [tabsLayouts]); + + const input = props.items.map((_, index) => index); + const indicatorAnimatedStyle = useAnimatedStyle(() => { + if (indicatorRange.length !== props.items.length) { + return { + opacity: 0, + }; + } + + const x = interpolate(pageOffset.value, input, indicatorRange, Extrapolate.CLAMP); + + return { + transform: [ + { + translateX: x, + }, + ], + opacity: withTiming(1), + }; + }, [indicatorRange, value, pageOffset.value, input]); + + const containerStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: + scrollY.value > headerHeight.value ? scrollY.value - headerHeight.value : 0, + }, + ], + }; + }); + + const borderStyle = useAnimatedStyle(() => { + return { + borderBottomColor: + (scrollY && scrollY.value > headerHeight.value) || + (props.scrollY && props.scrollY.value > headerHeight.value) + ? theme.colors.border + : 'transparent', + }; + }); + + return ( + + + {props.items.map((item, index) => ( + handleLayout(index, event)} + onPress={() => { + if (!isScrollInMomentum.value) { + props.onChange(item, index); + setActiveIndex(index); + } + }} + key={`tab-${index}`} + activeOpacity={0.6} + > + + + {item.label} + + {item.withDot && ( + + + + )} + + + ))} + + + + ); +}; + +const WrapText = ({ + pageOffset, + index, + children, +}: { + index: number; + pageOffset: any; + children?: React.ReactNode; +}) => { + const theme = useTheme(); + + const textStyle = useAnimatedStyle(() => { + return { + color: interpolateColor( + pageOffset.value, + [index - 1, index, index + 1], + [ + theme.colors.textSecondary, + theme.colors.textPrimary, + theme.colors.textSecondary, + ], + ), + }; + }, [pageOffset.value]); + + return ( + + {children} + + ); +}; + +export const ScrollableTabsBar = memo(ScrollableTabsBarComponent); + +const styles = Steezy.create(({ colors }) => ({ + container: { + zIndex: 3, + }, + wrap: { + borderBottomWidth: ns(0.5), + }, + center: { + justifyContent: 'center', + alignItems: 'center', + }, + item: { + paddingTop: 4, + paddingBottom: 24, + paddingHorizontal: 16, + }, + indicator: { + position: 'absolute', + bottom: 16, + width: INDICATOR_WIDTH, + height: 3, + borderRadius: 3, + }, + indent: { + paddingHorizontal: 16, + }, + itemDotContainer: { + position: 'absolute', + flexDirection: 'column', + justifyContent: 'center', + top: 10, + bottom: 0, + right: 2, + }, + itemDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.accentRed, + }, +})); diff --git a/packages/mobile/src/tabs/Wallet/components/Tabs/TabsBar.tsx b/packages/mobile/src/tabs/Wallet/components/Tabs/TabsBar.tsx index e3c62b595..b08a40ebc 100644 --- a/packages/mobile/src/tabs/Wallet/components/Tabs/TabsBar.tsx +++ b/packages/mobile/src/tabs/Wallet/components/Tabs/TabsBar.tsx @@ -27,7 +27,6 @@ interface TabsBarProps { containerStyle?: StyleProp; value: string; indent?: boolean; - center?: boolean; sticky?: any; scrollY?: Animated.SharedValue; children?: React.ReactNode; @@ -36,7 +35,7 @@ interface TabsBarProps { const INDICATOR_WIDTH = ns(24); export const TabsBarComponent = (props: TabsBarProps) => { - const { value, indent = true, center } = props; + const { value, indent = true } = props; const { setActiveIndex, pageOffset, scrollY, headerHeight, isScrollInMomentum } = useTabCtx(); const theme = useTheme(); diff --git a/packages/mobile/src/tabs/Wallet/components/Tabs/TabsHeader.tsx b/packages/mobile/src/tabs/Wallet/components/Tabs/TabsHeader.tsx index 6bac54e17..92048497e 100644 --- a/packages/mobile/src/tabs/Wallet/components/Tabs/TabsHeader.tsx +++ b/packages/mobile/src/tabs/Wallet/components/Tabs/TabsHeader.tsx @@ -7,11 +7,13 @@ import { useWindowDimensions } from 'react-native'; import { useAnimatedStyle } from 'react-native-reanimated'; import { useTabCtx } from './TabsContainer'; import { goBack } from '$navigation/imperative'; +import LinearGradient from 'react-native-linear-gradient'; interface TabsHeaderProps { style?: Style; withBackButton?: boolean; children?: React.ReactNode; + leftContentGradient?: boolean; } export const TabsHeader: React.FC = (props) => { @@ -37,7 +39,19 @@ export const TabsHeader: React.FC = (props) => { function renderLeftContent() { if (shouldRenderGoBackButton) { return ( - + + {props.leftContentGradient && ( + <> + + + + )} ({ alignItems: 'center', justifyContent: 'center', }, + leftContentGradient: { + height: 64, + width: 24, + position: 'absolute', + right: -22, + bottom: 0, + }, + background: { + backgroundColor: colors.backgroundPage, + position: 'absolute', + right: 0, + top: 0, + left: -16, + bottom: 0, + }, })); diff --git a/packages/mobile/src/tabs/Wallet/components/Tabs/index.ts b/packages/mobile/src/tabs/Wallet/components/Tabs/index.ts index f55410b4d..af3ed1780 100644 --- a/packages/mobile/src/tabs/Wallet/components/Tabs/index.ts +++ b/packages/mobile/src/tabs/Wallet/components/Tabs/index.ts @@ -1,16 +1,18 @@ import { TabsContainer } from './TabsContainer'; import { TabsBar } from './TabsBar'; import { TabsSection } from './TabsSection'; -import { TabsFlashList } from './TabsFlashList' +import { TabsFlashList } from './TabsFlashList'; import { TabsScrollView } from './TabsScrollView'; import { TabsHeader } from './TabsHeader'; import { TabsPagerView } from './TabsPagerView'; +import { ScrollableTabsBar } from './ScrollableTabsBar'; export const Tabs = Object.assign(TabsContainer, { Bar: TabsBar, + ScrollableBar: ScrollableTabsBar, Section: TabsSection, FlashList: TabsFlashList, ScrollView: TabsScrollView, Header: TabsHeader, - PagerView: TabsPagerView -}) \ No newline at end of file + PagerView: TabsPagerView, +}); diff --git a/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx b/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx index 2940ee1f8..fa345b163 100644 --- a/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx +++ b/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx @@ -25,7 +25,6 @@ import { HideableAmount } from '$core/HideableAmount/HideableAmount'; import { openWallet } from '$core/Wallet/ToncoinScreen'; import { TronBalance } from '@tonkeeper/core/src/TronAPI/TronAPIGenerated'; import { WalletCurrency } from '@tonkeeper/core'; -import { useTonInscriptions } from '@tonkeeper/shared/query/hooks/useTonInscriptions'; import { formatter } from '@tonkeeper/shared/formatter'; import { Text } from '@tonkeeper/uikit'; import { JettonVerification } from '$store/models'; @@ -33,6 +32,7 @@ import { ListItemProps } from '$uikit/List/ListItem'; import { config } from '$config'; import { useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; import { CardsWidget } from '$components'; +import { useInscriptionBalances } from '$hooks/useInscriptionBalances'; enum ContentType { Token, @@ -171,6 +171,7 @@ const RenderItem = ({ item }: { item: Content }) => { }; interface BalancesListProps { + currency: WalletCurrency; tokens: any; // TODO: balance: any; // TODO: tonPrice: TokenPrice; @@ -184,6 +185,7 @@ interface BalancesListProps { export const WalletContentList = memo( ({ + currency, tokens, balance, tonPrice, @@ -197,7 +199,7 @@ export const WalletContentList = memo( const fiatCurrency = useWalletCurrency(); const shouldShowTonDiff = fiatCurrency !== WalletCurrency.TON; - const inscriptions = useTonInscriptions(); + const { enabled: inscriptions } = useInscriptionBalances(); const wallet = useWallet(); const isWatchOnly = wallet && wallet.isWatchOnly; @@ -319,9 +321,9 @@ export const WalletContentList = memo( })), ); - if (inscriptions?.items?.length > 0) { + if (inscriptions?.length > 0) { content.push( - ...inscriptions.items.map((item) => ({ + ...inscriptions.map((item) => ({ key: 'inscriptions' + item.ticker, onPress: () => openTonInscription({ ticker: item.ticker, type: item.type }), type: ContentType.Token, @@ -329,6 +331,10 @@ export const WalletContentList = memo( picture: DEFAULT_TOKEN_LOGO, title: item.ticker, value: formatter.formatNano(item.balance, { decimals: item.decimals }), + subvalue: formatter.format('0', { currency, currencySeparator: 'wide' }), + rate: { + price: formatter.format('0', { currency, currencySeparator: 'wide' }), + }, })), ); } diff --git a/packages/mobile/src/wallet/managers/TokenApprovalManager.ts b/packages/mobile/src/wallet/managers/TokenApprovalManager.ts index 5cbcd11f4..4d2e4b8f4 100644 --- a/packages/mobile/src/wallet/managers/TokenApprovalManager.ts +++ b/packages/mobile/src/wallet/managers/TokenApprovalManager.ts @@ -10,6 +10,7 @@ export enum TokenApprovalStatus { export enum TokenApprovalType { Collection = 'collection', Token = 'token', + Inscription = 'inscription', } export interface ApprovalStatus { @@ -54,24 +55,23 @@ export class TokenApprovalManager { } updateTokenStatus( - address: string, + identifier: string, status: TokenApprovalStatus, type: TokenApprovalType, ) { const { tokens } = this.state.data; - const rawAddress = Address.parse(address).toRaw(); - const token = { ...tokens[rawAddress] }; + const token = { ...tokens[identifier] }; if (token) { token.current = status; token.updated_at = Date.now(); - this.state.set({ tokens: { ...tokens, [rawAddress]: token } }); + this.state.set({ tokens: { ...tokens, [identifier]: token } }); } else { this.state.set({ tokens: { ...tokens, - [rawAddress]: { + [identifier]: { type, current: status, updated_at: Date.now(), diff --git a/packages/shared/i18n/locales/tonkeeper/it.json b/packages/shared/i18n/locales/tonkeeper/it.json index 025985b1b..2b08e8eb1 100644 --- a/packages/shared/i18n/locales/tonkeeper/it.json +++ b/packages/shared/i18n/locales/tonkeeper/it.json @@ -682,7 +682,8 @@ "screen_title" : "Wallet", "sell_btn" : "Vendi", "send_btn" : "Invia", - "tonkens_tab_lable" : "Token" + "tonkens_tab_lable" : "Token", + "inscriptions_tab_label": "Inscriptions" }, "wallet_chat" : "Chat", "wallet_community" : "Community", diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index e6a2d2231..f68d789a2 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -1045,6 +1045,7 @@ "send_btn" : "Отправить", "swap_btn" : "Обменять", "tonkens_tab_lable" : "Токены", + "inscriptions_tab_label": "Инскрипции", "updated_at" : "Обновлён %{value}" }, "wallet_chat" : "Чат", From 49b54f1b696b029780d70875ee1405cace9a2526 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Tue, 20 Feb 2024 18:03:13 +0300 Subject: [PATCH 39/61] fix(mobile): Update address reform text (#732) --- .../mobile/src/core/AddressUpdateInfo/AddressUpdateInfo.tsx | 4 ++++ packages/shared/i18n/locales/tonkeeper/en.json | 3 ++- packages/shared/i18n/locales/tonkeeper/ru-RU.json | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/mobile/src/core/AddressUpdateInfo/AddressUpdateInfo.tsx b/packages/mobile/src/core/AddressUpdateInfo/AddressUpdateInfo.tsx index 541a24338..d881bf55d 100644 --- a/packages/mobile/src/core/AddressUpdateInfo/AddressUpdateInfo.tsx +++ b/packages/mobile/src/core/AddressUpdateInfo/AddressUpdateInfo.tsx @@ -39,6 +39,10 @@ export const AddressUpdateInfo: FC = () => { + + {t('address_update.post_published_date')} + + {t('address_update.post_top')} diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index f7bec7a69..76d1ad188 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -42,9 +42,10 @@ "first_option": "Most likely the destination wallet is already actively used and you won’t notice any difference.", "learn_more": "Learn more", "new_style": "New address", - "notification_desc_did_change": "On October 5 your wallet address did update to the UQ format which is just better. The old address will also work. You don’t need to do anything.", + "notification_desc_did_change": "Your wallet address has been updated to the UQ format. The old address also continues to work.", "notification_desc_will_change": "On October 5 your wallet address will update to the UQ format which is just better. The old address will also work. You don’t need to do anything.", "old_style": "Old address", + "post_published_date": "Published: September 27, 2023", "post_dates": "\nOctober 5, 2023: all addresses switch to the UQ format in Tonkeeper.\n\nJanuary 1, 2024: Tonkeeper stops checking the contract status and uses the “bounceable” flag in the address. Coins sent to non-published contracts with an EQ address will bounce back to the sender.", "post_rest": "There are two address styles on the TON blockchain and until now only one was used for apps and wallets.\n\nThe EQ format is best for smart contracts that process incoming funds. If a smart contract isn’t published yet — i.e., the code isn’t on the blockchain — then the TONs sent to that address will bounce back to the sender. And this is a safety feature: if there is any error, TONs bounce back.\n\nThe UQ format is best for wallets. Coins never bounce because wallets are designed to simply store funds. Each wallet starts as a plain address without a code on the blockchain. So, it would make no sense for coins to bounce back.\n\nThis year we are switching wallets to the more suitable UQ format. And if you continue sending funds to an old EQ address someone gave you, there will be two options:\n", "post_top": "By the end of this year the entire TON network will display wallet addresses differently. The new address will start with UQ instead of EQ. And the last four letters will change too. The old address will also work and direct to the same wallet. It doesn't effect the safety of funds stored in your wallet anyhow.", diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index f68d789a2..a0ad87cb1 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -43,9 +43,10 @@ "first_option" : "Наиболее вероятно, что кошелёк получателя уже использовался и вы не заметите никакой разницы.", "learn_more" : "Подробнее", "new_style" : "Новый адрес", - "notification_desc_did_change" : "Пятого октября адрес вашего кошелька изменил свой формат на UQ, который ему лучше подходит. Старый адрес также продолжит работать. От вас не потребуется никаких действий.", + "notification_desc_did_change" : "Адрес вашего кошелька изменил свой формат на UQ. Старый адрес также продолжает работать.", "notification_desc_will_change" : "Пятого октября адрес вашего кошелька изменит свой формат на UQ, который ему лучше подходит. Старый адрес также продолжит работать. От вас не потребуется никаких действий.", "old_style" : "Старый адрес", + "post_published_date": "Опубликовано: 27 сентября 2023", "post_dates" : "\n5 октября 2023: все адреса переводятся в формат UQ в Tonkeeper.\n\n1 января 2024: Tonkeeper прекращает проверку статуса контракта и использует в адресе флаг «bounceable». Монеты, отправленные на неопубликованные контракты с EQ-адресом, вернутся обратно отправителю.", "post_rest" : "В сети TON возможно использование двух стилей адресов, но только один из них используется для всех типов аккаунтов: и приложений, и кошельков.\n\nФормат EQ лучше всего подходит для смарт-контрактов, обрабатывающих входящие средства. Если смарт-контракт ещё не опубликован (т.е. кода нет в блокчейне), то TONы, отправленные на этот адрес, вернутся отправителю. Это такая функция безопасности — в случае какой-либо ошибки TONы возвращаются.\n\nФормат UQ лучше всего подходит для кошельков. Так как кошельки предназначены для хранения средств, то отправка денег должна сработать даже когда у кошелька ещё не опубликован код. Поэтому, было бы бессмысленно делать так, чтобы монеты возвращались.\n\nВ этом году мы переводим кошельки на более правильный формат “UQ”. Если вы продолжите отправлять средства на EQ-адрес, который вам кто-то давно дал, то возможны два варианта:\n", "post_top" : "К концу этого года вся сеть TON будет отображать адреса кошельков по-другому. Новый адрес будет начинаться с UQ вместо EQ. И последние четыре буквы тоже изменятся. Старый адрес продолжит работать и вести на тот же кошелёк. Всё это никак не повлияет на безопасность средств, хранящихся у вас в кошельке.", From 9cf8008d451b2b5cc18105a51dc10448cf6961ef Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Tue, 20 Feb 2024 23:55:44 +0300 Subject: [PATCH 40/61] fix(mobile): ManageTokens screen fixes (#735) * fix(mobile): ManageTokens screen fixes * fix(mobile): Disable gesture for screen because of swipe conflicts --- .../src/core/ManageTokens/ManageTokens.tsx | 4 +- .../core/ManageTokens/ManageTokens.types.ts | 4 +- .../src/navigation/MainStack/MainStack.tsx | 6 +- .../Wallet/components/Tabs/TabsContainer.tsx | 2 +- .../Wallet/components/Tabs/TabsPagerView.tsx | 62 +++---------------- .../shared/i18n/locales/tonkeeper/en.json | 3 +- .../shared/i18n/locales/tonkeeper/it.json | 3 +- .../src/components/List/ListSeparator.tsx | 2 +- 8 files changed, 23 insertions(+), 63 deletions(-) diff --git a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx index e22df6099..0715a61d2 100644 --- a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx +++ b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx @@ -1,6 +1,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; -import { Screen, Spacer, SText, View, List, Button } from '$uikit'; +import { Screen, Spacer, SText, View, Button } from '$uikit'; +import { List } from '@tonkeeper/uikit'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Tabs } from '../../tabs/Wallet/components/Tabs'; import { Steezy } from '$styles'; @@ -111,6 +112,7 @@ export const ManageTokens: FC = () => { (data) => { return ( { name={MainStackRouteNames.Inscription} component={InscriptionScreen} /> - + { } return ctx; -} \ No newline at end of file +} diff --git a/packages/mobile/src/tabs/Wallet/components/Tabs/TabsPagerView.tsx b/packages/mobile/src/tabs/Wallet/components/Tabs/TabsPagerView.tsx index 8995dc22a..d8a8ac736 100644 --- a/packages/mobile/src/tabs/Wallet/components/Tabs/TabsPagerView.tsx +++ b/packages/mobile/src/tabs/Wallet/components/Tabs/TabsPagerView.tsx @@ -1,26 +1,14 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { StyleSheet, View } from 'react-native'; -import FastImage from 'react-native-fast-image'; import PagerView from 'react-native-pager-view'; -import Animated, { - Extrapolate, - interpolate, - runOnJS, - useAnimatedReaction, - useAnimatedStyle, - useEvent, - useHandler, - withSpring, -} from 'react-native-reanimated'; +import Animated, { useEvent, useHandler } from 'react-native-reanimated'; import { useTabCtx } from './TabsContainer'; -import funny from '$assets/funny.json'; -import { Haptics, ns } from '$utils'; -import { useBottomTabBarHeight } from '$hooks/useBottomTabBarHeight'; import { useTabPress } from '@tonkeeper/router'; interface TabsPagerViewProps { initialPage?: number; children: React.ReactNode; + onActiveIndexChange?: (activeIndex: number) => void; } const AnimatedPagerView = Animated.createAnimatedComponent(PagerView); @@ -43,10 +31,9 @@ function usePageScrollHandler(handlers: any, dependencies?: any) { } export const TabsPagerView: React.FC = (props) => { - const { setPageFN, pageOffset, setNativeActiveIndex, scrollY } = useTabCtx(); + const { setPageFN, pageOffset, setNativeActiveIndex, activeIndex, scrollY } = + useTabCtx(); const refPagerView = useRef(null); - const tabBarHeight = useBottomTabBarHeight(); - const numOfTabs = useMemo(() => React.Children.count(props.children), [props.children]); useTabPress(() => { if (scrollY.value === 0 && pageOffset.value !== 0) { @@ -54,6 +41,10 @@ export const TabsPagerView: React.FC = (props) => { } }); + useEffect(() => { + props.onActiveIndexChange?.(activeIndex); + }, [activeIndex, props]); + React.useEffect(() => { const setPage = (index: number) => { requestAnimationFrame(() => refPagerView.current?.setPage(index)); @@ -72,43 +63,8 @@ export const TabsPagerView: React.FC = (props) => { }, }); - useAnimatedReaction( - () => pageOffset.value, - () => { - if (pageOffset.value > 1.8 && pageOffset.value < 2) { - runOnJS(Haptics.notificationSuccess)(); - } - }, - ); - - const funnyStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateX: - pageOffset.value > numOfTabs - ? 50 - : interpolate( - pageOffset.value, - [numOfTabs - 0.2, numOfTabs - 0.1], - [50, -80], - Extrapolate.CLAMP, - ), - }, - ], - }; - }); - return ( - - - Date: Wed, 21 Feb 2024 06:30:00 +0300 Subject: [PATCH 41/61] localization(mobile): Pull battery locales (#737) --- .../shared/i18n/locales/tonkeeper/it.json | 3 +- .../shared/i18n/locales/tonkeeper/tr-TR.json | 169 +++++++++++++++++- .../i18n/locales/tonkeeper/zh-Hans-CN.json | 49 ++++- 3 files changed, 213 insertions(+), 8 deletions(-) diff --git a/packages/shared/i18n/locales/tonkeeper/it.json b/packages/shared/i18n/locales/tonkeeper/it.json index 025985b1b..4d27e883f 100644 --- a/packages/shared/i18n/locales/tonkeeper/it.json +++ b/packages/shared/i18n/locales/tonkeeper/it.json @@ -559,6 +559,7 @@ "tab_swap" : "Swap", "tab_wallet" : "Wallet", "today" : "Oggi", + "tonkeeper_pro" : "Tonkeeper Pro", "ton_login_back_to_button" : "Torna a %{name}", "ton_login_caption" : "%{name} sta richiedendo l'accesso all'indirizzo del tuo wallet", "ton_login_connect_button" : "Collega wallet", @@ -697,4 +698,4 @@ "wallet_toncommunity_chat_link" : "https://t.me/toncoin_it_chat", "wallet_toncommunity_link" : "https://t.me/toncoin_it", "yesterday" : "Ieri" -} +} \ No newline at end of file diff --git a/packages/shared/i18n/locales/tonkeeper/tr-TR.json b/packages/shared/i18n/locales/tonkeeper/tr-TR.json index 727dc3c32..b9e6ec722 100644 --- a/packages/shared/i18n/locales/tonkeeper/tr-TR.json +++ b/packages/shared/i18n/locales/tonkeeper/tr-TR.json @@ -40,7 +40,19 @@ "add_other_coins" : "Diğer kriptoları ekleyin", "address_copied" : "Adres kopyalandı", "address_update" : { - "title" : "Adres güncelleme" + "first_option" : "Muhtemelen hedef cüzdan zaten aktif olarak kullanılıyor olduğundan herhangi bir fark fark etmeyeceksiniz.", + "learn_more" : "Daha fazla bilgi edinin", + "new_style" : "Yeni adres", + "notification_desc_did_change" : "5 Ekim'de cüzdan adresiniz daha iyi bir format olan UQ formatına güncellendi. Eski adresiniz de kullanılmaya devam edeceğinden hiçbir şey yapmanıza gerek bulunmamaktadır.", + "notification_desc_will_change" : "5 Ekim'de cüzdan adresiniz daha iyi bir format olan UQ formatına güncellenecektir. Eski adresiniz de kullanılmaya devam edeceğinden hiçbir şey yapmanıza gerek bulunmamaktadır.", + "old_style" : "Eski adres", + "post_dates" : "\n5 Ekim 2023: Tonkeeper'da tüm adresler UQ formatına geçiş yapacak. 1 Ocak 2024: Tonkeeper, sözleşme durumunu kontrol etmeyi bırakacak ve adresin doğruluğunu kontrol etmek için 'bounceable' bayrağını kullanacak. Bu sayede, EQ adresi ile başlamayan, henüz yayınlanmamış sözleşmelere gönderilen tutarlar göndericiye geri gönderilecektir.", + "post_rest" : "TON blockchain'de iki adres stili bulunmaktadır ve şimdiye kadar uygulamalar ve cüzdanlar için yalnızca biri kullanılmaktadır. EQ formatı, gelen fonları işleyen akıllı sözleşmeler için en iyi formattır. Bir akıllı sözleşme henüz yayınlanmadıysa - yani kod blokzincirine yüklenmediyse - o adrese gönderilen TON'lar gönderene geri dönecektir. Bu bir güvenlik özelliği olup, herhangi bir hata olduğu taktirde, TON coinlerin geri göndericinin hesabına geri dönmesini sağlar. UQ formatı, cüzdanlar için en iyi formattır. Cüzdanlar sadece fonları saklamak için tasarlandığından, böyle bir durumda gönderilen kripto tutarları asla cüzdana geri dönmez. Her cüzdan, blok zincirinde kod yüklenmiş olmaksızın yalın bir adres olarak kullanılmaya başlandığından, meblağların geri dönmesi anlamsız olur. Bu yıl cüzdan adreslerinde daha uygun format olan UQ formatının kullanımına geçiş yapıyoruz. Eğer size verilen eski bir EQ adresine fon göndermeye devam ederseniz, şu iki seçeneğe sahip olacaksınız;", + "post_top" : "Bu yılın sonuna kadar TON ağındaki tüm cüzdan adresleri farklı formatlarıyla görüntülenmeye başlanacaktır. Yeni adres UQ ile başlayacak ve son dört harfi değişecektir. Eski adres de kullanılmaya devam edecek olup aynı cüzdana yönlendirecektir. Bu durum cüzdanınızda saklanan kriptoların güvenliğini herhangi bir şekilde etkilemeyecektir.", + "second_option" : "Cüzdanın herhangi bir ödeme için kullanılmamış olması ihtimali daha azdır. Ve Tonkeeper gelecek yıldan itibaren o adrese coin göndermeyecektir. Alıcından yeni bir UQ adresi istemeniz gerekecektir.", + "title" : "Adres güncelleme", + "why_change" : "Bu değişiklik neden yapıldı?", + "your_wallet" : "Cüzdanınız" }, "all_regions" : "Tüm bölgeler", "appearance_accent_name" : { @@ -67,6 +79,8 @@ "accepted" : "Onaylandı", "accepted_at_collection" : "%{date} tarihinde onaylandı", "accepted_at_token" : "%{date} tarihinde onaylandı", + "accepted_collection" : "Kabul edilen koleksiyon", + "accepted_token" : "Kabul edilen token", "approve_all" : "Tümünü onayla", "approve_collection_many" : "\"%{collection}\" koleksiyonundan gelen token'ları onaylayın", "approve_collection_one" : "\"%{collection}\" koleksiyonundan gelen token'ı onaylayın", @@ -86,7 +100,11 @@ "id_token" : "Token Kimliği", "manage_tokens" : "Token'ları yönetin", "move_to_accepted" : "Onaylananlar'a taşıyın", + "move_to_accepted_collection" : "Koleksiyonu cüzdanda göster", + "move_to_accepted_token" : "Cüzdandaki token'ları göster", "move_to_declined" : "Reddedilenler'e taşıyın", + "move_to_declined_collection" : "Koleksiyonu cüzdanda gizle", + "move_to_declined_token" : "Token'ı cüzdanda gizle", "name" : "İsim", "pending" : "Beklemede", "show_all" : "Tümünü göster", @@ -96,6 +114,7 @@ "one" : "%{count} token", "other" : "%{count} token" }, + "unverified_token" : "Doğrulanmamış Token", "verify_collection" : "Koleksiyonu doğrulayın", "verify_description_collection" : "Bu token'lar bilinmeyen bir token yayıncısı tarafından basılmış. Token'ların sahtelerini tespit etmek için, token yayıncının resmi kaynağıyla karşılaştırarak koleksiyon kimliğini doğrulayın. Token görünürlüğünü daha sonra ayarlar içerisinden değiştirebilirsiniz.", "verify_description_token" : "Bu token bilinmeyen bir token yayıncısı tarafından basılmış. Token'ların sahtelerini tespit etmek için, token yayıncının resmi kaynağıyla karşılaştırarak token kimliğini doğrulayın. Token görünürlüğünü daha sonra ayarlar içerisinden değiştirebilirsiniz.", @@ -105,6 +124,40 @@ }, "auth_failed" : "Kimlik doğrulama başarısız oldu", "balances_setup_wallet" : "Cüzdanı ayarlayın", + "battery" : { + "description" : { + "empty" : "Ana bakiyeniz boş olsa bile token ve NFT gönderin, staking işlemleri gerçekleştirin.", + "other" : "%{cnt} işlem için bataryanızda yeterli şarj seviyesi mevcut." + }, + "ok" : "TAMAM", + "packages" : { + "buy" : "Satın al", + "disclaimer" : "Bu yaklaşık işlem sayısıdır. Bazı işlemlerinizin maliyeti daha yüksek olabilir.", + "ok" : "TAMAM", + "refilled" : "Bataryanız şarj oldu", + "subtitle" : { + "large" : "Büyük paket", + "medium" : "Orta paket", + "small" : "Küçük paket" + }, + "title" : "{{price}} karşılığı {{cnt}} işlem" + }, + "promocode" : { + "apply" : "Uygula", + "button" : "Promosyon Kodu ile yükleme yapın", + "placeholder" : "Kod", + "success" : "Bataryanız şarj oldu", + "title" : "Promosyon Kodu" + }, + "screen_title" : "Batarya", + "settings" : "Batarya", + "title" : { + "almost_empty" : "Gas ücretleri için batarya neredeyse boş", + "empty" : "Gaz ücretleri için bataryanızı şarj edin", + "full" : "Gas ücretleri için batarya dolu", + "medium" : "Gas ücretleri için bataryanın yarısı dolu" + } + }, "browser" : { "about_dapps_caption" : "Oturum açma ve ödemeler için Tonkeeper'ı kullanabileceğiniz uygulamaları ve hizmetleri keşfedin.", "about_dapps_learn_more" : "Daha fazla bilgi edinin", @@ -118,6 +171,8 @@ }, "apps_all" : "Tümü", "connected" : "Bağlı", + "connected_empty_text" : "Uygulamaları ve hizmetleri Tonkeeper tarayıcısında keşfedin.", + "connected_empty_title" : "Bağlı uygulamalar burada gösterilecektir", "connected_title" : "Bağlandı", "empty_search" : "Aramanız hiçbir sonuç vermedi", "explore" : "Keşfet", @@ -153,6 +208,10 @@ "check_words_success" : "Tebrikler! Cüzdan kurulumunuzu tamamladınız", "check_words_title" : "Haydi şimdi kontrol edelim", "choose_country" : { + "auto" : "Otomatik", + "cancel" : "İptal", + "empty_placeholder" : "Aramanız hiçbir sonuç vermedi", + "search" : "Arama", "title" : "Ülkenizi seçin" }, "choose_currency" : { @@ -177,6 +236,7 @@ "NGN" : "Nijerya nairası", "RUB" : "Rus rublesi", "THB" : "Tayland bahtı", + "TON" : "Toncoin", "TRY" : "Türk lirası", "UAH" : "Ukrayna hryvnyası", "USD" : "Amerikan doları", @@ -186,6 +246,7 @@ "header_title" : "Birincil para birimi" }, "confirm" : "Onaylayın", + "confirm_renew_all_domains_title" : "İşlemi onaylayın", "confirm_sending_amount" : "Tutar", "confirm_sending_fee" : "Ücret", "confirm_sending_inactive_warn_about" : "Ne yapmalısınız?", @@ -201,6 +262,18 @@ "confirm_sending_sent_caption_ton" : "İşleminiz ağa gönderildi ve bir kaç saniye içinde işleme alınacak.", "confirm_sending_submit" : "Onaylayın ve Gönderin", "confirm_sending_title" : "Göndermeyi onaylayın", + "confirmSendModal" : { + "network_fee" : "Ağ ücreti", + "refund" : "İade", + "title" : "İşlemi onaylayın", + "to_your_address" : "Adresinize", + "transaction_type" : { + "burn" : "Yak", + "receive" : "Al", + "send" : "Gönder" + }, + "will_be_paid_with_battery" : "Batarya ile ödenecek" + }, "continue" : "Devam et", "copied" : "Kopyalandı", "copy_error_log" : "Hata kaydını kopyalayın", @@ -216,12 +289,26 @@ "deploy_contract_button" : "Onaylayın ve dağıtımını gerçekleştirin", "deploy_contract_title" : "Sözleşmenin dağıtımını gerçekleştirin", "disable_nft_marketplace_banner_description" : "Toplayın ve takas edin.", + "dns_addresses" : { + "few" : "%{count} adres", + "many" : "%{count} adres", + "one" : "%{count} adres", + "other" : "%{count} adres" + }, "dns_address_linked" : "Adres bağlandı", "dns_address_unlinked" : "Adresin bağlantısı kaldırıldı", + "dns_alert_expiring_many" : { + "few" : "%{count} adet süresi dolmak üzere olan alan adınız mevcuttur. Tümünü %{untilDate} tarihine kadar yenilemeniz gerekmektedir.", + "many" : "%{count} adet süresi dolmak üzere olan alan adınız mevcuttur. Tümünü %{untilDate} tarihine kadar yenilemeniz gerekmektedir.", + "one" : "%{count} adet süresi dolmak üzere olan alan adınız mevcuttur. Tümünü %{untilDate} tarihine kadar yenilemeniz gerekmektedir.", + "other" : "%{count} adet süresi dolmak üzere olan alan adınız mevcuttur. Tümünü %{untilDate} tarihine kadar yenilemeniz gerekmektedir." + }, + "dns_alert_expiring_one" : "%{domain} alan adının süresi %{count} gün içinde dolacaktır. %{untilDate} tarihine kadar yenilemeniz gerekmektedir.", "dns_current_address" : "Mevcut adresiniz", "dns_expiration_date" : "Son geçerlilik tarihi", "dns_link_title" : "İşlemi onaylayın", "dns_on_sale_text" : "Alan adı şu anda pazar yerinde satışta. Transfer için önce satıştan kaldırmalısınız.", + "dns_renew_all_until_btn" : "Tümünü %{untilDate} tarihine kadar yenileyin", "dns_renew_in_progress_btn" : "Alan adı yenileme işlemi devam ediyor... ", "dns_renew_toast_success" : "Alan adı 1 yıllığına yenilendi", "dns_renew_until_btn" : "%{untilDate} tarihine kadar yenileyin", @@ -234,18 +321,31 @@ "dns_replace_save" : "Kaydet", "dns_unlink_title" : "Bağlantıyı kaldırmayı onaylayın", "dns_wallet_address" : "Cüzdan adresi", + "domains_renewed" : "Yenilenen alan adları", "edit_coins_add" : "Ekle", "edit_coins_added" : "Eklendi", "edit_coins_added_toast" : "Eklendi", "edit_coins_hide" : "Gizle", "edit_coins_title" : "Kripto ekleyin", + "encryptedComments" : { + "encryptedComment" : "Şifrelenmiş yorum", + "encryptedCommentModal" : { + "button" : "Yorumun şifresini çöz", + "checkboxLabel" : "Tekrar gösterme", + "description" : "Yorum, gönderen tarafından şifrelenir ve şifresi yalnızca sizin tarafınızdan çözülebilir. Lütfen içeriğe dikkat edin ve dolandırıcılıklara karşı dikkatli olun.", + "title" : "Şifrelenmiş yorum" + }, + "show" : "Göster" + }, "error_network" : "Ağ hatası", "error_occurred" : "Bir hata oluştu", "exchange_method_dont_show_again" : "Tekrar gösterme", "exchange_method_open_warning" : "Tonkeeper tarafından işletilmeyen harici bir uygulamayı açıyorsunuz.", "exchange_modal" : { + "buy" : "Satın al", "hide" : "Gizle", "other_ways_to_buy" : "Diğer satın alma seçenekleri", + "sell" : "Sat", "show_all" : "Tümünü göster", "title" : "Satın Al veya Sat" }, @@ -255,7 +355,16 @@ "exchange_other_ways" : "TON satın almak veya satmak için diğer seçenekler", "exchange_telegram_bot" : "TELEGRAM BOTU", "exchange_title" : "TON satın alın", + "expiring_domains" : "Süresi dolan alan adları", "form_optional_indicator" : "İsteğe bağlı", + "import_add_wallet" : "Cüzdan Ekle", + "import_add_wallet_description" : "Yeni bir cüzdan oluşturun veya mevcut bir cüzdan ekleyin.", + "import_existing_wallet" : "Mevcut Cüzdan", + "import_existing_wallet_description" : "24 gizli kurtarma kelimesiyle cüzdanı içe aktarın", + "import_new_wallet" : "Yeni Cüzdan", + "import_new_wallet_description" : "Yeni cüzdan oluşturun", + "import_signer" : "İmza Sahiplerini Eşleştir", + "import_signer_description" : "Daha yüksek bir kontrol ve güvenlik seviyesi", "import_wallet_caption" : "Cüzdanınıza erişimi geri kazanmak için, cüzdanınızı oluşturduğunuz sırada size verilen 24 harften oluşan gizli kurtarma ifadesini girin.", "import_wallet_reset_caption" : "Cüzdanınızı oluşturduğunuz sırada size verilen 24 harften oluşan gizli kurtarma ifadesini girerek cüzdanınıza erişimi geri kazanın.", "import_wallet_title" : "Gizli kurtarma ifadenizi girin", @@ -284,6 +393,17 @@ "jettons_manage_tokens" : "Token'ları yönetin", "jettons_show_jettons" : "Cüzdandaki token'ları göster", "jetton_token" : "Token", + "language" : { + "language_alert" : { + "cancel" : "İptal", + "open" : "Ayarlar", + "title" : "Uygulamanın dilini değiştirmek için cihaz Ayarları'na gidin" + }, + "list_item" : { + "title" : "Dil", + "value" : "Türkçe" + } + }, "later" : "Daha sonra", "legal_font_license" : "Montserrat yazı tipi", "legal_header_title" : "Hukuki açıklama", @@ -355,6 +475,16 @@ "nft_token_id" : "Token Kimliği", "nft_transaction_head_placeholder" : "NFT", "nft_transfer_comment" : "Yorum", + "nft_transfer" : { + "confirm" : { + "fee" : { + "label" : "Ücret", + "refund_label" : "İade", + "value" : "%{value} TON" + } + }, + "title" : "NFT Transferi" + }, "nft_transfer_description" : "NFT bu adrese gönderilecek. Başka bir kullanıcıya NFT gönderirken dikkatli olun.", "nft_transfer_dns" : "Transfer edin", "nft_transfer_nft" : "Transfer edin", @@ -363,6 +493,7 @@ "nft_unlink_domain_button" : "{{address}} ile bağlantılı", "nft_unnamed_collection" : "İsimsiz koleksiyon", "nft_view_in_explorer" : "Explorer'da görüntüleyin", + "nokyc" : "KYC gerekli değildir", "notifications" : { "alert" : { "cancel" : "İptal", @@ -435,6 +566,8 @@ "receiveModal" : { "copy" : "Kopyala", "receive" : "Al", + "receive_description" : "Bu adrese yalnızca %{tokenName} ve TON ağındaki token'ları gönderin, öteki türlü kripto varlıklarınızı kaybedebilirsiniz.", + "receive_title" : "%{tokenName} Al", "receive_ton" : "Bu adrese yalnızca TON ağındaki TON ve token'ları gönderin, öteki türlü kripto varlıklarınızı kaybedebilirsiniz." }, "receive_qr_title" : "Almak için QR kodunu gösterin", @@ -443,10 +576,13 @@ "receive_title" : "%{currency} al", "receive_ton_and_jettons" : "TON ve diğer token'ları cüzdana alın", "refresh_app" : "Yeniden Başlat", + "region_nokyc" : "Tarafsız bölge", "reminder_notifications_caption" : "Cüzdanınıza TON, token ve NFT geldiğinde bildirimler alın.", "reminder_notifications_enable_button" : "Bildirimleri etkinleştirin", "reminder_notifications_later_button" : "Daha sonra", "reminder_notifications_title" : "Anlık bildirimler alın", + "renew_in_progress" : "Yenileme işlemi devam ediyor...", + "renew_progress_of" : "%{current} / %{count}", "require_create_wallet_modal_caption" : "Tonkeeper'ı kullanmak için bağlı bir cüzdana ihtiyacınız var. Bunun için ya yeni bir cüzdan oluşturun ya da mevcut bir cüzdanı içe aktarın.", "require_create_wallet_modal_create_new" : "Yeni cüzdan oluşturun", "require_create_wallet_modal_import" : "Mevcut cüzdanı içe aktarın", @@ -508,11 +644,13 @@ "comment_description_encrypted" : "Sadece alıcı ve siz tarafından görülebilecektir.", "comment_encrypt" : "Yorumu şifrele", "comment_label" : "Yorum", + "comment_label_encrypted" : "Şifrelenmiş yorum", "comment_label_required" : "Gerekli yorum", "comment_required_text" : "Transfer için borsanın notunu eklemelisiniz. Aksi takdirde gönderdiğiniz tutarlar kaybolacaktır.", "details_label" : "Detaylar", "details_max_balance_label" : "Maksimum %{currency} bakiye gönderiliyor", - "title" : "İşlemi onaylayın" + "title" : "İşlemi onaylayın", + "will_be_paid_with_battery" : "Batarya ile ödenecek" }, "done" : { "add_favorite" : "Adresi favorilere kaydet", @@ -564,6 +702,8 @@ "setup_notifications_caption" : "Cüzdanınıza TON, token ve NFT geldiğinde bildirimler alın.", "setup_notifications_enable_button" : "Bildirimleri etkinleştirin", "setup_notifications_title" : "Anlık bildirimler alın", + "signer_scan_result" : "İmzalı işlemi tarayın", + "signer_scan_tx_description" : "Lütfen Signer'ı açın ve işlemi QR koduyla tarayın", "skip" : "Atla", "spam_action" : "İstenmeyen mesaj", "staking" : { @@ -588,6 +728,7 @@ } }, "confirm_deposit" : "Onaylayın ve yatırın", + "confirm_unstake" : "Onaylayın ve geri çekin", "deposit" : "Yatırma", "desc_large" : "TONcoin stake edenler arasına katılın ve ödüller kazanın. Staking, TON ağını korumaya yardımcı olur.", "details" : { @@ -620,6 +761,7 @@ "desc" : "Tüm işlemler, döngü sona erdikten sonra geçerli olur.", "desc_liquid" : "Tüm çekim talepleri, döngü sona erdiğinde gerçekleştirilir.", "in" : ":", + "message" : "Unstake talebi, %{value} içindeki doğrulama döngüsünün bitiminden sonra işlenecektir.", "reward_title" : "Sonraki ödül", "title" : "Sonraki döngü" }, @@ -638,14 +780,18 @@ "tap_to_collect" : "Çekmek için dokunun" }, "estimated_profit" : "%{amount} TON - Eğer bugün TON stake ederseniz kazanacağınız yıllık kâr", + "estimated_profit_compare" : "Mevcut staking'inize göre yılda %{amount} TON daha kârlı", "get_withdrawal" : "Çekim yapın", "highest_apy" : "MAKS. APY", + "jetton_note" : "TON'unuzu %{poolName} havuzuna yatırdığınızda, bu tutar karşılığında havuzdaki payınızı temsil eden miktarda %{token} adlı bir token hesabınıza yatacaktır. Havuzda kâr biriktikçe, %{token} token'ınızın temsil ettiği TON miktarı artacaktır.", "learn_more" : "Daha fazla bilgi edinin", "message" : { "pendingDeposit" : "%{amount} TON eklenecek", "pendingWithdraw" : "%{amount} TON çekilecek", + "pendingWithdrawLiquid" : "%{amount} TON, döngünün bitiminden sonra stake edilecek", "readyWithdraw" : "%{amount} TON çekilmeye hazır.\nÇekmek için dokunun" }, + "no_funds" : "Stake havuzundan geri alınacak herhangi bir meblağ bulunmuyor", "not_exists" : "Geçersiz havuz adresi", "other" : "Diğer", "rewards" : { @@ -655,6 +801,7 @@ "value" : "≈ %{value} TON" }, "send_staked_ton" : "Stake edilmiş TON", + "staked" : "Stake edildi", "staked_ton" : "Stake edilmiş TON", "staking_desc" : "Minimum yatırma tutarı %{minStake} TON'dur. Azami %{maxApy}% oranında getiri elde edebilirsiniz.", "staking_pool_desc" : "APY ≈ %{apy}%", @@ -669,6 +816,7 @@ "title" : "Uyarı" }, "widget_desc" : "%{apy}% kadar APY", + "widget_staking_options" : "Staking seçenekleri", "widget_title" : "TON kazanın", "withdraw" : "Çekme", "withdrawal_fee_warning" : { @@ -721,6 +869,8 @@ "tab_swap" : "Takas", "tab_wallet" : "Cüzdan", "today" : "Bugün", + "tonkeeper_pro" : "Tonkeeper Pro", + "tonkeeper_pro_description" : "Tonkeeper Pro'nun aboneliği, kripto yönetimi için bir araç seti sunan genişletilmiş bir cüzdan özelliği ile birlikte gelir", "ton_login_back_to_button" : "%{name}'e geri dön", "ton_login_caption" : "%{name}, cüzdan adresinize erişim istiyor", "ton_login_connect_button" : "Cüzdanı bağlayın", @@ -743,6 +893,7 @@ "bid_collection_name" : "Koleksiyon yayıncısı", "bid_name" : "İsim", "comment" : "Yorum", + "description" : "Açıklama", "operation" : "İşlem", "payload" : "Bloğun yararlı yükü", "recipient" : "Alıcı", @@ -753,7 +904,8 @@ "subscription_merchant_label" : "Satıcı", "subscription_product_label" : "Üyelik", "transaction" : "İşlem", - "unsubscription_title" : "Üyelikten çıkıldı" + "unsubscription_title" : "Üyelikten çıkıldı", + "withdraw_amount" : "Çekilecek tutar" }, "transaction_exchange_from_currency" : "Satılan", "transaction_fee" : "Ücret", @@ -813,7 +965,10 @@ "transaction_your_bid" : "Teklifiniz", "transfer_deeplink_address_error" : "Hatalı alıcı adresi", "transfer_deeplink_amount_error" : "Hatalı tutar isteği", + "transfer_deeplink_expired_error" : "Süresi dolmuş bağlantı", "transfer_deeplink_nft_address_error" : "Hatalı NFT adresi", + "transfer_deeplink_unknown_token" : "Bilinmeyen token", + "transfer_deeplink_wrong_params" : "Yanlış parametreler", "transfer_from_old_wallet_btn" : "Transfer et", "transfer_from_old_wallet_caption" : "Tonkeeper, tüm coin'leri eski adresinizden yeni adresinize transfer edecek.", "transfer_from_old_wallet_in_progress" : "Transfer işlemi devam ediyor", @@ -838,7 +993,9 @@ }, "comment" : "Yorum", "insufficientFunds" : { + "rechargeBattery" : "Bataryayı şarj edin", "rechargeWallet" : "Cüzdana yeniden bakiye yükleyin", + "stakingDeposit" : "Katılım için minimum bakiye:\n%{amount} %{currency}\n", "stakingFee" : "İşlem için %{amount} TON gerekiyor. Tahminen %{fee} TON ücret kesilecek, geri kalanı iade edilecektir.", "title" : "Yetersiz bakiye", "toBePaid" : "Ödenecek tutar: %{amount} %{currency}\n", @@ -886,13 +1043,15 @@ "buy_btn" : "TON satın alın", "collectibles_tab_lable" : "Koleksiyon varlıkları", "edit_tokens_btn" : "Düzenle", + "nft_tab_lable" : "Koleksiyon varlıkları", "old_wallets_title" : "Eski cüzdanlar", "old_wallet_title" : "Eski cüzdan", "receive_btn" : "Al", "screen_title" : "Cüzdan", "send_btn" : "Gönder", "swap_btn" : "Takas", - "tonkens_tab_lable" : "Tokenlar" + "tonkens_tab_lable" : "Tokenlar", + "updated_at" : "%{value} tarihinde güncellendi" }, "wallet_chat" : "Sohbet", "wallet_community" : "Topluluk", @@ -905,4 +1064,4 @@ "wallet_swap" : "Takas", "wallet_title" : "Cüzdan", "yesterday" : "Dün" -} +} \ No newline at end of file diff --git a/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json b/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json index 4a51ac9de..b47eb09d4 100644 --- a/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json +++ b/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json @@ -88,6 +88,40 @@ }, "auth_failed" : "认证失败", "balances_setup_wallet" : "注册钱包", + "battery" : { + "description" : { + "empty" : "发送代币和 NFT,用空的主账户支付质押操作费用。", + "other" : "您已有足够的电量以进行 %{cnt} 次交易" + }, + "ok" : "确定", + "packages" : { + "buy" : "购买", + "disclaimer" : "这是估计的转账数量。您的某些交易可能会花费更多。", + "ok" : "确定", + "refilled" : "电池已充电", + "subtitle" : { + "large" : "大杯", + "medium" : "中杯", + "small" : "小杯" + }, + "title" : "{{cnt}} 笔交易,总共 {{price}}" + }, + "promocode" : { + "apply" : "应用", + "button" : "使用优惠码充值", + "placeholder" : "优惠码", + "success" : "电池已充电", + "title" : "优惠码" + }, + "screen_title" : "电池", + "settings" : "电池", + "title" : { + "almost_empty" : "电池即将耗尽", + "empty" : "为你的电池充电", + "full" : "电池已充满", + "medium" : "电池已经充满一半" + } + }, "browser" : { "about_dapps_caption" : "探索可以使用Tonkeeper进行登录和支付的应用和服务。", "about_dapps_learn_more" : "了解更多", @@ -182,6 +216,9 @@ "confirm_sending_sent_title" : "已发送币种", "confirm_sending_submit" : "确认并发送", "confirm_sending_title" : "确认发送", + "confirmSendModal" : { + "will_be_paid_with_battery" : "将使用电池进行支付" + }, "continue" : "继续", "copied" : "复制", "copy_error_log" : "复制错误指令", @@ -262,6 +299,11 @@ "jettons_manage_tokens" : "管理代币", "jettons_show_jettons" : "在钱包中显示代币", "jetton_token" : "代币", + "language" : { + "list_item" : { + "value" : "中文" + } + }, "later" : "稍后", "legal_font_license" : "Montserrat字体", "legal_header_title" : "法律", @@ -476,7 +518,8 @@ "comment_required_text" : "你必须包含交换的备注进行转账。否则你的资金将会丢失。", "details_label" : "详情", "details_max_balance_label" : "发送全部余额 %{currency}", - "title" : "确认交易" + "title" : "确认交易", + "will_be_paid_with_battery" : "将使用电池进行支付" }, "done" : { "add_favorite" : "将地址保存到收藏", @@ -658,6 +701,7 @@ "tab_swap" : "交换", "tab_wallet" : "钱包", "today" : "今天", + "tonkeeper_pro" : "Tonkeeper Pro", "ton_login_back_to_button" : "返回到%{name}", "ton_login_caption" : "%{name}正在请求访问您的钱包地址", "ton_login_connect_button" : "连接钱包", @@ -773,6 +817,7 @@ }, "comment" : "评论", "insufficientFunds" : { + "rechargeBattery" : "给电池充电", "rechargeWallet" : "充值钱包", "title" : "资金不足", "toBePaid" : "支付金额: %{amount} %{currency}\n", @@ -841,4 +886,4 @@ "wallet_toncommunity_chat_link" : "https://t.me/toncoin_chat", "wallet_toncommunity_link" : "https://t.me/toncoin", "yesterday" : "昨天" -} +} \ No newline at end of file From 7911a97b50c7ff5252aecad97416d2843cc04760 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Wed, 21 Feb 2024 13:43:35 +0300 Subject: [PATCH 42/61] fix(mobile): Remove unnecessary bit (#738) --- packages/@core-js/src/service/contractService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@core-js/src/service/contractService.ts b/packages/@core-js/src/service/contractService.ts index 79f3cd3e8..ebdb48760 100644 --- a/packages/@core-js/src/service/contractService.ts +++ b/packages/@core-js/src/service/contractService.ts @@ -116,7 +116,6 @@ export class ContractService { .storeAddress(tonAddress(createJettonTransferBodyParams.excessesAddress)) .storeBit(false) // null custom_payload .storeCoins(createJettonTransferBodyParams.forwardAmount ?? 1n) - .storeBit(createJettonTransferBodyParams.forwardBody != null) // forward_payload in this slice - false, separate cell - true .storeMaybeRef(this.prepareForwardBody(createJettonTransferBodyParams.forwardBody)) .endCell(); } From 846d9f8e3ecf84c1c5cf8686e5b9be5c87536422 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Wed, 21 Feb 2024 13:50:14 +0300 Subject: [PATCH 43/61] fix(mobile): Minor release fixes (#740) --- .../src/core/ManageTokens/ManageTokens.tsx | 1 + packages/mobile/src/core/Settings/Settings.tsx | 2 +- .../mobile/src/tabs/Wallet/WalletScreen.tsx | 8 +++++++- .../Wallet/components/WalletContentList.tsx | 5 +++-- .../ActivityActionModal/ActivityActionModal.tsx | 17 ++++++++++++----- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx index 0715a61d2..d4d589e35 100644 --- a/packages/mobile/src/core/ManageTokens/ManageTokens.tsx +++ b/packages/mobile/src/core/ManageTokens/ManageTokens.tsx @@ -58,6 +58,7 @@ const FLashListItem = ({ return ( { const searchEngineVariants = Object.values(SearchEngine); const handleSwitchLanguage = useCallback(() => { - if (Platform.OS === 'android' && Platform.Version < 33) { + if (Platform.OS === 'android') { return openSelectLanguage(); } diff --git a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx index 13ed7b6db..1d1bf6aba 100644 --- a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx +++ b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx @@ -51,6 +51,7 @@ import { getLocale } from '$utils/date'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { useWallet, useWalletCurrency, useWalletStatus } from '@tonkeeper/shared/hooks'; import { WalletSelector } from './components/WalletSelector'; +import { useInscriptionBalances } from '$hooks/useInscriptionBalances'; export const WalletScreen = memo(({ navigation }) => { const flags = useFlags(['disable_swap']); @@ -59,6 +60,7 @@ export const WalletScreen = memo(({ navigation }) => { const theme = useTheme(); const nav = useNavigation(); const tokens = useTonkens(); + const { enabled: inscriptions } = useInscriptionBalances(); const { enabled: nfts } = useApprovedNfts(); const wallet = useWallet(); const shouldUpdate = @@ -270,7 +272,9 @@ export const WalletScreen = memo(({ navigation }) => { }, [dimensions.width]); const isPagerView = - nfts.length && tokens.list.length >= 2 && tokens.list.length + nfts.length + 1 > 10; + nfts.length && + tokens.list.length + inscriptions.length >= 2 && + inscriptions.length + tokens.list.length + nfts.length + 1 > 10; if (!wallet) { return ( @@ -327,6 +331,7 @@ export const WalletScreen = memo(({ navigation }) => { { ) : ( void; isRefreshing: boolean; @@ -192,6 +193,7 @@ export const WalletContentList = memo( nfts, handleRefresh, isRefreshing, + inscriptions, isFocused, ListHeaderComponent, }) => { @@ -199,7 +201,6 @@ export const WalletContentList = memo( const fiatCurrency = useWalletCurrency(); const shouldShowTonDiff = fiatCurrency !== WalletCurrency.TON; - const { enabled: inscriptions } = useInscriptionBalances(); const wallet = useWallet(); const isWatchOnly = wallet && wallet.isWatchOnly; diff --git a/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx b/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx index 051410498..db6fa9a1e 100644 --- a/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx +++ b/packages/shared/modals/ActivityActionModal/ActivityActionModal.tsx @@ -13,14 +13,21 @@ type ActivityActionModalProps = { action: AnyActionItem; }; +/** Payload with possibly big content to render. + * We should wrap content into ScrollView + * if action has any of these fields. + * TODO: should measure content size and conditionally wrap into ScrollView + * */ +const possiblyLargePayloadFields = ['payload', 'comment', 'encrypted_comment']; + export const ActivityActionModal = memo((props) => { const { action } = props; - // TODO: need auto detect modal content size - const Content = - (action as any)?.payload?.comment || (action as any)?.payload?.encrypted_comment - ? Modal.ScrollView - : Modal.Content; + const shouldWrapIntoScrollView = + possiblyLargePayloadFields.findIndex((field) => (action as any)?.payload?.[field]) !== + -1; + + const Content = shouldWrapIntoScrollView ? Modal.ScrollView : Modal.Content; return ( From 5b8ebfb99d322118be24c0be11007808d210b8fc Mon Sep 17 00:00:00 2001 From: Andrey Sorokin Date: Thu, 22 Feb 2024 01:39:30 +0600 Subject: [PATCH 44/61] feature(mobile): nfc deeplink (#739) --- packages/mobile/android/app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 5ffa399cd..074ae426c 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + @@ -64,6 +65,7 @@ + From 699373f127e8b9c29a3e7482e62790ecf8175a53 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Mon, 26 Feb 2024 12:11:20 +0300 Subject: [PATCH 45/61] feature(mobile): Sort jettons by fiat balance (#743) * feature(mobile): Sort jettons by fiat balance * fix(mobile): Analytics --- packages/mobile/src/utils/stats.ts | 17 +++++------ .../src/wallet/managers/JettonsManager.ts | 30 ++++++++++++++++++- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/mobile/src/utils/stats.ts b/packages/mobile/src/utils/stats.ts index 0ca7f2b74..ae3f9b379 100644 --- a/packages/mobile/src/utils/stats.ts +++ b/packages/mobile/src/utils/stats.ts @@ -5,22 +5,20 @@ import Aptabase from '@aptabase/react-native'; import DeviceInfo from 'react-native-device-info'; import { firebase } from '@react-native-firebase/analytics'; -const appKey = config.get('aptabaseAppKey'); - firebase .analytics() .setUserId(DeviceInfo.getUniqueId()) .catch(() => null); -if (appKey) { - Aptabase.init(appKey, { - host: config.get('aptabaseEndpoint'), - appVersion: DeviceInfo.getVersion(), - }); -} - let TrakingEnabled = false; export function initStats() { + const appKey = config.get('aptabaseAppKey'); + if (appKey) { + Aptabase.init(appKey, { + host: config.get('aptabaseEndpoint'), + appVersion: DeviceInfo.getVersion(), + }); + } init(config.get('amplitudeKey'), '-', { minIdLength: 1, deviceId: '-', @@ -40,6 +38,7 @@ export function initStats() { export async function trackEvent(name: string, params: any = {}) { try { + const appKey = config.get('aptabaseAppKey'); if (!TrakingEnabled) { return; } diff --git a/packages/mobile/src/wallet/managers/JettonsManager.ts b/packages/mobile/src/wallet/managers/JettonsManager.ts index bcd825c91..ac86d4f5a 100644 --- a/packages/mobile/src/wallet/managers/JettonsManager.ts +++ b/packages/mobile/src/wallet/managers/JettonsManager.ts @@ -1,4 +1,4 @@ -import { TonAPI } from '@tonkeeper/core/src/TonAPI'; +import { JettonVerificationType, TonAPI } from '@tonkeeper/core/src/TonAPI'; import { Storage } from '@tonkeeper/core/src/declarations/Storage'; import { TokenRate, TonRawAddress } from '../WalletTypes'; import { Logger } from '@tonkeeper/core/src/utils/Logger'; @@ -7,6 +7,8 @@ import { JettonBalanceModel } from '../models/JettonBalanceModel'; import { Address } from '@tonkeeper/core/src/formatters/Address'; import { TokenApprovalManager } from './TokenApprovalManager'; import { TonPriceManager } from './TonPriceManager'; +import BigNumber from 'bignumber.js'; +import { AmountFormatter } from '@tonkeeper/core'; export type JettonsState = { jettonBalances: JettonBalanceModel[]; @@ -85,6 +87,32 @@ export class JettonsManager { return true; }) + .sort((a, b) => { + // Unverified or blacklisted tokens have to be at the end of array + if ( + [JettonVerificationType.None, JettonVerificationType.Blacklist].includes( + a.jetton.verification, + ) + ) { + return 1; + } + + if (!a.price?.prices) { + return 1; + } + + if (!b.price?.prices) { + return -1; + } + + const aTotal = BigNumber(a.price?.prices!.TON).multipliedBy( + AmountFormatter.fromNanoStatic(a.balance, a.jetton.decimals), + ); + const bTotal = BigNumber(b.price?.prices!.TON).multipliedBy( + AmountFormatter.fromNanoStatic(b.balance, b.jetton.decimals), + ); + return bTotal.gt(aTotal) ? 1 : -1; + }) .map((item) => { return new JettonBalanceModel(item); }); From dc48a916adf1000de71f9413b783c27023a67cef Mon Sep 17 00:00:00 2001 From: Andrey Sorokin Date: Wed, 28 Feb 2024 23:43:46 +0000 Subject: [PATCH 46/61] feature(mobile): onboarding (#736) --- .../AccessConfirmation/AccessConfirmation.tsx | 18 +- packages/mobile/src/core/App.tsx | 2 + .../core/BackupWords/BackupWords.interface.ts | 7 - .../src/core/BackupWords/BackupWords.style.ts | 29 -- .../src/core/BackupWords/BackupWords.tsx | 72 ---- .../mobile/src/core/ChangePin/ChangePin.tsx | 18 +- .../CheckSecretWords.style.ts | 42 --- .../CheckSecretWords/CheckSecretWords.tsx | 322 ------------------ .../src/core/CreatePin/CreatePin.interface.ts | 1 - .../mobile/src/core/CreatePin/CreatePin.tsx | 60 ++-- .../core/CreateWallet/CreateWallet.style.ts | 47 --- .../src/core/CreateWallet/CreateWallet.tsx | 198 ----------- .../DeleteAccountDone/DeleteAccountDone.tsx | 18 +- .../src/core/ImportWallet/ImportWallet.tsx | 15 +- ...ReminderEnableNotificationsModal.styles.ts | 23 -- .../ReminderEnableNotificationsModal.tsx | 73 ---- .../ReminderEnableNotificationsModal/index.ts | 1 - .../src/core/Notifications/Notifications.tsx | 89 +---- .../src/core/SecretWords/SecretWords.style.ts | 34 -- .../src/core/SecretWords/SecretWords.tsx | 89 ----- .../mobile/src/core/Security/Security.tsx | 61 +--- .../src/core/Settings/Settings.style.ts | 2 +- .../mobile/src/core/Settings/Settings.tsx | 56 +-- .../SetupBiometry/SetupBiometry.interface.ts | 9 - .../core/SetupBiometry/SetupBiometry.style.ts | 28 -- .../src/core/SetupBiometry/SetupBiometry.tsx | 145 -------- .../SetupNotifications.style.ts | 40 --- .../SetupNotifications/SetupNotifications.tsx | 60 ++-- packages/mobile/src/core/index.ts | 5 - .../mobile/src/hooks/useExitAppBackHandler.ts | 16 - packages/mobile/src/hooks/useImportWallet.ts | 13 +- .../mobile/src/hooks/useNotificationStatus.ts | 23 -- .../mobile/src/hooks/useNotifications.tsx | 26 -- .../src/hooks/useNotificationsBadge.tsx | 56 --- .../src/hooks/useNotificationsSwitch.tsx | 79 +++++ .../hooks/useShouldEnableNotifications.tsx | 11 - .../mobile/src/modals/BackupWarningModal.tsx | 115 +++++++ packages/mobile/src/modals/index.ts | 2 + .../CreateWalletStack/CreateWalletStack.tsx | 41 +-- .../src/navigation/CreateWalletStack/types.ts | 13 - .../ImportWalletStack/ImportWalletStack.tsx | 17 +- .../src/navigation/ImportWalletStack/types.ts | 6 - .../MainStack/MainStack.interface.ts | 6 +- .../src/navigation/MainStack/MainStack.tsx | 21 +- .../MainStack/TabStack/BackupIndicator.tsx | 11 + .../TabStack/NotificationsIndicator.tsx | 20 -- .../MainStack/TabStack/TabStack.tsx | 4 +- .../MigrationStack/MigrationStack.tsx | 2 +- packages/mobile/src/navigation/ModalStack.tsx | 3 +- packages/mobile/src/navigation/Providers.tsx | 14 +- .../SettingsStack/SettingsStack.interface.ts | 1 + .../SettingsStack/SettingsStack.tsx | 2 + packages/mobile/src/navigation/helper.ts | 45 +-- .../mobile/src/navigation/navigationNames.ts | 5 +- .../BackupCheckPhraseScreen.tsx | 150 ++++++++ .../screens/BackupCheckPhraseScreen/index.ts | 1 + .../BackupPhraseScreen/BackupPhraseScreen.tsx | 136 ++++++++ .../src/screens/BackupPhraseScreen/index.ts | 1 + .../src/screens/BackupScreen/BackupScreen.tsx | 95 ++++++ .../mobile/src/screens/BackupScreen/index.ts | 1 + .../ChangePinBiometry/ChangePinBiometry.tsx | 42 +-- .../screens/ChooseWallets/ChooseWallets.tsx | 67 ++-- .../MigrationCreatePasscode.tsx | 9 +- .../MigrationPasscode/MigrationPasscode.tsx | 12 +- .../MigrationStartScreen.tsx | 17 +- .../mobile/src/screens/ResetPin/ResetPin.tsx | 14 +- .../src/screens/StartScreen/StartScreen.tsx | 14 +- packages/mobile/src/screens/index.ts | 3 + .../StakingListCell/StakingListCell.tsx | 21 +- packages/mobile/src/store/wallet/index.ts | 1 - packages/mobile/src/store/wallet/sagas.ts | 20 +- .../Wallet/components/FinishSetupList.tsx | 198 +++++++++++ .../tabs/Wallet/components/StakingWidget.tsx | 21 +- .../Wallet/components/WalletContentList.tsx | 87 ++--- .../src/tonconnect/hooks/useRemoteBridge.ts | 6 +- .../uikit/InlineKeyboard/InlineKeyboard.tsx | 41 +-- packages/mobile/src/utils/biometry.ts | 72 ++-- packages/mobile/src/wallet/Biometry.ts | 44 +++ packages/mobile/src/wallet/Tonkeeper.ts | 57 ++-- packages/mobile/src/wallet/Wallet/Wallet.ts | 18 +- .../mobile/src/wallet/Wallet/WalletBase.ts | 5 +- packages/mobile/src/wallet/constants.ts | 4 +- packages/router/src/hooks/useNavigation.ts | 5 + packages/router/src/imperative.ts | 4 +- .../shared/components/PasscodeKeyboad.tsx | 228 ------------- packages/shared/hooks/useBiometrySettings.ts | 71 +++- .../shared/hooks/useRecoveryPhraseInputs.ts | 52 +-- .../shared/i18n/locales/tonkeeper/en.json | 80 +++-- .../shared/i18n/locales/tonkeeper/it.json | 6 +- .../shared/i18n/locales/tonkeeper/ru-RU.json | 76 +++-- .../shared/i18n/locales/tonkeeper/tr-TR.json | 6 +- .../i18n/locales/tonkeeper/zh-Hans-CN.json | 6 +- packages/shared/modals/AddWalletModal.tsx | 55 ++- .../assets/icons/png/ic-faceid-28@4x.png | Bin 0 -> 1940 bytes .../icons/png/ic-faceid-android-28@4x.png | Bin 0 -> 2600 bytes .../assets/icons/png/ic-fingerprint-28@4x.png | Bin 0 -> 4920 bytes .../png/ic-fingerprint-android-28@4x.png | Bin 0 -> 3810 bytes .../assets/icons/svg/28/ic-faceid-28.svg | 3 + .../icons/svg/28/ic-faceid-android-28.svg | 7 + .../assets/icons/svg/28/ic-fingerprint-28.svg | 3 + .../svg/28/ic-fingerprint-android-28.svg | 8 + .../BlockingLoader/BlockingLoader.ts | 35 ++ .../BlockingLoader/BlockingLoaderView.tsx | 113 ++++++ .../src/components/BlockingLoader/index.ts | 2 + .../uikit/src/components/Icon/Icon.types.ts | 12 + .../src/components/Icon/IconList.native.ts | 4 + .../uikit/src/components/List/ListHeader.tsx | 29 +- .../uikit/src/components/List/ListItem.tsx | 15 +- packages/uikit/src/components/Switch.tsx | 25 ++ .../Screen/KeyboardAvoidingContainer.tsx | 17 + .../uikit/src/containers/Screen/Screen.tsx | 47 ++- .../Screen/ScreenButtonContainer.tsx | 68 ++++ .../containers/Screen/ScreenButtonSpacer.tsx | 31 ++ .../containers/Screen/ScreenHeaderIndent.tsx | 17 + packages/uikit/src/containers/Screen/index.ts | 8 +- packages/uikit/src/index.ts | 2 + 116 files changed, 2012 insertions(+), 2296 deletions(-) delete mode 100644 packages/mobile/src/core/BackupWords/BackupWords.interface.ts delete mode 100644 packages/mobile/src/core/BackupWords/BackupWords.style.ts delete mode 100644 packages/mobile/src/core/BackupWords/BackupWords.tsx delete mode 100644 packages/mobile/src/core/CheckSecretWords/CheckSecretWords.style.ts delete mode 100644 packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx delete mode 100644 packages/mobile/src/core/CreatePin/CreatePin.interface.ts delete mode 100644 packages/mobile/src/core/CreateWallet/CreateWallet.style.ts delete mode 100644 packages/mobile/src/core/CreateWallet/CreateWallet.tsx delete mode 100644 packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.styles.ts delete mode 100644 packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx delete mode 100644 packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/index.ts delete mode 100644 packages/mobile/src/core/SecretWords/SecretWords.style.ts delete mode 100644 packages/mobile/src/core/SecretWords/SecretWords.tsx delete mode 100644 packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts delete mode 100644 packages/mobile/src/core/SetupBiometry/SetupBiometry.style.ts delete mode 100644 packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx delete mode 100644 packages/mobile/src/core/SetupNotifications/SetupNotifications.style.ts delete mode 100644 packages/mobile/src/hooks/useExitAppBackHandler.ts delete mode 100644 packages/mobile/src/hooks/useNotificationStatus.ts delete mode 100644 packages/mobile/src/hooks/useNotifications.tsx delete mode 100644 packages/mobile/src/hooks/useNotificationsBadge.tsx create mode 100644 packages/mobile/src/hooks/useNotificationsSwitch.tsx delete mode 100644 packages/mobile/src/hooks/useShouldEnableNotifications.tsx create mode 100644 packages/mobile/src/modals/BackupWarningModal.tsx create mode 100644 packages/mobile/src/modals/index.ts create mode 100644 packages/mobile/src/navigation/MainStack/TabStack/BackupIndicator.tsx delete mode 100644 packages/mobile/src/navigation/MainStack/TabStack/NotificationsIndicator.tsx create mode 100644 packages/mobile/src/screens/BackupCheckPhraseScreen/BackupCheckPhraseScreen.tsx create mode 100644 packages/mobile/src/screens/BackupCheckPhraseScreen/index.ts create mode 100644 packages/mobile/src/screens/BackupPhraseScreen/BackupPhraseScreen.tsx create mode 100644 packages/mobile/src/screens/BackupPhraseScreen/index.ts create mode 100644 packages/mobile/src/screens/BackupScreen/BackupScreen.tsx create mode 100644 packages/mobile/src/screens/BackupScreen/index.ts create mode 100644 packages/mobile/src/tabs/Wallet/components/FinishSetupList.tsx create mode 100644 packages/mobile/src/wallet/Biometry.ts delete mode 100644 packages/shared/components/PasscodeKeyboad.tsx create mode 100644 packages/uikit/assets/icons/png/ic-faceid-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-faceid-android-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-fingerprint-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-fingerprint-android-28@4x.png create mode 100644 packages/uikit/assets/icons/svg/28/ic-faceid-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-faceid-android-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-fingerprint-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-fingerprint-android-28.svg create mode 100644 packages/uikit/src/components/BlockingLoader/BlockingLoader.ts create mode 100644 packages/uikit/src/components/BlockingLoader/BlockingLoaderView.tsx create mode 100644 packages/uikit/src/components/BlockingLoader/index.ts create mode 100644 packages/uikit/src/components/Switch.tsx create mode 100644 packages/uikit/src/containers/Screen/KeyboardAvoidingContainer.tsx create mode 100644 packages/uikit/src/containers/Screen/ScreenButtonContainer.tsx create mode 100644 packages/uikit/src/containers/Screen/ScreenButtonSpacer.tsx create mode 100644 packages/uikit/src/containers/Screen/ScreenHeaderIndent.tsx diff --git a/packages/mobile/src/core/AccessConfirmation/AccessConfirmation.tsx b/packages/mobile/src/core/AccessConfirmation/AccessConfirmation.tsx index 85938b4c6..16e1a0da7 100644 --- a/packages/mobile/src/core/AccessConfirmation/AccessConfirmation.tsx +++ b/packages/mobile/src/core/AccessConfirmation/AccessConfirmation.tsx @@ -38,7 +38,7 @@ export const AccessConfirmation: FC = () => { const [isBiometryFailed, setBiometryFailed] = useState(false); - const { biometryEnabled, enableBiometry } = useBiometrySettings(); + const biometry = useBiometrySettings(); const pinRef = useRef(null); const isUnlock = route.name === AppStackRouteNames.MainAccessConfirmation; @@ -105,7 +105,7 @@ export const AccessConfirmation: FC = () => { } if (isBiometryFailed) { - enableBiometry(pin).catch(null); + biometry.enableBiometry(pin).catch(null); } }, 500); } catch (e) { @@ -115,7 +115,7 @@ export const AccessConfirmation: FC = () => { }, 300); } }, - [dispatch, enableBiometry, isBiometryFailed, isUnlock, triggerError, wallet], + [biometry, dispatch, isBiometryFailed, isUnlock, triggerError, wallet], ); const handleBiometry = useCallback(async () => { @@ -139,9 +139,7 @@ export const AccessConfirmation: FC = () => { pinRef.current?.triggerSuccess(); setTimeout(async () => { - if (generatedVault) { - setLastEnteredPasscode(passcode); - } + setLastEnteredPasscode(passcode); // Lock screen if (isUnlock) { @@ -162,10 +160,10 @@ export const AccessConfirmation: FC = () => { } triggerError(); } - }, [dispatch, generatedVault, isUnlock, triggerError, wallet]); + }, [dispatch, isUnlock, triggerError, wallet]); useEffect(() => { - if (params.withoutBiometryOnOpen || !biometryEnabled) { + if (params.withoutBiometryOnOpen || !biometry.isEnabled || !biometry.isAvailable) { return; } @@ -235,7 +233,9 @@ export const AccessConfirmation: FC = () => { disabled={value.length === 4} onChange={handleKeyboard} value={value} - biometryEnabled={biometryEnabled && !isBiometryFailed && !generatedVault} + biometryEnabled={ + biometry.isAvailable && biometry.isEnabled && !isBiometryFailed + } onBiometryPress={handleBiometry} /> diff --git a/packages/mobile/src/core/App.tsx b/packages/mobile/src/core/App.tsx index 6066f1388..202f13b14 100644 --- a/packages/mobile/src/core/App.tsx +++ b/packages/mobile/src/core/App.tsx @@ -19,6 +19,7 @@ import { HideableAmountProvider } from '$core/HideableAmount/HideableAmountProvi import { queryClient } from '@tonkeeper/shared/queryClient'; import { WalletProvider } from '../context'; +import { BlockingLoaderView } from '@tonkeeper/uikit'; const TonThemeProvider = ({ children }) => { const accent = useSelector(accentSelector); @@ -56,6 +57,7 @@ export function App() { {/* */} + {isAndroid ? ( ; -} diff --git a/packages/mobile/src/core/BackupWords/BackupWords.style.ts b/packages/mobile/src/core/BackupWords/BackupWords.style.ts deleted file mode 100644 index 91c84c1d8..000000000 --- a/packages/mobile/src/core/BackupWords/BackupWords.style.ts +++ /dev/null @@ -1,29 +0,0 @@ -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Content = styled.View` - padding: 0 ${ns(16)}px ${ns(32)}px; -`; - -export const Words = styled.View` - flex-direction: row; - padding-top: ${ns(24)}px; - width: 100%; - justify-content: center; -`; - -export const WordsColumn = styled.View` - flex: 0 0 auto; - width: ${ns(122)}px; -`; - -export const WordsItem = styled.View` - flex-direction: row; - height: ${ns(24)}px; - margin-top: ${ns(8)}px; - align-items: center; -`; - -export const WordsItemNumberWrapper = styled.View` - width: ${ns(24)}px; -`; diff --git a/packages/mobile/src/core/BackupWords/BackupWords.tsx b/packages/mobile/src/core/BackupWords/BackupWords.tsx deleted file mode 100644 index 787592a2e..000000000 --- a/packages/mobile/src/core/BackupWords/BackupWords.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { FC, useMemo } from 'react'; -import { ScrollView } from 'react-native'; - -import * as CreateWalletStyle from '../CreateWallet/CreateWallet.style'; -import {NavBar, NavBarHelper, Text} from '$uikit'; -import { ns } from '$utils'; -import * as S from './BackupWords.style'; -import { BackupWordsProps } from './BackupWords.interface'; -import {WordsItemNumberWrapper} from "./BackupWords.style"; -import { t } from '@tonkeeper/shared/i18n'; - -export const BackupWords: FC = ({ route }) => { - - const mnemonic = route.params.mnemonic; - - const data = useMemo(() => { - const words = mnemonic.split(' '); - return { - firstColumn: words.splice(0, 12), - secondColumn: words, - }; - }, [mnemonic]); - - function renderColumn(words: string[], column: number) { - return words.map((word, i) => { - let number = i + 1; - if (column === 2) { - number += 12; - } - return ( - - - {number}. - - {word} - - ); - }); - } - - return ( - <> - - - - - - - {t('secret_words_title')} - - - - - {t('secret_words_caption')} - - - - {renderColumn(data.firstColumn, 1)} - {renderColumn(data.secondColumn, 2)} - - - - - ); -}; diff --git a/packages/mobile/src/core/ChangePin/ChangePin.tsx b/packages/mobile/src/core/ChangePin/ChangePin.tsx index 71f95b435..8b7cdfa6b 100644 --- a/packages/mobile/src/core/ChangePin/ChangePin.tsx +++ b/packages/mobile/src/core/ChangePin/ChangePin.tsx @@ -7,14 +7,12 @@ import { t } from '@tonkeeper/shared/i18n'; import { goBack } from '$navigation/imperative'; import { vault } from '$wallet'; import { useBiometrySettings } from '@tonkeeper/shared/hooks'; -import * as LocalAuthentication from 'expo-local-authentication'; -import { detectBiometryType } from '$utils'; import { MainStackRouteNames } from '$navigation'; import { useNavigation } from '@tonkeeper/router'; export const ChangePin: FC = () => { const [oldPasscode, setOldPasscode] = useState(null); - const { biometryEnabled, disableBiometry } = useBiometrySettings(); + const biometry = useBiometrySettings(); const nav = useNavigation(); const handleCreated = useCallback( @@ -28,12 +26,14 @@ export const ChangePin: FC = () => { Toast.success(t('passcode_changed')); - if (biometryEnabled) { - await disableBiometry(); - const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); - const biometryType = detectBiometryType(types); + if (biometry.isEnabled) { + await biometry.disableBiometry(); - nav.replace(MainStackRouteNames.ChangePinBiometry, { biometryType, passcode }); + if (biometry.isAvailable) { + nav.replace(MainStackRouteNames.ChangePinBiometry, { passcode }); + } else { + nav.goBack(); + } } else { nav.goBack(); } @@ -42,7 +42,7 @@ export const ChangePin: FC = () => { goBack(); } }, - [biometryEnabled, disableBiometry, nav, oldPasscode], + [biometry, nav, oldPasscode], ); return ( diff --git a/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.style.ts b/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.style.ts deleted file mode 100644 index ec07fdb9f..000000000 --- a/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.style.ts +++ /dev/null @@ -1,42 +0,0 @@ -import styled from '$styled'; -import { isAndroid, nfs, ns } from '$utils'; - -export const Inputs = styled.View` - padding-vertical: ${ns(32)}px; -`; - -export const InputWrap = styled.View` - margin-top: ${ns(16)}px; - position: relative; -`; - -export const InputNumber = styled.TextInput.attrs({ - editable: false, - scrollEnabled: false, - allowFontScaling: false, -})` - font-family: ${({ theme }) => theme.font.regular}; - font-size: ${ns(16)}px; - color: ${({ theme }) => theme.colors.foregroundSecondary}; - margin: 0; - text-align: right; - border-width: 0; - width: ${ns(28)}px; - left: ${ns(16)}px; - top: 0; - padding-left: 0; - padding-right: 0; - padding-top: ${ns(isAndroid ? 16.5 : 18.5)}px; - padding-bottom: ${ns(isAndroid ? 14.5 : 17.5)}px; - ${isAndroid ? 'text-align-vertical: top;' : ''} - z-index: 3; - position: absolute; -`; - -export const HeaderTitle = styled.Text.attrs({ allowFontScaling: false })` - font-family: ${({ theme }) => theme.font.semiBold}; - color: ${({ theme }) => theme.colors.foregroundPrimary}; - font-size: ${nfs(24)}px; - line-height: ${nfs(32)}px; - text-align: center; -`; diff --git a/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx b/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx deleted file mode 100644 index 2287080c9..000000000 --- a/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; -import * as _ from 'lodash'; -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { useDispatch, useSelector } from 'react-redux'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { TapGestureHandler, TextInput } from 'react-native-gesture-handler'; - -import * as CreateWalletStyle from '../CreateWallet/CreateWallet.style'; -import { Button, Input, NavBar, NavBarHelper, Text } from '$uikit'; -import { ns, parseLockupConfig } from '$utils'; -import { walletActions, walletGeneratedVaultSelector } from '$store/wallet'; -import * as S from './CheckSecretWords.style'; -import { css } from '$styled'; -import { NavBarHeight } from '$shared/constants'; -import { openCreatePin, openSetupNotifications, openSetupWalletDone } from '$navigation'; -import { Toast } from '$store'; -import { t } from '@tonkeeper/shared/i18n'; -import { tk } from '$wallet'; -import { popToTop } from '$navigation/imperative'; -import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; -import { getLastEnteredPasscode } from '$store/wallet/sagas'; - -export const CheckSecretWords: FC = () => { - const dispatch = useDispatch(); - const generatedVault = useSelector(walletGeneratedVaultSelector); - const word2Ref = useRef(null); - const word3Ref = useRef(null); - const { top: topInset, bottom: bottomInset } = useSafeAreaInsets(); - - const [word1, setWord1] = useState(''); - const [word2, setWord2] = useState(''); - const [word3, setWord3] = useState(''); - const [failed, setFailed] = useState<{ [index: number]: boolean }>({}); - const [isConfigInputShown, setConfigInputShown] = useState(false); - const [config, setConfig] = useState(''); - const [loading, setLoading] = useState(false); - - const unlockVault = useUnlockVault(); - - const words = useMemo(() => { - return generatedVault!.mnemonic.split(' '); - }, [generatedVault]); - - const data = useMemo(() => { - let result = []; - const countInGroup = 8; - for (let i = 0; i < 3; i++) { - result.push(_.random(1, 8, false) + i * countInGroup); - } - - return result; - }, []); - - const validateInputWord = useCallback( - (index: number, text: string, failedCopy?: { [index: number]: boolean }) => { - if (!failedCopy) { - failedCopy = { ...failed }; - } - - failedCopy[index] = text.length > 0 && words[data[index] - 1] !== text; - setFailed(failedCopy); - - return failedCopy; - }, - [failed, words, data], - ); - - const handleWord1Submit = useCallback(() => { - word2Ref.current?.focus(); - }, [word2Ref]); - - const handleWord2Submit = useCallback(() => { - word3Ref.current?.focus(); - }, [word3Ref]); - - const isCanSend = useMemo(() => { - // if (words[data[0] - 1] !== word1) { - // return false; - // } - // - // if (words[data[1] - 1] !== word2) { - // return false; - // } - // - // if (words[data[2] - 1] !== word3) { - // return false; - // } - - return word1.length > 0 && word2.length > 0 && word3.length > 0; - }, [word1, word2, word3]); - - const handleChangeWord1 = useCallback( - (value) => { - setWord1(value.trim()); - - if (failed[0]) { - validateInputWord(0, value.trim()); - } - }, - [setWord1, validateInputWord, failed], - ); - - const handleChangeWord2 = useCallback( - (value) => { - setWord2(value.trim()); - - if (failed[1]) { - validateInputWord(1, value.trim()); - } - }, - [setWord2, validateInputWord, failed], - ); - - const handleChangeWord3 = useCallback( - (value) => { - setWord3(value.trim()); - - if (failed[2]) { - validateInputWord(2, value.trim()); - } - }, - [setWord3, validateInputWord, failed], - ); - - const handleSubmit = useCallback(async () => { - if (!isCanSend || loading) { - return; - } - - let failedCopy = { ...failed }; - failedCopy = validateInputWord(0, word1, failedCopy); - failedCopy = validateInputWord(1, word2, failedCopy); - failedCopy = validateInputWord(2, word3, failedCopy); - - const hasFailed = Object.values(failedCopy).filter((item) => !!item).length > 0; - if (hasFailed) { - Toast.fail(t('import_wallet_wrong_words_err')); - return; - } - - let configParsed: any = null; - if (isConfigInputShown && config) { - try { - configParsed = parseLockupConfig(config); - } catch (e) { - Toast.fail(`Lockup: ${e.message}`); - return; - } - - generatedVault!.setConfig(configParsed); - } - - if (tk.walletForUnlock) { - setLoading(true); - try { - await unlockVault(); - const pin = getLastEnteredPasscode(); - - const isNotificationsDenied = await tk.wallet.notifications.getIsDenied(); - - dispatch( - walletActions.createWallet({ - pin, - onDone: (identifiers) => { - if (isNotificationsDenied) { - openSetupWalletDone(identifiers); - } else { - openSetupNotifications(identifiers); - } - setLoading(false); - }, - onFail: () => { - setLoading(false); - }, - }), - ); - } catch { - setLoading(false); - } - } else { - openCreatePin(); - } - }, [ - loading, - isCanSend, - failed, - validateInputWord, - word1, - word2, - word3, - isConfigInputShown, - config, - generatedVault, - unlockVault, - dispatch, - ]); - - const handleBlur = useCallback( - (index: number) => () => { - let text = ''; - if (index === 0) { - text = word1; - } else if (index === 1) { - text = word2; - } else { - text = word3; - } - - validateInputWord(index, text); - }, - [validateInputWord, word1, word2, word3], - ); - - const handleShowConfigInput = useCallback(() => { - setConfigInputShown(true); - }, []); - - const handleConfigChange = useCallback((text) => { - setConfig(text); - }, []); - - function renderContent() { - return ( - - - - {t('check_words_title')} - - - - {t('check_words_caption', { - wordNum1: data[0], - wordNum2: data[1], - wordNum3: data[2], - })} - - - - {isConfigInputShown && ( - - )} - - {data[0]}: - - - - {data[1]}: - - - - {data[2]}: - - - - - - ); - } - - return ( - <> - - {renderContent()} - - ); -}; diff --git a/packages/mobile/src/core/CreatePin/CreatePin.interface.ts b/packages/mobile/src/core/CreatePin/CreatePin.interface.ts deleted file mode 100644 index ae53ce39f..000000000 --- a/packages/mobile/src/core/CreatePin/CreatePin.interface.ts +++ /dev/null @@ -1 +0,0 @@ -export interface CreatePinProps {} diff --git a/packages/mobile/src/core/CreatePin/CreatePin.tsx b/packages/mobile/src/core/CreatePin/CreatePin.tsx index 58fa3ac2f..07c69aa1f 100644 --- a/packages/mobile/src/core/CreatePin/CreatePin.tsx +++ b/packages/mobile/src/core/CreatePin/CreatePin.tsx @@ -1,63 +1,51 @@ import React, { FC, useCallback } from 'react'; -import * as LocalAuthentication from 'expo-local-authentication'; import { useDispatch } from 'react-redux'; -import * as SecureStore from 'expo-secure-store'; -import { CreatePinProps } from './CreatePin.interface'; import * as S from '../AccessConfirmation/AccessConfirmation.style'; import { NavBar } from '$uikit'; -import { detectBiometryType } from '$utils'; -import { debugLog } from '$utils/debugLog'; -import { openSetupBiometry, openSetupWalletDone } from '$navigation'; +import { openSetupNotifications, openSetupWalletDone } from '$navigation'; import { walletActions } from '$store/wallet'; import { CreatePinForm } from '$shared/components'; import { tk } from '$wallet'; import { popToTop } from '$navigation/imperative'; +import { useParams } from '@tonkeeper/router/src/imperative'; +import { BlockingLoader } from '@tonkeeper/uikit'; -export const CreatePin: FC = () => { +export const CreatePin: FC = () => { + const params = useParams<{ isImport?: boolean }>(); const dispatch = useDispatch(); - const doCreateWallet = useCallback( - (pin: string) => { + const isImport = !!params.isImport; + + const handlePinCreated = useCallback( + async (pin: string) => { + BlockingLoader.show(); dispatch( walletActions.createWallet({ pin, onDone: (identifiers) => { - openSetupWalletDone(identifiers); + if (isImport) { + tk.saveLastBackupTimestampAll(identifiers); + } + if (tk.wallet.notifications.isAvailable) { + openSetupNotifications(identifiers); + } else { + openSetupWalletDone(identifiers); + } + BlockingLoader.hide(); + }, + onFail: () => { + BlockingLoader.hide(); }, - onFail: () => {}, }), ); }, - [dispatch], - ); - - const handlePinCreated = useCallback( - async (pin: string) => { - try { - const [types, isProtected] = await Promise.all([ - LocalAuthentication.supportedAuthenticationTypesAsync(), - SecureStore.isAvailableAsync(), - ]); - - const biometryType = detectBiometryType(types); - if (biometryType && isProtected) { - openSetupBiometry(pin, biometryType); - } else { - doCreateWallet(pin); - } - } catch (err) { - console.log('ERR1', err); - debugLog('supportedAuthenticationTypesAsync', err.message); - doCreateWallet(pin); - } - }, - [doCreateWallet], + [dispatch, isImport], ); return ( - + ); diff --git a/packages/mobile/src/core/CreateWallet/CreateWallet.style.ts b/packages/mobile/src/core/CreateWallet/CreateWallet.style.ts deleted file mode 100644 index 4302f6160..000000000 --- a/packages/mobile/src/core/CreateWallet/CreateWallet.style.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Animated from 'react-native-reanimated'; -import LottieView from 'lottie-react-native'; - -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled.View` - flex: 1; -`; - -export const Step = styled(Animated.View)` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -`; - -export const Content = styled.View` - flex: 1; - align-items: center; - justify-content: center; - padding: ${ns(32)}px; -`; - -export const Icon = styled(Animated.View)` - width: ${ns(84)}px; - height: ${ns(84)}px; -`; - -export const TitleWrapper = styled.View` - margin-top: ${ns(16)}px; -`; - -export const CaptionWrapper = styled.View` - margin-top: ${ns(4)}px; -`; - -export const ButtonWrap = styled.View` - padding-horizontal: ${ns(32)}px; - height: ${ns(56)}px; -`; - -export const LottieIcon = styled(LottieView)` - width: ${ns(128)}px; - height: ${ns(128)}px; -`; diff --git a/packages/mobile/src/core/CreateWallet/CreateWallet.tsx b/packages/mobile/src/core/CreateWallet/CreateWallet.tsx deleted file mode 100644 index 222ad0422..000000000 --- a/packages/mobile/src/core/CreateWallet/CreateWallet.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - Easing, - interpolate, - runOnJS, - useAnimatedStyle, - useSharedValue, - withDelay, - withTiming, -} from 'react-native-reanimated'; -import LottieView from 'lottie-react-native'; - -import * as S from './CreateWallet.style'; -import { walletActions, walletGeneratedVaultSelector } from '$store/wallet'; -import { Text } from '$uikit/Text/Text'; -import { Button } from '$uikit/Button/Button'; -import { NavBar } from '$uikit/NavBar/NavBar'; -import { deviceWidth, ns, triggerNotificationSuccess } from '$utils'; -import { openSecretWords } from '$navigation'; -import { t } from '@tonkeeper/shared/i18n'; - -export const CreateWallet: FC = () => { - const dispatch = useDispatch(); - const { bottom } = useSafeAreaInsets(); - const generatedVault = useSelector(walletGeneratedVaultSelector); - const [step, setStep] = useState(1); - const iconRef = useRef(null); - const checkIconRef = useRef(null); - const slideAnimation = useSharedValue(0); - - useEffect(() => { - const timer = setTimeout(() => { - dispatch(walletActions.generateVault()); - }, 1000); - return () => clearTimeout(timer); - }, []); - - const handleDoneStep1Anim = useCallback(() => { - setStep(2); - checkIconRef.current?.play(); - triggerNotificationSuccess(); - }, [checkIconRef]); - - const handleDoneStep3Anim = useCallback(() => { - iconRef.current?.play(); - }, []); - - useEffect(() => { - if (step === 1) { - if (generatedVault) { - slideAnimation.value = withTiming( - 1, - { - duration: 350, - easing: Easing.inOut(Easing.ease), - }, - (isFinished) => { - if (isFinished) { - runOnJS(handleDoneStep1Anim)(); - } - }, - ); - } - } else if (step === 2) { - slideAnimation.value = withDelay( - 2500, - withTiming( - 2, - { - duration: 350, - easing: Easing.inOut(Easing.ease), - }, - (isFinished) => { - if (isFinished) { - runOnJS(handleDoneStep3Anim)(); - } - }, - ), - ); - } - }, [generatedVault, step]); - - useEffect(() => { - let timer: any = 0; - if (step === 2) { - timer = setTimeout(() => { - setStep(3); - }, 3000); - } - - return () => clearTimeout(timer); - }, [step]); - - const handleContinue = useCallback(() => { - openSecretWords(); - }, []); - - const step1Style = useAnimatedStyle(() => ({ - opacity: interpolate(Math.min(slideAnimation.value, 1), [0, 1], [1, 0]), - transform: [ - { - translateX: interpolate( - Math.min(slideAnimation.value, 1), - [0, 1], - [0, -deviceWidth], - ), - }, - ], - })); - - const step2Style = useAnimatedStyle(() => ({ - opacity: interpolate(Math.min(slideAnimation.value, 2), [0, 1, 2], [0, 1, 0]), - transform: [ - { - translateX: interpolate( - Math.min(slideAnimation.value, 2), - [0, 1, 2], - [deviceWidth, 0, -deviceWidth], - ), - }, - ], - })); - - const step3Style = useAnimatedStyle(() => { - const value = Math.min(Math.max(1, slideAnimation.value), 2); - return { - opacity: interpolate(value, [1, 2], [0, 1]), - transform: [ - { - translateX: interpolate(value, [1, 2], [deviceWidth, 0]), - }, - ], - }; - }); - - return ( - - - - - - - - {t('create_wallet_generating')} - - - - - - - - - - {t('create_wallet_generated')} - - - - - - - - - - {t('create_wallet_title')} - - - - - {t('create_wallet_caption')} - - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/DeleteAccountDone/DeleteAccountDone.tsx b/packages/mobile/src/core/DeleteAccountDone/DeleteAccountDone.tsx index 57bbedcc5..98f0d95bb 100644 --- a/packages/mobile/src/core/DeleteAccountDone/DeleteAccountDone.tsx +++ b/packages/mobile/src/core/DeleteAccountDone/DeleteAccountDone.tsx @@ -2,13 +2,11 @@ import React, { useCallback, useMemo, useRef } from 'react'; import LottieView from 'lottie-react-native'; import { Text } from '$uikit'; -import * as CreateWalletStyle from '$core/CreateWallet/CreateWallet.style'; -import { goBack, popToTop } from '$navigation/imperative'; import * as S from './DeleteAccountDone.style'; -import { ns } from '$utils'; import { t } from '@tonkeeper/shared/i18n'; import { useDispatch } from 'react-redux'; import { walletActions } from '$store/wallet'; +import { Steezy, View } from '@tonkeeper/uikit'; export const DeleteAccountDone: React.FC = () => { const dispatch = useDispatch(); @@ -28,7 +26,7 @@ export const DeleteAccountDone: React.FC = () => { }, [dispatch]); return ( - + { {t('account_deleted')} - + ); }; + +const styles = Steezy.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 32, + marginTop: -16, + }, +}); diff --git a/packages/mobile/src/core/ImportWallet/ImportWallet.tsx b/packages/mobile/src/core/ImportWallet/ImportWallet.tsx index 4384bcd0f..588dd9151 100644 --- a/packages/mobile/src/core/ImportWallet/ImportWallet.tsx +++ b/packages/mobile/src/core/ImportWallet/ImportWallet.tsx @@ -11,6 +11,8 @@ import { import { useNavigation } from '@tonkeeper/router'; import { useImportWallet } from '$hooks/useImportWallet'; import { tk } from '$wallet'; +import { ImportWalletInfo } from '$wallet/WalletTypes'; +import { DEFAULT_WALLET_VERSION } from '$wallet/constants'; export const ImportWallet: FC<{ route: RouteProp; @@ -24,9 +26,14 @@ export const ImportWallet: FC<{ const handleWordsFilled = useCallback( async (mnemonic: string, lockupConfig: any, onEnd: () => void) => { try { - const walletsInfo = await tk.getWalletsInfo(mnemonic, isTestnet); + let walletsInfo: ImportWalletInfo[] | null = null; - const shouldChooseWallets = !lockupConfig && walletsInfo.length > 1; + try { + walletsInfo = await tk.getWalletsInfo(mnemonic, isTestnet); + } catch {} + + const shouldChooseWallets = + !lockupConfig && walletsInfo && walletsInfo.length > 1; if (shouldChooseWallets) { nav.navigate(ImportWalletStackRouteNames.ChooseWallets, { @@ -39,7 +46,9 @@ export const ImportWallet: FC<{ return; } - const versions = walletsInfo.map((item) => item.version); + const versions = walletsInfo + ? walletsInfo.map((item) => item.version) + : [DEFAULT_WALLET_VERSION]; await doImportWallet(mnemonic, lockupConfig, versions, isTestnet); onEnd(); diff --git a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.styles.ts b/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.styles.ts deleted file mode 100644 index 8ce7db68a..000000000 --- a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.styles.ts +++ /dev/null @@ -1,23 +0,0 @@ -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled.View` - margin-top: ${ns(32)}px; - padding-horizontal: ${ns(16)}px; - align-items: center; - justify-content: center; -`; - -export const Content = styled.View` - margin-bottom: ${ns(32)}px; -`; - -export const IconWrap = styled.View` - margin-bottom: ${ns(16)}px; - align-items: center; - justify-content: center; -`; - -export const Footer = styled.View` - padding-horizontal: ${ns(16)}px; -`; diff --git a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx b/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx deleted file mode 100644 index fa696c4af..000000000 --- a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/ReminderEnableNotificationsModal.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { t } from '@tonkeeper/shared/i18n'; -import { Button, Icon, Text } from '$uikit'; -import * as S from './ReminderEnableNotificationsModal.styles'; -import { useNotifications } from '$hooks/useNotifications'; -import { SheetActions, useNavigation } from '@tonkeeper/router'; -import { Modal, View } from '@tonkeeper/uikit'; -import { push } from '$navigation/imperative'; - -export const ReminderEnableNotificationsModal = () => { - const nav = useNavigation(); - const notifications = useNotifications(); - - const handleEnable = React.useCallback(async () => { - const isSubscribe = await notifications.subscribe(); - if (isSubscribe) { - nav.goBack(); - } - }, []); - - const handleLater = React.useCallback(async () => { - nav.goBack(); - }, []); - - return ( - - - - - - - - - - - {t('reminder_notifications_title')} - - - {t('reminder_notifications_caption')} - - - - - - - - - - - ); -}; - -export async function openReminderEnableNotificationsModal() { - push('SheetsProvider', { - $$action: SheetActions.ADD, - component: ReminderEnableNotificationsModal, - params: {}, - path: 'MARKETPLACES', - }); -} diff --git a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/index.ts b/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/index.ts deleted file mode 100644 index 378289712..000000000 --- a/packages/mobile/src/core/ModalContainer/ReminderEnableNotificationsModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ReminderEnableNotificationsModal'; diff --git a/packages/mobile/src/core/Notifications/Notifications.tsx b/packages/mobile/src/core/Notifications/Notifications.tsx index a4c289ae6..54676f1e7 100644 --- a/packages/mobile/src/core/Notifications/Notifications.tsx +++ b/packages/mobile/src/core/Notifications/Notifications.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React from 'react'; import { InternalNotification, Screen, @@ -9,88 +9,19 @@ import { View, } from '$uikit'; import { ns } from '$utils'; -import { debugLog } from '$utils/debugLog'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -import { Linking } from 'react-native'; import { CellSection } from '$shared/components'; -import { NotificationsStatus, useNotificationStatus } from '$hooks/useNotificationStatus'; -import messaging from '@react-native-firebase/messaging'; -import { useNotifications } from '$hooks/useNotifications'; import { t } from '@tonkeeper/shared/i18n'; -import { useNotificationsBadge } from '$hooks/useNotificationsBadge'; -import { Toast, ToastSize, useConnectedAppsList, useConnectedAppsStore } from '$store'; +import { useConnectedAppsList } from '$store'; import { Steezy } from '$styles'; -import { getChainName } from '$shared/dynamicConfig'; import { SwitchDAppNotifications } from '$core/Notifications/SwitchDAppNotifications'; -import { useWallet } from '@tonkeeper/shared/hooks'; +import { useNotificationsSwitch } from '$hooks/useNotificationsSwitch'; export const Notifications: React.FC = () => { - const wallet = useWallet(); - const handleOpenSettings = useCallback(() => Linking.openSettings(), []); - const notifications = useNotifications(); const tabBarHeight = useBottomTabBarHeight(); - const isSwitchFrozen = useRef(false); - const notificationStatus = useNotificationStatus(); - const notificationsBadge = useNotificationsBadge(); - const shouldEnableNotifications = notificationStatus === NotificationsStatus.DENIED; - const updateNotificationsSubscription = useConnectedAppsStore( - (state) => state.actions.updateNotificationsSubscription, - ); - const [isSubscribeNotifications, setIsSubscribeNotifications] = React.useState(false); - - React.useEffect(() => { - const init = async () => { - const status = await messaging().hasPermission(); - - const isGranted = - status === NotificationsStatus.AUTHORIZED || - status === NotificationsStatus.PROVISIONAL; - - setIsSubscribeNotifications(isGranted && notifications.isSubscribed); - }; - - init(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - React.useEffect(() => { - if (notificationsBadge.isVisible) { - notificationsBadge.hide(); - } - }, [notificationsBadge, notificationsBadge.isVisible]); - - const handleToggleNotifications = React.useCallback( - async (value: boolean) => { - if (isSwitchFrozen.current) { - return; - } - - try { - isSwitchFrozen.current = true; - setIsSubscribeNotifications(value); - - const isSuccess = value - ? await notifications.subscribe() - : await notifications.unsubscribe(); - - updateNotificationsSubscription(getChainName(), wallet.address.ton.friendly); - - if (!isSuccess) { - // Revert - setIsSubscribeNotifications(!value); - } - } catch (err) { - Toast.fail(t('notifications_not_supported'), { size: ToastSize.Small }); - debugLog('[NotificationsSettings]', err); - setIsSubscribeNotifications(!value); // Revert - } finally { - isSwitchFrozen.current = false; - } - }, - [notifications, updateNotificationsSubscription, wallet], - ); - const connectedApps = useConnectedAppsList(); + const { isSubscribed, isDenied, openSettings, toggleNotifications } = + useNotificationsSwitch(); return ( @@ -101,14 +32,14 @@ export const Notifications: React.FC = () => { paddingBottom: tabBarHeight, }} > - {shouldEnableNotifications && ( + {isDenied && ( )} @@ -116,9 +47,9 @@ export const Notifications: React.FC = () => { {connectedApps.length ? ( diff --git a/packages/mobile/src/core/SecretWords/SecretWords.style.ts b/packages/mobile/src/core/SecretWords/SecretWords.style.ts deleted file mode 100644 index 4d9ff759a..000000000 --- a/packages/mobile/src/core/SecretWords/SecretWords.style.ts +++ /dev/null @@ -1,34 +0,0 @@ -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Content = styled.View` - padding: 0 0 ${ns(32)}px; - flex: 1; -`; - -export const Words = styled.View` - flex-direction: row; - padding-top: ${ns(24)}px; - width: 100%; - justify-content: space-between; -`; - -export const WordsColumn = styled.View` - flex: 0 0 auto; - width: ${ns(122)}px; -`; - -export const WordsItem = styled.View` - flex-direction: row; - height: ${ns(24)}px; - margin-top: ${ns(8)}px; - align-items: center; -`; - -export const WordsItemNumberWrapper = styled.View` - width: ${ns(24)}px; -`; - -export const WordsItemValueWrapper = styled.View` - margin-left: ${ns(4)}px; -`; diff --git a/packages/mobile/src/core/SecretWords/SecretWords.tsx b/packages/mobile/src/core/SecretWords/SecretWords.tsx deleted file mode 100644 index 1a38dca79..000000000 --- a/packages/mobile/src/core/SecretWords/SecretWords.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { FC, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { ScrollView, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import * as CreateWalletStyle from '../CreateWallet/CreateWallet.style'; -import { Button, NavBar, NavBarHelper, Text } from '$uikit'; -import { ns } from '$utils'; -import { walletGeneratedVaultSelector } from '$store/wallet'; -import * as S from './SecretWords.style'; -import { t } from '@tonkeeper/shared/i18n'; -import { openCheckSecretWords } from '$navigation'; -import { tk } from '$wallet'; -import { popToTop } from '$navigation/imperative'; - -export const SecretWords: FC = () => { - const generatedVault = useSelector(walletGeneratedVaultSelector); - const { bottom: bottomInset } = useSafeAreaInsets(); - - const data = useMemo(() => { - const words = generatedVault!.mnemonic.split(' '); - return { - firstColumn: words.splice(0, 12), - secondColumn: words, - }; - }, [generatedVault]); - - function renderColumn(words: string[], column: number) { - return words.map((word, i) => { - let number = i + 1; - if (column === 2) { - number += 12; - } - return ( - - - - {number}. - - - - {word} - - - ); - }); - } - - const handleContinue = useCallback(() => { - openCheckSecretWords(); - }, []); - - return ( - - - - - - - {t('secret_words_title')} - - - - {t('secret_words_caption')} - - - - {renderColumn(data.firstColumn, 1)} - {renderColumn(data.secondColumn, 2)} - - - - - - - - ); -}; diff --git a/packages/mobile/src/core/Security/Security.tsx b/packages/mobile/src/core/Security/Security.tsx index 4fb0d457f..922d72fa9 100644 --- a/packages/mobile/src/core/Security/Security.tsx +++ b/packages/mobile/src/core/Security/Security.tsx @@ -1,41 +1,26 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { FC, useCallback } from 'react'; import Animated from 'react-native-reanimated'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import Clipboard from '@react-native-community/clipboard'; -import * as LocalAuthentication from 'expo-local-authentication'; -import { Switch } from 'react-native'; import * as S from './Security.style'; import { NavBar, ScrollHandler, Text } from '$uikit'; import { CellSection, CellSectionItem } from '$shared/components'; -import { walletActions } from '$store/wallet'; import { MainStackRouteNames, openChangePin } from '$navigation'; -import { detectBiometryType, ns, platform, triggerImpactLight } from '$utils'; +import { getBiometryName, ns } from '$utils'; import { Toast } from '$store'; import { t } from '@tonkeeper/shared/i18n'; import { useBiometrySettings, useWallet } from '@tonkeeper/shared/hooks'; import { useNavigation } from '@tonkeeper/router'; import { vault } from '$wallet'; +import { Haptics, Switch } from '@tonkeeper/uikit'; export const Security: FC = () => { - const dispatch = useDispatch(); const tabBarHeight = useBottomTabBarHeight(); const wallet = useWallet(); const nav = useNavigation(); - const { biometryEnabled } = useBiometrySettings(); - - const [isBiometryEnabled, setBiometryEnabled] = useState(biometryEnabled); - const [biometryAvail, setBiometryAvail] = useState(-1); - const isTouchId = - biometryAvail !== LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION; - - useEffect(() => { - LocalAuthentication.supportedAuthenticationTypesAsync().then((types) => - setBiometryAvail(detectBiometryType(types) || -1), - ); - }, []); + const biometry = useBiometrySettings(); const handleCopyLockupConfig = useCallback(() => { try { @@ -48,33 +33,21 @@ export const Security: FC = () => { const handleBiometry = useCallback( (triggerHaptic: boolean) => () => { - const newValue = !isBiometryEnabled; - setBiometryEnabled(newValue); - if (triggerHaptic) { - triggerImpactLight(); + Haptics.impactLight(); } - dispatch( - walletActions.toggleBiometry({ - isEnabled: newValue, - onFail: () => setBiometryEnabled(!newValue), - }), - ); + biometry.toggleBiometry(); }, - [dispatch, isBiometryEnabled], + [biometry], ); - useEffect(() => { - setBiometryEnabled(biometryEnabled); - }, [biometryEnabled]); - const handleChangePasscode = useCallback(() => { openChangePin(); }, []); const handleResetPasscode = useCallback(async () => { - if (!biometryEnabled) { + if (!biometry.isEnabled) { return; } @@ -82,10 +55,10 @@ export const Security: FC = () => { const passcode = await vault.exportPasscodeWithBiometry(); nav.navigate(MainStackRouteNames.ResetPin, { passcode }); } catch {} - }, [biometryEnabled, nav]); + }, [biometry.isEnabled, nav]); function renderBiometryToggler() { - if (biometryAvail === -1) { + if (!biometry.isAvailable) { return null; } @@ -95,23 +68,17 @@ export const Security: FC = () => { + } > {t('security_use_biometry_switch', { - biometryType: isTouchId - ? t(`platform.${platform}.fingerprint`) - : t(`platform.${platform}.face_recognition`), + biometryType: getBiometryName(biometry.type, { accusative: true }), })} - {t('security_use_biometry_tip', { - biometryType: isTouchId - ? t(`platform.${platform}.fingerprint`) - : t(`platform.${platform}.face_recognition`), - })} + {t('security_use_biometry_tip')} @@ -135,7 +102,7 @@ export const Security: FC = () => { {t('security_change_passcode')} - {biometryEnabled ? ( + {biometry.isEnabled ? ( theme.colors.accentNegative}; width: ${ns(8)}px; height: ${ns(8)}px; diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index 5812d2233..40db782d9 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -16,6 +16,7 @@ import { List } from '@tonkeeper/uikit'; import { AppStackRouteNames, MainStackRouteNames, + SettingsStackRouteNames, openDeleteAccountDone, openDevMenu, openLegalDocuments, @@ -36,7 +37,6 @@ import { import { checkIsTonDiamondsNFT, hNs, ns, throttle } from '$utils'; import { LargeNavBarInteractiveDistance } from '$uikit/LargeNavBar/LargeNavBar'; import { CellSectionItem } from '$shared/components'; -import { useNotificationsBadge } from '$hooks/useNotificationsBadge'; import { useFlags } from '$utils/flags'; import { SearchEngine, useBrowserStore, useNotificationsStore } from '$store'; import AnimatedLottieView from 'lottie-react-native'; @@ -46,7 +46,12 @@ import { trackEvent } from '$utils/stats'; import { openAppearance } from '$core/ModalContainer/AppearanceModal'; import { config } from '$config'; import { shouldShowNotifications } from '$store/zustand/notifications/selectors'; -import { useNftsState, useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; +import { + useNftsState, + useWallet, + useWalletCurrency, + useWalletStatus, +} from '@tonkeeper/shared/hooks'; import { tk } from '$wallet'; import { mapNewNftToOldNftData } from '$utils/mapNewNftToOldNftData'; import { WalletListItem } from '@tonkeeper/shared/components'; @@ -67,7 +72,6 @@ export const Settings: FC = () => { const nav = useNavigation(); const tabBarHeight = useBottomTabBarHeight(); - const notificationsBadge = useNotificationsBadge(); const fiatCurrency = useWalletCurrency(); const dispatch = useDispatch(); @@ -78,6 +82,8 @@ export const Settings: FC = () => { const shouldShowTokensButton = useShouldShowTokensButton(); const showNotifications = useNotificationsStore(shouldShowNotifications); + const { lastBackupAt } = useWalletStatus(); + const isBatteryVisible = !!wallet && !wallet.isWatchOnly && !config.get('disable_battery'); @@ -179,8 +185,8 @@ export const Settings: FC = () => { }, []); const handleBackupSettings = useCallback(() => { - dispatch(walletActions.backupWallet()); - }, [dispatch]); + nav.navigate(SettingsStackRouteNames.Backup); + }, [nav]); const handleAppearance = useCallback(() => { openAppearance(); @@ -216,17 +222,17 @@ export const Settings: FC = () => { [nav], ); - const notificationIndicator = React.useMemo(() => { - if (notificationsBadge.isVisible) { - return ( - - - - ); + const backupIndicator = React.useMemo(() => { + if (lastBackupAt !== null) { + return null; } - return null; - }, [notificationsBadge.isVisible]); + return ( + + + + ); + }, [lastBackupAt]); const accountNfts = useNftsState((s) => s.accountNfts); @@ -279,7 +285,14 @@ export const Settings: FC = () => { name={'ic-key-28'} /> } - title={t('settings_backup_seed')} + title={ + + + {t('settings_backup_seed')} + + {backupIndicator} + + } onPress={handleBackupSettings} /> )} @@ -312,14 +325,7 @@ export const Settings: FC = () => { {!!wallet && showNotifications && ( } - title={ - - - {t('settings_notifications')} - - {notificationIndicator} - - } + title={t('settings_notifications')} onPress={handleNotifications} /> )} @@ -559,11 +565,11 @@ const styles = Steezy.create({ marginTop: -2, marginBottom: -2, }, - notificationsTextContainer: { + backupTextContainer: { alignItems: 'center', flexDirection: 'row', }, - notificationIndicatorContainer: { + backupIndicatorContainer: { height: 24, paddingTop: 9.5, paddingBottom: 6.5, diff --git a/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts b/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts deleted file mode 100644 index 768549066..000000000 --- a/packages/mobile/src/core/SetupBiometry/SetupBiometry.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RouteProp } from '@react-navigation/native'; -import { - CreateWalletStackParamList, - CreateWalletStackRouteNames, -} from '$navigation/CreateWalletStack/types'; - -export interface SetupBiometryProps { - route: RouteProp; -} diff --git a/packages/mobile/src/core/SetupBiometry/SetupBiometry.style.ts b/packages/mobile/src/core/SetupBiometry/SetupBiometry.style.ts deleted file mode 100644 index 8ce8da7b5..000000000 --- a/packages/mobile/src/core/SetupBiometry/SetupBiometry.style.ts +++ /dev/null @@ -1,28 +0,0 @@ -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled.View` - flex: 1; - padding: ${ns(32)}px; - padding-top: 0; -`; - -export const Content = styled.View` - flex: 1; - align-items: center; - justify-content: center; -`; - -export const IconWrap = styled.View` - width: ${ns(160)}px; - height: ${ns(160)}px; -`; - -export const CaptionWrapper = styled.View` - margin-top: ${ns(4)}px; -`; - -export const Footer = styled.View` - flex: 0 0 auto; - padding-top: ${ns(16)}px; -`; diff --git a/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx b/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx deleted file mode 100644 index 252329251..000000000 --- a/packages/mobile/src/core/SetupBiometry/SetupBiometry.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; -import LottieView from 'lottie-react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import * as LocalAuthentication from 'expo-local-authentication'; -import { useDispatch } from 'react-redux'; - -import { SetupBiometryProps } from './SetupBiometry.interface'; -import * as S from './SetupBiometry.style'; -import { Button, NavBar, Text } from '$uikit'; -import { ns, platform } from '$utils'; -import { openSetupNotifications, openSetupWalletDone } from '$navigation'; -import { walletActions } from '$store/wallet'; -import { t } from '@tonkeeper/shared/i18n'; -import { Steezy } from '@tonkeeper/uikit'; -import { tk } from '$wallet'; - -const LottieFaceId = require('$assets/lottie/faceid.json'); -const LottieTouchId = require('$assets/lottie/touchid.json'); - -export const SetupBiometry: FC = ({ route }) => { - const { pin, biometryType } = route.params; - - const dispatch = useDispatch(); - - const { bottom: bottomInset } = useSafeAreaInsets(); - const iconRef = useRef(null); - const isTouchId = - biometryType !== LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION; - const [isLoading, setLoading] = useState(false); - - useMemo(() => { - const timer = setTimeout(() => { - iconRef.current?.play(); - }, 400); - - return () => clearTimeout(timer); - }, []); - - const [isCreatingWallet, setCreatingWallet] = useState(false); - - const doCreateWallet = useCallback( - (isBiometryEnabled: boolean) => () => { - if (isCreatingWallet) { - return; - } - - setCreatingWallet(true); - dispatch( - walletActions.createWallet({ - isBiometryEnabled, - pin, - onDone: async (identifiers) => { - if (tk.wallet.notifications.isAvailable) { - openSetupNotifications(identifiers); - } else { - openSetupWalletDone(identifiers); - } - setCreatingWallet(false); - }, - onFail: () => { - setCreatingWallet(false); - setLoading(false); - }, - }), - ); - }, - [isCreatingWallet, dispatch, pin], - ); - - const biometryNameGenitive = useMemo(() => { - return isTouchId - ? t(`platform.${platform}.fingerprint_genitive`) - : t(`platform.${platform}.face_recognition_genitive`); - }, [isTouchId]); - - const biometryName = useMemo(() => { - return isTouchId - ? t(`platform.${platform}.fingerprint`) - : t(`platform.${platform}.face_recognition`); - }, [isTouchId]); - - const handleEnable = useCallback(() => { - setLoading(true); - doCreateWallet(true)(); - }, [doCreateWallet]); - - return ( - <> - - {t('later')} - - } - /> - - - - - {t('setup_biometry_title', { biometryType: biometryNameGenitive })} - - - - {t('setup_biometry_caption', { - biometryType: isTouchId - ? t(`platform.${platform}.capitalized_fingerprint`) - : t(`platform.${platform}.capitalized_face_recognition`), - })} - - - - - - - - - ); -}; - -const styles = Steezy.create({ - lottieIcon: { - width: 160, - height: 160, - }, -}); diff --git a/packages/mobile/src/core/SetupNotifications/SetupNotifications.style.ts b/packages/mobile/src/core/SetupNotifications/SetupNotifications.style.ts deleted file mode 100644 index 244763887..000000000 --- a/packages/mobile/src/core/SetupNotifications/SetupNotifications.style.ts +++ /dev/null @@ -1,40 +0,0 @@ -import styled from '$styled'; -import { nfs, ns } from '$utils'; - -export const Wrap = styled.View` - flex: 1; - padding: ${ns(32)}px; - padding-top: 0; -`; - -export const Content = styled.View` - flex: 1; - align-items: center; - justify-content: center; -`; - -export const IconWrap = styled.View` - margin-bottom: ${ns(16)}px; -`; - -export const Title = styled.Text` - font-family: ${({ theme }) => theme.font.semiBold}; - color: ${({ theme }) => theme.colors.foregroundPrimary}; - font-size: ${nfs(24)}px; - line-height: 32px; - text-align: center; -`; - -export const Caption = styled.Text` - font-family: ${({ theme }) => theme.font.medium}; - color: ${({ theme }) => theme.colors.foregroundSecondary}; - font-size: ${nfs(16)}px; - line-height: 24px; - margin-top: ${ns(4)}px; - text-align: center; -`; - -export const Footer = styled.View` - flex: 0 0 auto; - padding-top: ${ns(16)}px; -`; diff --git a/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx b/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx index afd4f279a..b98cfafef 100644 --- a/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx +++ b/packages/mobile/src/core/SetupNotifications/SetupNotifications.tsx @@ -1,7 +1,4 @@ import React, { useCallback } from 'react'; -import { Button, Icon, Screen, Spacer, Text } from '$uikit'; -import * as S from '$core/SetupNotifications/SetupNotifications.style'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { t } from '@tonkeeper/shared/i18n'; import { openSetupWalletDone } from '$navigation'; import { ns } from '$utils'; @@ -13,6 +10,7 @@ import { ImportWalletStackParamList, ImportWalletStackRouteNames, } from '$navigation/ImportWalletStack/types'; +import { Button, Icon, Screen, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; interface Props { route: RouteProp; @@ -22,7 +20,6 @@ export const SetupNotifications: React.FC = (props) => { const { identifiers } = props.route.params; const [loading, setLoading] = React.useState(false); - const safeArea = useSafeAreaInsets(); const handleDone = useCallback(() => { openSetupWalletDone(identifiers); @@ -49,37 +46,52 @@ export const SetupNotifications: React.FC = (props) => { return ( - {t('later')} - + /> } /> - - - - - - + + + + + + + {t('setup_notifications_title')} - + {t('setup_notifications_caption')} - - - - - + + + + + diff --git a/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx b/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx index d143de0b3..59f73b1c1 100644 --- a/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx +++ b/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx @@ -2,27 +2,15 @@ import { ImportWalletStackParamList, ImportWalletStackRouteNames, } from '$navigation/ImportWalletStack/types'; -import { BottomButtonWrap, BottomButtonWrapHelper } from '$shared/components'; import { Checkbox } from '$uikit'; import { WalletContractVersion } from '$wallet/WalletTypes'; import { t } from '@tonkeeper/shared/i18n'; -import { - Button, - List, - Screen, - Spacer, - Steezy, - Text, - View, - isAndroid, -} from '@tonkeeper/uikit'; +import { Button, List, Screen, Spacer, Steezy, Text, isAndroid } from '@tonkeeper/uikit'; import { FC, useCallback, useState } from 'react'; import { RouteProp } from '@react-navigation/native'; import { Address } from '@tonkeeper/shared/Address'; import { formatter } from '@tonkeeper/shared/formatter'; import { useImportWallet } from '$hooks/useImportWallet'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { ScreenHeaderHeight } from '@tonkeeper/uikit/src/containers/Screen/utils/constants'; export const ChooseWallets: FC<{ route: RouteProp; @@ -30,8 +18,6 @@ export const ChooseWallets: FC<{ const { mnemonic, lockupConfig, isTestnet, walletsInfo, isMigration } = props.route.params; - const safeArea = useSafeAreaInsets(); - const doImportWallet = useImportWallet(); const [selectedVersions, setSelectedVersions] = useState( @@ -68,25 +54,21 @@ export const ChooseWallets: FC<{ const tokensText = `, ${t('choose_wallets.tokens')}`; - const headerHeight = ScreenHeaderHeight + safeArea.top; - return ( - + - - - {t('choose_wallets.title')} - - - - {t('choose_wallets.subtitle')} - - + + {t('choose_wallets.title')} + + + + {t('choose_wallets.subtitle')} + - + {walletsInfo.map((walletInfo) => ( ))} - + - - - -