Skip to content

Commit

Permalink
feat: lnurl withdraw specify amount
Browse files Browse the repository at this point in the history
  • Loading branch information
limpbrains committed Jul 10, 2023
1 parent 9790748 commit d23c5f6
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 33 deletions.
69 changes: 69 additions & 0 deletions src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { ReactElement, memo } from 'react';
import { useSelector } from 'react-redux';
import {
NativeStackNavigationOptions,
NativeStackNavigationProp,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
import { LNURLWithdrawParams } from 'js-lnurl';

import BottomSheetWrapper from '../../components/BottomSheetWrapper';
import { __E2E__ } from '../../constants/env';
import { useSnapPoints } from '../../hooks/bottomSheet';
import Amount from '../../screens/Wallets/LNURLWithdraw/Amount';
import Confirm from '../../screens/Wallets/LNURLWithdraw/Confirm';
import { viewControllerSelector } from '../../store/reselect/ui';
import { NavigationContainer } from '../../styles/components';

export type LNURLWithdrawNavigationProp =
NativeStackNavigationProp<LNURLWithdrawStackParamList>;

export type LNURLWithdrawStackParamList = {
Amount: { wParams: LNURLWithdrawParams };
Confirm: { amount: number; wParams: LNURLWithdrawParams };
};

const Stack = createNativeStackNavigator<LNURLWithdrawStackParamList>();

const screenOptions: NativeStackNavigationOptions = {
headerShown: false,
...(__E2E__ ? { animationDuration: 0 } : {}),
};

const LNURLWithdrawNavigation = (): ReactElement => {
const snapPoints = useSnapPoints('large');
const { isOpen, wParams } = useSelector((state) => {
return viewControllerSelector(state, 'lnurlWithdraw');
});

if (!wParams) {
return <></>;
}

// if max === min withdrawable amount, skip the Amount screen
const initialRouteName =
wParams.minWithdrawable === wParams.maxWithdrawable ? 'Confirm' : 'Amount';

return (
<BottomSheetWrapper view="lnurlWithdraw" snapPoints={snapPoints}>
<NavigationContainer key={isOpen.toString()}>
<Stack.Navigator
screenOptions={screenOptions}
initialRouteName={initialRouteName}>
<Stack.Screen
name="Amount"
component={Amount}
initialParams={{ wParams }}
/>
<Stack.Screen
name="Confirm"
component={Confirm}
initialParams={{ wParams, amount: wParams.minWithdrawable }}
/>
</Stack.Navigator>
</NavigationContainer>
</BottomSheetWrapper>
);
};

export default memo(LNURLWithdrawNavigation);
2 changes: 2 additions & 0 deletions src/navigation/root/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import BackupNavigation from '../bottom-sheet/BackupNavigation';
import PINNavigation from '../bottom-sheet/PINNavigation';
import ForceTransfer from '../bottom-sheet/ForceTransfer';
import CloseChannelSuccess from '../bottom-sheet/CloseChannelSuccess';
import LNURLWithdrawNavigation from '../bottom-sheet/LNURLWithdrawNavigation';
import { __E2E__ } from '../../constants/env';
import type { RootStackParamList } from '../types';

Expand Down Expand Up @@ -244,6 +245,7 @@ const RootNavigator = (): ReactElement => {
<ForceTransfer />
<CloseChannelSuccess />
<BackupSubscriber />
<LNURLWithdrawNavigation />

<Dialog
visible={showDialog && isAuthenticated}
Expand Down
4 changes: 4 additions & 0 deletions src/navigation/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { PinStackParamList } from '../bottom-sheet/PINNavigation';
import type { ProfileLinkStackParamList } from '../bottom-sheet/ProfileLinkNavigation';
import type { ReceiveStackParamList } from '../bottom-sheet/ReceiveNavigation';
import type { SendStackParamList } from '../bottom-sheet/SendNavigation';
import type { LNURLWithdrawStackParamList } from '../bottom-sheet/LNURLWithdrawNavigation';

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

export type SendScreenProps<T extends keyof SendStackParamList> =
NativeStackScreenProps<SendStackParamList, T>;

export type LNURLWithdrawProps<T extends keyof LNURLWithdrawStackParamList> =
NativeStackScreenProps<LNURLWithdrawStackParamList, T>;
219 changes: 219 additions & 0 deletions src/screens/Wallets/LNURLWithdraw/Amount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import React, {
ReactElement,
memo,
useCallback,
useMemo,
useState,
useEffect,
} from 'react';
import { StyleSheet, View } from 'react-native';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';

import LNURLWNumberpad from './LNURLWNumberpad';
import { TouchableOpacity } from '../../../styles/components';
import { Caption13Up, Text02B } from '../../../styles/text';
import { SwitchIcon } from '../../../styles/icons';
import { IColors } from '../../../styles/colors';
import GradientView from '../../../components/GradientView';
import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader';
import SafeAreaInset from '../../../components/SafeAreaInset';
import Money from '../../../components/Money';
import NumberPadTextField from '../../../components/NumberPadTextField';
import Button from '../../../components/Button';
import { EBalanceUnit } from '../../../store/types/wallet';
import { sendMax } from '../../../utils/wallet/transactions';
import {
selectedNetworkSelector,
selectedWalletSelector,
} from '../../../store/reselect/wallet';
import { balanceUnitSelector } from '../../../store/reselect/settings';
import { useSwitchUnit } from '../../../hooks/wallet';
import { useCurrency } from '../../../hooks/displayValues';
import { getNumberPadText } from '../../../utils/numberpad';
import { convertToSats } from '../../../utils/conversion';
import type { LNURLWithdrawProps } from '../../../navigation/types';

const Amount = ({
navigation,
route,
}: LNURLWithdrawProps<'Amount'>): ReactElement => {
const { t } = useTranslation('wallet');
const { wParams } = route.params;
const { minWithdrawable, maxWithdrawable } = wParams;
const { fiatTicker } = useCurrency();
const [nextUnit, switchUnit] = useSwitchUnit();
const selectedWallet = useSelector(selectedWalletSelector);
const selectedNetwork = useSelector(selectedNetworkSelector);
const unit = useSelector(balanceUnitSelector);
const [text, setText] = useState('');
const [error, setError] = useState(false);

// Set initial text for NumberPadTextField
useEffect(() => {
const result = getNumberPadText(minWithdrawable, unit);
setText(result);
}, [selectedWallet, selectedNetwork, minWithdrawable, unit]);

const amount = useMemo((): number => {
return convertToSats(text, unit);
}, [text, unit]);

const maxWithdrawableProps = {
...(unit !== EBalanceUnit.fiat ? { symbol: true } : { showFiat: true }),
...(error && { color: 'brand' as keyof IColors }),
};

const isMaxSendAmount = amount === maxWithdrawable;

const onChangeUnit = (): void => {
const result = getNumberPadText(amount, nextUnit);
setText(result);
switchUnit();
};

const onMaxAmount = useCallback((): void => {
const result = getNumberPadText(maxWithdrawable, unit);
setText(result);
sendMax({ selectedWallet, selectedNetwork });
}, [maxWithdrawable, unit, selectedWallet, selectedNetwork]);

const onError = (): void => {
setError(true);
setTimeout(() => setError(false), 500);
};

const isValid = amount >= minWithdrawable && amount <= maxWithdrawable;

return (
<GradientView style={styles.container}>
<BottomSheetNavigationHeader title={t('lnurl_w_title')} />
<View style={styles.content}>
<NumberPadTextField value={text} testID="SendNumberField" />

<View style={styles.numberPad} testID="SendAmountNumberPad">
<View style={styles.actions}>
<View>
<Caption13Up style={styles.maxWithdrawableText} color="gray1">
{t('lnurl_w_max')}
</Caption13Up>
<Money
key="small"
sats={maxWithdrawable}
size="text02m"
decimalLength="long"
testID="maxWithdrawable"
{...maxWithdrawableProps}
/>
</View>
<View style={styles.actionButtons}>
<View style={styles.actionButtonContainer}>
<TouchableOpacity
style={styles.actionButton}
color="white08"
testID="SendNumberPadMax"
onPress={onMaxAmount}>
<Text02B
size="12px"
color={isMaxSendAmount ? 'orange' : 'brand'}>
{t('send_max')}
</Text02B>
</TouchableOpacity>
</View>

<View style={styles.actionButtonContainer}>
<TouchableOpacity
style={styles.actionButton}
color="white08"
onPress={onChangeUnit}
testID="SendNumberPadUnit">
<SwitchIcon color="brand" width={16.44} height={13.22} />
<Text02B
style={styles.actionButtonText}
size="12px"
color="brand">
{nextUnit === 'BTC' && 'BTC'}
{nextUnit === 'satoshi' && 'sats'}
{nextUnit === 'fiat' && fiatTicker}
</Text02B>
</TouchableOpacity>
</View>
</View>
</View>

<LNURLWNumberpad
value={text}
maxAmount={maxWithdrawable}
onChange={setText}
onError={onError}
/>
</View>

<View style={styles.buttonContainer}>
<Button
size="large"
text={t('continue')}
disabled={!isValid}
testID="ContinueAmount"
onPress={(): void => {
navigation.navigate('Confirm', { amount, wParams });
}}
/>
</View>
</View>
<SafeAreaInset type="bottom" minPadding={16} />
</GradientView>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 16,
},
numberPad: {
flex: 1,
marginTop: 'auto',
maxHeight: 450,
},
actions: {
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
borderBottomWidth: 1,
marginTop: 28,
marginBottom: 5,
paddingBottom: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
},
maxWithdrawableText: {
marginBottom: 5,
},
actionButtons: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginLeft: 'auto',
},
actionButtonContainer: {
alignItems: 'center',
},
actionButton: {
marginLeft: 16,
paddingVertical: 7,
paddingHorizontal: 8,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
},
actionButtonText: {
marginLeft: 11,
},
buttonContainer: {
justifyContent: 'flex-end',
},
});

export default memo(Amount);
Loading

0 comments on commit d23c5f6

Please sign in to comment.