From b2b52ec17f3cc8b66a9799f9329e1a5dd74786da Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Tue, 9 Apr 2024 18:28:57 +0300 Subject: [PATCH] fix(mobile): Battery QA fixes (#784) * fix(mobile): Don't reset battery balance in case of error * fix(mobile): Touch zone * fix(mobile): supported transaction setting from battery config * fix disable send with relayer for SingRaw * feature(mobile): Animated battery icon * fix(mobile): switch attemptWithRelayer default value to false * fix(mobile): update order --- packages/mobile/src/blockchain/wallet.ts | 8 +- packages/mobile/src/config/index.ts | 10 ++ .../InsufficientFunds/InsufficientFunds.tsx | 5 +- .../NFTOperations/Modals/SignRawModal.tsx | 1 + .../mobile/src/uikit/TransitionOpacity.tsx | 18 ++- .../src/wallet/managers/BatteryManager.ts | 10 +- .../components/BatteryIcon/BatteryIcon.tsx | 43 ++--- .../BatterySupportedTransactions.tsx | 14 +- .../RefillBattery/RefillBattery.tsx | 27 ++-- .../RefillBattery/RefillBatteryIAP.tsx | 6 + .../RefillBattery/RestorePurchases.tsx | 2 +- .../shared/i18n/locales/tonkeeper/en.json | 2 +- .../shared/i18n/locales/tonkeeper/ru-RU.json | 4 +- packages/shared/modals/RefillBatteryModal.tsx | 1 + packages/shared/utils/battery.ts | 4 +- packages/shared/utils/blockchain.ts | 2 +- .../src/components/AnimatedBatteryIcon.tsx | 152 ++++++++++++++++++ packages/uikit/src/index.ts | 1 + 18 files changed, 249 insertions(+), 61 deletions(-) create mode 100644 packages/uikit/src/components/AnimatedBatteryIcon.tsx diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index e9199ba5e..c90a63b81 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -487,7 +487,13 @@ export class TonWallet { let feeNano: BigNumber; let isBattery = false; try { - const [fee, battery] = await this.calcFee(boc); + const [fee, battery] = await this.calcFee( + boc, + undefined, + tk.wallet.battery.state.data.supportedTransactions[ + BatterySupportedTransaction.Jetton + ], + ); feeNano = fee; isBattery = battery; } catch (e) { diff --git a/packages/mobile/src/config/index.ts b/packages/mobile/src/config/index.ts index a295db0dd..70559990b 100644 --- a/packages/mobile/src/config/index.ts +++ b/packages/mobile/src/config/index.ts @@ -43,9 +43,15 @@ export type AppConfigVars = { tonapiTestnetHost: string; tronapiHost: string; tronapiTestnetHost: string; + batteryHost: string; batteryTestnetHost: string; batteryMeanFees: string; + batteryReservedAmount: string; + batteryMeanPrice_swap: string; + batteryMeanPrice_jetton: string; + batteryMeanPrice_nft: string; + holdersAppEndpoint: string; holdersService: string; aptabaseEndpoint: string; @@ -83,6 +89,10 @@ const defaultConfig: Partial = { batteryHost: 'https://battery.tonkeeper.com', batteryTestnetHost: 'https://testnet-battery.tonkeeper.com', batteryMeanFees: '0.0055', + batteryReservedAmount: '0.3', + batteryMeanPrice_swap: '0.22', + batteryMeanPrice_jetton: '0.06', + batteryMeanPrice_nft: '0.03', disable_battery: true, disable_battery_iap_module: Platform.OS !== 'android', // Enable for iOS, disable for Android disable_battery_send: true, diff --git a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx index e4a2401d3..80b68734a 100644 --- a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx +++ b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx @@ -1,20 +1,17 @@ import React, { memo, useCallback, useMemo } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { Modal, Spacer } from '@tonkeeper/uikit'; -import { openExploreTab } from '$navigation'; +import { openExploreTab, openRefillBatteryModal } from '$navigation'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Button, Icon, Text } from '$uikit'; import * as S from './InsufficientFunds.style'; import { delay, fromNano } from '$utils'; import { debugLog } from '$utils/debugLog'; import BigNumber from 'bignumber.js'; -import { store } from '$store'; import { formatter } from '$utils/formatter'; import { push } from '$navigation/imperative'; import { useBatteryBalance } from '@tonkeeper/shared/query/hooks/useBatteryBalance'; import { config } from '$config'; -import { openRefillBatteryModal } from '@tonkeeper/shared/modals/RefillBatteryModal'; -import { tk } from '$wallet'; import { Wallet } from '$wallet/Wallet'; import { AmountFormatter } from '@tonkeeper/core'; diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 861748f32..071b32479 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -327,6 +327,7 @@ export const openSignRawModal = async ( const { emulateResult, battery } = await emulateBoc( boc, + undefined, options.experimentalWithBattery, ); consequences = emulateResult; diff --git a/packages/mobile/src/uikit/TransitionOpacity.tsx b/packages/mobile/src/uikit/TransitionOpacity.tsx index d5638fd2c..a5fd2e483 100644 --- a/packages/mobile/src/uikit/TransitionOpacity.tsx +++ b/packages/mobile/src/uikit/TransitionOpacity.tsx @@ -15,7 +15,7 @@ interface TransitionOpacity { style?: StyleProp; duration?: number; children: React.ReactNode; -} +} export const TransitionOpacity: React.FC = (props) => { const { @@ -32,12 +32,16 @@ export const TransitionOpacity: React.FC = (props) => { React.useEffect(() => { if (isVisible) { - setIsShown(true); - opacity.value = withDelay( - 250, - withTiming(1, { + opacity.value = withTiming( + 1, + { duration, - }), + }, + (isComplete) => { + if (isComplete) { + runOnJS(setIsShown)(true); + } + }, ); } else { opacity.value = withTiming( @@ -52,7 +56,7 @@ export const TransitionOpacity: React.FC = (props) => { }, ); } - }, [isVisible]); + }, [duration, isVisible, opacity]); const opacityStyle = useAnimatedStyle(() => ({ opacity: opacity.value, diff --git a/packages/mobile/src/wallet/managers/BatteryManager.ts b/packages/mobile/src/wallet/managers/BatteryManager.ts index 64a07f883..06ffb3edb 100644 --- a/packages/mobile/src/wallet/managers/BatteryManager.ts +++ b/packages/mobile/src/wallet/managers/BatteryManager.ts @@ -6,9 +6,9 @@ import { TonProofManager } from '$wallet/managers/TonProofManager'; import { logger, NamespacedLogger } from '$logger'; export enum BatterySupportedTransaction { - NFT = 'nft', - Jetton = 'jetton', Swap = 'swap', + Jetton = 'jetton', + NFT = 'nft', } export interface BatteryState { @@ -22,9 +22,9 @@ export class BatteryManager { isLoading: false, balance: undefined, supportedTransactions: { - [BatterySupportedTransaction.NFT]: true, - [BatterySupportedTransaction.Jetton]: true, [BatterySupportedTransaction.Swap]: true, + [BatterySupportedTransaction.Jetton]: true, + [BatterySupportedTransaction.NFT]: true, }, }); @@ -63,7 +63,7 @@ export class BatteryManager { ); this.state.set({ isLoading: false, balance: data.balance }); } catch (err) { - this.state.set({ isLoading: false, balance: '0' }); + this.state.set({ isLoading: false }); return null; } } diff --git a/packages/shared/components/BatteryIcon/BatteryIcon.tsx b/packages/shared/components/BatteryIcon/BatteryIcon.tsx index e4a407da8..beb12784f 100644 --- a/packages/shared/components/BatteryIcon/BatteryIcon.tsx +++ b/packages/shared/components/BatteryIcon/BatteryIcon.tsx @@ -1,36 +1,43 @@ import React, { memo } from 'react'; import { useBatteryBalance } from '../../query/hooks/useBatteryBalance'; -import { Icon, IconNames, Steezy, TouchableOpacity } from '@tonkeeper/uikit'; +import { + AnimatedBatteryIcon, + AnimatedBatterySize, + Icon, + Steezy, + TouchableOpacity, +} from '@tonkeeper/uikit'; import { BatteryState, getBatteryState } from '../../utils/battery'; import { config } from '@tonkeeper/mobile/src/config'; import { useBatteryUIStore } from '@tonkeeper/mobile/src/store/zustand/batteryUI'; import { openRefillBatteryModal } from '@tonkeeper/mobile/src/navigation'; -const iconNames: { [key: string]: ((isViewed: boolean) => IconNames) | IconNames } = { - [BatteryState.Empty]: (isViewed) => - isViewed ? 'ic-empty-battery-flash-34' : 'ic-empty-battery-accent-flash-34', - [BatteryState.AlmostEmpty]: 'ic-almost-empty-battery-34', - [BatteryState.Medium]: 'ic-medium-battery-34', - [BatteryState.Full]: 'ic-full-battery-34', -}; - -const hitSlop = { top: 8, bottom: 8, right: 8, left: 8 }; +const hitSlop = { top: 12, bottom: 12, right: 24, left: 8 }; export const BatteryIcon = memo(() => { const { balance } = useBatteryBalance(); const isViewedBatteryScreen = useBatteryUIStore((state) => state.isViewedBatteryScreen); if (config.get('disable_battery')) return null; - const iconName = iconNames[getBatteryState(balance)]; - return ( - + {getBatteryState(balance) === BatteryState.Empty ? ( + + ) : ( + + )} ); }); diff --git a/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx b/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx index cb6e24412..efb1bab78 100644 --- a/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx +++ b/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx @@ -7,12 +7,12 @@ import { capitalizeFirstLetter } from '../../utils/date'; import { useExternalState } from '../../hooks/useExternalState'; import { tk } from '@tonkeeper/mobile/src/wallet'; import { BatterySupportedTransaction } from '@tonkeeper/mobile/src/wallet/managers/BatteryManager'; +import { Platform } from 'react-native'; export interface SupportedTransaction { type: BatterySupportedTransaction; name: string; nameSingle: string; - meanPrice: string; } export const supportedTransactions: SupportedTransaction[] = [ @@ -20,19 +20,16 @@ export const supportedTransactions: SupportedTransaction[] = [ type: BatterySupportedTransaction.Swap, name: 'battery.transactions.types.swap', nameSingle: 'battery.transactions.type.swap', - meanPrice: '0.22', }, { type: BatterySupportedTransaction.NFT, name: 'battery.transactions.types.nft', nameSingle: 'battery.transactions.type.transfer', - meanPrice: '0.025', }, { type: BatterySupportedTransaction.Jetton, name: 'battery.transactions.types.jetton', nameSingle: 'battery.transactions.type.transfer', - meanPrice: '0.055', }, ]; @@ -72,28 +69,29 @@ export const BatterySupportedTransactions = memo {supportedTransactions.map((transaction) => ( handleSwitchSupport(transaction.type)( !supportedTransactionsValues[transaction.type], ) } - key={transaction.type} title={capitalizeFirstLetter(t(transaction.name))} subtitle={t('battery.transactions.charges_per_action', { count: calculateChargesAmount( - transaction.meanPrice, + config.get(`batteryMeanPrice_${transaction.type}`), config.get('batteryMeanFees'), ), transactionName: t(transaction.nameSingle), })} rightContent={ - props.editable && ( + props.editable ? ( - ) + ) : null } /> ))} diff --git a/packages/shared/components/RefillBattery/RefillBattery.tsx b/packages/shared/components/RefillBattery/RefillBattery.tsx index edd12060e..8026a5a4a 100644 --- a/packages/shared/components/RefillBattery/RefillBattery.tsx +++ b/packages/shared/components/RefillBattery/RefillBattery.tsx @@ -7,8 +7,9 @@ import { import { memo } from 'react'; import { useBatteryBalance } from '../../query/hooks/useBatteryBalance'; import { + AnimatedBatteryIcon, + AnimatedBatterySize, Icon, - IconNames, Spacer, Steezy, Text, @@ -24,13 +25,6 @@ import { RefillBatterySettingsWidget } from './RefillBatterySettingsWidget'; import Animated from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -const iconNames: { [key: string]: IconNames } = { - [BatteryState.Empty]: 'ic-empty-battery-128', - [BatteryState.AlmostEmpty]: 'ic-almost-empty-battery-128', - [BatteryState.Medium]: 'ic-medium-battery-128', - [BatteryState.Full]: 'ic-full-battery-128', -}; - export interface RefillBatteryProps { navigateToTransactions: () => void; } @@ -38,7 +32,6 @@ export interface RefillBatteryProps { export const RefillBattery = memo((props) => { const { balance } = useBatteryBalance(); const batteryState = getBatteryState(balance ?? '0'); - const iconName = iconNames[batteryState]; const availableNumOfTransactionsCount = calculateAvailableNumOfTransactions( balance ?? '0', ); @@ -52,7 +45,16 @@ export const RefillBattery = memo((props) => { contentContainerStyle={{ paddingBottom: bottomInsets + 16 }} > - + {batteryState === BatteryState.Empty ? ( + + ) : ( + + + + )} {t(`battery.title`)} @@ -100,4 +102,9 @@ export const styles = Steezy.create({ indent: { paddingHorizontal: 16, }, + animatedBatteryContainer: { + paddingHorizontal: 30, + paddingTop: 6, + paddingBottom: 8, + }, }); diff --git a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx index 24693f563..a97eb9911 100644 --- a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx +++ b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx @@ -19,6 +19,7 @@ import { useTokenPrice } from '@tonkeeper/mobile/src/hooks/useTokenPrice'; import { CryptoCurrencies } from '@tonkeeper/mobile/src/shared/constants'; import BigNumber from 'bignumber.js'; import { config } from '@tonkeeper/mobile/src/config'; +import { useExternalState } from '../../hooks/useExternalState'; export interface InAppPackage { icon: IconNames; @@ -53,6 +54,10 @@ export const RefillBatteryIAP = memo(() => { const [purchaseInProgress, setPurchaseInProgress] = useState(false); const { products, getProducts, requestPurchase, finishTransaction } = useIAP(); const tonPriceInUsd = useTokenPrice(CryptoCurrencies.Ton).usd; + const batteryBalance = useExternalState( + tk.wallet.battery.state, + (state) => state.balance, + ); useEffect(() => { getProducts({ @@ -140,6 +145,7 @@ export const RefillBatteryIAP = memo(() => { {t(`battery.packages.subtitle`, { count: new BigNumber(item.userProceed) .div(tonPriceInUsd) + .minus(!batteryBalance ? config.get('batteryReservedAmount') : 0) .div(config.get('batteryMeanFees')) .decimalPlaces(0) .toNumber(), diff --git a/packages/shared/components/RefillBattery/RestorePurchases.tsx b/packages/shared/components/RefillBattery/RestorePurchases.tsx index 0e6a76854..ce6339bf7 100644 --- a/packages/shared/components/RefillBattery/RestorePurchases.tsx +++ b/packages/shared/components/RefillBattery/RestorePurchases.tsx @@ -52,7 +52,7 @@ export const RestorePurchases = memo(() => { onPress={handleRestorePurchases} type="body2" textAlign="center" - color="textPrimary" + color="textSecondary" > {t('battery.packages.restore')} diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 5609c8404..cdfb21c72 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -131,7 +131,7 @@ "types": { "nft": "NFT transfers", "swap": "swaps", - "jetton": "jetton transfers", + "jetton": "token transfers", "ton": "TON transfers" }, "type": { diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 34105708f..45b4acf87 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -1090,7 +1090,7 @@ "ok": "OK", "title": "Батарейка Tonkeeper", "description": { - "empty": "Обменивайте и отправляйте токены", + "empty": "Обменивайте и отправляйте токены.", "other": { "few": "В вашей батарейке %{count} заряда", "many": "В вашей батарейке %{count} зарядов", @@ -1109,7 +1109,7 @@ "title": { "large": "Большой", "medium": "Средний", - "small": "Маленький" + "small": "Малый" }, "subtitle": { "few": "%{count} заряда", diff --git a/packages/shared/modals/RefillBatteryModal.tsx b/packages/shared/modals/RefillBatteryModal.tsx index 38955c239..a18cdc5c3 100644 --- a/packages/shared/modals/RefillBatteryModal.tsx +++ b/packages/shared/modals/RefillBatteryModal.tsx @@ -58,6 +58,7 @@ export const RefillBatteryModal = memo(() => { const handleBack = useCallback(() => stepViewRef.current?.goBack(), []); + // TODO: rewrite to react-native-pager-view return ( <> + + + ); + case AnimatedBatterySize.Large: + return ( + + + + ); + default: + return null; + } +} + +const inRange = (value: number, start: number, end: number) => + Math.min(end, Math.max(start, value)); + +const MIN_PROGRESS_RANGE = 0.14; + +export function AnimatedBatteryIcon(props: AnimatedBatteryIconProps) { + const iconConfig = mapIconConfigBySize[props.size]; + const bodyStyle = Steezy.useStyle(styles.batteryBody); + const emptyBodyStyle = Steezy.useStyle(styles.emptyBatteryBody); + const progress = inRange(props.progress ?? 0, MIN_PROGRESS_RANGE, 1); + + const batteryBodyAnimatedStyle = useAnimatedStyle( + () => ({ + height: withTiming( + interpolate( + progress, + [0, 1], + [0, iconConfig.height - iconConfig.top - iconConfig.bottom], + ), + { duration: 400 }, + ), + }), + [iconConfig, progress], + ); + + return ( + + + + + + + ); +} + +const styles = Steezy.create(({ colors }) => ({ + relativeContainer: { + position: 'relative', + }, + batteryBodyContainer: { + position: 'absolute', + justifyContent: 'flex-end', + }, + batteryBody: { + backgroundColor: colors.accentBlue, + }, + emptyBatteryBody: { + backgroundColor: colors.accentOrange, + }, +})); diff --git a/packages/uikit/src/index.ts b/packages/uikit/src/index.ts index 0559047b9..9958c728b 100644 --- a/packages/uikit/src/index.ts +++ b/packages/uikit/src/index.ts @@ -28,6 +28,7 @@ export { TransitionOpacity } from './components/TransitionOpacity'; export * from './components/Flash'; export * from './components/BlockingLoader'; export { Switch } from './components/Switch'; +export * from './components/AnimatedBatteryIcon'; // Containers export { HeaderButtonHitSlop } from './containers/Screen/utils/constants';