diff --git a/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts b/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts index 02ee15352..23d039035 100644 --- a/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts +++ b/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts @@ -21,6 +21,11 @@ export interface Status { } export interface Config { + /** + * with zero balance it is possible to transfer some jettons (stablecoins, jusdt, etc) to this address to refill the balance. Such transfers would be paid by Battery Service. + * @example "0:07331e629e39d006d86a8cc7659c10a97c671f7535dc8b7f251a1a944dda348e" + */ + fund_receiver: string; /** * when building a message to transfer an NFT or Jetton, use this address to send excess funds back to Battery Service. * @example "0:da6b1b6663a0e4d18cc8574ccd9db5296e367dd9324706f3bbd9eb1cd2caf0bf" @@ -31,6 +36,11 @@ export interface Config { export interface Balance { /** @example "10.250" */ balance: string; + /** + * reserved amount in units (TON/USD) + * @example "0.3" + */ + reserved: string; /** @example "usd" */ units: BalanceUnitsEnum; } diff --git a/packages/@core-js/src/service/contractService.ts b/packages/@core-js/src/service/contractService.ts index dd1d47ea0..b9522cdf5 100644 --- a/packages/@core-js/src/service/contractService.ts +++ b/packages/@core-js/src/service/contractService.ts @@ -11,6 +11,7 @@ import nacl from 'tweetnacl'; export enum OpCodes { JETTON_TRANSFER = 0xf8a7ea5, NFT_TRANSFER = 0x5fcc3d14, + STONFI_SWAP = 0x25938561, } export enum WalletVersion { diff --git a/packages/@core-js/src/service/transactionService.ts b/packages/@core-js/src/service/transactionService.ts index 2762b95bc..5ee4dbdc3 100644 --- a/packages/@core-js/src/service/transactionService.ts +++ b/packages/@core-js/src/service/transactionService.ts @@ -33,7 +33,7 @@ export function tonAddress(address: AnyAddress) { } export class TransactionService { - private static TTL = 5 * 60; + public static TTL = 5 * 60; private static getTimeout() { return Math.floor(Date.now() / 1e3) + TransactionService.TTL; @@ -104,6 +104,21 @@ export class TransactionService { let builder = beginCell(); switch (opCode) { + case OpCodes.STONFI_SWAP: + builder = builder + .storeUint(OpCodes.STONFI_SWAP, 32) + .storeAddress(slice.loadAddress()) + .storeCoins(slice.loadCoins()) + .storeAddress(slice.loadAddress()); + + if (slice.loadBoolean()) { + slice.loadAddress(); + } + + return builder + .storeBit(1) + .storeAddress(Address.parse(customExcessesAccount)) + .endCell(); case OpCodes.NFT_TRANSFER: builder = builder .storeUint(OpCodes.NFT_TRANSFER, 32) @@ -130,7 +145,11 @@ export class TransactionService { slice.loadMaybeAddress(); while (slice.remainingRefs) { - builder = builder.storeRef(slice.loadRef()); + const forwardCell = slice.loadRef(); + // recursively rebuild forward payloads + builder = builder.storeRef( + this.rebuildBodyWithCustomExcessesAccount(forwardCell, customExcessesAccount), + ); } return builder diff --git a/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts b/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts index a985e378f..60b517363 100644 --- a/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts +++ b/packages/@core-js/src/utils/AmountFormatter/AmountFormatter.ts @@ -21,6 +21,8 @@ export type AmountFormatOptions = { ignoreZeroTruncate?: boolean; absolute?: boolean; withPositivePrefix?: boolean; + // Truncate decimals. Required for backward compatibility + forceRespectDecimalPlaces?: boolean; }; export type AmountFormatNanoOptions = AmountFormatOptions & { @@ -144,7 +146,10 @@ export class AmountFormatter { }; // truncate decimals 1.00 -> 1 - if (!options.ignoreZeroTruncate && bn.isLessThan('1000')) { + if ( + options.forceRespectDecimalPlaces || + (!options.ignoreZeroTruncate && bn.isLessThan('1000')) + ) { bn = bn.decimalPlaces(decimals, BigNumber.ROUND_DOWN); return bn.toFormat(formatConf); } diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 22aeaee46..8c0ffa385 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 "4.2.0" + versionName "4.3.0" 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 f3ca23162..ed578fecd 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -1056,7 +1056,7 @@ outputFileListPaths = ( ); outputPaths = ( - $SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift, + "$SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1298,7 +1298,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.2.0; + MARKETING_VERSION = 4.3.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1332,7 +1332,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.2.0; + MARKETING_VERSION = 4.3.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index c90a63b81..4c57219d9 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -26,7 +26,11 @@ import { import { tk } from '$wallet'; import { Address, Cell, internal } from '@ton/core'; -import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; +import { + NetworkOverloadedError, + emulateBoc, + sendBoc, +} from '@tonkeeper/shared/utils/blockchain'; import { OperationEnum, TonAPI, TypeEnum } from '@tonkeeper/core/src/TonAPI'; import { setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; import { WalletNetwork } from '$wallet/WalletTypes'; @@ -34,6 +38,7 @@ import { createTonApiInstance } from '$wallet/utils'; import { config } from '$config'; import { toNano } from '$utils'; import { BatterySupportedTransaction } from '$wallet/managers/BatteryManager'; +import { compareAddresses } from '$utils/address'; const TonWeb = require('tonweb'); @@ -324,9 +329,15 @@ export class TonWallet { private async calcFee( boc: string, params?, - withRelayer = true, + withRelayer = false, + forceRelayer = false, ): Promise<[BigNumber, boolean]> { - const { emulateResult, battery } = await emulateBoc(boc, params, withRelayer); + const { emulateResult, battery } = await emulateBoc( + boc, + params, + withRelayer, + forceRelayer, + ); return [new BigNumber(emulateResult.event.extra).multipliedBy(-1), battery]; } @@ -423,6 +434,7 @@ export class TonWallet { tk.wallet.battery.state.data.supportedTransactions[ BatterySupportedTransaction.Jetton ], + compareAddresses(address, tk.wallet.battery.fundReceiver), ); return [Ton.fromNano(feeNano.toString()), isBattery]; @@ -469,7 +481,7 @@ export class TonWallet { } const excessesAccount = sendWithBattery - ? await tk.wallet.battery.getExcessesAccount() + ? tk.wallet.battery.excessesAccount : tk.wallet.address.ton.raw; const boc = this.createJettonTransfer({ @@ -493,6 +505,7 @@ export class TonWallet { tk.wallet.battery.state.data.supportedTransactions[ BatterySupportedTransaction.Jetton ], + compareAddresses(address, tk.wallet.battery.fundReceiver), ); feeNano = fee; isBattery = battery; @@ -518,6 +531,9 @@ export class TonWallet { try { await sendBoc(boc, isBattery); } catch (e) { + if (e instanceof NetworkOverloadedError) { + throw e; + } if (!store.getState().main.isTimeSynced) { throw new Error('wrong_time'); } @@ -625,9 +641,7 @@ export class TonWallet { ? AddressFormatter.isBounceable(address) : false, }); - const [feeNano, isBattery] = await this.calcFee(boc); - return [Ton.fromNano(feeNano.toString()), isBattery]; } @@ -730,6 +744,9 @@ export class TonWallet { try { await sendBoc(boc, false); } catch (e) { + if (e instanceof NetworkOverloadedError) { + throw e; + } if (!store.getState().main.isTimeSynced) { throw new Error('wrong_time'); } diff --git a/packages/mobile/src/components/ScanQRButton.tsx b/packages/mobile/src/components/ScanQRButton.tsx deleted file mode 100644 index 07da2171d..000000000 --- a/packages/mobile/src/components/ScanQRButton.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { memo, useMemo } from 'react'; -import { Icon, TouchableOpacity } from '$uikit'; -import { Steezy } from '$styles'; -import { store } from '$store'; -import { openScanQR, openSend } from '$navigation'; -import { CryptoCurrencies } from '$shared/constants'; -import { DeeplinkOrigin, useDeeplinking } from '$libs/deeplinking'; -import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; -import { Address } from '@tonkeeper/core'; - -export const ScanQRButton = memo(() => { - const deeplinking = useDeeplinking(); - - const hitSlop = useMemo( - () => ({ - top: 26, - bottom: 26, - left: 26, - right: 26, - }), - [], - ); - - const handlePressScanQR = React.useCallback(() => { - if (store.getState().wallet.wallet) { - openScanQR((address) => { - if (Address.isValid(address)) { - setTimeout(() => { - openSend({ currency: CryptoCurrencies.Ton, address }); - }, 200); - - return true; - } - - const resolver = deeplinking.getResolver(address, { - delay: 200, - origin: DeeplinkOrigin.QR_CODE, - }); - - if (resolver) { - resolver(); - return true; - } - - return false; - }); - } else { - openRequireWalletModal(); - } - }, []); - - return ( - - - - ); -}); - -const styles = Steezy.create({ - container: { - zIndex: 3, - }, -}); diff --git a/packages/mobile/src/context/WalletContext.tsx b/packages/mobile/src/context/WalletContext.tsx index 6e9d02742..c5ab86eba 100644 --- a/packages/mobile/src/context/WalletContext.tsx +++ b/packages/mobile/src/context/WalletContext.tsx @@ -14,6 +14,5 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }) => { return unsubscribe; }, []); - return {children}; }; diff --git a/packages/mobile/src/core/Colectibles/Collectibles.tsx b/packages/mobile/src/core/Colectibles/Collectibles.tsx new file mode 100644 index 000000000..7534a03a2 --- /dev/null +++ b/packages/mobile/src/core/Colectibles/Collectibles.tsx @@ -0,0 +1,59 @@ +import React, { memo, useMemo } from 'react'; +import { View } from '$uikit'; +import { NFTCardItem } from './NFTCardItem'; +import { Steezy } from '$styles'; +import { useWindowDimensions } from 'react-native'; +import { useApprovedNfts } from '$hooks/useApprovedNfts'; +import { Screen } from '@tonkeeper/uikit'; +import { t } from '@tonkeeper/shared/i18n'; + +const mockupCardSize = { + width: 114, + height: 166, +}; + +const numColumn = 3; +const heightRatio = mockupCardSize.height / mockupCardSize.width; + +export const Collectibles = memo(() => { + const nfts = useApprovedNfts(); + const dimensions = useWindowDimensions(); + + const size = useMemo(() => { + const width = (dimensions.width - 48) / numColumn; + const height = width * heightRatio; + + return { width, height }; + }, [dimensions.width]); + + return ( + + + ( + + + + )} + /> + + ); +}); + +const styles = Steezy.create({ + collectiblesContainer: { + marginHorizontal: 16, + gap: 8, + }, + nftElements: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + columnWrapper: { + gap: 8, + }, +}); diff --git a/packages/mobile/src/core/Colectibles/NFTCardItem.style.ts b/packages/mobile/src/core/Colectibles/NFTCardItem.style.ts new file mode 100644 index 000000000..4e2b00d2c --- /dev/null +++ b/packages/mobile/src/core/Colectibles/NFTCardItem.style.ts @@ -0,0 +1,75 @@ +import styled, { RADIUS } from '$styled'; +import { ns } from '$utils'; +import FastImage from 'react-native-fast-image'; +import { Highlight } from '$uikit'; +import { Dimensions } from 'react-native'; + +const deviceWidth = Dimensions.get('window').width; +export const NUM_OF_COLUMNS = Math.trunc(Math.max(2, Math.min(deviceWidth / 171, 3))); +const availableWidth = NUM_OF_COLUMNS === 2 ? (deviceWidth - ns(48)) / 2 : 171; // Padding and margin between NFTs + +export const Wrap = styled.View<{ withMargin: boolean }>` + width: ${availableWidth}px; + margin-right: ${({ withMargin }) => (withMargin ? ns(16) : 0)}px; + margin-bottom: ${ns(16)}px; +`; + +export const Background = styled.View` + background: ${({ theme }) => theme.colors.backgroundSecondary}; + border-radius: ${ns(RADIUS.normal)}px; + position: absolute; + z-index: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; +`; + +export const Pressable = styled(Highlight)` + border-radius: ${ns(RADIUS.normal)}px; +`; + +export const Image = styled(FastImage).attrs({ + resizeMode: 'cover', +})` + position: relative; + z-index: 2; + width: 100%; + height: ${ns(171)}px; + border-top-left-radius: ${ns(RADIUS.normal)}px; + border-top-right-radius: ${ns(RADIUS.normal)}px; + background: ${({ theme }) => theme.colors.backgroundTertiary}; +`; + +export const Badges = styled.View` + position: absolute; + bottom: ${8}px; + right: ${8}px; + flex-direction: row; + align-items: center; +`; + +export const FireBadge = styled.View` + position: absolute; + bottom: ${0}px; + right: ${0}px; + flex-direction: row; + align-items: center; +`; + +export const OnSaleBadge = styled.View` + position: absolute; + top: ${0}px; + right: ${0}px; + flex-direction: row; + align-items: center; +`; + +export const AppearanceBadge = styled.View` + width: ${ns(32)}px; + height: ${ns(32)}px; + border-radius: ${ns(32 / 2)}px; + background: ${({ theme }) => theme.colors.backgroundSecondary}; + align-items: center; + justify-content: center; +`; diff --git a/packages/mobile/src/tabs/Wallet/NFTCardItem.tsx b/packages/mobile/src/core/Colectibles/NFTCardItem.tsx similarity index 97% rename from packages/mobile/src/tabs/Wallet/NFTCardItem.tsx rename to packages/mobile/src/core/Colectibles/NFTCardItem.tsx index df9c7c968..adade1249 100644 --- a/packages/mobile/src/tabs/Wallet/NFTCardItem.tsx +++ b/packages/mobile/src/core/Colectibles/NFTCardItem.tsx @@ -6,7 +6,7 @@ import { checkIsTonDiamondsNFT } from '$utils'; import { useFlags } from '$utils/flags'; import _ from 'lodash'; import React, { memo, useCallback, useMemo } from 'react'; -import * as S from '../../core/NFTs/NFTItem/NFTItem.style'; +import * as S from './NFTCardItem.style'; import { useExpiringDomains } from '$store/zustand/domains/useExpiringDomains'; import { AnimationDirection, HideableAmount } from '$core/HideableAmount/HideableAmount'; import { HideableImage } from '$core/HideableAmount/HideableImage'; @@ -100,8 +100,6 @@ const styles = Steezy.create(({ colors, corners }) => ({ container: { position: 'relative', flex: 1, - marginHorizontal: 4, - marginBottom: 8, backgroundColor: colors.backgroundContent, borderRadius: corners.medium, overflow: 'hidden', diff --git a/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx b/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx index a4c9029e5..a12a629a1 100644 --- a/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx +++ b/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx @@ -14,6 +14,7 @@ import { TouchableOpacity, View, WalletColor, + WalletIcon, getWalletColorHex, isAndroid, ns, @@ -29,7 +30,7 @@ import React, { useRef, useState, } from 'react'; -import { Keyboard, LayoutChangeEvent, Text as RNText } from 'react-native'; +import { Keyboard, LayoutChangeEvent } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { EmojiPicker } from './EmojiPicker'; @@ -163,7 +164,7 @@ export const CustomizeWallet: FC = memo((props) => { { backgroundColor: getWalletColorHex(selectedColor) }, ]} > - {emoji} + diff --git a/packages/mobile/src/core/CustomizeWallet/EmojiPicker/EmojiPicker.tsx b/packages/mobile/src/core/CustomizeWallet/EmojiPicker/EmojiPicker.tsx index 0d90f00f2..8a14f9842 100644 --- a/packages/mobile/src/core/CustomizeWallet/EmojiPicker/EmojiPicker.tsx +++ b/packages/mobile/src/core/CustomizeWallet/EmojiPicker/EmojiPicker.tsx @@ -1,8 +1,9 @@ import { isAndroid } from '$utils'; import { FlashList } from '@shopify/flash-list'; -import { Steezy, View, ns } from '@tonkeeper/uikit'; +import { Steezy, View, WalletIcon, ns } from '@tonkeeper/uikit'; +import { WALLET_ICONS } from '@tonkeeper/uikit/src/utils/walletIcons'; import React, { memo, useCallback } from 'react'; -import { Text, TouchableOpacity } from 'react-native'; +import { TouchableOpacity } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; interface Emoji { @@ -12,6 +13,11 @@ interface Emoji { const emojis: Emoji[] = require('./emojis.json'); +const items: Emoji[] = [ + ...WALLET_ICONS.map((value) => ({ emoji: value, name: value })), + ...emojis, +]; + interface EmojiPickerProps { onChange: (value: string) => void; } @@ -22,7 +28,7 @@ export const EmojiPicker: React.FC = memo(({ onChange }) => { return ( onChange(item.emoji)}> - {item.emoji} + ); @@ -33,7 +39,7 @@ export const EmojiPicker: React.FC = memo(({ onChange }) => { return ( item.name} renderItem={renderEmoji} diff --git a/packages/mobile/src/core/HideableAmount/ShowBalance.tsx b/packages/mobile/src/core/HideableAmount/ShowBalance.tsx index abea7aa99..b6890c3c1 100644 --- a/packages/mobile/src/core/HideableAmount/ShowBalance.tsx +++ b/packages/mobile/src/core/HideableAmount/ShowBalance.tsx @@ -57,6 +57,6 @@ const styles = Steezy.create(({ colors }) => ({ borderRadius: 100, }, stars: { - paddingTop: 5.5, + paddingTop: 8, }, })); diff --git a/packages/mobile/src/core/InscriptionScreen.tsx b/packages/mobile/src/core/InscriptionScreen.tsx index ab0a3ec8e..5ffe31f52 100644 --- a/packages/mobile/src/core/InscriptionScreen.tsx +++ b/packages/mobile/src/core/InscriptionScreen.tsx @@ -1,6 +1,7 @@ import { useTonInscription } from '@tonkeeper/shared/query/hooks/useTonInscription'; import { useParams } from '@tonkeeper/router/src/imperative'; import { + ActionButtons, DEFAULT_TOKEN_LOGO, IconButton, IconButtonList, @@ -62,21 +63,24 @@ export const InscriptionScreen = memo(() => { - - - {!wallet.isWatchOnly ? ( - - ) : null} - - + + @@ -86,7 +90,7 @@ export const InscriptionScreen = memo(() => { const styles = Steezy.create(({ colors }) => ({ tokenContainer: { paddingTop: 16, - paddingBottom: 28, + paddingBottom: 24, flexDirection: 'row', justifyContent: 'space-between', marginHorizontal: 28, @@ -96,14 +100,6 @@ const styles = Steezy.create(({ colors }) => ({ height: 64, borderRadius: 64 / 2, }, - buttons: { - borderTopWidth: 1, - borderBottomWidth: 1, - borderTopColor: colors.backgroundContent, - borderBottomColor: colors.backgroundContent, - paddingTop: 16, - paddingBottom: 12, - }, tokenText: { paddingTop: 2, }, diff --git a/packages/mobile/src/core/Jetton/Jetton.style.ts b/packages/mobile/src/core/Jetton/Jetton.style.ts index e244d6eb5..06077fa30 100644 --- a/packages/mobile/src/core/Jetton/Jetton.style.ts +++ b/packages/mobile/src/core/Jetton/Jetton.style.ts @@ -11,7 +11,6 @@ export const Wrap = styled.View` export const ChartWrap = styled.View` margin-bottom: ${ns(24.5)}px; - margin-top: 18px; `; export const HeaderWrap = styled.View` @@ -44,7 +43,7 @@ export const FlexRow = styled.View` flex-direction: row; justify-content: space-between; margin-top: ${ns(16)}px; - margin-bottom: ${ns(28)}px; + padding-horizontal: ${ns(12)}px; `; export const JettonAmountWrapper = styled.View` diff --git a/packages/mobile/src/core/Jetton/Jetton.tsx b/packages/mobile/src/core/Jetton/Jetton.tsx index 28aac5c49..99a1b5c5d 100644 --- a/packages/mobile/src/core/Jetton/Jetton.tsx +++ b/packages/mobile/src/core/Jetton/Jetton.tsx @@ -17,7 +17,15 @@ import { Events, JettonVerification, SendAnalyticsFrom } from '$store/models'; import { t } from '@tonkeeper/shared/i18n'; import { trackEvent } from '$utils/stats'; import { Address } from '@tonkeeper/core'; -import { Icon, Screen, Spacer, Steezy, TouchableOpacity, View } from '@tonkeeper/uikit'; +import { + ActionButtons, + Icon, + Screen, + Spacer, + Steezy, + TouchableOpacity, + View, +} from '@tonkeeper/uikit'; import { useJettonActivityList } from '@tonkeeper/shared/query/hooks/useJettonActivityList'; import { ActivityList } from '@tonkeeper/shared/components'; @@ -121,42 +129,37 @@ export const Jetton: React.FC = ({ route }) => { ) : null} - {jettonPrice.formatted.fiat ? ( - <> - - - {t('jetton_price')} {jettonPrice.formatted.fiat} - - - ) : null} {jetton.metadata.image ? ( ) : null} - - - {!isWatchOnly ? ( - - ) : null} - - {!isWatchOnly && showSwap && !flags.disable_swap ? ( - } - title={t('wallet.swap_btn')} - /> - ) : null} - - + + + {shouldShowChart && ( <> @@ -177,7 +180,8 @@ export const Jetton: React.FC = ({ route }) => { ); }, [ jetton, - jettonPrice, + jettonPrice.formatted.totalFiat, + lockedJettonPrice.formatted.totalFiat, isWatchOnly, handleSend, handleReceive, @@ -185,6 +189,7 @@ export const Jetton: React.FC = ({ route }) => { flags.disable_swap, handlePressSwap, shouldShowChart, + shouldExcludeChartPeriods, fiatCurrency, route.params.jettonAddress, ]); @@ -214,7 +219,7 @@ export const Jetton: React.FC = ({ route }) => { ) } - title={jetton.metadata?.name || Address.toShort(jetton.jettonAddress)} + title={jetton.metadata?.symbol || Address.toShort(jetton.jettonAddress)} rightContent={ { - const tabBarHeight = useBottomTabBarHeight(); + const insets = useSafeAreaInsets(); return ( @@ -17,7 +18,7 @@ export const FontLicense: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: ns(16), - paddingBottom: tabBarHeight, + paddingBottom: insets.bottom + 16, }} scrollEventThrottle={16} > @@ -124,4 +125,4 @@ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -`; \ No newline at end of file +`; diff --git a/packages/mobile/src/core/LegalDocuments/LegalDocuments.tsx b/packages/mobile/src/core/LegalDocuments/LegalDocuments.tsx index efd71caa2..948a500da 100644 --- a/packages/mobile/src/core/LegalDocuments/LegalDocuments.tsx +++ b/packages/mobile/src/core/LegalDocuments/LegalDocuments.tsx @@ -1,5 +1,4 @@ import React, { FC, useCallback, useMemo } from 'react'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import Animated from 'react-native-reanimated'; import { Icon, NavBar, ScrollHandler, Text } from '$uikit'; import { ns } from '$utils'; @@ -7,9 +6,10 @@ import { CellSection, CellSectionItem } from '$shared/components'; import * as S from './LegalDocuments.style'; import { openDAppBrowser, openFontLicense } from '$navigation'; import { t } from '@tonkeeper/shared/i18n'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const LegalDocuments: FC = () => { - const tabBarHeight = useBottomTabBarHeight(); + const insets = useSafeAreaInsets(); const handleTerms = useCallback(() => { openDAppBrowser('https://tonkeeper.com/terms'); @@ -36,7 +36,7 @@ export const LegalDocuments: FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: ns(16), - paddingBottom: tabBarHeight, + paddingBottom: insets.bottom + 16, }} scrollEventThrottle={16} > diff --git a/packages/mobile/src/core/Logs/Logs.tsx b/packages/mobile/src/core/Logs/Logs.tsx index c94633a71..5e5a50ff6 100644 --- a/packages/mobile/src/core/Logs/Logs.tsx +++ b/packages/mobile/src/core/Logs/Logs.tsx @@ -1,5 +1,4 @@ import React, { FC, useCallback, useMemo } from 'react'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useDispatch, useSelector } from 'react-redux'; import Clipboard from '@react-native-community/clipboard'; @@ -10,10 +9,11 @@ import { format, ns } from '$utils'; import { Toast } from '$store'; import { t } from '@tonkeeper/shared/i18n'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Logs: FC = () => { - const tabBarHeight = useBottomTabBarHeight(); const dispatch = useDispatch(); + const insets = useSafeAreaInsets(); const { logs } = useSelector(mainSelector); @@ -33,27 +33,24 @@ export const Logs: FC = () => { ]; }, [logs]); - const handleItemPress = useCallback( - (item) => { - const payload = [ - `Screen: ${item.screen}`, - `Time: ${item.time}`, - `Message: ${item.message}`, - `Stack: ${item.trace}`, - ].join('\n'); + const handleItemPress = useCallback((item) => { + const payload = [ + `Screen: ${item.screen}`, + `Time: ${item.time}`, + `Message: ${item.message}`, + `Stack: ${item.trace}`, + ].join('\n'); - Clipboard.setString(payload); - Toast.success(t('copied')); - }, - [t, dispatch], - ); + Clipboard.setString(payload); + Toast.success(t('copied')); + }, []); return ( Logs { diff --git a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.style.ts b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.style.ts index 0a2a7d642..87d40e452 100644 --- a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.style.ts +++ b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.style.ts @@ -9,38 +9,19 @@ export const LoaderWrap = styled.View` `; export const Header = styled.View` - flex-direction: row; - padding: ${ns(0)}px ${ns(16)}px ${ns(32)}px; + align-items: center; + padding-top: ${ns(48)}px; `; export const MerchantPhoto = styled(FastImage).attrs({ resizeMode: 'cover', })` - width: ${ns(72)}px; - height: ${ns(72)}px; - border-radius: ${ns(72 / 2)}px; + width: ${ns(96)}px; + height: ${ns(96)}px; + border-radius: ${ns(96 / 2)}px; background: ${({ theme }) => theme.colors.backgroundSecondary}; `; -export const MerchantInfoWrap = styled.View` - margin-left: ${ns(16)}px; - flex: 1; - align-items: flex-start; -`; - -export const MerchantInfo = styled.View` - height: ${ns(72)}px; - justify-content: center; -`; - -export const ProductNameWrapper = styled.View` - margin-top: ${ns(2)}px; -`; - -export const Content = styled.View` - padding-horizontal: ${ns(16)}px; -`; - export const ButtonWrap = styled.View` margin-top: ${ns(16)}px; flex: 0 0 auto; diff --git a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx index 8f57058ef..9fa748ae4 100644 --- a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx +++ b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx @@ -1,19 +1,14 @@ -import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Linking } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import TonWeb from 'tonweb'; import { getUnixTime } from 'date-fns'; import { CreateSubscriptionProps } from './CreateSubscription.interface'; import * as S from './CreateSubscription.style'; -import { Button, Icon, Loader, Text } from '$uikit'; -import { List } from '@tonkeeper/uikit'; +import { Button, Loader, Text } from '$uikit'; +import { List, Spacer } from '@tonkeeper/uikit'; import { SubscriptionModel } from '$store/models'; -import { - format, - formatSubscriptionPeriod, - toLocaleNumber, - triggerNotificationSuccess, -} from '$utils'; +import { format, formatSubscriptionPeriod, toLocaleNumber } from '$utils'; import { subscriptionsActions } from '$store/subscriptions'; import { CryptoCurrencies, Decimals } from '$shared/constants'; import { formatCryptoCurrency } from '$utils/currency'; @@ -29,6 +24,7 @@ import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Modal, View } from '@tonkeeper/uikit'; import { config } from '$config'; import { tk } from '$wallet'; +import { ActionFooter, useActionFooter } from '../NFTOperations/NFTOperationFooter'; export const CreateSubscription: FC = ({ invoiceId = null, @@ -43,15 +39,13 @@ export const CreateSubscription: FC = ({ const { amount: balance } = useWalletInfo(); const [isLoading, setLoading] = useState(!isEdit); - const [failed, setFailed] = useState(0); const [fee, setFee] = useState( passedFee || (subscription && subscription.fee ? subscription.fee : '~'), ); const [info, setInfo] = useState(subscription); - const [isSending, setSending] = useState(false); - const [isSuccess, setSuccess] = useState(false); const [totalMoreThanBalance, setTotalMoreThanBalance] = React.useState(false); - const closeTimer = useRef(null); + + const { footerRef, onConfirm } = useActionFooter(); useEffect(() => { if (fee !== '~') { @@ -61,39 +55,6 @@ export const CreateSubscription: FC = ({ } }, [info, balance, fee]); - useEffect(() => { - if (isSuccess) { - triggerNotificationSuccess(); - closeTimer.current = setTimeout(() => { - nav.goBack(); - - if (!isEdit && !subscription) { - const returnUrl = info!.userReturnUrl; - Alert.alert( - t('subscription_back_to_merchant_title'), - t('subscription_back_to_merchant_caption'), - [ - { - text: t('cancel'), - style: 'cancel', - }, - { - text: t('subscription_back_to_merchant_button'), - onPress: () => { - Linking.openURL(returnUrl).catch((err) => { - console.log(err); - }); - }, - }, - ], - ); - } - }, 2500); - } - - return () => closeTimer.current && clearTimeout(closeTimer.current); - }, [isSuccess]); - const loadInfo = useCallback(() => { const host = config.get('subscriptionsHost', tk.wallet.isTestnet); network @@ -110,7 +71,7 @@ export const CreateSubscription: FC = ({ Toast.fail(e.message); nav.goBack(); }); - }, [invoiceId]); + }, [invoiceId, nav]); useEffect(() => { if (!isEdit) { @@ -169,22 +130,47 @@ export const CreateSubscription: FC = ({ return format((info.chargedAt + info.intervalSec) * 1000, 'EEE, MMM d'); }, [info]); - const handleSubscribe = useCallback(() => { - setSending(true); - dispatch( - subscriptionsActions.subscribe({ - subscription: info!, - onDone: () => { - setSuccess(true); - }, - onFail: () => { - setSending(false); - }, - }), - ); - }, [dispatch, info]); + const handleSubscribe = useCallback(async () => { + onConfirm(async ({ startLoading }) => { + startLoading(); + + await new Promise((resolve, reject) => { + dispatch( + subscriptionsActions.subscribe({ + subscription: info!, + onDone: () => { + resolve(); + + if (!isEdit && !subscription) { + const returnUrl = info!.userReturnUrl; + Alert.alert( + t('subscription_back_to_merchant_title'), + t('subscription_back_to_merchant_caption'), + [ + { + text: t('cancel'), + style: 'cancel', + }, + { + text: t('subscription_back_to_merchant_button'), + onPress: () => { + Linking.openURL(returnUrl).catch((err) => { + console.log(err); + }); + }, + }, + ], + ); + } + }, + onFail: reject, + }), + ); + }); + })(); + }, [dispatch, info, isEdit, onConfirm, subscription]); - const handleUnsubscribe = useCallback(() => { + const handleUnsubscribe = useCallback(async () => { Alert.alert( t('subscription_cancel_alert_title'), t('subscription_cancel_alert_caption', { @@ -199,23 +185,24 @@ export const CreateSubscription: FC = ({ text: t('subscription_cancel_alert_submit_btn'), style: 'destructive', onPress: () => { - setSending(true); - dispatch( - subscriptionsActions.unsubscribe({ - subscription: info!, - onDone: () => { - setSuccess(true); - }, - onFail: () => { - setSending(false); - }, - }), - ); + onConfirm(async ({ startLoading }) => { + startLoading(); + + await new Promise((resolve, reject) => { + dispatch( + subscriptionsActions.unsubscribe({ + subscription: info!, + onDone: resolve, + onFail: reject, + }), + ); + }); + })(); }, }, ], ); - }, [dispatch, info, nextBill, t]); + }, [dispatch, info, nextBill, onConfirm]); const isButtonShown = useMemo(() => { if (!info) { @@ -227,7 +214,7 @@ export const CreateSubscription: FC = ({ } return info.status === 'new' || info.isActive; - }, [info, isEdit, isSuccess, isSending]); + }, [info, isEdit]); const handleOpenMerchant = useCallback(() => { Linking.openURL(info!.returnUrl).catch((err) => { @@ -236,45 +223,32 @@ export const CreateSubscription: FC = ({ }, [info]); function renderButton() { - if (isSuccess) { - return ( - - - - - {t('subscription_sent')} - - - - ); - } - - if (isSending) { - return ( - - - - ); - } - if (isEdit || info?.isActive) { return ( - + ); } return ( - + redirectToActivity={false} + ref={footerRef} + /> ); } @@ -287,37 +261,33 @@ export const CreateSubscription: FC = ({ ); } - // ToDo: Сделать верстку, когда будет дизайн - if (failed) { - return Failed; - } - return ( <> - - - - {info.merchantName} - - - - {info.productName} - - - - {info.status === 'created' && ( + + + {info.merchantName} + + + + {info.productName} + + {info.status === 'created' && ( + <> + - )} - + + )} + = ({ /> )} - - {isButtonShown && {renderButton()}} - + {isButtonShown && renderButton()} ); } return ( - + {renderContent()} diff --git a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx index 80b68734a..7693153c8 100644 --- a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx +++ b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import { t } from '@tonkeeper/shared/i18n'; -import { Modal, Spacer } from '@tonkeeper/uikit'; +import { Modal, Spacer, WalletIcon } from '@tonkeeper/uikit'; import { openExploreTab, openRefillBatteryModal } from '$navigation'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Button, Icon, Text } from '$uikit'; @@ -14,6 +14,7 @@ import { useBatteryBalance } from '@tonkeeper/shared/query/hooks/useBatteryBalan import { config } from '$config'; import { Wallet } from '$wallet/Wallet'; import { AmountFormatter } from '@tonkeeper/core'; +import { tk } from '$wallet'; export interface InsufficientFundsParams { /** @@ -35,6 +36,7 @@ export interface InsufficientFundsParams { stakingFee?: string; fee?: string; isStakingDeposit?: boolean; + walletIdentifier?: string; } export const InsufficientFundsModal = memo((props) => { @@ -46,6 +48,7 @@ export const InsufficientFundsModal = memo((props) => { stakingFee, fee, isStakingDeposit, + walletIdentifier, } = props; const { balance: batteryBalance } = useBatteryBalance(); const nav = useNavigation(); @@ -120,6 +123,8 @@ export const InsufficientFundsModal = memo((props) => { ); }, [currency, fee, formattedAmount, formattedBalance, isStakingDeposit, stakingFee]); + const wallet = walletIdentifier ? tk.wallets.get(walletIdentifier)! : tk.wallet; + return ( @@ -128,7 +133,15 @@ export const InsufficientFundsModal = memo((props) => { - {t('txActions.signRaw.insufficientFunds.title')} + {tk.wallets.size > 1 ? ( + <> + {t('txActions.signRaw.insufficientFunds.title_multiwallet')}{' '} + {' '} + {wallet.config.name} + + ) : ( + t('txActions.signRaw.insufficientFunds.title') + )} {content} diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 071b32479..6552d6b32 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -6,7 +6,19 @@ import { calculateMessageTransferAmount, delay } from '$utils'; import { debugLog } from '$utils/debugLog'; import { t } from '@tonkeeper/shared/i18n'; import { Toast } from '$store'; -import { List, Modal, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; +import { + List, + Modal, + Spacer, + Steezy, + Text, + View, + WalletIcon, + isAndroid, + Icon, + ListItemContent, + TouchableOpacity, +} from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { @@ -29,7 +41,7 @@ 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 { getWalletSeqno, setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; import { useWalletCurrency } from '@tonkeeper/shared/hooks'; import { ActionAmountType, @@ -43,6 +55,9 @@ import { TokenDetailsParams } from '../../../../components/TokenDetails/TokenDet import { ModalStackRouteNames } from '$navigation'; import { CanceledActionError } from '$core/Send/steps/ConfirmStep/ActionErrors'; import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; +import { openAboutRiskAmountModal } from '@tonkeeper/shared/modals/AboutRiskAmountModal'; +import { toNano } from '@ton/core'; +import BigNumber from 'bignumber.js'; interface SignRawModalProps { consequences?: MessageConsequences; @@ -103,7 +118,7 @@ export const SignRawModal = memo((props) => { const boc = TransactionService.createTransfer(contract, { messages: TransactionService.parseSignRawMessages( params.messages, - isBattery ? await tk.wallet.battery.getExcessesAccount() : undefined, + isBattery ? tk.wallet.battery.excessesAccount : undefined, ), seqno: await getWalletSeqno(wallet), sendMode: 3, @@ -164,8 +179,7 @@ export const SignRawModal = memo((props) => { return { isNegative: formatter.isNegative(extra), value: formatter.format(extra, { - ignoreZeroTruncate: true, - withoutTruncate: true, + decimals: 4, postfix: 'TON', absolute: true, }), @@ -208,6 +222,27 @@ export const SignRawModal = memo((props) => { return undefined; }; + const totalRiskedAmount = useMemo(() => { + if (consequences?.risk) { + return wallet.compareWithTotal(consequences.risk.ton, consequences.risk.jettons); + } + }, [consequences, wallet]); + + const totalAmountTitle = useMemo(() => { + if (totalRiskedAmount) { + return ( + t('confirmSendModal.total_risk', { + totalAmount: formatter.format(totalRiskedAmount.totalFiat, { + currency: fiatCurrency, + }), + }) + + (consequences?.risk.nfts.length > 0 + ? ` + ${consequences?.risk.nfts.length} NFT` + : '') + ); + } + }, [consequences?.risk.nfts.length, fiatCurrency, totalRiskedAmount]); + return ( ((props) => { {t('confirmSendModal.wallet')} - - {wallet.config.emoji} - + {wallet.config.name} @@ -257,32 +294,59 @@ export const SignRawModal = memo((props) => { /> ))} + + + + } + subvalue={extra.fiat} + subtitle={isBattery && t('confirmSendModal.will_be_paid_with_battery')} + title={ + extra.isNegative + ? t('confirmSendModal.network_fee') + : t('confirmSendModal.refund') + } + value={`≈ ${extra.value}`} + /> - - - {extra.isNegative - ? t('confirmSendModal.network_fee') - : t('confirmSendModal.refund')} - - - ≈ {extra.value} · {extra.fiat} - - - {isBattery && ( - - - {t('confirmSendModal.will_be_paid_with_battery')} - - - )} + {totalRiskedAmount ? ( + <> + + + {totalAmountTitle} + + + openAboutRiskAmountModal( + totalAmountTitle!, + consequences?.risk.nfts.length > 0, + ) + } + > + + + + + + ) : null} ); @@ -325,16 +389,20 @@ export const openSignRawModal = async ( secretKey: Buffer.alloc(64), }); + const totalAmount = calculateMessageTransferAmount(params.messages); const { emulateResult, battery } = await emulateBoc( boc, - undefined, + [ + setBalanceForEmulation( + new BigNumber(totalAmount).plus(toNano('2').toString()).toString(), + ), + ], // Emulate with higher balance to calculate fair amount to send options.experimentalWithBattery, ); consequences = emulateResult; isBattery = battery; if (!isBattery) { - const totalAmount = calculateMessageTransferAmount(params.messages); const checkResult = await checkIsInsufficient(totalAmount, wallet); if (checkResult.insufficient) { Toast.hide(); @@ -342,6 +410,7 @@ export const openSignRawModal = async ( return openInsufficientFundsModal({ totalAmount, balance: checkResult.balance, + walletIdentifier, }); } } @@ -382,7 +451,15 @@ export const openSignRawModal = async ( } }; -const styles = Steezy.create({ +const styles = Steezy.create(({ colors }) => ({ + icon: { + width: 44, + height: 44, + borderRadius: 44 / 2, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.backgroundContentTint, + }, feeContainer: { paddingHorizontal: 32, paddingTop: 12, @@ -392,6 +469,7 @@ const styles = Steezy.create({ subtitleContainer: { flexDirection: 'row', gap: 4, + alignItems: 'center', }, withBatteryContainer: { paddingHorizontal: 32, @@ -399,4 +477,14 @@ const styles = Steezy.create({ actionsList: { marginBottom: 0, }, -}); + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, + totalAmountContainer: { + gap: 4, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, +})); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx index 936bab614..5a0bf6b9f 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx @@ -19,6 +19,8 @@ import { import { tk } from '$wallet'; import { TabsStackRouteNames } from '$navigation'; import { Wallet } from '$wallet/Wallet'; +import { NetworkOverloadedError } from '@tonkeeper/shared/utils/blockchain'; +import { SlideButton, Steezy } from '@tonkeeper/uikit'; enum States { INITIAL, @@ -95,7 +97,11 @@ export const useActionFooter = (wallet?: Wallet) => { (wallet ?? tk.wallet).activityList.reload(); }); } catch (error) { - if (error instanceof DismissedActionError) { + if (error instanceof NetworkOverloadedError) { + ref.current?.setError(error.message); + await delay(3500); + ref.current?.setState(States.INITIAL); + } else if (error instanceof DismissedActionError) { ref.current?.setState(States.ERROR); await delay(1750); ref.current?.setState(States.INITIAL); @@ -131,10 +137,12 @@ interface ActionFooterProps { responseOptions?: TxResponseOptions; withCloseButton?: boolean; confirmTitle?: string; + secondary?: boolean; onPressConfirm: () => Promise; onCloseModal?: () => void; disabled?: boolean; redirectToActivity?: boolean; + withSlider?: boolean; } export const ActionFooter = React.forwardRef( @@ -193,22 +201,33 @@ export const ActionFooter = React.forwardRef isVisible={state === States.INITIAL} entranceAnimation={false} > - - {withCloseButton ? ( - <> - closeModal(false)}> - {t('cancel')} - - - - ) : null} - props.onPressConfirm()} - > - {props.confirmTitle ?? t('nft_confirm_operation')} - - + {props.withSlider ? ( + + props.onPressConfirm()} + text={t('nft_operation_slide_to_confirm')} + /> + + ) : ( + + {withCloseButton ? ( + <> + closeModal(false)}> + {t('cancel')} + + + + ) : null} + props.onPressConfirm()} + mode={props.secondary ? 'secondary' : 'primary'} + > + {props.confirmTitle ?? t('nft_confirm_operation')} + + + )} }, ); +const styles = Steezy.create({ + slideContainer: { + paddingHorizontal: 16, + paddingVertical: 16, + }, +}); + export const NFTOperationFooter = ActionFooter; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts index 422469074..8c9e4d655 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts @@ -15,6 +15,7 @@ import { Ton } from '$libs/Ton'; import { Configuration, NFTApi } from '@tonkeeper/core/src/legacy'; import { tk } from '$wallet'; import { config } from '$config'; +import { sendBoc } from '@tonkeeper/shared/utils/blockchain'; const { NftItem } = TonWeb.token.nft; @@ -171,10 +172,7 @@ export class NFTOperations { const queryMsg = await methods.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - await tk.wallet.tonapi.blockchain.sendBlockchainMessage( - { boc }, - { format: 'text' }, - ); + await sendBoc(boc, false); onDone?.(boc); }, @@ -277,10 +275,7 @@ export class NFTOperations { const queryMsg = await transfer.getQuery(); const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - await tk.wallet.tonapi.blockchain.sendBlockchainMessage( - { boc }, - { format: 'text' }, - ); + await sendBoc(boc, false); }, }; } diff --git a/packages/mobile/src/core/NFTSend/NFTSend.tsx b/packages/mobile/src/core/NFTSend/NFTSend.tsx index 79ae333fc..ba62d509b 100644 --- a/packages/mobile/src/core/NFTSend/NFTSend.tsx +++ b/packages/mobile/src/core/NFTSend/NFTSend.tsx @@ -291,7 +291,7 @@ export const NFTSend: FC = (props) => { throw new CanceledActionError(); } - const excessesAccount = isBattery && (await wallet.battery.getExcessesAccount()); + const excessesAccount = isBattery && tk.wallet.battery.excessesAccount; const nftTransferMessages = [ internal({ diff --git a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts index 485ca184c..39e786921 100644 --- a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts +++ b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts @@ -98,3 +98,9 @@ export const Icon = styled(FastImage).attrs({ export const ItemSkeleton = styled.View` align-self: flex-end; `; + +export const WalletNameRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: flex-end; +`; diff --git a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx index ba0a1e898..b8eff459b 100644 --- a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx @@ -21,6 +21,7 @@ import { truncateDecimal } from '$utils'; import { BatteryState } from '@tonkeeper/shared/utils/battery'; import { useBatteryState } from '@tonkeeper/shared/query/hooks/useBatteryState'; import { tk } from '$wallet'; +import { Steezy, WalletIcon, isAndroid } from '@tonkeeper/uikit'; interface Props extends StepComponentProps { recipient: SendRecipient | null; @@ -106,9 +107,15 @@ const ConfirmStepComponent: FC = (props) => { {t('send_screen_steps.comfirm.wallet')} - - {tk.wallet.config.emoji} {tk.wallet.config.name} - + + + + {tk.wallet.config.name} + @@ -219,3 +226,10 @@ const ConfirmStepComponent: FC = (props) => { }; export const ConfirmStep = memo(ConfirmStepComponent); + +const styles = Steezy.create({ + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git a/packages/mobile/src/core/NFTs/AboutCollection/AboutCollection.tsx b/packages/mobile/src/core/NFTs/AboutCollection/AboutCollection.tsx deleted file mode 100644 index 5dd123444..000000000 --- a/packages/mobile/src/core/NFTs/AboutCollection/AboutCollection.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const AboutCollection: React.FC = ({ name, }) => { - -} diff --git a/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.interface.ts b/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.interface.ts deleted file mode 100644 index b2490242d..000000000 --- a/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MarketplaceBannerProps { - onButtonPress: () => void; -} diff --git a/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.style.ts b/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.style.ts deleted file mode 100644 index fd2fbcbf1..000000000 --- a/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.style.ts +++ /dev/null @@ -1,63 +0,0 @@ -import styled, { RADIUS } from '$styled'; -import { hNs, nfs, ns } from '$utils'; -import FastImage from 'react-native-fast-image'; - -export const Wrap = styled.View` - justify-content: space-between; - flex: 1; - margin: 0 ${hNs(32)}px 0 ${hNs(32)}px; -`; - -export const ButtonWrap = styled.View` - align-items: center; - justify-content: center; - flex: 0 0 auto; -`; - -export const Background = styled.View` - background: ${({ theme }) => theme.colors.backgroundSecondary}; - border-radius: ${ns(RADIUS.normal)}px; - position: absolute; - z-index: 1; - top: 0; - left: 0; - right: 0; - bottom: 0; -`; - -export const ImagesFirstContainer = styled.View` - flex-direction: row; - justify-content: center; - margin-bottom: ${ns(8)}px; -`; - -export const ImagesSecondContainer = styled.View` - flex-direction: row; - justify-content: center; - margin-bottom: ${ns(32)}px; -`; - -export const Cont = styled.View` - z-index: 2; - flex: 1; - align-items: center; - justify-content: center; -`; - -export const TextCont = styled.View` - align-items: center; -`; - -export const TitleWrapper = styled.View` - margin-bottom: ${ns(8)}px; -`; - -export const Image = styled(FastImage).attrs({ - resizeMode: 'cover', - priority: FastImage.priority.high, -})<{ isLast?: boolean }>` - width: ${ns(56)}px; - height: ${hNs(56)}px; - border-radius: ${ns(12)}px; - margin-right: ${({ isLast }) => ns(isLast ? 0 : 8)}px; -`; diff --git a/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.tsx b/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.tsx deleted file mode 100644 index e765d0d1f..000000000 --- a/packages/mobile/src/core/NFTs/MarketplaceBanner/MarketplaceBanner.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useContext } from 'react'; -import * as S from './MarketplaceBanner.style'; -import { t } from '@tonkeeper/shared/i18n'; -import {Button, ScrollPositionContext, Text} from '$uikit'; -import { MarketplaceBannerProps } from './MarketplaceBanner.interface'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -import { ns } from '$utils'; -import { useFocusEffect } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useFlags } from '$utils/flags'; - -const images = { - 1: [ - require('$assets/marketplaceBannerNFTs/1.png'), - require('$assets/marketplaceBannerNFTs/2.png'), - require('$assets/marketplaceBannerNFTs/3.png'), - ], - 2: [ - require('$assets/marketplaceBannerNFTs/4.png'), - require('$assets/marketplaceBannerNFTs/5.png'), - require('$assets/marketplaceBannerNFTs/6.png'), - require('$assets/marketplaceBannerNFTs/7.png'), - ], -}; - -export const MarketplaceBanner: React.FC = ({ - onButtonPress, -}) => { - const flags = useFlags(['disable_nft_markets']); - - const tabBarHeight = useBottomTabBarHeight(); - const { changeEnd } = useContext(ScrollPositionContext); - const { top: topInset } = useSafeAreaInsets(); - - useFocusEffect( - React.useCallback(() => { - changeEnd(true); - }, [changeEnd]), - ); - - return ( - - - - {images[1].map((img, idx, arr) => ( - - ))} - - - {images[2].map((img, idx, arr) => ( - - ))} - - - - - {t('nft_marketplace_banner_title')} - - - - { - flags.disable_nft_markets - ? t('disable_nft_marketplace_banner_description') - : t('nft_marketplace_banner_description') - } - - - {!flags.disable_nft_markets && ( - - )} - - - ); -}; diff --git a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.interface.ts b/packages/mobile/src/core/NFTs/NFTItem/NFTItem.interface.ts deleted file mode 100644 index 8dfa15c74..000000000 --- a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NFTModel } from '$store/models'; - -export interface NFTItemProps { - isLastInRow: boolean; - item: NFTModel; -}; diff --git a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.style.ts b/packages/mobile/src/core/NFTs/NFTItem/NFTItem.style.ts deleted file mode 100644 index 526c2731c..000000000 --- a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.style.ts +++ /dev/null @@ -1,150 +0,0 @@ -import styled, { RADIUS } from '$styled'; -import { ns } from '$utils'; -import FastImage from 'react-native-fast-image'; -import { Highlight } from '$uikit'; -import { Dimensions, StyleSheet } from 'react-native'; - -const deviceWidth = Dimensions.get('window').width; -export const NUM_OF_COLUMNS = Math.trunc(Math.max(2, Math.min(deviceWidth / 171, 3))); -const availableWidth = NUM_OF_COLUMNS === 2 ? (deviceWidth - ns(48)) / 2 : 171; // Padding and margin between NFTs - -export const Wrap = styled.View<{ withMargin: boolean }>` - width: ${availableWidth}px; - margin-right: ${({ withMargin }) => (withMargin ? ns(16) : 0)}px; - margin-bottom: ${ns(16)}px; -`; - -export const Background = styled.View` - background: ${({ theme }) => theme.colors.backgroundSecondary}; - border-radius: ${ns(RADIUS.normal)}px; - position: absolute; - z-index: 0; - top: 0; - left: 0; - right: 0; - bottom: 0; -`; - -export const TextWrap = styled.View` - z-index: 2; - padding: ${ns(10)}px ${ns(16)}px ${ns(12)}px ${ns(16)}px; -`; - -export const Pressable = styled(Highlight)` - border-radius: ${ns(RADIUS.normal)}px; -`; - -export const Image = styled(FastImage).attrs({ - resizeMode: 'cover', -})` - position: relative; - z-index: 2; - width: 100%; - height: ${ns(171)}px; - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.backgroundTertiary}; -`; - -export const DNSBackground = styled.View` - position: relative; - z-index: 2; - width: 100%; - height: ${ns(171)}px; - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.accentPrimary}; - padding-vertical: ${ns(16)}px; - padding-horizontal: ${ns(20)}px; -`; - -export const SmallImage = styled(FastImage).attrs({ - resizeMode: 'cover', -})` - z-index: 2; - flex: 1; - width: 100%; - height: 100%; - /* height: ${ns(114)}px; */ - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.backgroundTertiary}; -`; - -export const SmallDNSBackground = styled.View` - position: relative; - z-index: 2; - width: 100%; - height: ${ns(114)}px; - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.accentPrimary}; - padding-vertical: ${ns(16)}px; - padding-horizontal: ${ns(20)}px; -`; - -export const Badges = styled.View` - position: absolute; - bottom: ${8}px; - right: ${8}px; - flex-direction: row; - align-items: center; -`; - -export const FireBadge = styled.View` - position: absolute; - bottom: ${0}px; - right: ${0}px; - flex-direction: row; - align-items: center; -`; - -export const OnSaleBadge = styled.View` - position: absolute; - top: ${0}px; - right: ${0}px; - flex-direction: row; - align-items: center; -`; - -export const OnSaleBadgeIcon = styled.Image.attrs({ - source: require('$assets/sale_badge_32.png'), -})` - width: ${ns(32)}px; - height: ${ns(32)}px; -`; - -export const AppearanceBadge = styled.View` - width: ${ns(32)}px; - height: ${ns(32)}px; - border-radius: ${ns(32 / 2)}px; - background: ${({ theme }) => theme.colors.backgroundSecondary}; - align-items: center; - justify-content: center; -`; - -export const DNSBadge = styled.View` - width: ${ns(32)}px; - height: ${ns(32)}px; - border-radius: ${ns(32 / 2)}px; - background: rgba(255, 255, 255, 0.2); - align-items: center; - justify-content: center; -`; - -export const textStyles = StyleSheet.create({ - domainText: { - fontWeight: '700', - fontSize: 20, - lineHeight: 26, - }, - domainZoneText: { - opacity: 0.72, - }, -}); - -export const CollectionNameWrap = styled.View<{ withIcon?: boolean }>` - flex-direction: row; - align-items: center; - padding-right: ${({ withIcon }) => (!withIcon ? 0 : ns(12))}px; -`; diff --git a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.tsx b/packages/mobile/src/core/NFTs/NFTItem/NFTItem.tsx deleted file mode 100644 index a2f4322d2..000000000 --- a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { NFTItemProps } from '$core/NFTs/NFTItem/NFTItem.interface'; -import * as S from './NFTItem.style'; -import { openNFT } from '$navigation'; -import { checkIsTonDiamondsNFT } from '$utils'; -import _ from 'lodash'; -import { Icon, Text } from '$uikit'; -import { t } from '@tonkeeper/shared/i18n'; -import { useFlags } from '$utils/flags'; -import { Address, DNS, KnownTLDs } from '@tonkeeper/core'; - -export const NFTItem: React.FC = ({ item, isLastInRow }) => { - const flags = useFlags(['disable_apperance']); - - const isTonDiamondsNft = checkIsTonDiamondsNFT(item); - const isOnSale = useMemo(() => !!item.sale, [item.sale]); - - const isTG = DNS.getTLD(item.dns || item.name) === KnownTLDs.TELEGRAM; - const isDNS = !!item.dns && !isTG; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleOpenNftItem = useCallback( - _.throttle(() => openNFT({ currency: item.currency, address: item.address }), 1000), - [item], - ); - - const title = useMemo(() => { - if (isDNS) { - return item.dns; - } - - return item.name || Address.toShort(item.address); - }, [isDNS, item.dns, item.name, item.address]); - - const renderPicture = () => { - if (isDNS) { - return ( - - - {item.dns?.replace('.ton', '')} - - - {'.ton'} - - {isOnSale ? : null} - - - - - - - ); - } - - return ( - - {isOnSale ? : null} - - {isTonDiamondsNft && !flags.disable_apperance ? ( - - - - ) : null} - - - ); - }; - - return ( - - - - {renderPicture()} - - - {title} - - - - {isDNS - ? 'TON DNS' - : item?.collection - ? item.collection.name - : t('nft_single_nft')} - - {item.isApproved && ( - - )} - - - - - ); -}; diff --git a/packages/mobile/src/core/Notifications/Notification.tsx b/packages/mobile/src/core/Notifications/Notification.tsx index 15afa856b..033f94db8 100644 --- a/packages/mobile/src/core/Notifications/Notification.tsx +++ b/packages/mobile/src/core/Notifications/Notification.tsx @@ -192,7 +192,7 @@ export const Notification: React.FC = (props) => { }, [props.closeOtherSwipeable]); return ( - + ({ listStyle: { marginBottom: 0, }, - containerStyle: { - marginBottom: 8, - }, leftContentStyle: { alignItems: 'flex-start', alignSelf: 'flex-start', diff --git a/packages/mobile/src/core/Notifications/Notifications.tsx b/packages/mobile/src/core/Notifications/Notifications.tsx index 54676f1e7..a03c561e9 100644 --- a/packages/mobile/src/core/Notifications/Notifications.tsx +++ b/packages/mobile/src/core/Notifications/Notifications.tsx @@ -9,19 +9,19 @@ import { View, } from '$uikit'; import { ns } from '$utils'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { CellSection } from '$shared/components'; import { t } from '@tonkeeper/shared/i18n'; import { useConnectedAppsList } from '$store'; import { Steezy } from '$styles'; import { SwitchDAppNotifications } from '$core/Notifications/SwitchDAppNotifications'; import { useNotificationsSwitch } from '$hooks/useNotificationsSwitch'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Notifications: React.FC = () => { - const tabBarHeight = useBottomTabBarHeight(); const connectedApps = useConnectedAppsList(); const { isSubscribed, isDenied, openSettings, toggleNotifications } = useNotificationsSwitch(); + const insets = useSafeAreaInsets(); return ( @@ -29,7 +29,7 @@ export const Notifications: React.FC = () => { {isDenied && ( diff --git a/packages/mobile/src/core/Notifications/NotificationsActivity.tsx b/packages/mobile/src/core/Notifications/NotificationsActivity.tsx index b557a98da..0d79fb960 100644 --- a/packages/mobile/src/core/Notifications/NotificationsActivity.tsx +++ b/packages/mobile/src/core/Notifications/NotificationsActivity.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import { Button, Icon, Screen, Spacer, Text, View } from '$uikit'; +import { Button, Icon, Spacer, Text, View } from '$uikit'; import { Notification } from '$core/Notifications/Notification'; import { Steezy } from '$styles'; import { openNotifications } from '$navigation'; @@ -8,6 +8,7 @@ import { INotification, useDAppsNotifications } from '$store'; import { FlashList } from '@shopify/flash-list'; import { LayoutAnimation } from 'react-native'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import { Screen } from '@tonkeeper/uikit'; export enum ActivityListItem { Notification = 'Notification', @@ -51,7 +52,6 @@ export const NotificationsActivity: React.FC = () => { const list = useRef | null>(null); const closeOtherSwipeable = useRef void)>(null); const lastSwipeableId = useRef(null); - const tabBarHeight = useBottomTabBarHeight(); const handleOpenNotificationSettings = useCallback(() => { openNotifications(); @@ -137,10 +137,10 @@ export const NotificationsActivity: React.FC = () => { ) : ( item.id} renderItem={renderNotificationsItem} - contentContainerStyle={{ paddingBottom: tabBarHeight + 8 }} data={flashListData} ListEmptyComponent={ListEmpty} /> @@ -154,6 +154,9 @@ const styles = Steezy.create({ marginVertical: 14, marginHorizontal: 16, }, + gap8: { + gap: 8, + }, emptyContainer: { paddingHorizontal: 32, flex: 1, diff --git a/packages/mobile/src/core/Security/Security.tsx b/packages/mobile/src/core/Security/Security.tsx index 922d72fa9..8e704f244 100644 --- a/packages/mobile/src/core/Security/Security.tsx +++ b/packages/mobile/src/core/Security/Security.tsx @@ -1,6 +1,5 @@ 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 S from './Security.style'; @@ -14,9 +13,10 @@ import { useBiometrySettings, useWallet } from '@tonkeeper/shared/hooks'; import { useNavigation } from '@tonkeeper/router'; import { vault } from '$wallet'; import { Haptics, Switch } from '@tonkeeper/uikit'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Security: FC = () => { - const tabBarHeight = useBottomTabBarHeight(); + const insets = useSafeAreaInsets(); const wallet = useWallet(); const nav = useNavigation(); @@ -93,7 +93,7 @@ export const Security: FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: ns(16), - paddingBottom: tabBarHeight, + paddingBottom: insets.bottom + 16, }} scrollEventThrottle={16} > diff --git a/packages/mobile/src/core/Send/Send.tsx b/packages/mobile/src/core/Send/Send.tsx index fab73db27..a95c7797f 100644 --- a/packages/mobile/src/core/Send/Send.tsx +++ b/packages/mobile/src/core/Send/Send.tsx @@ -52,6 +52,7 @@ import { import { getTimeSec } from '$utils/getTimeSec'; import { Toast } from '$store'; import { config } from '$config'; +import { NetworkOverloadedError } from '@tonkeeper/shared/utils/blockchain'; const tokensWithAllowedEncryption = [TokenType.TON, TokenType.Jetton]; @@ -259,18 +260,20 @@ export const Send: FC = ({ route }) => { setPreparing(false); } }, [ - amount, - comment, - currency, - decimals, + recipient, dispatch, - isCommentEncrypted, + currencyAdditionalParams, + currency, + parsedAmount, + amount.all, + amount.value, tokenType, jettonWalletAddress, - parsedAmount, - recipient, - trcPayload, + decimals, + comment, + isCommentEncrypted, trcToken, + trcPayload, ]); const unlock = useUnlockVault(); @@ -323,9 +326,13 @@ export const Send: FC = ({ route }) => { setSending(false); onDone(); }, - onFail: () => { + onFail: (error) => { setSending(false); - onFail(new DismissedActionError()); + onFail( + error instanceof NetworkOverloadedError + ? error + : new DismissedActionError(), + ); }, }), ); diff --git a/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx b/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx index 770caa4e4..cad9853d0 100644 --- a/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx +++ b/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx @@ -100,7 +100,7 @@ const AddressStepComponent: FC = (props) => { return new TonWeb.Address(resolvedDomain.wallet.address).toString( true, true, - true, + false, ) as string; } diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts index 6477a0a47..96de5f44f 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts @@ -104,3 +104,9 @@ export const WarningRow = styled.View` export const WarningIcon = styled.View` margin-top: ${ns(2)}px; `; + +export const WalletNameRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: flex-end; +`; diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx index c9478a2ae..0c3d9b829 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx @@ -25,6 +25,7 @@ import { BatteryState } from '@tonkeeper/shared/utils/battery'; import { TokenType } from '$core/Send/Send.interface'; import { useBalancesState, useWallet } from '@tonkeeper/shared/hooks'; import { tk } from '$wallet'; +import { Steezy, WalletIcon, isAndroid } from '@tonkeeper/uikit'; const ConfirmStepComponent: FC = (props) => { const { @@ -226,9 +227,15 @@ const ConfirmStepComponent: FC = (props) => { {t('send_screen_steps.comfirm.wallet')} - - {tk.wallet.config.emoji} {tk.wallet.config.name} - + + + + {tk.wallet.config.name} + @@ -370,3 +377,10 @@ const ConfirmStepComponent: FC = (props) => { }; export const ConfirmStep = memo(ConfirmStepComponent); + +const styles = Steezy.create({ + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index da00ea3d4..ba607996e 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -3,13 +3,11 @@ import { useDispatch } from 'react-redux'; import Rate, { AndroidMarket } from 'react-native-rate'; 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'; import { TapGestureHandler } from 'react-native-gesture-handler'; import * as S from './Settings.style'; -import { Icon, PopupSelect, ScrollHandler, Spacer, Text } from '$uikit'; -import { Icon as NewIcon } from '@tonkeeper/uikit'; +import { Icon, PopupSelect, Spacer, Text } from '$uikit'; +import { Icon as NewIcon, Screen } from '@tonkeeper/uikit'; import { useShouldShowTokensButton } from '$hooks/useShouldShowTokensButton'; import { useNavigation } from '@tonkeeper/router'; import { List } from '@tonkeeper/uikit'; @@ -17,7 +15,6 @@ import { AppStackRouteNames, MainStackRouteNames, SettingsStackRouteNames, - openDeleteAccountDone, openDevMenu, openLegalDocuments, openManageTokens, @@ -42,7 +39,6 @@ import { SearchEngine, useBrowserStore } from '$store'; import AnimatedLottieView from 'lottie-react-native'; import { Steezy } from '$styles'; import { i18n, t } from '@tonkeeper/shared/i18n'; -import { trackEvent } from '$utils/stats'; import { openAppearance } from '$core/ModalContainer/AppearanceModal'; import { config } from '$config'; import { @@ -56,10 +52,12 @@ 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'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Settings: FC = () => { const animationRef = useRef(null); const devMenuHandlerRef = useRef(null); + const { bottom: paddingBottom } = useSafeAreaInsets(); const flags = useFlags([ 'disable_apperance', @@ -70,7 +68,6 @@ export const Settings: FC = () => { ]); const nav = useNavigation(); - const tabBarHeight = useBottomTabBarHeight(); const fiatCurrency = useWalletCurrency(); const dispatch = useDispatch(); @@ -126,20 +123,8 @@ export const Settings: FC = () => { }, []); const handleResetWallet = useCallback(() => { - Alert.alert(t('settings_reset_alert_title'), t('settings_reset_alert_caption'), [ - { - text: t('cancel'), - style: 'cancel', - }, - { - text: t('settings_reset_alert_button'), - style: 'destructive', - onPress: () => { - dispatch(walletActions.cleanWallet()); - }, - }, - ]); - }, [dispatch]); + nav.navigate('/logout-warning'); + }, [nav]); const handleStopWatchWallet = useCallback(() => { Alert.alert(t('settings_delete_watch_account'), undefined, [ @@ -211,25 +196,8 @@ export const Settings: FC = () => { }, []); const handleDeleteAccount = useCallback(() => { - Alert.alert( - t('settings_delete_alert_title', { space: Platform.OS === 'ios' ? '\n' : ' ' }), - t('settings_delete_alert_caption'), - [ - { - text: t('cancel'), - style: 'cancel', - }, - { - text: t('settings_delete_alert_button'), - style: 'destructive', - onPress: () => { - trackEvent('delete_wallet'); - openDeleteAccountDone(); - }, - }, - ], - ); - }, []); + nav.openModal('/logout-warning', { isDelete: true }); + }, [nav]); const handleCustomizePress = useCallback( () => nav.navigate(AppStackRouteNames.CustomizeWallet), @@ -251,7 +219,7 @@ export const Settings: FC = () => { const accountNfts = useNftsState((s) => s.accountNfts); const hasDiamods = useMemo(() => { - if (!wallet || wallet.isWatchOnly) { + if (!wallet || wallet?.isWatchOnly) { return false; } @@ -266,13 +234,12 @@ export const Settings: FC = () => { return ( - - + + @@ -323,7 +290,7 @@ export const Settings: FC = () => { onPress={handleManageTokens} /> )} - {hasSubscriptions && ( + {!!wallet && !wallet.isWatchOnly && hasSubscriptions && ( { - - - + + ); }; diff --git a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts index ce064bf96..9271de075 100644 --- a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts +++ b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts @@ -90,10 +90,10 @@ export const DetailsButtonContainer = styled.View` export const HeaderWrap = styled.View` align-items: center; - padding-horizontal: ${ns(16)}px; `; export const FlexRow = styled.View` + padding-horizontal: ${ns(12)}px; flex-direction: row; justify-content: space-between; margin-top: ${ns(16)}px; diff --git a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx index e2717c095..c6db0f3d4 100644 --- a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx +++ b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx @@ -9,7 +9,6 @@ import { import { Button, Highlight, - IconButton, ScrollHandler, Spacer, StakedTonIcon, @@ -27,12 +26,11 @@ import { t } from '@tonkeeper/shared/i18n'; import { useFlag } from '$utils/flags'; import { formatter } from '@tonkeeper/shared/formatter'; import { IStakingLink, StakingLinkType } from './types'; -import { Icon, List, Steezy } from '@tonkeeper/uikit'; +import { ActionButtons, Icon, List, Steezy } from '@tonkeeper/uikit'; import { getLinkIcon, getLinkTitle, getSocialLinkType } from './utils'; import { SafeAreaView } from 'react-native-safe-area-context'; 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'; @@ -154,19 +152,6 @@ export const StakingPoolDetails: FC = (props) => { const isLiquidTF = pool.implementation === PoolImplementationType.LiquidTF; - const nextReward = useMemo(() => { - if (!stakingJetton || !pool.cycle_length) { - return; - } - - return formatter.format( - new BigNumber(balance.totalTon) - .multipliedBy(pool.apy / 100) - .dividedBy(31536000) - .multipliedBy(pool.cycle_length), - ); - }, [balance.totalTon, pool.apy, pool.cycle_length, stakingJetton]); - const stakingJettonView = useMemo( () => stakingJettonMetadata ? ( @@ -249,27 +234,24 @@ export const StakingPoolDetails: FC = (props) => { - - {!isWatchOnly ? ( - <> - - - - - - - - ) : null} + {hasPendingDeposit ? ( <> @@ -336,23 +318,11 @@ export const StakingPoolDetails: FC = (props) => { ) : null} - {/* {hasAnyBalance && stakingJetton && isLiquidTF ? ( - <> - - - - - - ) : null} */} - - {t('staking.details.about_pool')} - {stakingJettonMetadata && hasAnyBalance ? ( <> {stakingJettonView} - - + ) : null} diff --git a/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.style.ts b/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.style.ts index 92e7d7acf..7afe38ee4 100644 --- a/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.style.ts +++ b/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.style.ts @@ -81,3 +81,9 @@ export const Icon = styled(FastImage).attrs({ export const ItemSkeleton = styled.View` align-self: flex-end; `; + +export const WalletNameRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: flex-end; +`; diff --git a/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.tsx index 8e6c4684a..d9da8fa0b 100644 --- a/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/StakingSend/steps/ConfirmStep/ConfirmStep.tsx @@ -21,6 +21,7 @@ import { t } from '@tonkeeper/shared/i18n'; import { PoolInfo } from '@tonkeeper/core/src/TonAPI'; import { SkeletonLine } from '$uikit/Skeleton/SkeletonLine'; import { tk } from '$wallet'; +import { Steezy, WalletIcon, isAndroid } from '@tonkeeper/uikit'; interface Props extends StepComponentProps { transactionType: StakingTransactionType; @@ -119,9 +120,15 @@ const ConfirmStepComponent: FC = (props) => { {t('send_screen_steps.comfirm.wallet')} - - {tk.wallet.config.emoji} {tk.wallet.config.name} - + + + + {tk.wallet.config.name} + @@ -212,3 +219,10 @@ const ConfirmStepComponent: FC = (props) => { }; export const ConfirmStep = memo(ConfirmStepComponent); + +const styles = Steezy.create({ + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git a/packages/mobile/src/core/Wallet/ToncoinScreen.tsx b/packages/mobile/src/core/Wallet/ToncoinScreen.tsx index 1f7aec007..b52a3b131 100644 --- a/packages/mobile/src/core/Wallet/ToncoinScreen.tsx +++ b/packages/mobile/src/core/Wallet/ToncoinScreen.tsx @@ -6,17 +6,17 @@ import { Button, PopupMenu, PopupMenuItem } from '$uikit'; import { MainStackRouteNames, openDAppBrowser, openSend } from '$navigation'; import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; import { walletActions } from '$store/wallet'; -import { Linking, View } from 'react-native'; +import { View } from 'react-native'; import { delay, ns } from '$utils'; import { CryptoCurrencies, CryptoCurrency, Decimals } from '$shared/constants'; -import { i18n, t } from '@tonkeeper/shared/i18n'; +import { t } from '@tonkeeper/shared/i18n'; import { useNavigation } from '@tonkeeper/router'; import { Chart } from '$shared/components/Chart/new/Chart'; import { formatter } from '$utils/formatter'; import { Toast } from '$store'; import { useFlags } from '$utils/flags'; import { HideableAmount } from '$core/HideableAmount/HideableAmount'; -import { Icon, Screen, TonIcon, IconButton, IconButtonList } from '@tonkeeper/uikit'; +import { Icon, Screen, TonIcon, ActionButtons, Spacer } from '@tonkeeper/uikit'; import { ActivityList } from '@tonkeeper/shared/components'; import { useTonActivityList } from '@tonkeeper/shared/query/hooks/useTonActivityList'; @@ -114,22 +114,6 @@ const HeaderList = memo(() => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleOpenAction = useCallback(async (action: any) => { - try { - let shouldOpenInBrowser = action.openInBrowser; - if (action.scheme) { - shouldOpenInBrowser = await Linking.canOpenURL(action.scheme); - } - if (shouldOpenInBrowser) { - return Linking.openURL(action.url); - } - openDAppBrowser(action.url); - } catch (e) { - console.log(e); - openDAppBrowser(action.url); - } - }, []); - const { amount, tokenPrice } = useWalletInfo(); const handleReceive = useCallback(() => { @@ -147,13 +131,6 @@ const HeaderList = memo(() => { openSend({ currency: 'ton' }); }, [wallet]); - const handleOpenExchange = useCallback(() => { - if (!wallet) { - return openRequireWalletModal(); - } - nav.openModal('Exchange'); - }, [nav, wallet]); - const handlePressSwap = useCallback(() => { if (wallet) { nav.openModal('Swap'); @@ -196,39 +173,36 @@ const HeaderList = memo(() => { - - - - {!isWatchOnly ? ( - - ) : null} - - {!isWatchOnly ? ( - - ) : null} - {!flags.disable_swap && !isWatchOnly && ( - - )} - - - + + + + + {shouldShowChart && ( <> diff --git a/packages/mobile/src/core/Wallet/Wallet.style.ts b/packages/mobile/src/core/Wallet/Wallet.style.ts index 3a7d33303..f7de96af4 100644 --- a/packages/mobile/src/core/Wallet/Wallet.style.ts +++ b/packages/mobile/src/core/Wallet/Wallet.style.ts @@ -11,6 +11,10 @@ export const Header = styled.View` margin-horizontal: ${ns(-16)}px; `; +export const ActionsWrap = styled.View` + padding-horizontal: ${ns(16)}px; +`; + export const TokenInfoWrap = styled.View` align-items: center; padding-horizontal: ${ns(28)}px; @@ -24,7 +28,6 @@ export const FlexRow = styled.View` flex-direction: row; justify-content: space-between; margin-top: ${ns(16)}px; - margin-bottom: ${ns(28)}px; `; export const ExploreButtons = styled.View` diff --git a/packages/mobile/src/hooks/useHaveNfts.ts b/packages/mobile/src/hooks/useHaveNfts.ts new file mode 100644 index 000000000..375cd6cce --- /dev/null +++ b/packages/mobile/src/hooks/useHaveNfts.ts @@ -0,0 +1,6 @@ +import { useApprovedNfts } from '$hooks/useApprovedNfts'; + +export function useHaveNfts(): boolean { + const nfts = useApprovedNfts(); + return nfts.enabled.length > 0; +} diff --git a/packages/mobile/src/modals/ExchangeModal.tsx b/packages/mobile/src/modals/ExchangeModal.tsx index 711c1c9fb..7f1653d21 100644 --- a/packages/mobile/src/modals/ExchangeModal.tsx +++ b/packages/mobile/src/modals/ExchangeModal.tsx @@ -13,6 +13,7 @@ import { openChooseCountry } from '$navigation'; import { useSelectedCountry } from '$store/zustand/methodsToBuy/useSelectedCountry'; import { CountryButton } from '@tonkeeper/shared/components'; import { config } from '$config'; +import { useWallet } from '@tonkeeper/shared/hooks'; export const ExchangeModal = () => { const [showAll, setShowAll] = React.useState(false); @@ -65,6 +66,9 @@ export const ExchangeModal = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); }, [showAll]); + const wallet = useWallet(); + const watchOnly = wallet && wallet.isWatchOnly; + const [segmentIndex, setSegmentIndex] = useState(0); const isLoading = buy.length === 0 && sell.length === 0; @@ -79,11 +83,15 @@ export const ExchangeModal = () => { } title={ - setSegmentIndex(segment)} - index={segmentIndex} - items={[t('exchange_modal.buy'), t('exchange_modal.sell')]} - /> + watchOnly ? ( + categories && categories[0] && categories[0].title + ) : ( + setSegmentIndex(segment)} + index={segmentIndex} + items={[t('exchange_modal.buy'), t('exchange_modal.sell')]} + /> + ) } /> @@ -97,9 +105,11 @@ export const ExchangeModal = () => { {categories.map((category, cIndex) => ( {cIndex > 0 ? : null} - - {category.title} - + {!(watchOnly && cIndex === 0) ? ( + + {category.title} + + ) : null} {category.items.map((item, idx, arr) => ( = memo((props) => { + const { isDelete } = props; + + const nav = useNavigation(); + const dispatch = useDispatch(); + + const [checked, setChecked] = useState(false); + + const handleContinue = useCallback(() => { + nav.closeModal?.(); + + if (isDelete) { + trackEvent('delete_wallet'); + InteractionManager.runAfterInteractions(() => { + openDeleteAccountDone(); + }); + } else { + InteractionManager.runAfterInteractions(() => { + dispatch(walletActions.cleanWallet()); + }); + } + }, [dispatch, isDelete, nav]); + + const handleBackup = useCallback(() => { + nav.replaceModal('/backup-warning'); + }, [nav]); + + return ( + + + + + + {isDelete ? t('logout_modal.delete_title') : t('logout_modal.title')} + + + + {isDelete ? t('logout_modal.delete_caption') : t('logout_modal.caption')} + + + setChecked((s) => !s)} /> + + + setChecked((s) => !s)}> + {t('logout_modal.agreement')} + + + + + {t('logout_modal.backup_button')} + + + + + } + hideBackButton /> diff --git a/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx b/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx index 59f73b1c1..71f0242b8 100644 --- a/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx +++ b/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx @@ -11,6 +11,7 @@ import { RouteProp } from '@react-navigation/native'; import { Address } from '@tonkeeper/shared/Address'; import { formatter } from '@tonkeeper/shared/formatter'; import { useImportWallet } from '$hooks/useImportWallet'; +import { DEFAULT_WALLET_VERSION } from '$wallet/constants'; export const ChooseWallets: FC<{ route: RouteProp; @@ -22,7 +23,10 @@ export const ChooseWallets: FC<{ const [selectedVersions, setSelectedVersions] = useState( walletsInfo - .filter((item) => item.balance > 0 || item.tokens) + .filter( + (item) => + item.balance > 0 || item.tokens || item.version === DEFAULT_WALLET_VERSION, + ) .map((item) => item.version), ); const [loading, setLoading] = useState(false); diff --git a/packages/mobile/src/screens/StartScreen/StartScreen.tsx b/packages/mobile/src/screens/StartScreen/StartScreen.tsx index 6431b3ca8..58cc6eecb 100644 --- a/packages/mobile/src/screens/StartScreen/StartScreen.tsx +++ b/packages/mobile/src/screens/StartScreen/StartScreen.tsx @@ -16,6 +16,9 @@ import { MainStackRouteNames } from '$navigation'; import { useDispatch } from 'react-redux'; import { walletActions } from '$store/wallet'; import { useNavigation } from '@tonkeeper/router'; +import { network } from '@tonkeeper/core'; +import { config } from '$config'; +import DeviceInfo from 'react-native-device-info'; const HEIGHT_RATIO = deviceHeight / 844; @@ -31,10 +34,30 @@ export const StartScreen = memo(() => { const logoShapesPosX = origShapesWidth / 2 - dimensions.width / 2; const logoShapesPosY = origShapesHeight / 2 - (origShapesHeight * ratioHeight) / 2; + const unsubscribeNotifications = useCallback(async () => { + // unsubscribe from all notifications, if app was reinstalled + try { + const deviceId = DeviceInfo.getUniqueId(); + const endpoint = `${config.get('tonapiIOEndpoint')}/unsubscribe`; + + await network.post(endpoint, { + params: { + device: deviceId, + }, + }); + } catch {} + }, []); + const handleCreatePress = useCallback(() => { dispatch(walletActions.generateVault()); + unsubscribeNotifications(); nav.navigate(MainStackRouteNames.CreateWalletStack); - }, [dispatch, nav]); + }, [dispatch, nav, unsubscribeNotifications]); + + const handleImportPress = useCallback(() => { + unsubscribeNotifications(); + nav.navigate(MainStackRouteNames.ImportWalletStack); + }, [nav, unsubscribeNotifications]); return ( @@ -79,7 +102,7 @@ export const StartScreen = memo(() => {