Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion e2e/channels.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ d('Transfer', () => {
4,
);
await element(by.id('ExternalAmountContinue')).tap();

// change fee
await element(by.id('SetCustomFee')).tap();
await element(
by.id('NRemove').withAncestor(by.id('FeeCustomNumberPad')),
).tap();
await element(by.id('FeeCustomContinue')).tap();
await element(by.id('N5').withAncestor(by.id('FeeCustomNumberPad'))).tap();
await element(by.id('FeeCustomContinue')).tap();

// Swipe to confirm (set x offset to avoid navigating back)
await element(by.id('GRAB')).swipe('right', 'slow', 0.95, 0.5, 0.5);
await waitFor(element(by.id('ExternalSuccess')))
Expand Down Expand Up @@ -319,7 +329,7 @@ d('Transfer', () => {
await waitFor(
element(by.id('MoneyText').withAncestor(by.id('TotalBalance'))),
)
.toHaveText('99 225')
.toHaveText('99 059')
.withTimeout(10000);

// close the channel
Expand Down
3 changes: 3 additions & 0 deletions src/navigation/transfer/TransferNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import ExternalConnection from '../../screens/Transfer/ExternalNode/Connection';
import ExternalAmount from '../../screens/Transfer/ExternalNode/Amount';
import ExternalConfirm from '../../screens/Transfer/ExternalNode/Confirm';
import ExternalSuccess from '../../screens/Transfer/ExternalNode/Success';
import ExternalFeeCustom from '../../screens/Transfer/ExternalNode/FeeCustom';
import { TChannel } from '../../store/types/lightning';

export type TransferNavigationProp =
Expand Down Expand Up @@ -59,6 +60,7 @@ export type TransferStackParamList = {
SavingsConfirm: { channels: TChannel[] } | undefined;
SavingsAdvanced: undefined;
SavingsProgress: { channels: TChannel[] };
ExternalFeeCustom: undefined;
};

const Stack = createNativeStackNavigator<TransferStackParamList>();
Expand Down Expand Up @@ -93,6 +95,7 @@ const Transfer = (): ReactElement => {
<Stack.Screen name="ExternalAmount" component={ExternalAmount} />
<Stack.Screen name="ExternalConfirm" component={ExternalConfirm} />
<Stack.Screen name="ExternalSuccess" component={ExternalSuccess} />
<Stack.Screen name="ExternalFeeCustom" component={ExternalFeeCustom} />
</Stack.Navigator>
);
};
Expand Down
6 changes: 3 additions & 3 deletions src/screens/Transfer/ExternalNode/Amount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const ExternalAmount = ({
useCallback(() => {
const setupTransaction = async (): Promise<void> => {
await resetSendTransaction();
await setupOnChainTransaction({ satsPerByte: fees.fast });
await setupOnChainTransaction({ satsPerByte: fees.fast, rbf: false });
};
setupTransaction();

Expand Down Expand Up @@ -151,7 +151,7 @@ const ExternalAmount = ({
/>
</View>

<View style={styles.numberPad} testID="SendAmountNumberPad">
<View style={styles.numberPadContainer} testID="SendAmountNumberPad">
<View style={styles.actions}>
<View>
<Caption13Up style={styles.availableAmountText} color="secondary">
Expand Down Expand Up @@ -235,7 +235,7 @@ const styles = StyleSheet.create({
amountContainer: {
marginTop: 'auto',
},
numberPad: {
numberPadContainer: {
flex: 1,
marginTop: 'auto',
maxHeight: 435,
Expand Down
42 changes: 36 additions & 6 deletions src/screens/Transfer/ExternalNode/Confirm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React, { ReactElement, useState } from 'react';
import React, { ReactElement, useCallback, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import { Image, StyleSheet, View } from 'react-native';
import { Trans, useTranslation } from 'react-i18next';

import { View as ThemedView } from '../../../styles/components';
import {
View as ThemedView,
TouchableOpacity,
} from '../../../styles/components';
import { Caption13Up, Display } from '../../../styles/text';
import { LightningIcon } from '../../../styles/icons';
import { LightningIcon, PencilIcon } from '../../../styles/icons';
import SafeAreaInset from '../../../components/SafeAreaInset';
import NavigationHeader from '../../../components/NavigationHeader';
import SwipeToConfirm from '../../../components/SwipeToConfirm';
Expand All @@ -14,6 +18,7 @@ import { createFundedChannel } from '../../../utils/wallet/transfer';
import { useAppSelector } from '../../../hooks/redux';
import { TransferScreenProps } from '../../../navigation/types';
import { transactionFeeSelector } from '../../../store/reselect/wallet';
import { updateSendTransaction } from '../../../store/actions/wallet';

const image = require('../../../assets/illustrations/coin-stack-x.png');

Expand All @@ -29,6 +34,16 @@ const ExternalConfirm = ({
const lspFee = 0;
const totalFee = localBalance + transactionFee;

useFocusEffect(
useCallback(() => {
// Using a placeholder address here to enable the FeeCustom screen to function properly.
// The actual funding address will be updated later in createFundedChannel.
updateSendTransaction({
outputs: [{ address: 'xxx', value: localBalance, index: 0 }],
});
}, [localBalance]),
);

const onConfirm = async (): Promise<void> => {
setLoading(true);

Expand All @@ -50,6 +65,10 @@ const ExternalConfirm = ({
navigation.navigate('ExternalSuccess');
};

const onChangeFee = (): void => {
navigation.navigate('ExternalFeeCustom');
};

return (
<ThemedView style={styles.root}>
<SafeAreaInset type="top" />
Expand All @@ -68,12 +87,18 @@ const ExternalConfirm = ({

<View style={styles.fees}>
<View style={styles.feesRow}>
<View style={styles.feeItem}>
<TouchableOpacity
style={styles.feeItem}
onPress={onChangeFee}
testID="SetCustomFee">
<Caption13Up style={styles.feeItemLabel} color="secondary">
{t('spending_confirm.network_fee')}
</Caption13Up>
<Money sats={transactionFee} size="bodySSB" symbol={true} />
</View>
<View style={styles.networkFee}>
<Money sats={transactionFee} size="bodySSB" symbol={true} />
<PencilIcon height={13} width={13} />
</View>
</TouchableOpacity>
<View style={styles.feeItem}>
<Caption13Up style={styles.feeItemLabel} color="secondary">
{t('spending_confirm.lsp_fee')}
Expand Down Expand Up @@ -143,6 +168,11 @@ const styles = StyleSheet.create({
feeItemLabel: {
marginBottom: 8,
},
networkFee: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
imageContainer: {
flexShrink: 1,
alignItems: 'center',
Expand Down
182 changes: 182 additions & 0 deletions src/screens/Transfer/ExternalNode/FeeCustom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React, { ReactElement, memo, useEffect, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { StyleSheet, View } from 'react-native';
import { FadeIn, FadeOut } from 'react-native-reanimated';

import Amount from '../../../components/Amount';
import NavigationHeader from '../../../components/NavigationHeader';
import NumberPad from '../../../components/NumberPad';
import SafeAreaInset from '../../../components/SafeAreaInset';
import Button from '../../../components/buttons/Button';
import { useDisplayValues } from '../../../hooks/displayValues';
import { useAppSelector } from '../../../hooks/redux';
import { TransferScreenProps } from '../../../navigation/types';
import { onChainFeesSelector } from '../../../store/reselect/fees';
import { transactionSelector } from '../../../store/reselect/wallet';
import { AnimatedView, View as ThemedView } from '../../../styles/components';
import { BodyM, Caption13Up, Display } from '../../../styles/text';
import { showToast } from '../../../utils/notifications';
import { handleNumberPadPress } from '../../../utils/numberpad';
import { getFeeInfo } from '../../../utils/wallet';
import { getTotalFee, updateFee } from '../../../utils/wallet/transactions';

const FeeCustom = ({
navigation,
}: TransferScreenProps<'ExternalFeeCustom'>): ReactElement => {
const { t } = useTranslation('lightning');
const feeEstimates = useAppSelector(onChainFeesSelector);
const transaction = useAppSelector(transactionSelector);
const [feeRate, setFeeRate] = useState<number>(transaction.satsPerByte);
const [maxFee, setMaxFee] = useState(0);
const minFee = feeEstimates.minimum;

useEffect(() => {
const feeInfo = getFeeInfo({
satsPerByte: transaction.satsPerByte,
transaction,
});
if (feeInfo.isOk()) {
setMaxFee(feeInfo.value.maxSatPerByte);
}
}, [transaction]);

const totalFee = getTotalFee({
satsPerByte: feeRate,
message: transaction.message,
});

const totalFeeDisplay = useDisplayValues(totalFee);
const totalFeeText = useMemo(() => {
if (totalFeeDisplay.fiatFormatted === '—') {
return t('wallet:send_fee_total', { totalFee });
}
return t('wallet:send_fee_total_fiat', {
feeSats: totalFee,
fiatSymbol: totalFeeDisplay.fiatSymbol,
fiatFormatted: totalFeeDisplay.fiatFormatted,
});
}, [totalFee, totalFeeDisplay.fiatFormatted, totalFeeDisplay.fiatSymbol, t]);

const onPress = (key: string): void => {
const current = feeRate.toString();
const newAmount = handleNumberPadPress(key, current, { maxLength: 3 });
setFeeRate(Number(newAmount));
};

const onContinue = (): void => {
if (Number(feeRate) > maxFee) {
showToast({
type: 'info',
title: t('wallet:max_possible_fee_rate'),
description: t('wallet:max_possible_fee_rate_msg'),
});
return;
}
if (Number(feeRate) < minFee) {
showToast({
type: 'info',
title: t('wallet:min_possible_fee_rate'),
description: t('wallet:min_possible_fee_rate_msg'),
});
return;
}
const res = updateFee({
satsPerByte: Number(feeRate),
transaction,
});
if (res.isErr()) {
showToast({
type: 'warning',
title: t('wallet:send_fee_error'),
description: res.error.message,
});
}
if (res.isOk()) {
navigation.goBack();
}
};

const isValid = feeRate !== 0;

return (
<ThemedView style={styles.root}>
<SafeAreaInset type="top" />
<NavigationHeader
title={t('external.nav_title')}
onClosePress={(): void => navigation.navigate('Wallet')}
/>
<View style={styles.content}>
<Display>
<Trans
t={t}
i18nKey="transfer.custom_fee"
components={{ accent: <Display color="purple" /> }}
/>
</Display>

<View style={styles.amountContainer}>
<Caption13Up color="secondary" style={styles.title}>
{t('sat_vbyte')}
</Caption13Up>
<Amount value={feeRate} />
<View style={styles.feeText}>
{isValid && (
<AnimatedView entering={FadeIn} exiting={FadeOut}>
<BodyM color="secondary">{totalFeeText}</BodyM>
</AnimatedView>
)}
</View>
</View>

<View style={styles.numberPadContainer} testID="FeeCustomNumberPad">
<NumberPad style={styles.numberPad} type="simple" onPress={onPress} />
</View>

<View style={styles.buttonContainer}>
<Button
size="large"
text={t('continue')}
onPress={onContinue}
testID="FeeCustomContinue"
/>
</View>
</View>
<SafeAreaInset type="bottom" minPadding={16} />
</ThemedView>
);
};

const styles = StyleSheet.create({
root: {
flex: 1,
},
content: {
flex: 1,
paddingTop: 16,
paddingHorizontal: 16,
},
amountContainer: {
marginTop: 'auto',
},
title: {
marginBottom: 16,
},
feeText: {
marginTop: 8,
minHeight: 22, // avoid jumping when fee is changing
},
numberPadContainer: {
flex: 1,
marginTop: 'auto',
maxHeight: 360,
},
numberPad: {
marginTop: 16,
marginHorizontal: -16,
},
buttonContainer: {
justifyContent: 'flex-end',
},
});

export default memo(FeeCustom);
3 changes: 3 additions & 0 deletions src/utils/i18n/locales/en/lightning.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
"confirm": {
"string": "Please\n<accent>confirm</accent>"
},
"custom_fee": {
"string": "Custom <accent>fee</accent>"
},
"swipe": {
"string": "Swipe To Transfer"
}
Expand Down
1 change: 0 additions & 1 deletion src/utils/wallet/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ export const createFundedChannel = async ({
const address = getAddressFromScriptPubKey(output_script, network);

updateSendTransaction({
rbf: false,
outputs: [{ address, value: value_satoshis, index: 0 }],
});

Expand Down