Add percent to send max (#142)

* updating imports

* fixed infinate loop bug

* added percent dropdown to send max
This commit is contained in:
Blake Kaufman
2025-08-18 14:25:19 -04:00
committed by GitHub
parent e76cd37b9c
commit 6522aac4e5
33 changed files with 620 additions and 434 deletions
+1
View File
@@ -17,5 +17,6 @@ module.exports = {
Buffer: 'readonly',
btoa: 'readonly', // both are native to Hermes now
atob: 'readonly',
TextDecoder: 'readonly',
},
};
+1 -10
View File
@@ -6,17 +6,8 @@
*/
import {DefaultTheme, NavigationContainer} from '@react-navigation/native';
import './pollyfills';
import './i18n'; // for translation option
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import React, {
JSX,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, {JSX, useCallback, useEffect, useMemo, useState} from 'react';
import {registerRootComponent} from 'expo';
import {
getLocalStorageItem,
@@ -1,17 +1,20 @@
import React, {useState, useMemo} from 'react';
import {useNavigation} from '@react-navigation/native';
import {useTranslation} from 'react-i18next';
import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
import {useGlobalContextProvider} from '../../../../../../context-store/context';
import {useNodeContext} from '../../../../../../context-store/nodeContext';
import {useAppStatus} from '../../../../../../context-store/appStatus';
import {useActiveCustodyAccount} from '../../../../../../context-store/activeAccount';
import CustomButton from '../../../../../functions/CustomElements/button';
import displayCorrectDenomination from '../../../../../functions/displayCorrectDenomination';
import {
CENTER,
SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT,
} from '../../../../../constants';
import CustomButton from '../../../../../functions/CustomElements/button';
import {useState} from 'react';
import {useNodeContext} from '../../../../../../context-store/nodeContext';
import {useAppStatus} from '../../../../../../context-store/appStatus';
import displayCorrectDenomination from '../../../../../functions/displayCorrectDenomination';
import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
import {useActiveCustodyAccount} from '../../../../../../context-store/activeAccount';
import {useTranslation} from 'react-i18next';
export default function AcceptButtonSendPage({
canSendPayment,
@@ -33,141 +36,185 @@ export default function AcceptButtonSendPage({
seletctedToken,
isLRC20Payment,
}) {
const navigate = useNavigation();
const {t} = useTranslation();
const [isGeneratingInvoice, setIsGeneratingInvoice] = useState(false);
const {masterInfoObject} = useGlobalContextProvider();
const {liquidNodeInformation, fiatStats} = useNodeContext();
const {minMaxLiquidSwapAmounts} = useAppStatus();
const {currentWalletMnemoinc} = useActiveCustodyAccount();
const {t} = useTranslation();
const [isGeneratingInvoice, setIsGeneratingInvoice] = useState(false);
const navigate = useNavigation();
return (
<CustomButton
buttonStyles={{
opacity:
canSendPayment &&
!(
isLiquidPayment &&
(convertedSendAmount < minMaxLiquidSwapAmounts.min ||
convertedSendAmount > minMaxLiquidSwapAmounts.max)
) &&
!(
paymentInfo?.type === 'lnUrlPay' &&
(convertedSendAmount < minLNURLSatAmount ||
convertedSendAmount > maxLNURLSatAmount)
) &&
!(
paymentInfo?.type === 'Bitcoin' &&
convertedSendAmount < SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT
) &&
!(isLRC20Payment && sparkInformation.balance < 10)
? 1
: 0.5,
width: 'auto',
...CENTER,
}}
useLoading={isGeneratingInvoice}
actionFunction={handleEnterSendAmount}
textContent={t('constants.accept')}
/>
);
async function handleEnterSendAmount() {
const isLiquidAmountValid = useMemo(() => {
if (!isLiquidPayment) return true;
return (
convertedSendAmount >= minMaxLiquidSwapAmounts.min &&
convertedSendAmount <= minMaxLiquidSwapAmounts.max
);
}, [isLiquidPayment, convertedSendAmount, minMaxLiquidSwapAmounts]);
const isLNURLAmountValid = useMemo(() => {
if (paymentInfo?.type !== 'lnUrlPay') return true;
return (
convertedSendAmount >= minLNURLSatAmount &&
convertedSendAmount <= maxLNURLSatAmount
);
}, [
paymentInfo?.type,
convertedSendAmount,
minLNURLSatAmount,
maxLNURLSatAmount,
]);
const isBitcoinAmountValid = useMemo(() => {
if (paymentInfo?.type !== 'Bitcoin') return true;
return convertedSendAmount >= SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT;
}, [paymentInfo?.type, convertedSendAmount]);
const isLRC20Valid = useMemo(() => {
if (!isLRC20Payment) return true;
return sparkInformation.balance >= 10;
}, [isLRC20Payment, sparkInformation?.balance]);
const buttonOpacity = useMemo(() => {
return canSendPayment &&
isLiquidAmountValid &&
isLNURLAmountValid &&
isBitcoinAmountValid &&
isLRC20Valid
? 1
: 0.5;
}, [
canSendPayment,
isLiquidAmountValid,
isLNURLAmountValid,
isBitcoinAmountValid,
isLRC20Valid,
]);
const handleLiquidAmountError = () => {
const isMinError = convertedSendAmount < minMaxLiquidSwapAmounts.min;
const errorAmount = isMinError
? minMaxLiquidSwapAmounts.min
: minMaxLiquidSwapAmounts.max;
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.liquidError', {
overFlowType: isMinError ? 'Minimum' : 'Maximum',
amount: displayCorrectDenomination({
amount: errorAmount,
fiatStats,
masterInfoObject,
}),
}),
});
};
const handleBitcoinAmountError = () => {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.onchainError', {
amount: displayCorrectDenomination({
amount: SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT,
fiatStats,
masterInfoObject,
}),
}),
});
};
const handleLNURLPayError = () => {
const isMinError = convertedSendAmount < minLNURLSatAmount;
const errorAmount = isMinError ? minLNURLSatAmount : maxLNURLSatAmount;
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.lnurlPayError', {
overFlowType: isMinError ? 'Minimum' : 'Maximum',
amount: displayCorrectDenomination({
amount: errorAmount,
fiatStats,
masterInfoObject,
}),
}),
});
};
const handleLRC20Error = () => {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.lrc20FeeError', {
amount: displayCorrectDenomination({
amount: 10,
masterInfoObject,
fiatStats,
}),
balance: displayCorrectDenomination({
amount: sparkInformation.balance,
masterInfoObject,
fiatStats,
}),
}),
});
};
const handleInsufficientBalanceError = () => {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.balanceError'),
});
};
const handleNoSendAmountError = () => {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.noSendAmountError'),
});
};
const validatePaymentAmount = () => {
if (!paymentInfo?.sendAmount) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.noSendAmountError'),
});
return;
handleNoSendAmountError();
return false;
}
if (
isLiquidPayment &&
(convertedSendAmount < minMaxLiquidSwapAmounts.min ||
convertedSendAmount > minMaxLiquidSwapAmounts.max)
) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.liquidError', {
overFlowType:
convertedSendAmount < minMaxLiquidSwapAmounts.min
? 'Minimum'
: 'Maximum',
amount: displayCorrectDenomination({
amount:
convertedSendAmount < minMaxLiquidSwapAmounts.min
? minMaxLiquidSwapAmounts.min
: minMaxLiquidSwapAmounts.max,
fiatStats,
masterInfoObject,
}),
}),
});
return;
if (!isLiquidAmountValid) {
handleLiquidAmountError();
return false;
}
if (
paymentInfo?.type === 'Bitcoin' &&
convertedSendAmount < SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT
) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.onchainError', {
amount: displayCorrectDenomination({
amount: SMALLEST_ONCHAIN_SPARK_SEND_AMOUNT,
fiatStats,
masterInfoObject,
}),
}),
});
return;
if (!isBitcoinAmountValid) {
handleBitcoinAmountError();
return false;
}
if (
paymentInfo?.type === InputTypeVariant.LN_URL_PAY &&
(convertedSendAmount < minLNURLSatAmount ||
convertedSendAmount > maxLNURLSatAmount)
!isLNURLAmountValid
) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.lnurlPayError', {
overFlowType:
convertedSendAmount < minLNURLSatAmount ? 'Minimum' : 'Maximum',
amount: displayCorrectDenomination({
amount:
convertedSendAmount < minLNURLSatAmount
? minLNURLSatAmount
: maxLNURLSatAmount,
fiatStats,
masterInfoObject,
}),
}),
});
return;
handleLNURLPayError();
return false;
}
if (isLRC20Payment && sparkInformation.balance < 10) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.lrc20FeeError', {
amount: displayCorrectDenomination({
amount: 10,
masterInfoObject,
fiatStats,
}),
balance: displayCorrectDenomination({
amount: sparkInformation.balance,
masterInfoObject,
fiatStats,
}),
}),
});
return;
if (!isLRC20Valid) {
handleLRC20Error();
return false;
}
if (!canSendPayment && !!paymentInfo?.sendAmount) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.sendPages.acceptButton.balanceError'),
});
handleInsufficientBalanceError();
return false;
}
return true;
};
const handleEnterSendAmount = async () => {
if (!validatePaymentAmount()) {
return;
}
setIsGeneratingInvoice(true);
try {
await decodeSendAddress({
fiatStats,
btcAdress: btcAdress,
btcAdress,
goBackFunction: errorMessageNavigation,
setPaymentInfo,
liquidNodeInformation,
@@ -191,9 +238,23 @@ export default function AcceptButtonSendPage({
currentWalletMnemoinc,
t,
});
} catch (error) {
console.log('Accept button error:', error);
} finally {
setIsGeneratingInvoice(false);
} catch (err) {
console.log('accecpt button error', err);
}
}
};
return (
<CustomButton
buttonStyles={{
opacity: buttonOpacity,
width: 'auto',
...CENTER,
}}
useLoading={isGeneratingInvoice}
actionFunction={handleEnterSendAmount}
textContent={t('constants.accept')}
/>
);
}
@@ -1,5 +1,5 @@
import {StyleSheet, View} from 'react-native';
import {CENTER, ICONS, SIZES} from '../../../../../constants';
import {ICONS, SIZES} from '../../../../../constants';
import FormattedSatText from '../../../../../functions/CustomElements/satTextDisplay';
import ThemeImage from '../../../../../functions/CustomElements/themeImage';
import {useSparkWallet} from '../../../../../../context-store/sparkContext';
@@ -9,6 +9,12 @@ export default function NavbarBalance({seletctedToken, selectedLRC20Asset}) {
const {sparkInformation} = useSparkWallet();
const balance = seletctedToken?.balance || sparkInformation.balance;
const formattedTokensBalance =
selectedLRC20Asset !== 'Bitcoin'
? formatTokensNumber(balance, seletctedToken?.tokenMetadata?.decimals)
: balance;
return (
<View style={styles.container}>
<ThemeImage
@@ -22,11 +28,8 @@ export default function NavbarBalance({seletctedToken, selectedLRC20Asset}) {
neverHideBalance={true}
styles={styles.headerText}
balance={
selectedLRC20Asset !== 'Bitcoin'
? formatTokensNumber(
balance,
seletctedToken?.tokenMetadata?.decimals,
)
selectedLRC20Asset !== 'Bitcoin' && formattedTokensBalance > 1
? Number(formattedTokensBalance).toFixed(2)
: balance
}
useCustomLabel={
@@ -44,7 +47,6 @@ const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
...CENTER,
flexGrow: 1,
paddingHorizontal: 35,
justifyContent: 'center',
@@ -52,7 +54,7 @@ const styles = StyleSheet.create({
walletIcon: {marginRight: 5, width: 23, height: 23},
headerText: {
fontSize: SIZES.large,
includeFontPadding: false,
fontSize: SIZES.xLarge,
},
});
@@ -116,8 +116,7 @@ export default function SelectLRC20Token({
<CustomKeyboardAvoidingView
useStandardWidth={true}
useLocalPadding={true}
isKeyboardActive={isKeyboardActive}
style={styles.container}>
isKeyboardActive={isKeyboardActive}>
<CustomSettingsTopBar
customBackFunction={goBackFunction}
label={t('wallet.sendPages.selectLRC20Token.title')}
@@ -170,7 +169,12 @@ const styles = StyleSheet.create({
alignItems: 'center',
...CENTER,
},
innerContainer: {flex: 1, width: INSET_WINDOW_WIDTH, ...CENTER},
innerContainer: {
flex: 1,
width: INSET_WINDOW_WIDTH,
...CENTER,
marginTop: 10,
},
titleText: {
fontSize: SIZES.large,
@@ -1,14 +1,21 @@
import {useState} from 'react';
import {useCallback, useState} from 'react';
import {CENTER} from '../../../../../constants';
import {SATSPERBITCOIN} from '../../../../../constants/math';
import CustomButton from '../../../../../functions/CustomElements/button';
import {crashlyticsLogReport} from '../../../../../functions/crashlyticsLogs';
import {sparkPaymenWrapper} from '../../../../../functions/spark/payments';
import {getLNAddressForLiquidPayment} from '../functions/payments';
import {calculateBoltzFeeNew} from '../../../../../functions/boltz/boltzFeeNew';
import {useActiveCustodyAccount} from '../../../../../../context-store/activeAccount';
import {useTranslation} from 'react-i18next';
import DropdownMenu from '../../../../../functions/CustomElements/dropdownMenu';
import {StyleSheet, View} from 'react-native';
const MAX_SEND_OPTIONS = [
{label: '25%', value: '25'},
{label: '50%', value: '50'},
{label: '75%', value: '75'},
{label: '100%', value: '100'},
];
export default function SendMaxComponent({
fiatStats,
sparkInformation,
@@ -18,91 +25,134 @@ export default function SendMaxComponent({
paymentFee,
paymentType,
minMaxLiquidSwapAmounts,
seletctedToken,
selectedLRC20Asset,
}) {
const {t} = useTranslation();
const [isGettingMax, setIsGettingMax] = useState(false);
const {currentWalletMnemoinc} = useActiveCustodyAccount();
return (
<CustomButton
buttonStyles={{
width: 'auto',
...CENTER,
marginBottom: 10,
}}
useLoading={isGettingMax}
actionFunction={sendMax}
textContent={t('wallet.sendPages.sendMaxComponent.sendMax')}
/>
);
async function sendMax() {
try {
crashlyticsLogReport('Starting send max process');
setIsGettingMax(true);
if (paymentInfo.type === 'liquid') {
const supportFee = masterInfoObject?.enabledDeveloperSupport.isEnabled
? Math.ceil(
Number(sparkInformation.balance) *
const handleSelctProcesss = useCallback(
async item => {
try {
if (isGettingMax) return;
await new Promise(res => setTimeout(res, 250));
const balance = seletctedToken?.balance || sparkInformation.balance;
const selectedPercent = !item ? 100 : item.value;
const sendingBalance = Math.round(balance * (selectedPercent / 100));
console.log(selectedPercent, balance, sendingBalance);
crashlyticsLogReport('Starting send max process');
setIsGettingMax(true);
if (paymentInfo.type === 'liquid') {
const supportFee =
Math.ceil(
Number(sendingBalance) *
masterInfoObject?.enabledDeveloperSupport.baseFeePercent,
) + Number(masterInfoObject?.enabledDeveloperSupport?.baseFee)
: 0;
const boltzFee = calculateBoltzFeeNew(
Number(sparkInformation.balance),
'ln-liquid',
minMaxLiquidSwapAmounts.reverseSwapStats,
);
) + Number(masterInfoObject?.enabledDeveloperSupport?.baseFee);
setPaymentInfo(prev => ({
...prev,
sendAmount: String(
Number(sparkInformation.balance) - (supportFee + boltzFee) * 1.5,
),
}));
return;
const boltzFee = calculateBoltzFeeNew(
Number(sendingBalance),
'ln-liquid',
minMaxLiquidSwapAmounts.reverseSwapStats,
);
setPaymentInfo(prev => ({
...prev,
sendAmount: String(
Number(sendingBalance) - (supportFee + boltzFee) * 1.5,
),
}));
return;
}
if (selectedLRC20Asset !== 'Bitcoin') {
setPaymentInfo(prev => ({
...prev,
sendAmount: String(sendingBalance),
}));
} else {
let address = paymentInfo?.address;
if (paymentInfo.type === 'lnUrlPay') {
const invoice = await getLNAddressForLiquidPayment(
paymentInfo,
Number(sendingBalance),
);
address = invoice;
}
const feeResponse = await sparkPaymenWrapper({
getFee: true,
address: address,
paymentType: paymentType.toLowerCase(),
amountSats: Number(sendingBalance),
masterInfoObject,
seletctedToken: selectedLRC20Asset,
mnemonic: currentWalletMnemoinc,
});
if (!feeResponse.didWork) throw new Error(feeResponse.error);
const maxAmountSats =
Number(sendingBalance) -
(feeResponse.fee + feeResponse.supportFee) * 1.1;
const convertedMax =
masterInfoObject.userBalanceDenomination != 'fiat'
? Math.round(Number(maxAmountSats))
: (
Number(maxAmountSats) /
Math.round(SATSPERBITCOIN / fiatStats?.value)
).toFixed(3);
setPaymentInfo(prev => ({
...prev,
sendAmount: String(convertedMax),
feeQuote: feeResponse.feeQuote,
paymentFee: feeResponse.fee,
supportFee: feeResponse.supportFee,
}));
}
} catch (err) {
console.log(err, 'ERROR');
} finally {
setIsGettingMax(false);
}
let address = paymentInfo?.address;
},
[isGettingMax, seletctedToken, selectedLRC20Asset],
);
if (paymentInfo.type === 'lnUrlPay') {
const invoice = await getLNAddressForLiquidPayment(
paymentInfo,
Number(sparkInformation.balance),
);
address = invoice;
}
const feeResponse = await sparkPaymenWrapper({
getFee: true,
address: address,
paymentType: paymentType.toLowerCase(),
amountSats: Number(sparkInformation.balance),
masterInfoObject,
mnemonic: currentWalletMnemoinc,
});
if (!feeResponse.didWork) throw new Error(feeResponse.error);
const maxAmountSats =
Number(sparkInformation.balance) -
(feeResponse.fee + feeResponse.supportFee) * 1.1;
const convertedMax =
masterInfoObject.userBalanceDenomination != 'fiat'
? Math.round(Number(maxAmountSats))
: (
Number(maxAmountSats) /
Math.round(SATSPERBITCOIN / fiatStats?.value)
).toFixed(3);
setPaymentInfo(prev => ({
...prev,
sendAmount: String(convertedMax),
feeQuote: feeResponse.feeQuote,
paymentFee: feeResponse.fee,
supportFee: feeResponse.supportFee,
}));
return;
} catch (err) {
console.log(err, 'ERROR');
} finally {
setIsGettingMax(false);
}
}
return (
<View style={styles.dropdownMenuContainer}>
<DropdownMenu
selectedValue={
selectedLRC20Asset === 'Bitcoin'
? t('wallet.sendPages.sendMaxComponent.sendMax')
: t('wallet.sendPages.sendMaxComponent.tokensMax')
}
onSelect={handleSelctProcesss}
options={MAX_SEND_OPTIONS}
showClearIcon={false}
showVerticalArrows={false}
customButtonStyles={styles.containerStyles}
textStyles={styles.textStyles}
useIsLoading={isGettingMax}
disableDropdownPress={isGettingMax}
customFunction={
selectedLRC20Asset === 'Bitcoin' ? handleSelctProcesss : undefined
}
/>
</View>
);
}
const styles = StyleSheet.create({
textStyles: {textAlign: 'center'},
dropdownMenuContainer: {
marginBottom: 10,
},
containerStyles: {
flex: 0,
...CENTER,
},
});
@@ -139,6 +139,7 @@ export default function SendPaymentScreen(props) {
);
return;
}
crashlyticsLogReport('Starting decode address');
await decodeSendAddress({
fiatStats,
@@ -168,31 +169,10 @@ export default function SendPaymentScreen(props) {
}, []);
useEffect(() => {
console.log(
!Object.keys(paymentInfo).length,
'|',
!masterInfoObject[QUICK_PAY_STORAGE_KEY].isFastPayEnabled,
'|',
!canSendPayment,
'|',
// paymentInfo.type === InputTypeVariant.LN_URL_PAY,
// '|',
!(
masterInfoObject[QUICK_PAY_STORAGE_KEY].fastPayThresholdSats >=
convertedSendAmount
),
'|',
// paymentInfo.type === 'liquid' && !paymentInfo.data.isBip21,
'FAST PAY SETTINGS',
masterInfoObject[QUICK_PAY_STORAGE_KEY].fastPayThresholdSats,
convertedSendAmount,
);
if (!Object.keys(paymentInfo).length) return;
if (!masterInfoObject[QUICK_PAY_STORAGE_KEY].isFastPayEnabled) return;
if (!canSendPayment) return;
if (canEditPaymentAmount) return;
// if (paymentInfo.type === InputTypeVariant.LN_URL_PAY) return;
if (
!(
masterInfoObject[QUICK_PAY_STORAGE_KEY].fastPayThresholdSats >=
@@ -200,7 +180,6 @@ export default function SendPaymentScreen(props) {
)
)
return;
// if (paymentInfo.type === 'liquid' && !paymentInfo.data.isBip21) return;
setTimeout(() => {
sendPayment();
@@ -241,6 +220,11 @@ export default function SendPaymentScreen(props) {
);
}
const clearSettings = () => {
setPaymentInfo(prev => ({...prev, canEditPayment: true, sendAmount: ''}));
setMasterTokenInfo({});
};
return (
<CustomKeyboardAvoidingView
useLocalPadding={true}
@@ -248,12 +232,12 @@ export default function SendPaymentScreen(props) {
useStandardWidth={true}>
<View style={styles.topBar}>
<TouchableOpacity
style={{position: 'absolute', zIndex: 99, left: 0}}
style={styles.backArrow}
onPress={
enabledLRC20 &&
Object.keys(seletctedToken).length &&
paymentInfo.type === 'spark'
? () => setMasterTokenInfo({})
? clearSettings
: goBackFunction
}>
<ThemeImage
@@ -329,19 +313,18 @@ export default function SendPaymentScreen(props) {
</ScrollView>
{canEditPaymentAmount && (
<>
{(paymentInfo.type !== 'spark' ||
(paymentInfo.type === 'spark' && !enabledLRC20)) && (
<SendMaxComponent
fiatStats={fiatStats}
sparkInformation={sparkInformation}
paymentInfo={paymentInfo}
setPaymentInfo={setPaymentInfo}
masterInfoObject={masterInfoObject}
paymentFee={paymentFee}
paymentType={paymentInfo?.paymentNetwork}
minMaxLiquidSwapAmounts={minMaxLiquidSwapAmounts}
/>
)}
<SendMaxComponent
fiatStats={fiatStats}
sparkInformation={sparkInformation}
paymentInfo={paymentInfo}
setPaymentInfo={setPaymentInfo}
masterInfoObject={masterInfoObject}
paymentFee={paymentFee}
paymentType={paymentInfo?.paymentNetwork}
minMaxLiquidSwapAmounts={minMaxLiquidSwapAmounts}
selectedLRC20Asset={selectedLRC20Asset}
seletctedToken={seletctedToken}
/>
<CustomSearchInput
onFocusFunction={() => setIsAmountFocused(false)}
@@ -536,4 +519,5 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
},
backArrow: {position: 'absolute', zIndex: 99, left: 0},
});
+51 -19
View File
@@ -14,6 +14,7 @@ import GetThemeColors from '../../hooks/themeColors';
import {useGlobalThemeContext} from '../../../context-store/theme';
import {useTranslation} from 'react-i18next';
import CountryFlag from 'react-native-country-flag';
import FullLoadingScreen from './loadingScreen';
const DropdownMenu = ({
options,
@@ -26,18 +27,20 @@ const DropdownMenu = ({
customButtonStyles = {},
dropdownItemCustomStyles = {},
showFlag = false,
useIsLoading = false,
disableDropdownPress = false,
customFunction,
}) => {
const {t} = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [buttonLayout, setButtonLayout] = useState(null);
const [itemSelectorLayout, setItemSelectorLayout] = useState(null);
const dropdownRef = useRef(null);
const {theme, darkModeType} = useGlobalThemeContext();
const {backgroundOffset, backgroundColor} = GetThemeColors();
const [dropdownHeight, setDropdownHeight] = useState(0);
const placeholderText = placeholder || t('constants.selectOption');
const placeholderText = placeholder || t('constants.selctOption');
const handleSelect = item => {
onSelect(item);
setIsOpen(false);
@@ -46,6 +49,7 @@ const DropdownMenu = ({
const handleLayout = event => {
setButtonLayout(event.nativeEvent.layout);
};
const handleItemSelectorHeight = event => {
setItemSelectorLayout(event.nativeEvent.layout);
};
@@ -70,12 +74,19 @@ const DropdownMenu = ({
};
const handleDropdownToggle = async () => {
if (disableDropdownPress) return;
if (!isOpen) {
// Recalculate position before opening
await measureButtonPosition();
}
if (customFunction) {
customFunction();
return;
}
setIsOpen(!isOpen);
};
// Calculate if dropdown should open upwards based on screen position
const screenHeight = Dimensions.get('window').height;
const isTooLow =
@@ -86,16 +97,21 @@ const DropdownMenu = ({
showFlag && options.find(item => item.value === selectedValue)?.flagCode;
return (
<View style={styles.container} ref={dropdownRef} onLayout={handleLayout}>
<View
style={styles.container}
// ref={dropdownRef}
onLayout={handleLayout}>
<View style={styles.selectorContainer}>
<TouchableOpacity
activeOpacity={disableDropdownPress ? 1 : 0.2}
ref={dropdownRef}
onLayout={handleItemSelectorHeight}
style={{
...styles.dropdownButton,
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
...customButtonStyles,
}}
onPress={() => handleDropdownToggle()}>
onPress={handleDropdownToggle}>
{showFlag && flag && (
<CountryFlag
style={{padding: 0, marginRight: 5, backgroundColor: 'red'}}
@@ -103,15 +119,22 @@ const DropdownMenu = ({
size={15}
/>
)}
<ThemeText
styles={{
includeFontPadding: false,
flexShrink: 1,
...textStyles,
}}
CustomNumberOfLines={1}
content={selectedValue ? selectedValue : placeholderText}
/>
{useIsLoading ? (
<FullLoadingScreen
containerStyles={styles.loadingButton}
showText={false}
size="small"
/>
) : (
<ThemeText
styles={{
...styles.defTextStyle,
...textStyles,
}}
CustomNumberOfLines={1}
content={selectedValue ? selectedValue : placeholderText}
/>
)}
{showVerticalArrows && (
<View style={styles.verticalArrowsContainer}>
<ThemeImage
@@ -141,7 +164,6 @@ const DropdownMenu = ({
</TouchableOpacity>
)}
</View>
<Modal
visible={isOpen}
transparent={true}
@@ -159,9 +181,9 @@ const DropdownMenu = ({
buttonLayout && {
top: isTooLow
? buttonLayout.y - dropdownHeight - 5
: buttonLayout.y + itemSelectorLayout?.height + 5,
left: '7.3%',
width: itemSelectorLayout?.width,
: buttonLayout.y + buttonLayout.height + 5,
left: buttonLayout.x,
width: buttonLayout.width,
},
{
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
@@ -181,7 +203,7 @@ const DropdownMenu = ({
...dropdownItemCustomStyles,
}}
onPress={() => handleSelect(item)}>
{showFlag && flag && (
{showFlag && item.flagCode && (
<CountryFlag
style={{
padding: 0,
@@ -192,7 +214,10 @@ const DropdownMenu = ({
size={15}
/>
)}
<ThemeText content={t(item.label)} />
<ThemeText
styles={styles.defTextStyle}
content={t(item.label)}
/>
</TouchableOpacity>
))}
</ScrollView>
@@ -263,6 +288,13 @@ const styles = StyleSheet.create({
paddingHorizontal: 10,
borderBottomWidth: 1,
},
defTextStyle: {
includeFontPadding: false,
flexShrink: 1,
},
loadingButton: {
flex: 0,
},
});
export default DropdownMenu;
@@ -86,5 +86,6 @@ const styles = StyleSheet.create({
fontFamily: FONT.Title_Regular,
textAlign: 'center',
...CENTER,
includeFontPadding: false,
},
});
@@ -1,5 +1,14 @@
import MaskedView from '@react-native-masked-view/masked-view';
import * as React from 'react';
import {
Children,
cloneElement,
createContext,
isValidElement,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Animated,
Dimensions,
@@ -22,11 +31,11 @@ const SkeletonTextPlaceholder = ({
direction = 'right',
shimmerWidth,
}) => {
const [layout, setLayout] = React.useState();
const animatedValueRef = React.useRef(new Animated.Value(0));
const [layout, setLayout] = useState();
const animatedValueRef = useRef(new Animated.Value(0));
const isAnimationReady = Boolean(speed && layout?.width && layout?.height);
React.useEffect(() => {
useEffect(() => {
if (!isAnimationReady) return;
const loop = Animated.loop(
Animated.timing(animatedValueRef.current, {
@@ -40,7 +49,7 @@ const SkeletonTextPlaceholder = ({
return () => loop.stop();
}, [isAnimationReady, speed]);
const animatedGradientStyle = React.useMemo(() => {
const animatedGradientStyle = useMemo(() => {
const animationWidth = WINDOW_WIDTH + (shimmerWidth ?? 0);
return {
...StyleSheet.absoluteFillObject,
@@ -59,7 +68,7 @@ const SkeletonTextPlaceholder = ({
};
}, [direction, WINDOW_WIDTH, shimmerWidth]);
const transparentColor = React.useMemo(
const transparentColor = useMemo(
() => getTransparentColor(highlightColor.replace(/ /g, '')),
[highlightColor],
);
@@ -101,7 +110,7 @@ const SkeletonTextPlaceholder = ({
};
// Context to override text colors
const TextColorContext = React.createContext();
const TextColorContext = createContext();
const TextColorProvider = ({children, color}) => {
return (
@@ -113,7 +122,7 @@ const TextColorProvider = ({children, color}) => {
// Recursively transform elements to override text rendering
const transformElementsForMask = (children, textColor) => {
return React.Children.map(children, (child, index) => {
return Children.map(children, (child, index) => {
if (!child) return null;
if (typeof child === 'string') {
@@ -124,7 +133,7 @@ const transformElementsForMask = (children, textColor) => {
);
}
if (React.isValidElement(child)) {
if (isValidElement(child)) {
const props = {key: index};
// If it's a Text component, override the color
@@ -162,7 +171,7 @@ const transformElementsForMask = (children, textColor) => {
);
}
return React.cloneElement(child, props);
return cloneElement(child, props);
}
return child;
+9 -9
View File
@@ -1,5 +1,4 @@
import MaskedView from '@react-native-masked-view/masked-view';
import * as React from 'react';
import {
Animated,
Dimensions,
@@ -12,6 +11,7 @@ import {
} from 'react-native';
import {LinearGradient} from 'expo-linear-gradient';
import {SKELETON_ANIMATION_SPEED} from '../../constants';
import {Children, Fragment, useEffect, useMemo, useRef, useState} from 'react';
const WINDOW_WIDTH = Dimensions.get('window').width;
@@ -27,11 +27,11 @@ const SkeletonPlaceholder = ({
borderRadius,
shimmerWidth,
}) => {
const [layout, setLayout] = React.useState();
const animatedValueRef = React.useRef(new Animated.Value(0));
const [layout, setLayout] = useState();
const animatedValueRef = useRef(new Animated.Value(0));
const isAnimationReady = Boolean(speed && layout?.width && layout?.height);
React.useEffect(() => {
useEffect(() => {
if (!isAnimationReady) return;
const loop = Animated.loop(
@@ -46,7 +46,7 @@ const SkeletonPlaceholder = ({
return () => loop.stop();
}, [isAnimationReady, speed]);
const animatedGradientStyle = React.useMemo(() => {
const animatedGradientStyle = useMemo(() => {
const animationWidth = WINDOW_WIDTH + (shimmerWidth ?? 0);
return {
...StyleSheet.absoluteFillObject,
@@ -65,7 +65,7 @@ const SkeletonPlaceholder = ({
};
}, [direction, WINDOW_WIDTH, shimmerWidth]);
const placeholders = React.useMemo(() => {
const placeholders = useMemo(() => {
if (!enabled) return null;
return (
@@ -75,7 +75,7 @@ const SkeletonPlaceholder = ({
);
}, [backgroundColor, children, borderRadius, enabled]);
const transparentColor = React.useMemo(
const transparentColor = useMemo(
() => getTransparentColor(highlightColor.replace(/ /g, '')),
[highlightColor],
);
@@ -130,10 +130,10 @@ const getItemStyle = ({children: _, style, ...styleFromProps}) => {
const transformToPlaceholder = (rootElement, backgroundColor, radius) => {
if (!rootElement) return null;
return React.Children.map(rootElement, (element, index) => {
return Children.map(rootElement, (element, index) => {
if (!element) return null;
if (element.type === React.Fragment)
if (element.type === Fragment)
return (
<>
{transformToPlaceholder(
+33 -33
View File
@@ -1,33 +1,33 @@
import * as bip21 from 'bip21';
import {crashlyticsLogReport} from './crashlyticsLogs';
export function formatBip21Address({
address = '',
amount = 0,
message = '',
prefix = '',
}) {
try {
crashlyticsLogReport('Formatting bip21 liquid address');
return bip21.encode(
address,
{
amount: amount,
label: message,
message: message,
},
prefix,
);
} catch (err) {
console.log('format bip21 spark address error', err);
return '';
}
}
export function decodeBip21Address(address, prefix) {
try {
crashlyticsLogReport('decoding bip21 spark');
return bip21.decode(address, prefix);
} catch (err) {
console.log('format bip21 spark address error', err);
return '';
}
}
// import {decode, encode} from 'bip21';
// import {crashlyticsLogReport} from './crashlyticsLogs';
// export function formatBip21Address({
// address = '',
// amount = 0,
// message = '',
// prefix = '',
// }) {
// try {
// crashlyticsLogReport('Formatting bip21 liquid address');
// return encode(
// address,
// {
// amount: amount,
// label: message,
// message: message,
// },
// prefix,
// );
// } catch (err) {
// console.log('format bip21 spark address error', err);
// return '';
// }
// }
// export function decodeBip21Address(address, prefix) {
// try {
// crashlyticsLogReport('decoding bip21 spark');
// return decode(address, prefix);
// } catch (err) {
// console.log('format bip21 spark address error', err);
// return '';
// }
// }
+2 -2
View File
@@ -1,4 +1,4 @@
import * as SQLite from 'expo-sqlite';
import {openDatabaseAsync} from 'expo-sqlite';
export const ROOTSTOCK_DB_NAME = 'ROOTSTOCK_SWAPS';
export const ROOTSTOCK_TABLE_NAME = 'saved_rootstock_swaps';
@@ -6,7 +6,7 @@ let sqlLiteDB;
if (!sqlLiteDB) {
async function openDBConnection() {
sqlLiteDB = await SQLite.openDatabaseAsync(`${ROOTSTOCK_DB_NAME}.db`);
sqlLiteDB = await openDatabaseAsync(`${ROOTSTOCK_DB_NAME}.db`);
}
openDBConnection();
}
+14 -8
View File
@@ -1,10 +1,15 @@
import * as FileSystem from 'expo-file-system';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {BLITZ_PROFILE_IMG_STORAGE_REF} from '../constants';
import {getDownloadURL, getMetadata, ref} from '@react-native-firebase/storage';
import {storage} from '../../db/initializeFirebase';
import {
cacheDirectory,
downloadAsync,
getInfoAsync,
makeDirectoryAsync,
} from 'expo-file-system';
import {getLocalStorageItem, setLocalStorageItem} from './localStorage';
const FILE_DIR = FileSystem.cacheDirectory + 'profileImages/';
const FILE_DIR = cacheDirectory + 'profileImages/';
const CACHE_KEY = uuid => `${BLITZ_PROFILE_IMG_STORAGE_REF}/${uuid}`;
export async function getCachedProfileImage(uuid) {
@@ -19,22 +24,23 @@ export async function getCachedProfileImage(uuid) {
const updated = metadata.updated;
// Check for cached image info
const cacheEntry = await AsyncStorage.getItem(key);
const cacheEntry = await getLocalStorageItem(key);
const parsed = cacheEntry ? JSON.parse(cacheEntry) : null;
if (parsed?.updated === updated) {
const exists = await FileSystem.getInfoAsync(parsed.localUri);
const exists = await getInfoAsync(parsed.localUri);
if (exists.exists)
return {localUri: parsed.localUri, updated: parsed?.updated};
}
const url = await getDownloadURL(reference);
await FileSystem.makeDirectoryAsync(FILE_DIR, {intermediates: true});
await makeDirectoryAsync(FILE_DIR, {intermediates: true});
const localUri = `${FILE_DIR}${uuid}.jpg`;
await FileSystem.downloadAsync(url, localUri);
await downloadAsync(url, localUri);
const newEntry = {localUri, updated};
await AsyncStorage.setItem(key, JSON.stringify(newEntry));
await setLocalStorageItem(key, JSON.stringify(newEntry));
return {localUri, updated};
} catch (e) {
+3 -3
View File
@@ -1,7 +1,7 @@
import {getLocalStorageItem, setLocalStorageItem} from './localStorage';
import * as FileSystem from 'expo-file-system';
import {Platform} from 'react-native';
import customUUID from './customUUID';
import {getInfoAsync, makeDirectoryAsync} from 'expo-file-system';
export async function getOrCreateDirectory(uuidKey, workingDir) {
try {
@@ -16,11 +16,11 @@ export async function getOrCreateDirectory(uuidKey, workingDir) {
const checkPath =
Platform.OS === 'android' ? `file://${directoryPath}` : directoryPath;
const dirInfo = await FileSystem.getInfoAsync(checkPath);
const dirInfo = await getInfoAsync(checkPath);
console.log('Directory Info:', dirInfo);
if (!dirInfo.exists) {
await FileSystem.makeDirectoryAsync(checkPath, {intermediates: true});
await makeDirectoryAsync(checkPath, {intermediates: true});
console.log(`Directory created: ${checkPath}`);
await new Promise(resolve => setTimeout(resolve, 8000)); //adds buffer
} else {
+2 -2
View File
@@ -1,4 +1,4 @@
import * as Clipboard from 'expo-clipboard';
import {setStringAsync} from 'expo-clipboard';
export default async function copyToClipboard(
data,
@@ -7,7 +7,7 @@ export default async function copyToClipboard(
customText,
) {
try {
await Clipboard.setStringAsync(data);
await setStringAsync(data);
if (page === 'ChatGPT') return;
showToast({
type: 'clipboard',
+3 -2
View File
@@ -1,7 +1,8 @@
import * as Clipboard from 'expo-clipboard';
import {getStringAsync} from 'expo-clipboard';
export default async function getClipboardText() {
try {
const data = await Clipboard.getStringAsync();
const data = await getStringAsync();
if (!data || !data.trim().length)
throw new Error('errormessages.clipboardContentError');
+20
View File
@@ -61,3 +61,23 @@ export async function removeAllLocalData() {
// read key error
}
}
export async function getAllLocalKeys() {
try {
crashlyticsLogReport('Starting get all local storage keys');
const keys = await AsyncStorage.getAllKeys();
return keys;
} catch (e) {
console.log(e);
return [];
}
}
export async function getMultipleItems(itemsList) {
try {
crashlyticsLogReport('Starting get all local storage item function');
const items = await AsyncStorage.multiGet(itemsList);
return items;
} catch (e) {
console.log(e);
return [];
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import {getLocalStorageItem, setLocalStorageItem} from '../localStorage';
import * as SQLite from 'expo-sqlite';
import {getTwoWeeksAgoDate} from '../rotateAddressDateChecker';
import EventEmitter from 'events';
import {handleEventEmitterPost} from '../handleEventEmitters';
import {openDatabaseAsync} from 'expo-sqlite';
export const CACHED_MESSAGES_KEY = 'CASHED_CONTACTS_MESSAGES';
export const SQL_TABLE_NAME = 'messagesTable';
export const LOCALSTORAGE_LAST_RECEIVED_TIME_KEY =
@@ -16,7 +16,7 @@ let isProcessing = false;
if (!sqlLiteDB) {
async function openDBConnection() {
sqlLiteDB = await SQLite.openDatabaseAsync(`${CACHED_MESSAGES_KEY}.db`);
sqlLiteDB = await openDatabaseAsync(`${CACHED_MESSAGES_KEY}.db`);
}
openDBConnection();
}
+6 -3
View File
@@ -1,7 +1,10 @@
import * as Notifications from 'expo-notifications';
import {
scheduleNotificationAsync,
setNotificationHandler,
} from 'expo-notifications';
// Configure notification behavior
Notifications.setNotificationHandler({
setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldPlaySound: true,
@@ -17,7 +20,7 @@ Notifications.setNotificationHandler({
*/
export const pushInstantNotification = async (message, title = '') => {
try {
const notificationId = await Notifications.scheduleNotificationAsync({
const notificationId = await scheduleNotificationAsync({
content: {
title: title,
body: message,
+2 -2
View File
@@ -1,4 +1,4 @@
import * as SQLite from 'expo-sqlite';
import {openDatabaseAsync} from 'expo-sqlite';
// Database configuration
const DB_NAME = 'nwc_invoices.db';
@@ -13,7 +13,7 @@ class InvoiceDatabase {
// Initialize database connection
async initialize() {
try {
this.db = await SQLite.openDatabaseAsync(DB_NAME);
this.db = await openDatabaseAsync(DB_NAME);
await this.createTables();
this.isInitialized = true;
console.log('Invoice database initialized successfully');
+2 -4
View File
@@ -1,9 +1,9 @@
import * as SQLite from 'expo-sqlite';
import {getLocalStorageItem, setLocalStorageItem} from '../localStorage';
import {getTwoWeeksAgoDate} from '../rotateAddressDateChecker';
import {decryptMessage} from '../messaging/encodingAndDecodingMessages';
import EventEmitter from 'events';
import {handleEventEmitterPost} from '../handleEventEmitters';
import {openDatabaseAsync} from 'expo-sqlite';
export const POS_TRANSACTION_TABLE_NAME = 'POS_TRANSACTIONS';
export const POS_LAST_RECEIVED_TIME = 'LAST_RECEIVED_POS_EVENT';
@@ -17,9 +17,7 @@ let isProcessing = false;
if (!sqlLiteDB) {
async function openDBConnection() {
sqlLiteDB = await SQLite.openDatabaseAsync(
`${POS_TRANSACTION_TABLE_NAME}.db`,
);
sqlLiteDB = await openDatabaseAsync(`${POS_TRANSACTION_TABLE_NAME}.db`);
}
openDBConnection();
}
+21 -18
View File
@@ -1,4 +1,3 @@
import * as SecureStore from 'expo-secure-store';
import {
getLocalStorageItem,
removeAllLocalData,
@@ -7,19 +6,25 @@ import {
import {crashlyticsLogReport} from './crashlyticsLogs';
import {CUSTODY_ACCOUNTS_STORAGE_KEY} from '../constants';
import {BIOMETRIC_KEY} from '../constants';
import {
AFTER_FIRST_UNLOCK,
deleteItemAsync,
getItemAsync,
setItemAsync,
} from 'expo-secure-store';
const keychainService = '38WX44YTA6.com.blitzwallet.SharedKeychain';
export const MIGRATION_FLAG = 'secureStoreMigrationComplete';
export const SECURE_MIGRATION_V2_FLAG = 'secureStoreMigrationV2Complete';
const KEYCHAIN_OPTION = {
keychainService: keychainService,
keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,
keychainAccessible: AFTER_FIRST_UNLOCK,
};
async function storeData(key, value, options = {}) {
try {
crashlyticsLogReport('Starting store data to secure store function');
await SecureStore.setItemAsync(key, value, {
await setItemAsync(key, value, {
...KEYCHAIN_OPTION,
...options,
});
@@ -34,7 +39,7 @@ async function retrieveData(key, options = {}) {
try {
crashlyticsLogReport('Starting retrive data from secure store function');
const value = await SecureStore.getItemAsync(key, {
const value = await getItemAsync(key, {
...KEYCHAIN_OPTION,
...options,
});
@@ -49,13 +54,11 @@ async function retrieveData(key, options = {}) {
async function terminateAccount() {
try {
crashlyticsLogReport('Starting termiate data from secure store function');
await SecureStore.deleteItemAsync('pinHash', KEYCHAIN_OPTION);
await SecureStore.deleteItemAsync('encryptedMnemonic', KEYCHAIN_OPTION);
await SecureStore.deleteItemAsync(BIOMETRIC_KEY, KEYCHAIN_OPTION);
await SecureStore.deleteItemAsync(
CUSTODY_ACCOUNTS_STORAGE_KEY,
KEYCHAIN_OPTION,
);
await deleteItemAsync('pinHash', KEYCHAIN_OPTION);
await deleteItemAsync('encryptedMnemonic', KEYCHAIN_OPTION);
await deleteItemAsync(BIOMETRIC_KEY, KEYCHAIN_OPTION);
await deleteItemAsync(CUSTODY_ACCOUNTS_STORAGE_KEY, KEYCHAIN_OPTION);
const didRemove = await removeAllLocalData();
if (!didRemove) throw Error('not able to remove local storage data');
@@ -69,7 +72,7 @@ async function terminateAccount() {
async function deleteItem(key) {
try {
crashlyticsLogReport('Starting delte item from secure store function');
await SecureStore.deleteItemAsync(key, KEYCHAIN_OPTION);
await deleteItemAsync(key, KEYCHAIN_OPTION);
return true;
} catch (error) {
@@ -89,15 +92,15 @@ async function runPinAndMnemoicMigration() {
crashlyticsLogReport('Running SecureStore migration');
const [oldPin, oldMnemonic] = await Promise.all([
SecureStore.getItemAsync('pin'),
SecureStore.getItemAsync('mnemonic'),
getItemAsync('pin'),
getItemAsync('mnemonic'),
]);
if (oldPin || oldMnemonic) {
if (oldPin) await storeData('pinHash', oldPin);
if (oldMnemonic) await storeData('encryptedMnemonic', oldMnemonic);
await SecureStore.deleteItemAsync('pin');
await SecureStore.deleteItemAsync('mnemonic');
await deleteItemAsync('pin');
await deleteItemAsync('mnemonic');
}
await setLocalStorageItem(MIGRATION_FLAG, 'true');
@@ -119,8 +122,8 @@ async function runSecureStoreMigrationV2() {
// Get unencrypted PIN and mnemonic (possibly migrated from old V1 already)
const [plainPin, plainMnemonic] = await Promise.all([
SecureStore.getItemAsync('pin', KEYCHAIN_OPTION),
SecureStore.getItemAsync('mnemonic', KEYCHAIN_OPTION),
getItemAsync('pin', KEYCHAIN_OPTION),
getItemAsync('mnemonic', KEYCHAIN_OPTION),
]);
if (plainPin && plainMnemonic) {
+6 -1
View File
@@ -46,7 +46,7 @@ export function handleRestoreFromText(seedString) {
let maxIndex = seedString.length;
let currentWord = '';
while (currentIndex <= maxIndex) {
while (currentIndex <= maxIndex && wordArray.length < 13) {
const letter = seedString[currentIndex];
const isLetter = IS_LETTER_REGEX.test(letter);
if (!isLetter) {
@@ -62,6 +62,11 @@ export function handleRestoreFromText(seedString) {
if (!posibleOptins.length) {
const lastPosibleOption = currentWord.slice(0, currentWord.length - 1);
if (!lastPosibleOption) {
currentIndex += 1;
continue;
}
wordArray.push(lastPosibleOption);
currentWord = '';
continue;
@@ -1,5 +1,5 @@
import {decode, encode} from 'bip21';
import {crashlyticsLogReport} from '../crashlyticsLogs';
import * as bip21 from 'bip21';
/**
* Formats a Liquid 'spark' BIP21 payment URI from an address, amount, and optional message.
*
@@ -22,7 +22,7 @@ export function formatBip21SparkAddress({
try {
const formattedAmount = amountSat;
crashlyticsLogReport('Formatting bip21 liquid address');
const liquidBip21 = bip21.encode(
const liquidBip21 = encode(
address,
{
amount: formattedAmount,
@@ -50,7 +50,8 @@ export function formatBip21SparkAddress({
export function decodeBip21SparkAddress(address) {
try {
crashlyticsLogReport('decoding bip21 spark');
return bip21.decode(address, 'spark');
return decode(address, 'spark');
} catch (err) {
console.log('format bip21 spark address error', err);
return '';
+2 -2
View File
@@ -1,6 +1,6 @@
import * as SQLite from 'expo-sqlite';
import EventEmitter from 'events';
import {handleEventEmitterPost} from '../handleEventEmitters';
import {openDatabaseAsync} from 'expo-sqlite';
export const SPARK_TRANSACTIONS_DATABASE_NAME = 'SPARK_INFORMATION_DATABASE';
export const SPARK_TRANSACTIONS_TABLE_NAME = 'SPARK_TRANSACTIONS';
export const LIGHTNING_REQUEST_IDS_TABLE_NAME = 'LIGHTNING_REQUEST_IDS';
@@ -13,7 +13,7 @@ let sqlLiteDB;
if (!sqlLiteDB) {
async function openDBConnection() {
sqlLiteDB = await SQLite.openDatabaseAsync(
sqlLiteDB = await openDatabaseAsync(
`${SPARK_TRANSACTIONS_DATABASE_NAME}.db`,
);
}
+14 -9
View File
@@ -1,6 +1,11 @@
import * as FileSystem from 'expo-file-system';
import {Platform, Share} from 'react-native';
import {crashlyticsLogReport} from './crashlyticsLogs';
import {
documentDirectory,
EncodingType,
StorageAccessFramework,
writeAsStringAsync,
} from 'expo-file-system';
export default async function writeAndShareFileToFilesystem(
fileData,
@@ -11,9 +16,10 @@ export default async function writeAndShareFileToFilesystem(
try {
crashlyticsLogReport('Starting write to filesystem process');
const fileUri = `${FileSystem.documentDirectory}${fileName}`;
await FileSystem.writeAsStringAsync(fileUri, fileData, {
encoding: FileSystem.EncodingType.UTF8,
const fileUri = `${documentDirectory}${fileName}`;
await writeAsStringAsync(fileUri, fileData, {
encoding: EncodingType.UTF8,
});
if (Platform.OS === 'ios') {
@@ -26,19 +32,18 @@ export default async function writeAndShareFileToFilesystem(
} else {
try {
const permissions =
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (permissions.granted) {
const data =
await FileSystem.StorageAccessFramework.readAsStringAsync(fileUri);
const data = await StorageAccessFramework.readAsStringAsync(fileUri);
try {
const uri = await FileSystem.StorageAccessFramework.createFileAsync(
const uri = await StorageAccessFramework.createFileAsync(
permissions.directoryUri,
fileName,
fileType,
);
await FileSystem.writeAsStringAsync(uri, data);
await writeAsStringAsync(uri, data);
return {success: true, error: null};
} catch (err) {
console.log('writting file to filesystem for android err', err);
+28 -28
View File
@@ -1,31 +1,31 @@
import {useEffect, useState} from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {BLITZ_PROFILE_IMG_STORAGE_REF} from '../constants';
// import {useEffect, useState} from 'react';
// import {BLITZ_PROFILE_IMG_STORAGE_REF} from '../constants';
// import {getAllLocalKeys, getMultipleItems} from '../functions/localStorage';
export function useContactImage(uuid) {
const [uri, setUri] = useState({});
// export function useContactImage(uuid) {
// const [uri, setUri] = useState({});
useEffect(() => {
async function load() {
const keys = await AsyncStorage.getAllKeys();
const imgKeys = keys.filter(k =>
k.startsWith(BLITZ_PROFILE_IMG_STORAGE_REF),
);
const stores = await AsyncStorage.multiGet(imgKeys);
const initialCache = {};
stores.forEach(([key, value]) => {
if (value) {
const uuid = key.replace(BLITZ_PROFILE_IMG_STORAGE_REF + '/', '');
const parsed = JSON.parse(value);
initialCache[uuid] = parsed;
}
});
if (initialCache[uuid]?.localUri) {
setUri(initialCache[uuid]);
}
}
load();
}, [uuid]);
// useEffect(() => {
// async function load() {
// const keys = await getAllLocalKeys();
// const imgKeys = keys.filter(k =>
// k.startsWith(BLITZ_PROFILE_IMG_STORAGE_REF),
// );
// const stores = await getMultipleItems(imgKeys);
// const initialCache = {};
// stores.forEach(([key, value]) => {
// if (value) {
// const uuid = key.replace(BLITZ_PROFILE_IMG_STORAGE_REF + '/', '');
// const parsed = JSON.parse(value);
// initialCache[uuid] = parsed;
// }
// });
// if (initialCache[uuid]?.localUri) {
// setUri(initialCache[uuid]);
// }
// }
// load();
// }, [uuid]);
return uri;
}
// return uri;
// }
+2 -2
View File
@@ -56,7 +56,7 @@ export default function CreateAccountHome({navigation: {navigate}}) {
/>
<ThemeText
styles={{...styles.disclamer_text}}
styles={styles.disclamer_text}
content={t('createAccount.homePage.subtitle')}
/>
</GlobalThemeView>
@@ -87,6 +87,6 @@ const styles = StyleSheet.create({
disclamer_text: {
marginTop: 'auto',
fontSize: SIZES.small,
marginBottom: 5,
includeFontPadding: false,
},
});
+16 -10
View File
@@ -7,15 +7,20 @@ import React, {
useMemo,
} from 'react';
import {getDownloadURL, getMetadata, ref} from '@react-native-firebase/storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as FileSystem from 'expo-file-system';
import {useGlobalContacts} from './globalContacts';
import {useAppStatus} from './appStatus';
import {BLITZ_PROFILE_IMG_STORAGE_REF} from '../app/constants';
import {useGlobalContextProvider} from './context';
import {getLocalStorageItem, setLocalStorageItem} from '../app/functions';
import {storage} from '../db/initializeFirebase';
const FILE_DIR = FileSystem.cacheDirectory + 'profile_images/';
import {
cacheDirectory,
downloadAsync,
getInfoAsync,
makeDirectoryAsync,
} from 'expo-file-system';
import {getAllLocalKeys, getMultipleItems} from '../app/functions/localStorage';
const FILE_DIR = cacheDirectory + 'profile_images/';
const ImageCacheContext = createContext();
export function ImageCacheProvider({children}) {
@@ -27,11 +32,11 @@ export function ImageCacheProvider({children}) {
const refreshCacheObject = async () => {
try {
const keys = await AsyncStorage.getAllKeys();
const keys = await getAllLocalKeys();
const imgKeys = keys.filter(k =>
k.startsWith(BLITZ_PROFILE_IMG_STORAGE_REF),
);
const stores = await AsyncStorage.multiGet(imgKeys);
const stores = await getMultipleItems(imgKeys);
const initialCache = {};
stores.forEach(([key, value]) => {
if (value) {
@@ -94,7 +99,7 @@ export function ImageCacheProvider({children}) {
const cached = cache[uuid];
if (cached && cached.updated === updated) {
const fileInfo = await FileSystem.getInfoAsync(cached.localUri);
const fileInfo = await getInfoAsync(cached.localUri);
if (fileInfo.exists) return;
}
@@ -106,8 +111,8 @@ export function ImageCacheProvider({children}) {
const localUri = `${FILE_DIR}${uuid}.jpg`;
await FileSystem.makeDirectoryAsync(FILE_DIR, {intermediates: true});
await FileSystem.downloadAsync(url, localUri);
await makeDirectoryAsync(FILE_DIR, {intermediates: true});
await downloadAsync(url, localUri);
const newCacheEntry = {
uri: localUri,
@@ -115,7 +120,7 @@ export function ImageCacheProvider({children}) {
updated,
};
await AsyncStorage.setItem(key, JSON.stringify(newCacheEntry));
await setLocalStorageItem(key, JSON.stringify(newCacheEntry));
setCache(prev => ({...prev, [uuid]: newCacheEntry}));
return newCacheEntry;
@@ -134,7 +139,8 @@ export function ImageCacheProvider({children}) {
localUri: null,
updated: new Date().getTime(),
};
await AsyncStorage.setItem(key, JSON.stringify(newCacheEntry));
await setLocalStorageItem(key, JSON.stringify(newCacheEntry));
setCache(prev => ({...prev, [uuid]: newCacheEntry}));
return newCacheEntry;
} catch (err) {
+1
View File
@@ -3,6 +3,7 @@
*/
import './pollyfills.js';
import './disableFontScalling.js';
import './i18n'; // for translation option
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
+2 -1
View File
@@ -841,7 +841,8 @@
"noTokensFoundText": "No tokens found"
},
"sendMaxComponent": {
"sendMax": "Send Max"
"sendMax": "Send Max",
"tokensMax": "Send %"
},
"acceptButton": {
"noSendAmountError": "Please enter a send amount",
+2 -1
View File
@@ -687,7 +687,8 @@
"noTokensFoundText": "No se encontraron tokens"
},
"sendMaxComponent": {
"sendMax": "Enviar máximo"
"sendMax": "Enviar máximo",
"tokensMax": "Enviar %"
},
"acceptButton": {
"noSendAmountError": "Por favor ingresa una cantidad a enviar",