diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 103255b99..ad50aa3f5 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 600 - versionName "4.5.1" + versionName "4.5.2" 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 64ec64f4a..b42878e74 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -1298,7 +1298,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.5.1; + MARKETING_VERSION = 4.5.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1332,7 +1332,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.5.1; + MARKETING_VERSION = 4.5.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/packages/mobile/src/config/index.ts b/packages/mobile/src/config/index.ts index a997ff7c1..1c6172e96 100644 --- a/packages/mobile/src/config/index.ts +++ b/packages/mobile/src/config/index.ts @@ -50,6 +50,7 @@ export type AppConfigVars = { batteryTestnetHost: string; batteryMeanFees: string; batteryReservedAmount: string; + batteryMaxInputAmount: string; batteryMeanPrice_swap: string; batteryMeanPrice_jetton: string; batteryMeanPrice_nft: string; @@ -108,6 +109,7 @@ const defaultConfig: Partial = { batteryTestnetHost: 'https://testnet-battery.tonkeeper.com', batteryMeanFees: '0.0055', batteryReservedAmount: '0.3', + batteryMaxInputAmount: '3', batteryMeanPrice_swap: '0.22', batteryMeanPrice_jetton: '0.06', batteryMeanPrice_nft: '0.03', diff --git a/packages/mobile/src/core/BatterySend/BatterySend.tsx b/packages/mobile/src/core/BatterySend/BatterySend.tsx index 87fe61b33..93f9defef 100644 --- a/packages/mobile/src/core/BatterySend/BatterySend.tsx +++ b/packages/mobile/src/core/BatterySend/BatterySend.tsx @@ -10,6 +10,7 @@ import { Screen, Spacer, Steezy, + Toast, TouchableOpacity, View, } from '@tonkeeper/uikit'; @@ -118,6 +119,18 @@ export const BatterySend: React.FC = ({ route }) => { Keyboard.dismiss(); const parsedAmount = parseLocaleNumber(amount.value); + if (BigNumber(parsedAmount).isGreaterThan(rechargeMethod.maxInputAmount)) { + return Toast.fail( + t('battery.max_input_amount', { + amount: formatter.format(rechargeMethod.maxInputAmount, { + decimals: 0, + currency: rechargeMethod.symbol, + forceRespectDecimalPlaces: true, + }), + }), + ); + } + const commentCell = beginCell() .storeUint(0, 32) .storeStringTail(recipient?.address ?? '') diff --git a/packages/mobile/src/core/BatterySend/hooks/useRechargeMethod.tsx b/packages/mobile/src/core/BatterySend/hooks/useRechargeMethod.tsx index b52c4b5c1..16aba4c22 100644 --- a/packages/mobile/src/core/BatterySend/hooks/useRechargeMethod.tsx +++ b/packages/mobile/src/core/BatterySend/hooks/useRechargeMethod.tsx @@ -14,6 +14,7 @@ export interface IRechargeMethod extends RechargeMethod { fromTon: (amount: number | string) => number; isTon: boolean; minInputAmount: string; + maxInputAmount: string; iconSource: ImageProps['source']; balance: string; isGreaterThanBalance: (amount: string) => () => void; @@ -70,6 +71,7 @@ export function useRechargeMethod(rechargeMethod: RechargeMethod): IRechargeMeth ); const minInputAmount = fromTon(config.get('batteryReservedAmount')).toString(); + const maxInputAmount = fromTon(config.get('batteryMaxInputAmount')).toString(); return useMemo( () => ({ @@ -78,6 +80,7 @@ export function useRechargeMethod(rechargeMethod: RechargeMethod): IRechargeMeth fromTon, isTon, minInputAmount, + maxInputAmount, iconSource, balance, isGreaterThanBalance, @@ -88,6 +91,7 @@ export function useRechargeMethod(rechargeMethod: RechargeMethod): IRechargeMeth fromTon, isTon, minInputAmount, + maxInputAmount, iconSource, balance, isGreaterThanBalance, diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index b81d29421..ef7ee37ce 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -6,17 +6,17 @@ import { debugLog } from '$utils/debugLog'; import { t } from '@tonkeeper/shared/i18n'; import { Toast } from '$store'; import { + Icon, + isAndroid, List, + ListItemContent, Modal, Spacer, Steezy, Text, + TouchableOpacity, View, WalletIcon, - isAndroid, - Icon, - ListItemContent, - TouchableOpacity, } from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; import { SheetActions, useNavigation } from '@tonkeeper/router'; @@ -27,7 +27,7 @@ import { import { TonConnectRemoteBridge } from '$tonconnect/TonConnectRemoteBridge'; import { formatter } from '$utils/formatter'; import { tk } from '$wallet'; -import { MessageConsequences } from '@tonkeeper/core/src/TonAPI'; +import { ActionStatusEnum, MessageConsequences } from '@tonkeeper/core/src/TonAPI'; import { Address, TransactionService } from '@tonkeeper/core'; import { ActionListItemByType } from '@tonkeeper/shared/components/ActivityList/ActionListItemByType'; import { useGetTokenPrice } from '$hooks/useTokenPrice'; @@ -235,6 +235,11 @@ export const SignRawModal = memo((props) => { } }, [consequences?.risk.nfts.length, fiatCurrency, totalRiskedAmount]); + const hasFailedSwap = actions.some( + (action) => + action.status === ActionStatusEnum.Failed && action.type === ActionType.JettonSwap, + ); + return ( ((props) => { { const nfts = useNftsState((s) => Object.values(s.accountNfts).filter( (nft) => + !nft.sale && nft.collection && Address.compare(nft.collection.address, config.get('notcoin_nft_collection')), ), diff --git a/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts b/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts index c85a631b2..8e263758c 100644 --- a/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts +++ b/packages/mobile/src/core/Send/hooks/useSuggestedAddresses.ts @@ -8,6 +8,7 @@ import { Address } from '@tonkeeper/core'; import { tk } from '$wallet'; import { useWallet } from '@tonkeeper/shared/hooks'; import { ActionItem, ActionType } from '$wallet/models/ActivityModel'; +import { compareAddresses } from '$utils/address'; export const DOMAIN_ADDRESS_NOT_FOUND = 'DOMAIN_ADDRESS_NOT_FOUND'; @@ -76,6 +77,10 @@ export const useSuggestedAddresses = () => { const rawAddress = Address.parse(recipientAddress).toRaw(); + if (compareAddresses(tk.wallet.battery.state.data.fundReceiver, rawAddress)) { + return false; + } + if ( hiddenRecentAddresses.some((addr) => Address.compare(addr, rawAddress)) || isFavorite diff --git a/packages/mobile/src/modals/BurnVouchersModal.tsx b/packages/mobile/src/modals/BurnVouchersModal.tsx index 9657fe1e3..f8b005441 100644 --- a/packages/mobile/src/modals/BurnVouchersModal.tsx +++ b/packages/mobile/src/modals/BurnVouchersModal.tsx @@ -36,6 +36,7 @@ export const BurnVouchersModal = memo((props) => { const nfts = useNftsState((s) => Object.values(s.accountNfts).filter( (nft) => + !nft.sale && nft.collection && Address.parse(nft.collection.address).equals( Address.parse(config.get('notcoin_nft_collection')), diff --git a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx index 1aefd6699..7749a2c13 100644 --- a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx +++ b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx @@ -28,8 +28,6 @@ import { usePreparedWalletContent } from './content-providers/utils/usePreparedW import { FinishSetupList } from './components/FinishSetupList'; import { BackupIndicator } from './components/Tabs/BackupIndicator'; import { useExternalState } from '@tonkeeper/shared/hooks/useExternalState'; -import { openActivityActionModal } from '@tonkeeper/shared/modals/ActivityActionModal'; -import { ActionSource } from '$wallet/models/ActivityModel'; export const WalletScreen = memo(({ navigation }) => { const dispatch = useDispatch(); @@ -85,14 +83,6 @@ export const WalletScreen = memo(({ navigation }) => { return unsubscribe; }, [nav, navigation]); - useEffect(() => { - if (!isNotCoinReceived && notCoinActionId) { - openActivityActionModal(notCoinActionId, ActionSource.Ton, true); - - wallet.activityList.setNotCoinReceived(); - } - }, [notCoinActionId, isNotCoinReceived, wallet.activityList]); - const isWatchOnly = wallet && wallet.isWatchOnly; const ListHeader = useMemo( diff --git a/packages/mobile/src/tabs/Wallet/content-providers/dependencies/notcoinVouchers.ts b/packages/mobile/src/tabs/Wallet/content-providers/dependencies/notcoinVouchers.ts index d6c423d6a..b4322461e 100644 --- a/packages/mobile/src/tabs/Wallet/content-providers/dependencies/notcoinVouchers.ts +++ b/packages/mobile/src/tabs/Wallet/content-providers/dependencies/notcoinVouchers.ts @@ -12,6 +12,7 @@ export class NotCoinVouchersDependency extends DependencyPrototype nft.collection && + !nft.sale && Address.compare(nft.collection.address, config.get('notcoin_nft_collection')), ); diff --git a/packages/shared/components/ActivityList/ActionListItem.tsx b/packages/shared/components/ActivityList/ActionListItem.tsx index de76b90d9..ec7057f45 100644 --- a/packages/shared/components/ActivityList/ActionListItem.tsx +++ b/packages/shared/components/ActivityList/ActionListItem.tsx @@ -228,7 +228,7 @@ export const ActionListItem = memo((props) => { )} {isFailed && !ignoreFailed && ( - + {t('transactions.failed')} )} diff --git a/packages/shared/components/ActivityList/PureActionListItem.tsx b/packages/shared/components/ActivityList/PureActionListItem.tsx index 91f4d5ebb..8d4432b82 100644 --- a/packages/shared/components/ActivityList/PureActionListItem.tsx +++ b/packages/shared/components/ActivityList/PureActionListItem.tsx @@ -228,7 +228,7 @@ export const PureActionListItem = memo((props) => { )} {isFailed && !ignoreFailed && ( - + {t('transactions.failed')} )} diff --git a/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx b/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx index 6e8eed3df..735b3cb48 100644 --- a/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx +++ b/packages/shared/components/ActivityList/items/JettonSwapActionListItem.tsx @@ -3,7 +3,7 @@ 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 { Spacer, Text } from '@tonkeeper/uikit'; import { memo, useMemo } from 'react'; import { t } from '../../../i18n'; import { useHideableFormatter } from '@tonkeeper/mobile/src/core/HideableAmount/useHideableFormatter'; @@ -16,6 +16,8 @@ export const JettonSwapActionListItem = memo((pro const { payload } = action; const { formatNano } = useHideableFormatter(); + const isSwapFailed = action.status === ActionStatusEnum.Failed; + const subtitle = payload.user_wallet.name ? payload.user_wallet.name : Address.parse(payload.user_wallet.address, { @@ -73,17 +75,22 @@ export const JettonSwapActionListItem = memo((pro > - {action.status === ActionStatusEnum.Failed && ( - - {t('transactions.failed')} - + {isSwapFailed && ( + <> + + + {t('transactions.failed_with_reason.swap_refund_no_liq')} + + )} - - - {formatTransactionTime(new Date(action.event.timestamp * 1000))} - - + {!isSwapFailed && ( + + + {formatTransactionTime(new Date(action.event.timestamp * 1000))} + + + )} ); diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 750bf7b4d..cec06e570 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -124,6 +124,7 @@ "auth_failed": "Authentication failed", "balances_setup_wallet": "Set up wallet", "battery": { + "max_input_amount": "Max input amount is %{amount}", "send_widget": { "battery": "Battery" }, @@ -946,6 +947,9 @@ "contract_deploy": "Contract Deploy", "deposit": "Stake", "failed": "Failed", + "failed_with_reason": { + "swap_refund_no_liq": "Failed. Increase the slippage tolerance in the swap settings to avoid this error." + }, "nft_purchase": "NFT Purchase", "smartcontract_exec": "Call contract", "spam": "Spam", diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 08427a79c..30514da01 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -918,6 +918,9 @@ "contract_deploy": "Создание контракта", "deposit": "Депозит", "failed": "Неуспешно", + "failed_with_reason": { + "swap_refund_no_liq": "Неуспешно. Увеличьте допустимый процент изменения цены (Slippage) в настройках обмена, чтобы избежать подобных ошибок." + }, "nft_purchase": "Покупка NFT", "smartcontract_exec": "Вызов контракта", "spam": "Спам", @@ -1072,6 +1075,7 @@ "region_nokyc": "Нейтральные воды", "nokyc": "без KYC", "battery": { + "max_input_amount": "Максимальная сумма: %{amount}", "send_widget": { "battery": "Батарейка" }, diff --git a/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx b/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx index 4dd21d54a..6ae3dd9a3 100644 --- a/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx +++ b/packages/shared/modals/ActivityActionModal/content/JettonSwapActionContent.tsx @@ -16,6 +16,7 @@ import { ActionItem, ActionType, } from '@tonkeeper/mobile/src/wallet/models/ActivityModel'; +import { ActionStatusEnum } from '@tonkeeper/core/src/TonAPI'; interface JettonSwapActionContentProps { action: ActionItem; @@ -115,6 +116,10 @@ export const JettonSwapActionContent = memo((props return ( void; text: string; + disabled?: boolean; } const BUTTON_WIDTH = 92; @@ -37,10 +38,12 @@ const SPRING_CONFIG = { export const SlideButton = memo((props) => { const buttonStyle = Steezy.useStyle(styles.button); + const disabledButtonStyle = Steezy.useStyle(styles.buttonDisabled); const leftOffset = useSharedValue(0); const maxOffset = useSharedValue(0); const panGesture = Gesture.Pan() + .enabled(!props.disabled) .onStart(() => { runOnJS(triggerImpactMedium)(); }) @@ -67,7 +70,7 @@ export const SlideButton = memo((props) => { const textContainerStyle = useAnimatedStyle(() => { return { - opacity: withTiming(leftOffset.value ? 0 : 1, { duration: 175 }), + opacity: withTiming(props.disabled || leftOffset.value ? 0 : 1, { duration: 175 }), }; }); @@ -82,8 +85,18 @@ export const SlideButton = memo((props) => { - - + + @@ -109,4 +122,10 @@ const styles = Steezy.create(({ colors }) => ({ alignItems: 'center', justifyContent: 'center', }, + buttonDisabled: { + backgroundColor: colors.buttonPrimaryBackgroundDisabled, + }, + iconDisabled: { + opacity: 0.48, + }, }));