Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.

Commit d23c5f6

Browse files
committed
feat: lnurl withdraw specify amount
1 parent 9790748 commit d23c5f6

11 files changed

Lines changed: 499 additions & 33 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { ReactElement, memo } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import {
4+
NativeStackNavigationOptions,
5+
NativeStackNavigationProp,
6+
createNativeStackNavigator,
7+
} from '@react-navigation/native-stack';
8+
import { LNURLWithdrawParams } from 'js-lnurl';
9+
10+
import BottomSheetWrapper from '../../components/BottomSheetWrapper';
11+
import { __E2E__ } from '../../constants/env';
12+
import { useSnapPoints } from '../../hooks/bottomSheet';
13+
import Amount from '../../screens/Wallets/LNURLWithdraw/Amount';
14+
import Confirm from '../../screens/Wallets/LNURLWithdraw/Confirm';
15+
import { viewControllerSelector } from '../../store/reselect/ui';
16+
import { NavigationContainer } from '../../styles/components';
17+
18+
export type LNURLWithdrawNavigationProp =
19+
NativeStackNavigationProp<LNURLWithdrawStackParamList>;
20+
21+
export type LNURLWithdrawStackParamList = {
22+
Amount: { wParams: LNURLWithdrawParams };
23+
Confirm: { amount: number; wParams: LNURLWithdrawParams };
24+
};
25+
26+
const Stack = createNativeStackNavigator<LNURLWithdrawStackParamList>();
27+
28+
const screenOptions: NativeStackNavigationOptions = {
29+
headerShown: false,
30+
...(__E2E__ ? { animationDuration: 0 } : {}),
31+
};
32+
33+
const LNURLWithdrawNavigation = (): ReactElement => {
34+
const snapPoints = useSnapPoints('large');
35+
const { isOpen, wParams } = useSelector((state) => {
36+
return viewControllerSelector(state, 'lnurlWithdraw');
37+
});
38+
39+
if (!wParams) {
40+
return <></>;
41+
}
42+
43+
// if max === min withdrawable amount, skip the Amount screen
44+
const initialRouteName =
45+
wParams.minWithdrawable === wParams.maxWithdrawable ? 'Confirm' : 'Amount';
46+
47+
return (
48+
<BottomSheetWrapper view="lnurlWithdraw" snapPoints={snapPoints}>
49+
<NavigationContainer key={isOpen.toString()}>
50+
<Stack.Navigator
51+
screenOptions={screenOptions}
52+
initialRouteName={initialRouteName}>
53+
<Stack.Screen
54+
name="Amount"
55+
component={Amount}
56+
initialParams={{ wParams }}
57+
/>
58+
<Stack.Screen
59+
name="Confirm"
60+
component={Confirm}
61+
initialParams={{ wParams, amount: wParams.minWithdrawable }}
62+
/>
63+
</Stack.Navigator>
64+
</NavigationContainer>
65+
</BottomSheetWrapper>
66+
);
67+
};
68+
69+
export default memo(LNURLWithdrawNavigation);

src/navigation/root/RootNavigator.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import BackupNavigation from '../bottom-sheet/BackupNavigation';
5858
import PINNavigation from '../bottom-sheet/PINNavigation';
5959
import ForceTransfer from '../bottom-sheet/ForceTransfer';
6060
import CloseChannelSuccess from '../bottom-sheet/CloseChannelSuccess';
61+
import LNURLWithdrawNavigation from '../bottom-sheet/LNURLWithdrawNavigation';
6162
import { __E2E__ } from '../../constants/env';
6263
import type { RootStackParamList } from '../types';
6364

@@ -244,6 +245,7 @@ const RootNavigator = (): ReactElement => {
244245
<ForceTransfer />
245246
<CloseChannelSuccess />
246247
<BackupSubscriber />
248+
<LNURLWithdrawNavigation />
247249

248250
<Dialog
249251
visible={showDialog && isAuthenticated}

src/navigation/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { PinStackParamList } from '../bottom-sheet/PINNavigation';
2121
import type { ProfileLinkStackParamList } from '../bottom-sheet/ProfileLinkNavigation';
2222
import type { ReceiveStackParamList } from '../bottom-sheet/ReceiveNavigation';
2323
import type { SendStackParamList } from '../bottom-sheet/SendNavigation';
24+
import type { LNURLWithdrawStackParamList } from '../bottom-sheet/LNURLWithdrawNavigation';
2425

2526
// TODO: move all navigation related types here
2627
// https://reactnavigation.org/docs/typescript#organizing-types
@@ -105,3 +106,6 @@ export type ReceiveScreenProps<T extends keyof ReceiveStackParamList> =
105106

106107
export type SendScreenProps<T extends keyof SendStackParamList> =
107108
NativeStackScreenProps<SendStackParamList, T>;
109+
110+
export type LNURLWithdrawProps<T extends keyof LNURLWithdrawStackParamList> =
111+
NativeStackScreenProps<LNURLWithdrawStackParamList, T>;
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import React, {
2+
ReactElement,
3+
memo,
4+
useCallback,
5+
useMemo,
6+
useState,
7+
useEffect,
8+
} from 'react';
9+
import { StyleSheet, View } from 'react-native';
10+
import { useSelector } from 'react-redux';
11+
import { useTranslation } from 'react-i18next';
12+
13+
import LNURLWNumberpad from './LNURLWNumberpad';
14+
import { TouchableOpacity } from '../../../styles/components';
15+
import { Caption13Up, Text02B } from '../../../styles/text';
16+
import { SwitchIcon } from '../../../styles/icons';
17+
import { IColors } from '../../../styles/colors';
18+
import GradientView from '../../../components/GradientView';
19+
import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader';
20+
import SafeAreaInset from '../../../components/SafeAreaInset';
21+
import Money from '../../../components/Money';
22+
import NumberPadTextField from '../../../components/NumberPadTextField';
23+
import Button from '../../../components/Button';
24+
import { EBalanceUnit } from '../../../store/types/wallet';
25+
import { sendMax } from '../../../utils/wallet/transactions';
26+
import {
27+
selectedNetworkSelector,
28+
selectedWalletSelector,
29+
} from '../../../store/reselect/wallet';
30+
import { balanceUnitSelector } from '../../../store/reselect/settings';
31+
import { useSwitchUnit } from '../../../hooks/wallet';
32+
import { useCurrency } from '../../../hooks/displayValues';
33+
import { getNumberPadText } from '../../../utils/numberpad';
34+
import { convertToSats } from '../../../utils/conversion';
35+
import type { LNURLWithdrawProps } from '../../../navigation/types';
36+
37+
const Amount = ({
38+
navigation,
39+
route,
40+
}: LNURLWithdrawProps<'Amount'>): ReactElement => {
41+
const { t } = useTranslation('wallet');
42+
const { wParams } = route.params;
43+
const { minWithdrawable, maxWithdrawable } = wParams;
44+
const { fiatTicker } = useCurrency();
45+
const [nextUnit, switchUnit] = useSwitchUnit();
46+
const selectedWallet = useSelector(selectedWalletSelector);
47+
const selectedNetwork = useSelector(selectedNetworkSelector);
48+
const unit = useSelector(balanceUnitSelector);
49+
const [text, setText] = useState('');
50+
const [error, setError] = useState(false);
51+
52+
// Set initial text for NumberPadTextField
53+
useEffect(() => {
54+
const result = getNumberPadText(minWithdrawable, unit);
55+
setText(result);
56+
}, [selectedWallet, selectedNetwork, minWithdrawable, unit]);
57+
58+
const amount = useMemo((): number => {
59+
return convertToSats(text, unit);
60+
}, [text, unit]);
61+
62+
const maxWithdrawableProps = {
63+
...(unit !== EBalanceUnit.fiat ? { symbol: true } : { showFiat: true }),
64+
...(error && { color: 'brand' as keyof IColors }),
65+
};
66+
67+
const isMaxSendAmount = amount === maxWithdrawable;
68+
69+
const onChangeUnit = (): void => {
70+
const result = getNumberPadText(amount, nextUnit);
71+
setText(result);
72+
switchUnit();
73+
};
74+
75+
const onMaxAmount = useCallback((): void => {
76+
const result = getNumberPadText(maxWithdrawable, unit);
77+
setText(result);
78+
sendMax({ selectedWallet, selectedNetwork });
79+
}, [maxWithdrawable, unit, selectedWallet, selectedNetwork]);
80+
81+
const onError = (): void => {
82+
setError(true);
83+
setTimeout(() => setError(false), 500);
84+
};
85+
86+
const isValid = amount >= minWithdrawable && amount <= maxWithdrawable;
87+
88+
return (
89+
<GradientView style={styles.container}>
90+
<BottomSheetNavigationHeader title={t('lnurl_w_title')} />
91+
<View style={styles.content}>
92+
<NumberPadTextField value={text} testID="SendNumberField" />
93+
94+
<View style={styles.numberPad} testID="SendAmountNumberPad">
95+
<View style={styles.actions}>
96+
<View>
97+
<Caption13Up style={styles.maxWithdrawableText} color="gray1">
98+
{t('lnurl_w_max')}
99+
</Caption13Up>
100+
<Money
101+
key="small"
102+
sats={maxWithdrawable}
103+
size="text02m"
104+
decimalLength="long"
105+
testID="maxWithdrawable"
106+
{...maxWithdrawableProps}
107+
/>
108+
</View>
109+
<View style={styles.actionButtons}>
110+
<View style={styles.actionButtonContainer}>
111+
<TouchableOpacity
112+
style={styles.actionButton}
113+
color="white08"
114+
testID="SendNumberPadMax"
115+
onPress={onMaxAmount}>
116+
<Text02B
117+
size="12px"
118+
color={isMaxSendAmount ? 'orange' : 'brand'}>
119+
{t('send_max')}
120+
</Text02B>
121+
</TouchableOpacity>
122+
</View>
123+
124+
<View style={styles.actionButtonContainer}>
125+
<TouchableOpacity
126+
style={styles.actionButton}
127+
color="white08"
128+
onPress={onChangeUnit}
129+
testID="SendNumberPadUnit">
130+
<SwitchIcon color="brand" width={16.44} height={13.22} />
131+
<Text02B
132+
style={styles.actionButtonText}
133+
size="12px"
134+
color="brand">
135+
{nextUnit === 'BTC' && 'BTC'}
136+
{nextUnit === 'satoshi' && 'sats'}
137+
{nextUnit === 'fiat' && fiatTicker}
138+
</Text02B>
139+
</TouchableOpacity>
140+
</View>
141+
</View>
142+
</View>
143+
144+
<LNURLWNumberpad
145+
value={text}
146+
maxAmount={maxWithdrawable}
147+
onChange={setText}
148+
onError={onError}
149+
/>
150+
</View>
151+
152+
<View style={styles.buttonContainer}>
153+
<Button
154+
size="large"
155+
text={t('continue')}
156+
disabled={!isValid}
157+
testID="ContinueAmount"
158+
onPress={(): void => {
159+
navigation.navigate('Confirm', { amount, wParams });
160+
}}
161+
/>
162+
</View>
163+
</View>
164+
<SafeAreaInset type="bottom" minPadding={16} />
165+
</GradientView>
166+
);
167+
};
168+
169+
const styles = StyleSheet.create({
170+
container: {
171+
flex: 1,
172+
},
173+
content: {
174+
flex: 1,
175+
paddingHorizontal: 16,
176+
},
177+
numberPad: {
178+
flex: 1,
179+
marginTop: 'auto',
180+
maxHeight: 450,
181+
},
182+
actions: {
183+
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
184+
borderBottomWidth: 1,
185+
marginTop: 28,
186+
marginBottom: 5,
187+
paddingBottom: 16,
188+
flexDirection: 'row',
189+
justifyContent: 'space-between',
190+
alignItems: 'flex-end',
191+
},
192+
maxWithdrawableText: {
193+
marginBottom: 5,
194+
},
195+
actionButtons: {
196+
flexDirection: 'row',
197+
justifyContent: 'flex-end',
198+
marginLeft: 'auto',
199+
},
200+
actionButtonContainer: {
201+
alignItems: 'center',
202+
},
203+
actionButton: {
204+
marginLeft: 16,
205+
paddingVertical: 7,
206+
paddingHorizontal: 8,
207+
borderRadius: 8,
208+
flexDirection: 'row',
209+
alignItems: 'center',
210+
},
211+
actionButtonText: {
212+
marginLeft: 11,
213+
},
214+
buttonContainer: {
215+
justifyContent: 'flex-end',
216+
},
217+
});
218+
219+
export default memo(Amount);

0 commit comments

Comments
 (0)