Files
com.blitzwallet/app/components/admin/homeComponents/homeLightning/depositQRView.js
T
2026-07-22 08:48:27 -04:00

516 lines
15 KiB
JavaScript

import {
StyleSheet,
View,
Text,
TouchableOpacity,
ScrollView,
} from 'react-native';
import { useEffect, useMemo, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import {
CENTER,
CONTENT_KEYBOARD_OFFSET,
FONT,
SIZES,
} from '../../../../constants';
import {
HIDDEN_OPACITY,
INSET_WINDOW_WIDTH,
} from '../../../../constants/theme';
import { ThemeText } from '../../../../functions/CustomElements';
import CustomButton from '../../../../functions/CustomElements/button';
import QrCodeWrapper from '../../../../functions/CustomElements/QrWrapper';
import FullLoadingScreen from '../../../../functions/CustomElements/loadingScreen';
import { copyToClipboard } from '../../../../functions';
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
import GetThemeColors from '../../../../hooks/themeColors';
import { initializeAddressProcess } from '../../../../functions/receiveBitcoin/addressGeneration';
import { useAccumulationAddresses } from '../../../../hooks/useAccumulationAddresses';
import { ACCUMULATION_CHAINS } from '../../../../constants/accumulationAddresses';
import { useGlobalContextProvider } from '../../../../../context-store/context';
import { useNodeContext } from '../../../../../context-store/nodeContext';
import { useWebView } from '../../../../../context-store/webViewContext';
import { useFlashnet } from '../../../../../context-store/flashnetContext';
import { useSparkWallet } from '../../../../../context-store/sparkContext';
import { useActiveCustodyAccount } from '../../../../../context-store/activeAccount';
import { useKeysContext } from '../../../../../context-store/keys';
import { useRootstockProvider } from '../../../../../context-store/rootstockSwapContext';
import { useLiquidEvent } from '../../../../../context-store/liquidEventContext';
import { useAppStatus } from '../../../../../context-store/appStatus';
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
import { useToast } from '../../../../../context-store/toastManager';
import { applyErrorAnimationTheme } from '../../../../functions/lottieViewColorTransformer';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import LottieView from 'lottie-react-native';
const errorTxAnimation = require('../../../../assets/errorTxAnimation.json');
const capitalize = value =>
value ? value[0].toUpperCase() + value.slice(1) : '';
export default function DepositQRView({
config,
setContentHeight,
onBack,
isActive,
}) {
const navigate = useNavigation();
const { t } = useTranslation();
const { showToast } = useToast();
const { theme, darkModeType } = useGlobalThemeContext();
const { backgroundOffset, backgroundColor } = GetThemeColors();
const { fiatStats } = useNodeContext();
const { sendWebViewRequest } = useWebView();
const { swapLimits, poolInfoRef } = useFlashnet();
const { sparkInformation } = useSparkWallet();
const { masterInfoObject } = useGlobalContextProvider();
const { startRootstockEventListener, signer } = useRootstockProvider();
const { startLiquidEventListener } = useLiquidEvent();
const { isUsingAltAccount, currentWalletMnemoinc } =
useActiveCustodyAccount();
const { contactsPrivateKey, publicKey: contactsPublicKey } = useKeysContext();
const { createAddress, addressesForOption } = useAccumulationAddresses();
const { minMaxLiquidSwapAmounts, screenDimensions } = useAppStatus();
const { bottomPadding } = useGlobalInsets();
const qrContainerSize = Math.round(screenDimensions.width * 0.7);
const errorIconSize = Math.round(screenDimensions.width * 0.5);
const qrInnerSize = qrContainerSize - 25;
const [addressState, setAddressState] = useState({
generatedAddress: '',
isGeneratingInvoice: false,
errorMessageText: { type: '', text: '' },
fee: 0,
});
const option = config?.selectedRecieveOption?.toLowerCase();
const chainDisplayLabel =
ACCUMULATION_CHAINS.find(c => c.id === config?.sourceChain)?.label ??
capitalize(config?.sourceChain);
const errorAnimation = useMemo(() => {
return applyErrorAnimationTheme(
errorTxAnimation,
theme ? (darkModeType ? 'lightsOut' : 'dark') : 'light',
);
}, [theme, darkModeType]);
useEffect(() => {
if (isActive) setContentHeight(700);
}, [isActive]);
const handleCreateNew = async cancelled => {
const triple = {
sourceChain: config.sourceChain,
sourceAsset: config.sourceAsset,
destinationAsset: config.destinationAsset,
};
setAddressState({
generatedAddress: '',
isGeneratingInvoice: true,
errorMessageText: { type: null, text: '' },
fee: 0,
});
try {
if (config.depositAddress) {
setAddressState(prev => ({
...prev,
generatedAddress: config.depositAddress,
isGeneratingInvoice: false,
}));
return;
}
const result = await createAddress({ ...triple, forceNew: true });
// At the cap: fall back to an already-saved address instead of erroring.
if (result?.error === 'limit_reached') {
const saved = addressesForOption(triple)[0]?.depositAddress;
if (saved) {
setAddressState(prev => ({
...prev,
generatedAddress: saved,
isGeneratingInvoice: false,
}));
return;
}
if (cancelled) return;
setAddressState(prev => ({ ...prev, isGeneratingInvoice: false }));
navigate.navigate('ErrorScreen', {
errorMessage: t('screens.accumulationAddresses.create.limitReached'),
});
return;
}
if (result?.error) {
const saved = addressesForOption(triple)[0]?.depositAddress;
if (saved) {
if (cancelled) return;
setAddressState(prev => ({
...prev,
generatedAddress: saved,
isGeneratingInvoice: false,
}));
return;
}
if (cancelled) return;
setAddressState(prev => ({ ...prev, isGeneratingInvoice: false }));
navigate.navigate('ErrorScreen', {
errorMessage: t('screens.accumulationAddresses.errors.createFailed'),
});
return;
}
const newAddress =
typeof result.address === 'string'
? result.address
: result.address?.depositAddress;
if (cancelled) return;
setAddressState(prev => ({
...prev,
generatedAddress: newAddress || '',
isGeneratingInvoice: false,
}));
} catch {
if (cancelled) return;
setAddressState(prev => ({ ...prev, isGeneratingInvoice: false }));
}
};
useEffect(() => {
if (!config) return;
let cancelled = false;
if (config.selectedRecieveOption?.toLowerCase() === 'stablecoins') {
handleCreateNew(cancelled);
return; // skip initializeAddressProcess; a specific address was picked from the selector
}
setAddressState({
generatedAddress: '',
isGeneratingInvoice: true,
errorMessageText: { type: null, text: '' },
fee: 0,
});
async function runAddressInit() {
await initializeAddressProcess({
userBalanceDenomination: masterInfoObject.userBalanceDenomination,
receivingAmount: 0,
description: undefined,
masterInfoObject,
setAddressState,
selectedRecieveOption: config.selectedRecieveOption,
navigate,
signer,
currentWalletMnemoinc,
sendWebViewRequest,
sparkInformation,
endReceiveType:
config.selectedRecieveOption === 'spark' && config.fromStablecoin
? 'USD'
: 'BTC',
swapLimits,
setInitialSendAmount: () => {},
userReceiveAmount: 0,
poolInfoRef,
isHoldInvoice: false,
holdExpirySeconds: 2592000,
contactsPrivateKey,
contactsPublicKey,
createAddress,
sourceChain: config.sourceChain,
sourceAsset: config.sourceAsset,
destinationAsset: config.destinationAsset,
});
if (cancelled) return;
const option = config.selectedRecieveOption?.toLowerCase();
if (option === 'liquid') {
startLiquidEventListener(60);
} else if (option === 'rootstock') {
startRootstockEventListener({ durationMs: 1200000 });
}
}
runAddressInit();
return () => {
cancelled = true;
};
}, [config]);
const minimumDepositWarning = useMemo(() => {
if (option === 'stablecoins') {
return t('wallet.halfModal.depositMinimumStablecoin', {
amount: displayCorrectDenomination({
amount: Number(1).toFixed(2),
masterInfoObject: {
...masterInfoObject,
userBalanceDenomination: 'fiat',
},
fiatStats,
forceCurrency: 'USD',
convertAmount: false,
}),
});
}
if (option !== 'liquid' && option !== 'rootstock') return null;
const minSendAmount =
option === 'rootstock'
? minMaxLiquidSwapAmounts?.rsk?.min + 1000
: minMaxLiquidSwapAmounts?.min;
if (!Number.isFinite(minSendAmount)) return null;
const swapType = option === 'rootstock' ? 'Rootstock' : 'Liquid';
return t('wallet.receivePages.switchReceiveOptionPage.swapWarning', {
amount: displayCorrectDenomination({
amount: minSendAmount,
masterInfoObject,
fiatStats,
}),
swapType,
});
}, [fiatStats, masterInfoObject, minMaxLiquidSwapAmounts, option, t]);
const address = addressState.generatedAddress || '';
const addressSegments = useMemo(() => {
return (address.match(/.{1,5}/g) || []).map((group, i, all) => (
<Text
key={i}
style={{
fontFamily: i % 2 === 0 ? FONT.Title_SemiBold : FONT.Title_Regular,
}}
>
{group}
{i < all.length - 1 ? ' ' : ''}
</Text>
));
}, [address]);
if (!config) return null;
const title =
option === 'stablecoins'
? chainDisplayLabel
: capitalize(config.selectedRecieveOption);
const instruction =
option === 'stablecoins'
? t('wallet.halfModal.depositQRInstruction_stablecoins', {
asset: config.sourceAsset,
chain: chainDisplayLabel,
})
: t(
`wallet.halfModal.depositQRInstruction_${option}${
option === 'spark' && config.fromStablecoin
? '_dollars'
: option === 'spark' && config.fromTokens
? '_tokens'
: ''
}`,
);
const handleCopy = () => {
if (!address) return;
copyToClipboard(address, showToast);
};
if (addressState.isGeneratingInvoice) {
return (
<View style={styles.centerContent}>
<FullLoadingScreen showText={false} />
</View>
);
}
if (addressState.errorMessageText?.text) {
return (
<View style={styles.centerContent}>
<LottieView
source={errorAnimation}
loop={false}
autoPlay={true}
style={{
width: errorIconSize,
height: errorIconSize,
}}
/>
<ThemeText
styles={styles.errorText}
content={t('wallet.halfModal.depositQRError', { addressType: title })}
/>
<CustomButton
buttonStyles={{
width: '100%',
marginBottom: bottomPadding,
...CENTER,
marginTop: 'auto',
}}
actionFunction={onBack}
textContent={t('constants.back')}
/>
</View>
);
}
return (
<View style={styles.container}>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: bottomPadding },
]}
showsVerticalScrollIndicator={false}
>
<TouchableOpacity
style={styles.qrWrapper}
activeOpacity={0.8}
onPress={handleCopy}
>
<QrCodeWrapper
QRData={address}
qrSize={qrInnerSize}
outerContainerStyle={{
width: qrContainerSize,
height: qrContainerSize,
}}
innerContainerStyle={{
width: qrInnerSize,
height: qrInnerSize,
}}
/>
</TouchableOpacity>
<ThemeText
styles={styles.addressText}
content={addressSegments}
CustomNumberOfLines={4}
/>
<View
style={[
styles.divider,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
]}
/>
<ThemeText styles={styles.instruction} content={instruction} />
</ScrollView>
{minimumDepositWarning ? (
<ThemeText
styles={styles.warningDescription}
content={minimumDepositWarning}
/>
) : null}
<CustomButton
buttonStyles={{
width: '100%',
marginBottom: bottomPadding,
...CENTER,
marginTop: CONTENT_KEYBOARD_OFFSET,
}}
actionFunction={handleCopy}
textContent={t('wallet.halfModal.copyAddress')}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
width: INSET_WINDOW_WIDTH,
...CENTER,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
minHeight: 40,
// marginBottom: 12,
},
backChevron: {
position: 'absolute',
left: 0,
height: 40,
justifyContent: 'center',
},
centerContent: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
scrollContent: {
width: '100%',
flexGrow: 1,
alignItems: 'stretch',
},
qrWrapper: {
alignSelf: 'center',
},
addressHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginTop: 24,
},
addressLabel: {
fontSize: SIZES.small,
opacity: 0.6,
textTransform: 'uppercase',
includeFontPadding: false,
},
addressText: {
fontSize: SIZES.small,
lineHeight: 24,
marginTop: 10,
includeFontPadding: false,
textAlign: 'center',
width: '100%',
},
divider: {
height: 1,
width: '100%',
marginVertical: 16,
},
instruction: {
fontSize: SIZES.small,
opacity: HIDDEN_OPACITY,
textAlign: 'center',
includeFontPadding: false,
},
warningDescription: {
includeFontPadding: false,
fontSize: SIZES.small,
textAlign: 'center',
opacity: HIDDEN_OPACITY,
marginTop: 16,
},
errorText: {
width: '90%',
fontSize: SIZES.medium,
textAlign: 'center',
marginTop: 12,
includeFontPadding: false,
},
errorSubText: {
width: '90%',
fontSize: SIZES.smedium,
opacity: 0.5,
textAlign: 'center',
marginTop: 8,
includeFontPadding: false,
},
});