Skip to content

Commit

Permalink
feature(mobile): Battery top-up
Browse files Browse the repository at this point in the history
  • Loading branch information
voloshinskii committed May 13, 2024
1 parent c2942bf commit b7516ce
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 43 deletions.
29 changes: 24 additions & 5 deletions packages/mobile/src/core/BatterySend/BatterySend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Button,
HeaderSwitch,
Icon,
KeyboardSpacer,
List,
Radio,
Screen,
Expand Down Expand Up @@ -38,6 +39,7 @@ import { openSelectRechargeMethodModal } from '@tonkeeper/shared/modals/SelectRe
import { RechargeMethodsTypeEnum } from '@tonkeeper/core/src/BatteryAPI';
import { useJettonBalances } from '$hooks/useJettonBalances';
import { compareAddresses } from '$utils/address';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const packs = [
{
Expand Down Expand Up @@ -75,8 +77,11 @@ export const BatterySend: React.FC<BatterySendProps> = ({ route }) => {
const textInputRef = useRef<TextInput>(null);
const [dnsLoading, setDnsLoading] = useState(false);
const [isManualAmountInput, setIsManualAmountInput] = useState(false);

const shouldMinusReservedAmount =
useExternalState(tk.wallet.battery.state, (state) => state.reservedBalance) === '0';
useExternalState(tk.wallet.battery.state, (state) => state.reservedBalance) === '0' &&
!initialRecipientAddress;

const { enabled: jettonBalances } = useJettonBalances();

const [selectedJettonMaster, setSelectedJettonMaster] = useState<string | undefined>(
Expand Down Expand Up @@ -269,7 +274,14 @@ export const BatterySend: React.FC<BatterySendProps> = ({ route }) => {
);

const handleOpenSelectRechargeMethod = useCallback(() => {
openSelectRechargeMethodModal(selectedJettonMaster, setSelectedJettonMaster);
openSelectRechargeMethodModal(
selectedJettonMaster,
(selected: string | undefined) => {
setAmount({ value: '0', all: false });
setIsManualAmountInput(false);
setSelectedJettonMaster(selected);
},
);
}, [selectedJettonMaster]);

return (
Expand All @@ -279,7 +291,11 @@ export const BatterySend: React.FC<BatterySendProps> = ({ route }) => {
hideBackButton
rightContent={
<View style={styles.headerRightContent}>
<HeaderSwitch onPress={handleOpenSelectRechargeMethod} title={'TON'} />
<HeaderSwitch
icon={rechargeMethod.iconSource}
onPress={handleOpenSelectRechargeMethod}
title={rechargeMethod.symbol}
/>
<TouchableOpacity onPress={navigation.goBack}>
<View style={styles.backButton}>
<Icon name={'ic-close-16'} />
Expand All @@ -290,7 +306,7 @@ export const BatterySend: React.FC<BatterySendProps> = ({ route }) => {
isModal
title={'Recharge'}
/>
<Screen.ScrollView>
<Screen.ScrollView keyboardAvoiding withBottomInset>
<Spacer y={8} />
<View style={styles.contentContainer}>
{!initialRecipientAddress ? (
Expand Down Expand Up @@ -374,6 +390,9 @@ export const BatterySend: React.FC<BatterySendProps> = ({ route }) => {
withCoinSelector={false}
disabled={false}
hideSwap={true}
calculateFiatFrom={
shouldMinusReservedAmount ? rechargeMethod.minInputAmount : '0'
}
minAmount={rechargeMethod.minInputAmount}
decimals={rechargeMethod.decimals}
balance={tk.wallet.balances.state.data.ton}
Expand All @@ -388,7 +407,7 @@ export const BatterySend: React.FC<BatterySendProps> = ({ route }) => {
</Animated.View>
)}
<Button
disabled={!recipient?.address}
disabled={!recipient?.address || amount.value === '0'}
onPress={handleContinue}
title="Continue"
/>
Expand Down
17 changes: 16 additions & 1 deletion packages/mobile/src/core/BatterySend/hooks/useRechargeMethod.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { RechargeMethods, RechargeMethodsTypeEnum } from '@tonkeeper/core/src/BatteryAPI';
import { config } from '$config';
import BigNumber from 'bignumber.js';
import { useRates, useWalletCurrency } from '@tonkeeper/shared/hooks';
import { useCallback, useMemo } from 'react';
import { formatter } from '@tonkeeper/shared/formatter';
import { useJettonBalances } from '$hooks/useJettonBalances';
import { compareAddresses } from '$utils/address';

type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
Expand All @@ -14,6 +15,18 @@ export function useRechargeMethod(
const currency = useWalletCurrency();

const isTon = rechargeMethod.type === RechargeMethodsTypeEnum.Ton;
const { enabled: jettonBalances } = useJettonBalances();

const iconSource = useMemo(() => {
if (isTon) {
return require('@tonkeeper/uikit/assets/cryptoAssets/TON.png');
}
const jettonBalance = jettonBalances.find((jettonBalance) =>
compareAddresses(jettonBalance.jettonAddress, rechargeMethod.jetton_master),
)!;

return { uri: jettonBalance.metadata.image };
}, [isTon, jettonBalances, rechargeMethod.jetton_master]);

const rates = useRates();

Expand Down Expand Up @@ -45,6 +58,7 @@ export function useRechargeMethod(
fromTon,
isTon,
minInputAmount,
iconSource,
}),
[
formattedTonFiatAmount,
Expand All @@ -54,6 +68,7 @@ export function useRechargeMethod(
rechargeMethod.rate,
rechargeMethod.symbol,
minInputAmount,
iconSource,
],
);
}
42 changes: 33 additions & 9 deletions packages/mobile/src/shared/components/AmountInput/AmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface Props {
fiatRate: number;
hideSwap?: boolean;
withCoinSelector?: boolean;
calculateFiatFrom?: number | string;
withMaxButton?: boolean;
customCurrency?: (size: 'small' | 'large') => React.ReactNode;
fiatDecimals?: number;
Expand All @@ -55,6 +56,7 @@ const AmountInputComponent: React.FC<Props> = (props) => {
withMaxButton = true,
customCurrency,
fiatDecimals = 2,
calculateFiatFrom = '0',
disabled,
setAmount,
} = props;
Expand Down Expand Up @@ -115,11 +117,11 @@ const AmountInputComponent: React.FC<Props> = (props) => {
const { decimalSeparator } = getNumberFormatSettings();

const secondaryValue = isFiat
? fiatToCrypto(value, fiatRate, 2, true)
: cryptoToFiat(value, fiatRate, fiatDecimals, true);
? fiatToCrypto(value, fiatRate, 2, true, calculateFiatFrom)
: cryptoToFiat(value, fiatRate, fiatDecimals, true, calculateFiatFrom);

return secondaryValue === '0' ? `0${decimalSeparator}00` : secondaryValue;
}, [amount.all, balance, fiatDecimals, fiatRate, isFiat, value]);
}, [amount.all, balance, calculateFiatFrom, fiatDecimals, fiatRate, isFiat, value]);

const mainCurrencyCode = isFiat
? customCurrency
Expand All @@ -142,11 +144,21 @@ const AmountInputComponent: React.FC<Props> = (props) => {
setValue(nextValue);

setAmount({
value: isFiat ? fiatToCrypto(nextValue, fiatRate, decimals) : nextValue,
value: isFiat
? fiatToCrypto(nextValue, fiatRate, decimals, undefined, calculateFiatFrom)
: nextValue,
all: nextValue === balanceInputValue,
});
},
[balanceInputValue, decimals, disabled, fiatRate, isFiat, setAmount],
[
balanceInputValue,
calculateFiatFrom,
decimals,
disabled,
fiatRate,
isFiat,
setAmount,
],
);

const handlePressInput = useCallback(() => {
Expand All @@ -166,11 +178,21 @@ const AmountInputComponent: React.FC<Props> = (props) => {

setFiat(false);
} else {
setValue(trimZeroDecimals(cryptoToFiat(amount.value, fiatRate, 2)));
setValue(
trimZeroDecimals(
cryptoToFiat(
amount.value,
fiatRate,
fiatDecimals,
undefined,
calculateFiatFrom,
),
),
);

setFiat(true);
}
}, [amount.value, fiatRate, isFiat]);
}, [amount.value, calculateFiatFrom, fiatDecimals, fiatRate, isFiat]);

const amountContainerStyle = useAnimatedStyle(
() => ({
Expand Down Expand Up @@ -217,14 +239,16 @@ const AmountInputComponent: React.FC<Props> = (props) => {
const fakeInputStyle = useAnimatedStyle(() => ({ height: inputHeight.value }));

useEffect(() => {
const currentInputValue = isFiat ? fiatToCrypto(value, fiatRate, decimals) : value;
const currentInputValue = isFiat
? fiatToCrypto(value, fiatRate, decimals, undefined, calculateFiatFrom)
: value;

if (amount.value === currentInputValue) {
return;
}

const nextValue = isFiat
? cryptoToFiat(amount.value, fiatRate, fiatDecimals)
? cryptoToFiat(amount.value, fiatRate, fiatDecimals, undefined, calculateFiatFrom)
: amount.value;

setValue(nextValue);
Expand Down
12 changes: 10 additions & 2 deletions packages/mobile/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,21 @@ export const cryptoToFiat = (
fiatRate: number,
decimals: number,
skipFormatting?: boolean,
calculateFiatFrom: string | number = '0',
) => {
if (!fiatRate || fiatRate <= 0) {
return '0';
}

const bigNum = new BigNumber(parseLocaleNumber(input) || 0);

const calculated = bigNum.multipliedBy(fiatRate);
const amount = Math.max(0, bigNum.minus(calculateFiatFrom).toNumber());

if (amount === 0) {
return '0';
}

const calculated = new BigNumber(amount).multipliedBy(fiatRate);

const { decimalSeparator, groupingSeparator } = getNumberFormatSettings();

Expand All @@ -92,14 +99,15 @@ export const fiatToCrypto = (
fiatRate: number,
decimals: number,
skipFormatting?: boolean,
calculateFiatFrom: string | number = '0',
) => {
if (!fiatRate || fiatRate <= 0) {
return '0';
}

const bigNum = new BigNumber(parseLocaleNumber(input) || 0);

const calculated = bigNum.dividedBy(fiatRate);
const calculated = bigNum.dividedBy(fiatRate).plus(calculateFiatFrom);

const { decimalSeparator, groupingSeparator } = getNumberFormatSettings();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ export const RechargeMethodsModal = memo(() => {
nav.replaceModal('/recharge-by-promo');
}, []);

const handleRechargeBattery = useCallback(async () => {
nav.goBack();
await delay(700);
nav.navigate(AppStackRouteNames.BatterySend, {
recipient: tk.wallet.address.ton.friendly,
});
}, []);
const handleRechargeBattery = useCallback(
(withAddressSelect?: boolean) => async () => {
nav.goBack();
await delay(700);
nav.navigate(AppStackRouteNames.BatterySend, {
recipient: withAddressSelect ? undefined : tk.wallet.address.ton.friendly,
});
},
[],
);

return (
<Modal>
<Modal.Header title={t('battery.other_ways.title')} />
<Modal.Content>
<List>
<List.Item
onPress={handleRechargeBattery}
onPress={handleRechargeBattery(false)}
leftContent={
<Image
style={styles.icon.static}
Expand All @@ -40,7 +43,7 @@ export const RechargeMethodsModal = memo(() => {
chevron
/>
<List.Item
onPress={handleRechargeBattery}
onPress={handleRechargeBattery(true)}
leftContent={
<Image
style={styles.icon.static}
Expand Down
21 changes: 13 additions & 8 deletions packages/uikit/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export interface ButtonProps {
size?: ButtonSizes;
color?: ButtonColors;
title?: string;
subtitle?: string;
children?: React.ReactNode;
leftContent?: React.ReactNode;
subtitle?: string | ReactNode;
children?: ReactNode;
leftContent?: ReactNode;
onPress?: () => void;
disabled?: boolean;
loading?: boolean;
Expand Down Expand Up @@ -129,15 +129,20 @@ export const Button = memo<ButtonProps>((props) => {
style={titleStyle}
type={textType}
color={
['primary', 'green'].includes(color)
? 'buttonPrimaryForeground'
: 'textPrimary'
}
['primary', 'green'].includes(color)
? 'buttonPrimaryForeground'
: 'textPrimary'
}
>
{title}
</Text>
{subtitle ? (
<Text color="textSecondary" type={'body2'}>
<Text
numberOfLines={1}
color="textSecondary"
textAlign="center"
type={'body2'}
>
{subtitle}
</Text>
) : null}
Expand Down
8 changes: 3 additions & 5 deletions packages/uikit/src/components/HeaderSwitch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@ import { View } from './View';
import { Steezy } from '../styles';
import { Icon } from './Icon';
import { Text } from './Text';
import { Image } from 'react-native';
import { Image, ImageSourcePropType } from 'react-native';
import { TouchableOpacity } from './TouchableOpacity';

export interface HeaderSwitchProps {
title: string;
onPress?: () => void;
icon: ImageSourcePropType;
}

export const HeaderSwitch = memo<HeaderSwitchProps>((props) => {
return (
<TouchableOpacity onPress={props.onPress}>
<View style={styles.container}>
<Image
style={styles.image.static}
source={require('../../assets/cryptoAssets/TON.png')}
/>
<Image style={styles.image.static} source={props.icon} />
<Text type="label2" color="buttonSecondaryForeground">
{props.title}
</Text>
Expand Down
Loading

0 comments on commit b7516ce

Please sign in to comment.