From ff4c3ddfa084b033482865c09ed2314f752bdb26 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Thu, 4 Apr 2024 02:51:06 +0300 Subject: [PATCH] feature(mobile): Animated battery icon --- packages/mobile/src/config/index.ts | 10 ++ .../components/BatteryIcon/BatteryIcon.tsx | 41 +++-- .../BatterySupportedTransactions.tsx | 14 +- .../RefillBattery/RefillBattery.tsx | 27 ++-- .../RefillBattery/RefillBatteryIAP.tsx | 6 + packages/shared/modals/RefillBatteryModal.tsx | 1 + packages/shared/utils/battery.ts | 4 +- .../src/components/AnimatedBatteryIcon.tsx | 152 ++++++++++++++++++ packages/uikit/src/index.ts | 1 + 9 files changed, 218 insertions(+), 38 deletions(-) create mode 100644 packages/uikit/src/components/AnimatedBatteryIcon.tsx 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/shared/components/BatteryIcon/BatteryIcon.tsx b/packages/shared/components/BatteryIcon/BatteryIcon.tsx index 6a2bf91ac..beb12784f 100644 --- a/packages/shared/components/BatteryIcon/BatteryIcon.tsx +++ b/packages/shared/components/BatteryIcon/BatteryIcon.tsx @@ -1,19 +1,17 @@ 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: 12, bottom: 12, right: 24, left: 8 }; export const BatteryIcon = memo(() => { @@ -21,16 +19,25 @@ export const BatteryIcon = memo(() => { 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/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';