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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@react-navigation/native-stack": "7.2.0",
"@reduxjs/toolkit": "2.2.6",
"@shopify/react-native-skia": "next",
"@synonymdev/blocktank-lsp-http-client": "2.2.0",
"@synonymdev/blocktank-lsp-http-client": "2.5.0",
"@synonymdev/react-native-ldk": "0.0.159",
"@synonymdev/react-native-lnurl": "0.0.10",
"@synonymdev/react-native-pubky": "^0.3.0",
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 @@ -9,6 +9,7 @@ import {

import type { RecoveryStackParamList } from '../../screens/Recovery/RecoveryNavigator';
import type { BackupStackParamList } from '../../sheets/BackupNavigation';
import type { GiftStackParamList } from '../../sheets/GiftNavigation';
import type { LNURLWithdrawStackParamList } from '../../sheets/LNURLWithdrawNavigation';
import type { OrangeTicketStackParamList } from '../../sheets/OrangeTicketNavigation';
import type { PinStackParamList } from '../../sheets/PINNavigation';
Expand Down Expand Up @@ -103,6 +104,9 @@ export type OrangeTicketScreenProps<
T extends keyof OrangeTicketStackParamList,
> = NativeStackScreenProps<OrangeTicketStackParamList, T>;

export type GiftScreenProps<T extends keyof GiftStackParamList> =
NativeStackScreenProps<GiftStackParamList, T>;

export type TreasureHuntScreenProps<
T extends keyof TreasureHuntStackParamList,
> = NativeStackScreenProps<TreasureHuntStackParamList, T>;
82 changes: 82 additions & 0 deletions src/screens/Gift/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { ReactElement, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, View } from 'react-native';

import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader';
import GradientView from '../../components/GradientView';
import SafeAreaInset from '../../components/SafeAreaInset';
import Button from '../../components/buttons/Button';
import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { BodyM } from '../../styles/text';

const imageSrc = require('../../assets/illustrations/exclamation-mark.png');

const ErrorScreen = (): ReactElement => {
const { t } = useTranslation('other');
const sheetRef = useSheetRef('gift');

const onContinue = (): void => {
sheetRef.current?.close();
};

return (
<GradientView style={styles.root}>
<BottomSheetNavigationHeader
title={t('gift.error.title')}
showBackButton={false}
/>

<View style={styles.content}>
<BodyM color="secondary">{t('gift.error.text')}</BodyM>

<View style={styles.imageContainer}>
<Image style={styles.image} source={imageSrc} />
</View>

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

const styles = StyleSheet.create({
root: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 16,
},
imageContainer: {
flexShrink: 1,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
width: 256,
aspectRatio: 1,
marginTop: 'auto',
},
image: {
flex: 1,
resizeMode: 'contain',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 'auto',
gap: 16,
},
button: {
flex: 1,
},
});

export default memo(ErrorScreen);
187 changes: 187 additions & 0 deletions src/screens/Gift/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { EPaymentType } from 'beignet';
import React, { ReactElement, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, View } from 'react-native';

import { ActivityIndicator } from '../../components/ActivityIndicator';
import AmountToggle from '../../components/AmountToggle';
import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader';
import GradientView from '../../components/GradientView';
import SafeAreaInset from '../../components/SafeAreaInset';
import { useLightningMaxInboundCapacity } from '../../hooks/lightning';
import { GiftScreenProps } from '../../navigation/types';
import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { dispatch } from '../../store/helpers';
import { addActivityItem } from '../../store/slices/activity';
import { updateSettings } from '../../store/slices/settings';
import {
EActivityType,
TLightningActivityItem,
} from '../../store/types/activity';
import { createLightningInvoice } from '../../store/utils/lightning';
import { showSheet } from '../../store/utils/ui';
import { BodyM } from '../../styles/text';
import { giftOrder, giftPay, openChannel } from '../../utils/blocktank';
import { vibrate } from '../../utils/helpers';

const imageSrc = require('../../assets/illustrations/gift.png');

const Loading = ({
navigation,
route,
}: GiftScreenProps<'Loading'>): ReactElement => {
const { code, amount } = route.params;
const { t } = useTranslation('other');
const sheetRef = useSheetRef('gift');
const maxInboundCapacity = useLightningMaxInboundCapacity();

// biome-ignore lint/correctness/useExhaustiveDependencies: on mount
const getGift = useCallback(async (): Promise<void> => {
const getWithoutLiquidity = async (): Promise<void> => {
const orderResult = await giftOrder(code);

if (orderResult.isErr()) {
if (orderResult.error.message.includes('GIFT_CODE_ALREADY_USED')) {
navigation.navigate('Used', { amount });
} else {
navigation.navigate('Error');
}

return;
}

const { orderId } = orderResult.value;

if (!orderId) {
navigation.navigate('Error');
return;
}

const openResult = await openChannel(orderId);

if (openResult.isErr()) {
navigation.navigate('Error');
return;
}

const order = openResult.value;

const activityItem: TLightningActivityItem = {
id: order.channel?.fundingTx.id ?? '',
activityType: EActivityType.lightning,
txType: EPaymentType.received,
status: 'successful',
message: code,
address: '',
value: order.clientBalanceSat,
confirmed: true,
timestamp: new Date().getTime(),
};

dispatch(addActivityItem(activityItem));
dispatch(updateSettings({ hideOnboardingMessage: true }));
vibrate({ type: 'default' });
sheetRef.current?.close();
showSheet('receivedTx', {
id: activityItem.id,
activityType: EActivityType.lightning,
value: activityItem.value,
});
};

const getWithLiquidity = async (): Promise<void> => {
const invoiceResult = await createLightningInvoice({
amountSats: 0,
description: `blocktank-gift-code:${code}`,
expiryDeltaSeconds: 3600,
});

if (invoiceResult.isErr()) {
navigation.navigate('Error');
return;
}

const invoice = invoiceResult.value.to_str;
const result = await giftPay(invoice);

if (result.isErr()) {
if (result.error.message.includes('GIFT_CODE_ALREADY_USED')) {
navigation.navigate('Used', { amount });
} else {
navigation.navigate('Error');
}

return;
}

sheetRef.current?.close();
};

if (maxInboundCapacity >= amount) {
await getWithLiquidity();
} else {
await getWithoutLiquidity();
}
}, []);

useEffect(() => {
getGift();
}, [getGift]);

return (
<GradientView style={styles.root}>
<BottomSheetNavigationHeader title={t('gift.claiming.title')} />

<View style={styles.content}>
<AmountToggle amount={amount} />

<BodyM style={styles.text} color="secondary">
{t('gift.claiming.text')}
</BodyM>

<View style={styles.imageContainer}>
<Image style={styles.image} source={imageSrc} />
</View>

<View style={styles.footer}>
<ActivityIndicator size={32} />
</View>
</View>
<SafeAreaInset type="bottom" minPadding={16} />
</GradientView>
);
};

const styles = StyleSheet.create({
root: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 16,
},
text: {
marginTop: 32,
},
imageContainer: {
flexShrink: 1,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
width: 256,
aspectRatio: 1,
marginTop: 'auto',
},
image: {
flex: 1,
resizeMode: 'contain',
},
footer: {
marginTop: 'auto',
marginBottom: 16,
justifyContent: 'center',
alignItems: 'center',
},
});

export default Loading;
92 changes: 92 additions & 0 deletions src/screens/Gift/Used.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { ReactElement, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, View } from 'react-native';

import AmountToggle from '../../components/AmountToggle';
import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader';
import GradientView from '../../components/GradientView';
import SafeAreaInset from '../../components/SafeAreaInset';
import Button from '../../components/buttons/Button';
import { GiftScreenProps } from '../../navigation/types';
import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { BodyM } from '../../styles/text';

const imageSrc = require('../../assets/illustrations/exclamation-mark.png');

const UsedCard = ({ route }: GiftScreenProps<'Used'>): ReactElement => {
const { amount } = route.params;
const { t } = useTranslation('other');
const sheetRef = useSheetRef('gift');

const onContinue = (): void => {
sheetRef.current?.close();
};

return (
<GradientView style={styles.root}>
<BottomSheetNavigationHeader
title={t('gift.used.title')}
showBackButton={false}
/>

<View style={styles.content}>
<AmountToggle amount={amount} />

<BodyM style={styles.text} color="secondary">
{t('gift.used.text')}
</BodyM>

<View style={styles.imageContainer}>
<Image style={styles.image} source={imageSrc} />
</View>

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

const styles = StyleSheet.create({
root: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 16,
},
text: {
marginTop: 32,
},
imageContainer: {
flexShrink: 1,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
width: 256,
aspectRatio: 1,
marginTop: 'auto',
},
image: {
flex: 1,
resizeMode: 'contain',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 'auto',
gap: 16,
},
button: {
flex: 1,
},
});

export default memo(UsedCard);
Loading
Loading