516 lines
15 KiB
JavaScript
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,
|
|
},
|
|
});
|