Merge branch 'master' into clip

This commit is contained in:
Marcos Rodriguez Velez 2024-10-27 23:27:15 -04:00
commit f08eacac0b
76 changed files with 1903 additions and 1427 deletions

View File

@ -34,44 +34,6 @@ async function setPreferredCurrency(item: FiatUnitType): Promise<void> {
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, item.locale.replace('-', '_'));
}
async function getPreferredCurrency(): Promise<FiatUnitType> {
const preferredCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (preferredCurrency) {
const parsedPreferredCurrency = JSON.parse(preferredCurrency);
preferredFiatCurrency = FiatUnit[parsedPreferredCurrency.endPointKey];
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, preferredFiatCurrency.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, preferredFiatCurrency.locale.replace('-', '_'));
return preferredFiatCurrency;
}
return FiatUnit.USD;
}
async function _restoreSavedExchangeRatesFromStorage(): Promise<void> {
try {
const rates = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
exchangeRates = rates ? JSON.parse(rates) : { LAST_UPDATED_ERROR: false };
} catch (_) {
exchangeRates = { LAST_UPDATED_ERROR: false };
}
}
async function _restoreSavedPreferredFiatCurrencyFromStorage(): Promise<void> {
try {
const storedCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (!storedCurrency) throw new Error('No Preferred Fiat selected');
preferredFiatCurrency = JSON.parse(storedCurrency);
if (!FiatUnit[preferredFiatCurrency.endPointKey]) {
throw new Error('Invalid Fiat Unit');
}
} catch (_) {
const deviceCurrencies = RNLocalize.getCurrencies();
preferredFiatCurrency = deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]] ? FiatUnit[deviceCurrencies[0]] : FiatUnit.USD;
}
}
async function updateExchangeRate(): Promise<void> {
if (skipUpdateExchangeRate) return;
if (Date.now() - lastTimeUpdateExchangeRateWasCalled <= 10000) {
@ -92,19 +54,140 @@ async function updateExchangeRate(): Promise<void> {
exchangeRates[LAST_UPDATED] = Date.now();
exchangeRates[BTC_PREFIX + preferredFiatCurrency.endPointKey] = rate;
exchangeRates.LAST_UPDATED_ERROR = false;
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(exchangeRates));
try {
const exchangeRatesString = JSON.stringify(exchangeRates);
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, exchangeRatesString);
} catch (error) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
exchangeRates = { LAST_UPDATED_ERROR: false };
}
} catch (error) {
console.error('Error encountered when attempting to update exchange rate...', error);
const rate = JSON.parse((await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY)) || '{}');
rate.LAST_UPDATED_ERROR = true;
exchangeRates.LAST_UPDATED_ERROR = true;
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(rate));
try {
const ratesString = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
let rate;
if (ratesString) {
try {
rate = JSON.parse(ratesString);
} catch (parseError) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
rate = {};
}
} else {
rate = {};
}
rate.LAST_UPDATED_ERROR = true;
exchangeRates.LAST_UPDATED_ERROR = true;
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(rate));
} catch (storageError) {}
}
}
async function getPreferredCurrency(): Promise<FiatUnitType> {
const preferredCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (preferredCurrency) {
let parsedPreferredCurrency;
try {
parsedPreferredCurrency = JSON.parse(preferredCurrency);
if (!FiatUnit[parsedPreferredCurrency.endPointKey]) {
throw new Error('Invalid Fiat Unit');
}
preferredFiatCurrency = FiatUnit[parsedPreferredCurrency.endPointKey];
} catch (error) {
await AsyncStorage.removeItem(PREFERRED_CURRENCY_STORAGE_KEY);
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
}
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, preferredFiatCurrency.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, preferredFiatCurrency.locale.replace('-', '_'));
return preferredFiatCurrency;
}
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
return preferredFiatCurrency;
}
async function _restoreSavedExchangeRatesFromStorage(): Promise<void> {
try {
const rates = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
if (rates) {
try {
exchangeRates = JSON.parse(rates);
} catch (error) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
exchangeRates = { LAST_UPDATED_ERROR: false };
await updateExchangeRate();
}
} else {
exchangeRates = { LAST_UPDATED_ERROR: false };
}
} catch (error) {
exchangeRates = { LAST_UPDATED_ERROR: false };
await updateExchangeRate();
}
}
async function _restoreSavedPreferredFiatCurrencyFromStorage(): Promise<void> {
try {
const storedCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (!storedCurrency) throw new Error('No Preferred Fiat selected');
let parsedCurrency;
try {
parsedCurrency = JSON.parse(storedCurrency);
if (!FiatUnit[parsedCurrency.endPointKey]) {
throw new Error('Invalid Fiat Unit');
}
preferredFiatCurrency = FiatUnit[parsedCurrency.endPointKey];
} catch (error) {
await AsyncStorage.removeItem(PREFERRED_CURRENCY_STORAGE_KEY);
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
}
} catch (error) {
const deviceCurrencies = RNLocalize.getCurrencies();
if (deviceCurrencies[0] && FiatUnit[deviceCurrencies[0]]) {
preferredFiatCurrency = FiatUnit[deviceCurrencies[0]];
} else {
preferredFiatCurrency = FiatUnit.USD;
}
}
}
async function isRateOutdated(): Promise<boolean> {
try {
const rate = JSON.parse((await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY)) || '{}');
const rateString = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
let rate;
if (rateString) {
try {
rate = JSON.parse(rateString);
} catch (parseError) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
rate = {};
await updateExchangeRate();
}
} else {
rate = {};
}
return rate.LAST_UPDATED_ERROR || Date.now() - (rate[LAST_UPDATED] || 0) >= 31 * 60 * 1000;
} catch {
return true;
@ -169,18 +252,37 @@ function BTCToLocalCurrency(bitcoin: BigNumber.Value): string {
}
async function mostRecentFetchedRate(): Promise<CurrencyRate> {
const currencyInformation = JSON.parse((await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY)) || '{}');
try {
const currencyInformationString = await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY);
let currencyInformation;
if (currencyInformationString) {
try {
currencyInformation = JSON.parse(currencyInformationString);
} catch (parseError) {
await AsyncStorage.removeItem(EXCHANGE_RATES_STORAGE_KEY);
currencyInformation = {};
await updateExchangeRate();
}
} else {
currencyInformation = {};
}
const formatter = new Intl.NumberFormat(preferredFiatCurrency.locale, {
style: 'currency',
currency: preferredFiatCurrency.endPointKey,
});
const formatter = new Intl.NumberFormat(preferredFiatCurrency.locale, {
style: 'currency',
currency: preferredFiatCurrency.endPointKey,
});
const rate = currencyInformation[BTC_PREFIX + preferredFiatCurrency.endPointKey];
return {
LastUpdated: currencyInformation[LAST_UPDATED],
Rate: rate ? formatter.format(rate) : '...',
};
const rate = currencyInformation[BTC_PREFIX + preferredFiatCurrency.endPointKey];
return {
LastUpdated: currencyInformation[LAST_UPDATED],
Rate: rate ? formatter.format(rate) : '...',
};
} catch {
return {
LastUpdated: null,
Rate: null,
};
}
}
function satoshiToBTC(satoshi: number): string {

View File

@ -1,15 +1,10 @@
import React, { useCallback, useMemo } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text, TextInput, View } from 'react-native';
import { scanQrHelper } from '../helpers/scan-qr';
import React, { useCallback } from 'react';
import { Keyboard, StyleSheet, TextInput, View } from 'react-native';
import loc from '../loc';
import presentAlert from './Alert';
import ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import Clipboard from '@react-native-clipboard/clipboard';
import RNQRGenerator from 'rn-qr-generator';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import { AddressInputScanButton } from './AddressInputScanButton';
import { useTheme } from './themes';
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
interface AddressInputProps {
isLoading?: boolean;
@ -22,6 +17,8 @@ interface AddressInputProps {
editable?: boolean;
inputAccessoryViewID?: string;
onBlur?: () => void;
onFocus?: () => void;
testID?: string;
keyboardType?:
| 'default'
| 'numeric'
@ -41,6 +38,7 @@ interface AddressInputProps {
const AddressInput = ({
isLoading = false,
address = '',
testID = 'AddressInput',
placeholder = loc.send.details_address,
onChangeText,
onBarScanned,
@ -49,6 +47,7 @@ const AddressInput = ({
editable = true,
inputAccessoryViewID,
onBlur = () => {},
onFocus = () => {},
keyboardType = 'default',
}: AddressInputProps) => {
const { colors } = useTheme();
@ -58,142 +57,56 @@ const AddressInput = ({
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
scan: {
backgroundColor: colors.scanLabel,
},
scanText: {
color: colors.inverseForegroundColor,
input: {
color: colors.foregroundColor,
},
});
const validateAddressWithFeedback = useCallback((value: string) => {
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(value);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(value);
const isValid = isBitcoinAddress || isLightningInvoice;
triggerHapticFeedback(isValid ? HapticFeedbackTypes.NotificationSuccess : HapticFeedbackTypes.NotificationError);
return {
isValid,
type: isBitcoinAddress ? 'bitcoin' : isLightningInvoice ? 'lightning' : 'invalid',
};
}, []);
const onBlurEditing = () => {
validateAddressWithFeedback(address);
onBlur();
Keyboard.dismiss();
};
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
const onMenuItemPressed = useCallback(
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case actionKeys.ScanQR:
scanButtonTapped();
if (launchedBy) {
scanQrHelper(launchedBy)
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
let getImage: string | null = null;
if (Platform.OS === 'android') {
getImage = await Clipboard.getImage();
} else {
const hasImage = await Clipboard.hasImage();
if (hasImage) {
getImage = await Clipboard.getImageJPG();
}
}
if (getImage) {
try {
const base64Data = getImage.replace(/^data:image\/jpeg;base64,/, '');
const values = await RNQRGenerator.detect({
base64: base64Data,
});
if (values && values.values.length > 0) {
onChangeText(values.values[0]);
} else {
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
} else {
const clipboardText = await Clipboard.getString();
onChangeText(clipboardText);
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
break;
case actionKeys.ChoosePhoto:
showImagePickerAndReadImage()
.then(value => {
if (value) {
onChangeText(value);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
case actionKeys.ImportFile:
showFilePickerAndReadFile()
.then(value => {
if (value.data) {
onChangeText(value.data);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
}
Keyboard.dismiss();
},
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
return (
<View style={[styles.root, stylesHook.root]}>
<TextInput
testID="AddressInput"
testID={testID}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor="#81868e"
value={address}
style={styles.input}
style={[styles.input, stylesHook.input]}
editable={!isLoading && editable}
multiline={!editable}
inputAccessoryViewID={inputAccessoryViewID}
clearButtonMode="while-editing"
onBlur={onBlurEditing}
onFocus={onFocus}
autoCapitalize="none"
autoCorrect={false}
keyboardType={keyboardType}
/>
{editable ? (
<ToolTipMenu
actions={actions}
isButton
onPressMenuItem={onMenuItemPressed}
testID="BlueAddressInputScanQrButton"
disabled={isLoading}
onPress={toolTipOnPress}
buttonStyle={buttonStyle}
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
<Image source={require('../img/scan-white.png')} accessible={false} />
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
</ToolTipMenu>
<AddressInputScanButton
isLoading={isLoading}
launchedBy={launchedBy}
scanButtonTapped={scanButtonTapped}
onBarScanned={onBarScanned}
onChangeText={onChangeText}
/>
) : null}
</View>
);
@ -206,75 +119,16 @@ const styles = StyleSheet.create({
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
marginHorizontal: 18,
borderRadius: 4,
},
input: {
flex: 1,
marginHorizontal: 8,
minHeight: 33,
color: '#81868e',
},
scan: {
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
},
scanText: {
marginLeft: 4,
minHeight: 33,
},
});
const actionKeys = {
ScanQR: 'scan_qr',
PasteFromClipboard: 'copy_from_clipboard',
ChoosePhoto: 'choose_photo',
ImportFile: 'import_file',
};
const actionIcons = {
ScanQR: {
iconValue: Platform.OS === 'ios' ? 'qrcode' : 'ic_menu_camera',
},
ImportFile: {
iconValue: 'doc',
},
ChoosePhoto: {
iconValue: Platform.OS === 'ios' ? 'photo' : 'ic_menu_gallery',
},
Clipboard: {
iconValue: Platform.OS === 'ios' ? 'doc' : 'ic_menu_file',
},
};
const actions = [
{
id: actionKeys.ScanQR,
text: loc.wallets.list_long_scan,
icon: actionIcons.ScanQR,
},
{
id: actionKeys.PasteFromClipboard,
text: loc.wallets.paste_from_clipboard,
icon: actionIcons.Clipboard,
},
{
id: actionKeys.ChoosePhoto,
text: loc.wallets.list_long_choose,
icon: actionIcons.ChoosePhoto,
},
{
id: actionKeys.ImportFile,
text: loc.wallets.import_file,
icon: actionIcons.ImportFile,
},
];
export default AddressInput;

View File

@ -0,0 +1,176 @@
import React, { useCallback, useMemo } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu';
import loc from '../loc';
import { scanQrHelper } from '../helpers/scan-qr';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import presentAlert from './Alert';
import { useTheme } from './themes';
import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings';
interface AddressInputScanButtonProps {
isLoading: boolean;
launchedBy?: string;
scanButtonTapped: () => void;
onBarScanned: (ret: { data?: any }) => void;
onChangeText: (text: string) => void;
}
export const AddressInputScanButton = ({
isLoading,
launchedBy,
scanButtonTapped,
onBarScanned,
onChangeText,
}: AddressInputScanButtonProps) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
const stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
},
scanText: {
color: colors.inverseForegroundColor,
},
});
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
const actions = useMemo(() => {
const availableActions = [
CommonToolTipActions.ScanQR,
CommonToolTipActions.ChoosePhoto,
CommonToolTipActions.ImportFile,
{
...CommonToolTipActions.PasteFromClipboard,
hidden: !isClipboardGetContentEnabled,
},
];
return availableActions;
}, [isClipboardGetContentEnabled]);
const onMenuItemPressed = useCallback(
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case CommonToolTipActions.ScanQR.id:
scanButtonTapped();
if (launchedBy) {
scanQrHelper(launchedBy)
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
let getImage: string | null = null;
if (Platform.OS === 'android') {
getImage = await Clipboard.getImage();
} else {
const hasImage = await Clipboard.hasImage();
if (hasImage) {
getImage = await Clipboard.getImageJPG();
}
}
if (getImage) {
try {
const base64Data = getImage.replace(/^data:image\/jpeg;base64,/, '');
const values = await RNQRGenerator.detect({
base64: base64Data,
});
if (values && values.values.length > 0) {
onChangeText(values.values[0]);
} else {
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
} else {
const clipboardText = await Clipboard.getString();
onChangeText(clipboardText);
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
break;
case CommonToolTipActions.ChoosePhoto.id:
showImagePickerAndReadImage()
.then(value => {
if (value) {
onChangeText(value);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
case CommonToolTipActions.ImportFile.id:
showFilePickerAndReadFile()
.then(value => {
if (value.data) {
onChangeText(value.data);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
}
Keyboard.dismiss();
},
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
return (
<ToolTipMenu
actions={actions}
isButton
onPressMenuItem={onMenuItemPressed}
testID="BlueAddressInputScanQrButton"
disabled={isLoading}
onPress={toolTipOnPress}
buttonStyle={buttonStyle}
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
<Image source={require('../img/scan-white.png')} accessible={false} />
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
</ToolTipMenu>
);
};
const styles = StyleSheet.create({
scan: {
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
},
scanText: {
marginLeft: 4,
},
});

View File

@ -1,5 +1,5 @@
import React, { forwardRef } from 'react';
import { StyleProp, StyleSheet, Text, TouchableOpacity, TouchableOpacityProps, View, ViewStyle } from 'react-native';
import { ActivityIndicator, StyleProp, StyleSheet, Text, TouchableOpacity, TouchableOpacityProps, View, ViewStyle } from 'react-native';
import { Icon } from '@rneui/themed';
import { useTheme } from './themes';
@ -17,6 +17,7 @@ interface ButtonProps extends TouchableOpacityProps {
title?: string;
style?: StyleProp<ViewStyle>;
onPress?: () => void;
showActivityIndicator?: boolean;
}
export const Button = forwardRef<React.ElementRef<typeof TouchableOpacity>, ButtonProps>((props, ref) => {
@ -40,7 +41,9 @@ export const Button = forwardRef<React.ElementRef<typeof TouchableOpacity>, Butt
color: fontColor,
};
const buttonView = (
const buttonView = props.showActivityIndicator ? (
<ActivityIndicator size="small" color={textStyle.color} />
) : (
<>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={textStyle}>{props.title}</Text>}

View File

@ -1,19 +1,35 @@
import React from 'react';
import { Pressable, Platform } from 'react-native';
import ToolTipMenu from './TooltipMenu';
import { useTheme } from './themes';
import { Icon } from '@rneui/themed';
import { Platform } from 'react-native';
import { Action } from './types';
interface HeaderMenuButtonProps {
onPressMenuItem: (id: string) => void;
actions: Action[];
actions?: Action[] | Action[][];
disabled?: boolean;
}
const HeaderMenuButton: React.FC<HeaderMenuButtonProps> = ({ onPressMenuItem, actions, disabled }) => {
const { colors } = useTheme();
const styleProps = Platform.OS === 'android' ? { iconStyle: { transform: [{ rotate: '90deg' }] } } : {};
if (!actions || actions.length === 0) {
return (
<Pressable
testID="HeaderMenuButton"
disabled={disabled}
android_ripple={{ color: colors.lightButton }}
style={({ pressed }) => [{ opacity: pressed ? 0.5 : 1 }]}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} {...styleProps} />
</Pressable>
);
}
const menuActions = Array.isArray(actions[0]) ? (actions as Action[][]) : (actions as Action[]);
return (
<ToolTipMenu
testID="HeaderMenuButton"
@ -21,7 +37,7 @@ const HeaderMenuButton: React.FC<HeaderMenuButtonProps> = ({ onPressMenuItem, ac
isButton
isMenuPrimaryAction
onPressMenuItem={onPressMenuItem}
actions={actions}
actions={menuActions}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} {...styleProps} />
</ToolTipMenu>

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import { TouchableOpacity, Text, StyleSheet, LayoutAnimation, View } from 'react-native';
import { useStorage } from '../hooks/context/useStorage';
import loc, { formatBalanceWithoutSuffix } from '../loc';
@ -18,99 +18,94 @@ const TotalWalletsBalance: React.FC = () => {
useSettings();
const { colors } = useTheme();
const styleHooks = StyleSheet.create({
balance: {
color: colors.foregroundColor,
},
currency: {
color: colors.foregroundColor,
},
});
const styleHooks = useMemo(
() => ({
balance: { color: colors.foregroundColor },
currency: { color: colors.foregroundColor },
}),
[colors.foregroundColor],
);
// Calculate total balance from all wallets
const totalBalance = wallets.reduce((prev, curr) => {
if (!curr.hideBalance) {
return prev + curr.getBalance();
}
return prev;
}, 0);
const totalBalance = useMemo(() => wallets.reduce((prev, curr) => (!curr.hideBalance ? prev + curr.getBalance() : prev), 0), [wallets]);
const formattedBalance = useMemo(
() => formatBalanceWithoutSuffix(Number(totalBalance), totalBalancePreferredUnit, true),
() => formatBalanceWithoutSuffix(totalBalance, totalBalancePreferredUnit, true),
// eslint-disable-next-line react-hooks/exhaustive-deps
[totalBalance, totalBalancePreferredUnit, preferredFiatCurrency],
);
const toolTipActions = useMemo(() => {
let viewIn;
const viewInFiat = {
...CommonToolTipActions.ViewInFiat,
text: loc.formatString(loc.total_balance_view.view_in_fiat, { currency: preferredFiatCurrency.endPointKey }),
hidden: totalBalancePreferredUnit === BitcoinUnit.LOCAL_CURRENCY,
};
if (totalBalancePreferredUnit === BitcoinUnit.SATS) {
viewIn = {
...CommonToolTipActions.ViewInFiat,
text: loc.formatString(loc.total_balance_view.view_in_fiat, { currency: preferredFiatCurrency.endPointKey }),
};
} else if (totalBalancePreferredUnit === BitcoinUnit.LOCAL_CURRENCY) {
viewIn = CommonToolTipActions.ViewInBitcoin;
} else if (totalBalancePreferredUnit === BitcoinUnit.BTC) {
viewIn = CommonToolTipActions.ViewInSats;
} else {
viewIn = CommonToolTipActions.ViewInBitcoin;
}
const viewInSats = {
...CommonToolTipActions.ViewInSats,
hidden: totalBalancePreferredUnit === BitcoinUnit.SATS,
};
return [viewIn, CommonToolTipActions.CopyAmount, CommonToolTipActions.HideBalance];
const viewInBitcoin = {
...CommonToolTipActions.ViewInBitcoin,
hidden: totalBalancePreferredUnit === BitcoinUnit.BTC,
};
const viewInActions = {
id: 'viewInActions',
text: '',
subactions: [viewInFiat, viewInSats, viewInBitcoin],
displayInline: true,
};
return [viewInActions, CommonToolTipActions.CopyAmount, CommonToolTipActions.HideBalance];
}, [preferredFiatCurrency.endPointKey, totalBalancePreferredUnit]);
const onPressMenuItem = useMemo(
() => async (id: string) => {
const onPressMenuItem = useCallback(
async (id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
switch (id) {
case CommonToolTipActions.ViewInFiat.id:
case CommonToolTipActions.ViewInBitcoin.id:
case CommonToolTipActions.ViewInSats.id:
switch (totalBalancePreferredUnit) {
case BitcoinUnit.BTC:
await setTotalBalancePreferredUnitStorage(BitcoinUnit.SATS);
break;
case BitcoinUnit.SATS:
await setTotalBalancePreferredUnitStorage(BitcoinUnit.LOCAL_CURRENCY);
break;
case BitcoinUnit.LOCAL_CURRENCY:
await setTotalBalancePreferredUnitStorage(BitcoinUnit.BTC);
break;
default:
break;
}
break;
case CommonToolTipActions.HideBalance.id:
setIsTotalBalanceEnabledStorage(false);
break;
case CommonToolTipActions.CopyAmount.id:
Clipboard.setString(formattedBalance.toString());
break;
default:
break;
if (id === CommonToolTipActions.ViewInFiat.id) {
await setTotalBalancePreferredUnitStorage(BitcoinUnit.LOCAL_CURRENCY);
} else if (id === CommonToolTipActions.ViewInSats.id) {
await setTotalBalancePreferredUnitStorage(BitcoinUnit.SATS);
} else if (id === CommonToolTipActions.ViewInBitcoin.id) {
await setTotalBalancePreferredUnitStorage(BitcoinUnit.BTC);
} else if (id === CommonToolTipActions.HideBalance.id) {
setIsTotalBalanceEnabledStorage(false);
} else if (id === CommonToolTipActions.CopyAmount.id) {
Clipboard.setString(formattedBalance.toString());
}
},
[totalBalancePreferredUnit, setIsTotalBalanceEnabledStorage, formattedBalance, setTotalBalancePreferredUnitStorage],
[setIsTotalBalanceEnabledStorage, formattedBalance, setTotalBalancePreferredUnitStorage],
);
const handleBalanceOnPress = useCallback(async () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
const nextUnit =
totalBalancePreferredUnit === BitcoinUnit.BTC
? BitcoinUnit.SATS
: totalBalancePreferredUnit === BitcoinUnit.SATS
? BitcoinUnit.LOCAL_CURRENCY
: BitcoinUnit.BTC;
await setTotalBalancePreferredUnitStorage(nextUnit);
}, [totalBalancePreferredUnit, setTotalBalancePreferredUnitStorage]);
if (wallets.length <= 1) return null;
return (
(wallets.length > 1 && (
<ToolTipMenu actions={toolTipActions} onPressMenuItem={onPressMenuItem}>
<View style={styles.container}>
<Text style={styles.label}>{loc.wallets.total_balance}</Text>
<TouchableOpacity onPress={() => onPressMenuItem(CommonToolTipActions.ViewInBitcoin.id)}>
<Text style={[styles.balance, styleHooks.balance]}>
{formattedBalance}{' '}
{totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.currency, styleHooks.currency]}>{totalBalancePreferredUnit}</Text>
)}
</Text>
</TouchableOpacity>
</View>
</ToolTipMenu>
)) ||
null
<ToolTipMenu actions={toolTipActions} onPressMenuItem={onPressMenuItem}>
<View style={styles.container}>
<Text style={styles.label}>{loc.wallets.total_balance}</Text>
<TouchableOpacity onPress={handleBalanceOnPress}>
<Text style={[styles.balance, styleHooks.balance]}>
{formattedBalance}{' '}
{totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.currency, styleHooks.currency]}>{totalBalancePreferredUnit}</Text>
)}
</Text>
</TouchableOpacity>
</View>
</ToolTipMenu>
);
};

View File

@ -1,6 +1,5 @@
import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import Clipboard from '@react-native-clipboard/clipboard';
import { useNavigation } from '@react-navigation/native';
import { StyleSheet, Text, View } from 'react-native';
import { ListItem } from '@rneui/themed';
import Share from 'react-native-share';
@ -12,15 +11,15 @@ import { BitcoinUnit } from '../../models/bitcoinUnits';
import presentAlert from '../Alert';
import QRCodeComponent from '../QRCodeComponent';
import { useTheme } from '../themes';
import { Action } from '../types';
import { AddressTypeBadge } from './AddressTypeBadge';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useStorage } from '../../hooks/context/useStorage';
import ToolTipMenu from '../TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
interface AddressItemProps {
// todo: fix `any` after addresses.js is converted to the church of holy typescript
item: any;
balanceUnit: BitcoinUnit;
walletID: string;
@ -55,9 +54,9 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
},
});
const { navigate } = useNavigation<NavigationProps>();
const { navigate } = useExtendedNavigation<NavigationProps>();
const navigateToReceive = () => {
const navigateToReceive = useCallback(() => {
navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
@ -65,9 +64,9 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
address: item.address,
},
});
};
}, [navigate, walletID, item.address]);
const navigateToSignVerify = () => {
const navigateToSignVerify = useCallback(() => {
navigate('SignVerifyRoot', {
screen: 'SignVerify',
params: {
@ -75,21 +74,36 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
address: item.address,
},
});
};
}, [navigate, walletID, item.address]);
const menuActions = useMemo(() => getAvailableActions({ allowSignVerifyMessage }), [allowSignVerifyMessage]);
const menuActions = useMemo(
() =>
[
CommonToolTipActions.CopyTXID,
CommonToolTipActions.Share,
{
...CommonToolTipActions.SignVerify,
hidden: !allowSignVerifyMessage,
},
{
...CommonToolTipActions.ExportPrivateKey,
hidden: !allowSignVerifyMessage,
},
].filter(action => !action.hidden),
[allowSignVerifyMessage],
);
const balance = formatBalance(item.balance, balanceUnit, true);
const handleCopyPress = () => {
const handleCopyPress = useCallback(() => {
Clipboard.setString(item.address);
};
}, [item.address]);
const handleSharePress = () => {
const handleSharePress = useCallback(() => {
Share.open({ message: item.address }).catch(error => console.log(error));
};
}, [item.address]);
const handleCopyPrivkeyPress = () => {
const handleCopyPrivkeyPress = useCallback(() => {
const wallet = wallets.find(w => w.getID() === walletID);
if (!wallet) {
presentAlert({ message: 'Internal error: cant find wallet' });
@ -107,86 +121,60 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
} catch (error: any) {
presentAlert({ message: error.message });
}
};
}, [wallets, walletID, item.address]);
const onToolTipPress = async (id: string) => {
if (id === actionKeys.CopyToClipboard) {
handleCopyPress();
} else if (id === actionKeys.Share) {
handleSharePress();
} else if (id === actionKeys.SignVerify) {
navigateToSignVerify();
} else if (id === actionKeys.ExportPrivateKey) {
if (await confirm(loc.addresses.sensitive_private_key)) {
if (await isBiometricUseCapableAndEnabled()) {
if (!(await unlockWithBiometrics())) {
return;
const onToolTipPress = useCallback(
async (id: string) => {
if (id === CommonToolTipActions.CopyTXID.id) {
handleCopyPress();
} else if (id === CommonToolTipActions.Share.id) {
handleSharePress();
} else if (id === CommonToolTipActions.SignVerify.id) {
navigateToSignVerify();
} else if (id === CommonToolTipActions.ExportPrivateKey.id) {
if (await confirm(loc.addresses.sensitive_private_key)) {
if (await isBiometricUseCapableAndEnabled()) {
if (!(await unlockWithBiometrics())) {
return;
}
}
handleCopyPrivkeyPress();
}
handleCopyPrivkeyPress();
}
}
};
},
[handleCopyPress, handleSharePress, navigateToSignVerify, handleCopyPrivkeyPress, isBiometricUseCapableAndEnabled],
);
const renderPreview = () => {
return <QRCodeComponent value={item.address} isMenuAvailable={false} />;
};
const renderPreview = useCallback(() => <QRCodeComponent value={item.address} isMenuAvailable={false} />, [item.address]);
const render = () => {
return (
<ToolTipMenu
title={item.address}
actions={menuActions}
onPressMenuItem={onToolTipPress}
renderPreview={renderPreview}
onPress={navigateToReceive}
isButton
>
<ListItem key={item.key} containerStyle={stylesHook.container}>
<ListItem.Content style={stylesHook.list}>
<ListItem.Title style={stylesHook.list} numberOfLines={1} ellipsizeMode="middle">
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
</ListItem.Title>
<View style={styles.subtitle}>
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
</View>
</ListItem.Content>
<View>
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
{loc.addresses.transactions}: {item.transactions}
</Text>
return (
<ToolTipMenu
title={item.address}
actions={menuActions}
onPressMenuItem={onToolTipPress}
renderPreview={renderPreview}
onPress={navigateToReceive}
isButton
>
<ListItem key={item.key} containerStyle={stylesHook.container}>
<ListItem.Content style={stylesHook.list}>
<ListItem.Title style={stylesHook.list} numberOfLines={1} ellipsizeMode="middle">
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
</ListItem.Title>
<View style={styles.subtitle}>
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
</View>
</ListItem>
</ToolTipMenu>
);
};
return render();
};
const actionKeys = {
Share: 'share',
CopyToClipboard: 'copyToClipboard',
SignVerify: 'signVerify',
ExportPrivateKey: 'exportPrivateKey',
};
const actionIcons = {
Signature: {
iconValue: 'signature',
},
Share: {
iconValue: 'square.and.arrow.up',
},
Clipboard: {
iconValue: 'doc.on.doc',
},
ExportPrivateKey: {
iconValue: 'key',
},
</ListItem.Content>
<View>
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
{loc.addresses.transactions}: {item.transactions}
</Text>
</View>
</ListItem>
</ToolTipMenu>
);
};
const styles = StyleSheet.create({
@ -209,37 +197,4 @@ const styles = StyleSheet.create({
},
});
const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] => {
const actions = [
{
id: actionKeys.CopyToClipboard,
text: loc.transactions.details_copy,
icon: actionIcons.Clipboard,
},
{
id: actionKeys.Share,
text: loc.receive.details_share,
icon: actionIcons.Share,
},
];
if (allowSignVerifyMessage) {
actions.push({
id: actionKeys.SignVerify,
text: loc.addresses.sign_title,
icon: actionIcons.Signature,
});
}
if (allowSignVerifyMessage) {
actions.push({
id: actionKeys.ExportPrivateKey,
text: loc.addresses.copy_private_key,
icon: actionIcons.ExportPrivateKey,
});
}
return actions;
};
export { AddressItem };

View File

@ -19,7 +19,7 @@ export interface Action {
}
export interface ToolTipMenuProps {
actions: Action[];
actions: Action[] | Action[][];
children: React.ReactNode;
enableAndroidRipple?: boolean;
dismissMenu?: () => void;

View File

@ -324,6 +324,8 @@ lane :build_app_lane do
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
export_options_path = File.join(project_root, "ios", "export_options.plist")
clear_derived_data_lane
begin
build_ios_app(
scheme: "BlueWallet",

View File

@ -120,6 +120,11 @@
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E982CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E992CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E9A2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E9B2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B48A6A282C1DF01000030AB9 /* KeychainSwift */; };
B4AB225D2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
@ -131,7 +136,7 @@
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -347,6 +352,8 @@
B4549F352B82B10D002E3153 /* ci_post_clone.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = "<group>"; };
B461B850299599F800E431AA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = BlueWallet/AppDelegate.h; sourceTree = "<group>"; };
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
B4742E9C2CCDC31300380EEE /* en_US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_US; path = en_US.lproj/Interface.strings; sourceTree = "<group>"; };
B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITests.swift; sourceTree = "<group>"; };
B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITest.swift; sourceTree = "<group>"; };
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
@ -381,7 +388,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -458,6 +465,7 @@
6D32C5C42596CE2F008C077C /* EventEmitter.h */,
6D32C5C52596CE3A008C077C /* EventEmitter.m */,
84E05A832721191B001A0D3A /* Settings.bundle */,
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */,
);
name = BlueWallet;
sourceTree = "<group>";
@ -928,10 +936,9 @@
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */;
compatibilityVersion = "Xcode 15.0";
developmentRegion = English;
developmentRegion = en_US;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
af,
@ -957,6 +964,8 @@
tr,
xh,
nb,
en_US,
"en-US",
);
mainGroup = 83CBB9F61A601CBA00E9B192;
packageReferences = (
@ -985,6 +994,7 @@
6DF25A9F249DB97E001D06F5 /* LaunchScreen.storyboard in Resources */,
B440340F2BCC40A400162242 /* fiatUnits.json in Resources */,
84E05A842721191B001A0D3A /* Settings.bundle in Resources */,
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */,
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */,
B41C2E562BB3DCB8000FE097 /* PrivacyInfo.xcprivacy in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
@ -995,6 +1005,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4742E9A2CCDBE8300380EEE /* Localizable.xcstrings in Resources */,
6D2A6464258BA92D0092292B /* Stickers.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1005,6 +1016,7 @@
files = (
B41C2E582BB3DCB8000FE097 /* PrivacyInfo.xcprivacy in Resources */,
B44034112BCC40A400162242 /* fiatUnits.json in Resources */,
B4742E9B2CCDBE8300380EEE /* Localizable.xcstrings in Resources */,
6DD410B7266CAF5C0087DE03 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1013,6 +1025,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4742E982CCDBE8300380EEE /* Localizable.xcstrings in Resources */,
B40D4E36225841ED00428FCC /* Assets.xcassets in Resources */,
B40D4E34225841EC00428FCC /* Interface.storyboard in Resources */,
);
@ -1024,6 +1037,7 @@
files = (
B41C2E572BB3DCB8000FE097 /* PrivacyInfo.xcprivacy in Resources */,
B44034102BCC40A400162242 /* fiatUnits.json in Resources */,
B4742E992CCDBE8300380EEE /* Localizable.xcstrings in Resources */,
B4EE583C226703320003363C /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1336,6 +1350,7 @@
6D294A9B24D512770039E22B /* tr */,
6D294A9D24D5127F0039E22B /* xh */,
B4B31A352C77BBA000663334 /* nb */,
B4742E9C2CCDC31300380EEE /* en_US */,
);
name = Interface.storyboard;
sourceTree = "<group>";
@ -1713,6 +1728,7 @@
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
USE_HERMES = true;
WATCHOS_DEPLOYMENT_TARGET = 7.0;
@ -1777,6 +1793,7 @@
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;

View File

@ -56,6 +56,7 @@
center.delegate = self;
[self setupUserDefaultsListener];
[self registerNotificationCategories];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@ -72,7 +73,25 @@
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
}
- (void)registerNotificationCategories {
// Define two actions: "View Address in Browser" and "View Transaction in Browser"
UNNotificationAction *viewAddressTransactionsAction = [UNNotificationAction actionWithIdentifier:@"VIEW_ADDRESS_TRANSACTIONS"
title:NSLocalizedString(@"VIEW_ADDRESS_TRANSACTIONS_TITLE", nil)
options:UNNotificationActionOptionForeground];
UNNotificationAction *viewTransactionDetailsAction = [UNNotificationAction actionWithIdentifier:@"VIEW_TRANSACTION_DETAILS"
title:NSLocalizedString(@"VIEW_TRANSACTION_DETAILS_TITLE", nil)
options:UNNotificationActionOptionForeground];
UNNotificationCategory *transactionCategory = [UNNotificationCategory categoryWithIdentifier:@"TRANSACTION_CATEGORY"
actions:@[viewAddressTransactionsAction, viewTransactionDetailsAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionCustomDismissAction];
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:transactionCategory]];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
@ -259,12 +278,37 @@
{
[RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error];
}
// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
[RNCPushNotificationIOS didReceiveNotificationResponse:response];
NSDictionary *userInfo = response.notification.request.content.userInfo;
NSString *blockExplorer = [[[NSUserDefaults standardUserDefaults] initWithSuiteName:@"group.io.bluewallet.bluewallet"] stringForKey:@"blockExplorer"];
if (blockExplorer == nil || [blockExplorer length] == 0) {
blockExplorer = @"https://www.mempool.space";
}
NSString *address = userInfo[@"data"][@"address"];
NSString *txid = userInfo[@"data"][@"txid"];
if ([response.actionIdentifier isEqualToString:@"VIEW_ADDRESS_TRANSACTIONS"] && address) {
NSString *urlString = [NSString stringWithFormat:@"%@/address/%@", blockExplorer, address];
NSURL *url = [NSURL URLWithString:urlString];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}
else if ([response.actionIdentifier isEqualToString:@"VIEW_TRANSACTION_DETAILS"] && txid) {
NSString *urlString = [NSString stringWithFormat:@"%@/tx/%@", blockExplorer, txid];
NSURL *url = [NSURL URLWithString:urlString];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}
[RNCPushNotificationIOS didReceiveNotificationResponse:response];
completionHandler();
}
// Clear cache on app launch

View File

@ -0,0 +1,105 @@
/* Class = "WKInterfaceButton"; title = "Amount"; ObjectID = "0Hm-hv-Yi3"; */
"0Hm-hv-Yi3.title" = "Amount";
/* Class = "WKInterfaceButton"; title = "8"; ObjectID = "3FQ-tZ-9kd"; */
"3FQ-tZ-9kd.title" = "8";
/* Class = "WKInterfaceButton"; title = "Create"; ObjectID = "6eh-lx-UEe"; */
"6eh-lx-UEe.title" = "Create";
/* Class = "WKInterfaceButton"; title = "Create Invoice"; ObjectID = "7bc-tt-Pab"; */
"7bc-tt-Pab.title" = "Create Invoice";
/* Class = "WKInterfaceButton"; title = "5"; ObjectID = "AA6-Gq-qRe"; */
"AA6-Gq-qRe.title" = "5";
/* Class = "WKInterfaceLabel"; text = "memo"; ObjectID = "AJ8-p9-ID7"; */
"AJ8-p9-ID7.text" = "memo";
/* Class = "WKInterfaceController"; title = "BlueWallet"; ObjectID = "AgC-eL-Hgc"; */
"AgC-eL-Hgc.title" = "BlueWallet";
/* Class = "WKInterfaceLabel"; text = "Time"; ObjectID = "GqE-KB-TRD"; */
"GqE-KB-TRD.text" = "Time";
/* Class = "WKInterfaceLabel"; text = "No wallets available. Please, add one by opening BlueWallet on your iPhone."; ObjectID = "I2I-8t-hp3"; */
"I2I-8t-hp3.text" = "No wallets available. Please, add one by opening BlueWallet on your iPhone.";
/* Class = "WKInterfaceLabel"; text = "Alert Label"; ObjectID = "IdU-wH-bcW"; */
"IdU-wH-bcW.text" = "Alert Label";
/* Class = "WKInterfaceLabel"; text = "Label"; ObjectID = "JMO-XZ-1si"; */
"JMO-XZ-1si.text" = "Label";
/* Class = "WKInterfaceButton"; title = "9"; ObjectID = "NJM-uR-nyO"; */
"NJM-uR-nyO.title" = "9";
/* Class = "WKInterfaceButton"; title = "6"; ObjectID = "Nt9-we-M9f"; */
"Nt9-we-M9f.title" = "6";
/* Class = "WKInterfaceLabel"; text = "Wallet"; ObjectID = "PQi-JV-aYW"; */
"PQi-JV-aYW.text" = "Wallet";
/* Class = "WKInterfaceLabel"; text = "Balance"; ObjectID = "QYx-3e-6zf"; */
"QYx-3e-6zf.text" = "Balance";
/* Class = "WKInterfaceMenuItem"; title = "Customize"; ObjectID = "RHB-IJ-Utd"; */
"RHB-IJ-Utd.title" = "Customize";
/* Class = "WKInterfaceButton"; title = "0"; ObjectID = "S1H-Id-l6g"; */
"S1H-Id-l6g.title" = "0";
/* Class = "WKInterfaceButton"; title = "3"; ObjectID = "TKO-lc-aYf"; */
"TKO-lc-aYf.title" = "3";
/* Class = "WKInterfaceLabel"; text = "Balance"; ObjectID = "WTr-jJ-w7L"; */
"WTr-jJ-w7L.text" = "Balance";
/* Class = "WKInterfaceController"; title = "Transactions"; ObjectID = "XWa-4i-Abg"; */
"XWa-4i-Abg.title" = "Transactions";
/* Class = "WKInterfaceButton"; title = "2"; ObjectID = "aUI-EE-NVw"; */
"aUI-EE-NVw.title" = "2";
/* Class = "WKInterfaceButton"; title = "Receive"; ObjectID = "bPO-h8-ccD"; */
"bPO-h8-ccD.title" = "Receive";
/* Class = "WKInterfaceLabel"; text = "Label"; ObjectID = "c3W-8T-srG"; */
"c3W-8T-srG.text" = "Label";
/* Class = "WKInterfaceController"; title = "Receive"; ObjectID = "egq-Yw-qK5"; */
"egq-Yw-qK5.title" = "Receive";
/* Class = "WKInterfaceButton"; title = "Description"; ObjectID = "fcI-6Z-moQ"; */
"fcI-6Z-moQ.title" = "Description";
/* Class = "WKInterfaceButton"; title = "."; ObjectID = "g6Z-9t-ahQ"; */
"g6Z-9t-ahQ.title" = ".";
/* Class = "WKInterfaceButton"; title = "1"; ObjectID = "ghD-Jq-ubw"; */
"ghD-Jq-ubw.title" = "1";
/* Class = "WKInterfaceButton"; title = "View XPUB"; ObjectID = "j0O-fq-mwp"; */
"j0O-fq-mwp.title" = "View XPUB";
/* Class = "WKInterfaceButton"; title = "4"; ObjectID = "kH2-N1-Hbe"; */
"kH2-N1-Hbe.title" = "4";
/* Class = "WKInterfaceLabel"; text = "Creating Invoice..."; ObjectID = "n5f-iL-ib7"; */
"n5f-iL-ib7.text" = "Creating Invoice...";
/* Class = "WKInterfaceButton"; title = "7"; ObjectID = "ohU-B0-mvg"; */
"ohU-B0-mvg.title" = "7";
/* Class = "WKInterfaceLabel"; text = "No Transactions"; ObjectID = "pi4-Bk-Jiq"; */
"pi4-Bk-Jiq.text" = "No Transactions";
/* Class = "WKInterfaceButton"; title = "<"; ObjectID = "q8Q-tK-nzd"; */
"q8Q-tK-nzd.title" = "<";
/* Class = "WKInterfaceLabel"; text = "Wallet"; ObjectID = "qpj-I1-cWt"; */
"qpj-I1-cWt.text" = "Wallet";
/* Class = "WKInterfaceLabel"; text = "Amount"; ObjectID = "sAS-LI-RY7"; */
"sAS-LI-RY7.text" = "Amount";

62
ios/Localizable.xcstrings Normal file
View File

@ -0,0 +1,62 @@
{
"sourceLanguage" : "en_US",
"strings" : {
"VIEW_ADDRESS_TRANSACTIONS_TITLE" : {
"extractionState" : "manual",
"localizations" : {
"en_US" : {
"stringUnit" : {
"state" : "translated",
"value" : "View Address in Browser"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Ver dirección en el navegador"
}
}
}
},
"VIEW_IN_BROWSER_TITLE" : {
"localizations" : {
"en_US" : {
"stringUnit" : {
"state" : "translated",
"value" : "View in Browser"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ver en el navegador"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voir dans le navigateur"
}
}
}
},
"VIEW_TRANSACTION_DETAILS_TITLE" : {
"extractionState" : "manual",
"localizations" : {
"en_US" : {
"stringUnit" : {
"state" : "translated",
"value" : "View Transaction in Browser"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Ver transacción en el navegador"
}
}
}
}
},
"version" : "1.0"
}

View File

@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- BugsnagReactNative (8.1.1):
- BugsnagReactNative (8.1.2):
- React-Core
- BVLinearGradient (2.8.3):
- React-Core
@ -12,12 +12,12 @@ PODS:
- hermes-engine (0.75.4):
- hermes-engine/Pre-built (= 0.75.4)
- hermes-engine/Pre-built (0.75.4)
- lottie-ios (4.4.1)
- lottie-react-native (6.7.2):
- lottie-ios (4.5.0)
- lottie-react-native (7.0.0):
- DoubleConversion
- glog
- hermes-engine
- lottie-ios (= 4.4.1)
- lottie-ios (= 4.5.0)
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
@ -1319,7 +1319,7 @@ PODS:
- React
- react-native-randombytes (3.6.1):
- React-Core
- react-native-safe-area-context (4.11.1):
- react-native-safe-area-context (4.12.0):
- React-Core
- react-native-screen-capture (0.2.3):
- React
@ -1592,15 +1592,15 @@ PODS:
- React-Core
- RealmJS (20.0.0):
- React
- RNCAsyncStorage (1.24.0):
- RNCAsyncStorage (2.0.0):
- React-Core
- RNCClipboard (1.14.2):
- RNCClipboard (1.14.3):
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- RNDefaultPreference (1.4.4):
- React-Core
- RNDeviceInfo (11.1.0):
- RNDeviceInfo (13.2.0):
- React-Core
- RNFS (2.20.0):
- React-Core
@ -1627,11 +1627,30 @@ PODS:
- Yoga
- RNHandoff (0.0.3):
- React
- RNKeychain (8.2.0):
- RNKeychain (9.1.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNLocalize (3.2.1):
- React-Core
- RNPermissions (4.1.5):
- RNPermissions (5.0.2):
- React-Core
- RNQrGenerator (1.4.2):
- React
@ -1748,7 +1767,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (3.34.0):
- RNScreens (3.35.0):
- DoubleConversion
- glog
- hermes-engine
@ -1770,8 +1789,27 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNShare (10.2.1):
- RNShare (11.0.4):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSVG (15.8.0):
- React-Core
- RNVectorIcons (10.2.0):
@ -2143,7 +2181,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
BugsnagReactNative: c8b6afecdf4dc127246de7ebef082bc71d96ac51
BugsnagReactNative: d1d736effdbbf529126bc39a3a9ca23e305426dd
BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
@ -2151,8 +2189,8 @@ SPEC CHECKSUMS:
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
hermes-engine: ea92f60f37dba025e293cbe4b4a548fd26b610a0
lottie-ios: e047b1d2e6239b787cc5e9755b988869cf190494
lottie-react-native: 31197e5c65aa7cb59e6affcefaf901588bb708c4
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: 2a3335e7f3cfdc881f400b08c7e9e84d18920db1
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1
RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b
@ -2190,7 +2228,7 @@ SPEC CHECKSUMS:
react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5
react-native-menu: c30eb7a85d7b04d51945f61ea8a8986ed366ac5c
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 5141f11858b033636f1788b14f32eaba92cee810
react-native-safe-area-context: 142fade490cbebbe428640b8cbdb09daf17e8191
react-native-screen-capture: 75db9b051c41fea47fa68665506e9257d4b1dadc
react-native-secure-key-store: 910e6df6bc33cb790aba6ee24bc7818df1fe5898
react-native-tcp-socket: 8c3e8bef909ab06c557eeb95363fe029391ff09d
@ -2222,24 +2260,24 @@ SPEC CHECKSUMS:
ReactCommon: 6a952e50c2a4b694731d7682aaa6c79bc156e4ad
ReactNativeCameraKit: 9d46a5d7dd544ca64aa9c03c150d2348faf437eb
RealmJS: 6946b520bada5568b7e92a5242e138d3a19aa69f
RNCAsyncStorage: ec53e44dc3e75b44aa2a9f37618a49c3bc080a7a
RNCClipboard: 5e503962f0719ace8f7fdfe9c60282b526305c85
RNCAsyncStorage: d35c79ffba52c1013013e16b1fc295aec2feabb6
RNCClipboard: 2821ac938ef46f736a8de0c8814845dde2dcbdfb
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: b899ce37a403a4dea52b7cb85e16e49c04a5b88e
RNDeviceInfo: 29e01d5ae94bdb5a0f6c11a4c438132545b4df80
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 6dfe7692a191ee224748964127114edf057a1475
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
RNKeychain: 958a200b26c2df5036222105550290ac0ed98c90
RNLocalize: 4f22418187ecd5ca693231093ff1d912d1b3c9bc
RNPermissions: 9fa74223844f437bc309e112994859dc47194829
RNPermissions: 81b5a3e2441f0be92f807519c0a4c4f693b5e57c
RNQrGenerator: 5c12ab86443a07e923735800679da7b6fcaaeb31
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNRate: ef3bcff84f39bb1d1e41c5593d3eea4aab2bd73a
RNReactNativeHapticFeedback: 0d591ea1e150f36cb96d868d4e8d77272243d78a
RNReanimated: f6a10979b3701f8029c71dbfe35d0ff4328dce4c
RNScreens: 19719a9c326e925498ac3b2d35c4e50fe87afc06
RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c
RNScreens: c7ceced6a8384cb9be5e7a5e88e9e714401fd958
RNShare: eaeb5e7dc1618d19db6234da1af91fc60dd6bc0f
RNSVG: 8b1a777d54096b8c2a0fd38fc9d5a454332bbb4d
RNVectorIcons: 6382277afab3c54658e9d555ee0faa7a37827136
RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236

View File

@ -290,7 +290,7 @@
"details_outputs": "المخرجات",
"date": "التاريخ",
"details_received": "التاريخ",
"details_show_in_block_explorer": "العرض في مستكشف الكتل",
"details_view_in_browser": "العرض في مستكشف الكتل",
"details_title": "العملية",
"details_to": "إلى",
"enable_offline_signing": "هذه المحفظة لا يتم استعمالها مع التوقيع دون اتصال. هل ترغب في تمكينه الآن؟",

View File

@ -213,7 +213,7 @@
"details_outputs": "Sortides",
"date": "Data",
"details_received": "Rebut",
"details_show_in_block_explorer": "Mostrar en l'explorador de blocs",
"details_view_in_browser": "Mostrar en l'explorador de blocs",
"details_title": "Transacció",
"details_to": "A",
"list_conf": "Conf: {number}",

View File

@ -344,7 +344,7 @@
"details_outputs": "Výstupy",
"date": "Datum",
"details_received": "Přijato",
"details_show_in_block_explorer": "Zobrazit v průzkumníku bloků",
"details_view_in_browser": "Zobrazit v průzkumníku bloků",
"details_title": "Transakce",
"incoming_transaction": "Příchozí transakce",
"outgoing_transaction": "Odchozí transakce",

View File

@ -72,7 +72,7 @@
"cpfp_create": "Opret",
"details_copy": "Kopier",
"details_from": "Fra",
"details_show_in_block_explorer": "Vis i block-explorer",
"details_view_in_browser": "Vis i block-explorer",
"details_title": "Transaktion",
"details_to": "Til",
"list_title": "transaktioner",

View File

@ -339,7 +339,7 @@
"details_outputs": "Ausgänge",
"date": "Datum",
"details_received": "Empfangen",
"details_show_in_block_explorer": "Im Block-Explorer zeigen",
"details_view_in_browser": "Im Block-Explorer zeigen",
"details_title": "Transaktion",
"incoming_transaction": "Eingehende Transaktion",
"outgoing_transaction": "Ausgehende Transaktion",

View File

@ -223,7 +223,7 @@
"details_from": "Εισερχόμενες διευθύνσεις",
"date": "Ημερομηνία",
"details_received": "Ελήφθη",
"details_show_in_block_explorer": "Προβολή στον block explorer",
"details_view_in_browser": "Προβολή στον block explorer",
"details_title": "Συναλλαγή",
"details_to": "Εξερχόμενες διευθύνσεις",
"pending": "Σε επεξεργασία",

View File

@ -22,8 +22,8 @@
"close": "Close",
"change_input_currency": "Change input currency",
"refresh": "Refresh",
"pick_image": "Choose image from library",
"pick_file": "Choose a file",
"pick_image": "Choose from library",
"pick_file": "Choose file",
"enter_amount": "Enter amount",
"qr_custom_input_button": "Tap 10 times to enter custom input",
"unlock": "Unlock",
@ -228,7 +228,7 @@
"biom_no_passcode": "Your device does not have a passcode or biometrics enabled. In order to proceed, please configure a passcode or biometric in the Settings app.",
"biom_remove_decrypt": "All your wallets will be removed and your storage will be decrypted. Are you sure you want to proceed?",
"currency": "Currency",
"currency_source": "Price is obtained from",
"currency_source": "Rate is obtained from",
"currency_fetch_error": "There was an error while obtaining the rate for the selected currency.",
"default_desc": "When disabled, BlueWallet will immediately open the selected wallet at launch.",
"default_info": "Default info",
@ -247,18 +247,17 @@
"set_electrum_server_as_default": "Set {server} as the default Electrum server?",
"set_lndhub_as_default": "Set {url} as the default LNDhub server?",
"electrum_settings_server": "Electrum Server",
"electrum_settings_explain": "Leave blank to use the default.",
"electrum_status": "Status",
"electrum_clear_alert_title": "Clear history?",
"electrum_preferred_server": "Preferred Server",
"electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.", "electrum_clear_alert_title": "Clear history?",
"electrum_clear_alert_message": "Do you want to clear electrum servers history?",
"electrum_clear_alert_cancel": "Cancel",
"electrum_clear_alert_ok": "Ok",
"electrum_select": "Select",
"electrum_reset": "Reset to default",
"electrum_unable_to_connect": "Unable to connect to {server}.",
"electrum_history": "Server history",
"electrum_history": "History",
"electrum_reset_to_default": "Are you sure to want to reset your Electrum settings to default?",
"electrum_clear": "Clear",
"electrum_clear": "Clear History",
"encrypt_decrypt": "Decrypt Storage",
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
"encrypt_enc_and_pass": "Encrypted and Password Protected",
@ -345,7 +344,7 @@
"details_outputs": "Outputs",
"date": "Date",
"details_received": "Received",
"details_show_in_block_explorer": "View in Block Explorer",
"details_view_in_browser": "View in Browser",
"details_title": "Transaction",
"incoming_transaction": "Incoming Transaction",
"outgoing_transaction": "Outgoing Transaction",
@ -652,4 +651,4 @@
"notif_tx": "Notification transaction",
"not_found": "Payment code not found"
}
}
}

View File

@ -280,7 +280,7 @@
"details_outputs": "Outputs",
"date": "Fecha",
"details_received": "Recibido",
"details_show_in_block_explorer": "Mostrar en explorador de bloques",
"details_view_in_browser": "Mostrar en explorador de bloques",
"details_title": "Transaccion",
"details_to": "Destino",
"enable_offline_signing": "Esta billetera no se está usando en conjunción con una firma offline. ¿Quieres activarlo ahora? ",

View File

@ -344,7 +344,7 @@
"details_outputs": "Salidas",
"date": "Fecha",
"details_received": "Recibido",
"details_show_in_block_explorer": "Ver en el Explorador de Bloques",
"details_view_in_browser": "Ver en el Explorador de Bloques",
"details_title": "Transacción",
"incoming_transaction": "Transacción entrante",
"outgoing_transaction": "Transacción saliente",

View File

@ -293,7 +293,7 @@
"details_outputs": "خروجی‌ها",
"date": "تاریخ",
"details_received": "دریافت‌شده",
"details_show_in_block_explorer": "مشاهده در مرورگر بلاک",
"details_view_in_browser": "مشاهده در مرورگر بلاک",
"details_title": "تراکنش",
"details_to": "خروجی",
"enable_offline_signing": "این کیف پول در کنار امضای آفلاین استفاده نمی‌شود. آیا مایل به فعال‌کردن این امکان هستید؟",

View File

@ -309,7 +309,7 @@
"details_outputs": "Ulostulot",
"date": "Päivämäärä",
"details_received": "Vastaanotettu",
"details_show_in_block_explorer": "Näytä lohkoketjuselaimessa",
"details_view_in_browser": "Näytä lohkoketjuselaimessa",
"details_title": "Siirtotapahtuma",
"details_to": "Ulostulo",
"enable_offline_signing": "Tätä lompakkoa ei käytetä offline-allekirjoituksen yhteydessä. Haluatko ottaa sen käyttöön nyt? ",

View File

@ -294,7 +294,7 @@
"details_inputs": "Inputs",
"details_outputs": "Outputs",
"details_received": "Reçu",
"details_show_in_block_explorer": "Afficher dans le \"block explorer\"",
"details_view_in_browser": "Afficher dans le \"block explorer\"",
"details_title": "Transaction",
"details_to": "À",
"enable_offline_signing": "Ce portefeuille n'est pas utilisé en conjonction avec une signature hors ligne. Voulez-vous l'activer maintenant ? ",

View File

@ -323,7 +323,7 @@
"details_outputs": "פלטים",
"date": "תאריך",
"details_received": "התקבל",
"details_show_in_block_explorer": "צפייה בסייר בלוקים",
"details_view_in_browser": "צפייה בסייר בלוקים",
"details_title": "פעולה",
"incoming_transaction": "פעולה נכנסת",
"outgoing_transaction": "פעולה יוצאת",

View File

@ -80,7 +80,7 @@
"cpfp_create": "Stvori",
"details_copy": "Kopiraj",
"details_from": "Od",
"details_show_in_block_explorer": "Prikaži u blok eksploreru",
"details_view_in_browser": "Prikaži u blok eksploreru",
"details_title": "Transakcija",
"details_to": "Za",
"list_title": "transakcije",

View File

@ -288,7 +288,7 @@
"details_inputs": "Bejövő utalások",
"details_outputs": "Kimenő utalások",
"details_received": "Fogadott",
"details_show_in_block_explorer": "Mutasd a block explorerben",
"details_view_in_browser": "Mutasd a block explorerben",
"details_title": "Tranzakció",
"details_to": "Kimenő utalás",
"enable_offline_signing": "Ezt a pénztárcát nem használják offline aláírással. Szeretné most engedélyezni?",

View File

@ -259,7 +259,7 @@
"details_inputs": "Input",
"date": "Tanggal",
"details_received": "Diterima",
"details_show_in_block_explorer": "Tampilkan di block explorer",
"details_view_in_browser": "Tampilkan di block explorer",
"details_title": "Transaksi",
"pending": "tertunda",
"pending_with_amount": "Tertunda {amt1} ({amt2})",

View File

@ -288,7 +288,7 @@
"details_outputs": "Output",
"date": "Data",
"details_received": "Ricevuto",
"details_show_in_block_explorer": "Mostra sul block explorer",
"details_view_in_browser": "Mostra sul block explorer",
"details_title": "Transazione",
"details_to": "A",
"enable_offline_signing": "Questo wallet non sta venendo usato con la firma offline. Desideri attivarla adesso?",

View File

@ -339,7 +339,7 @@
"details_outputs": "アウトプット",
"date": "日付",
"details_received": "受取り済",
"details_show_in_block_explorer": "Block Explorer で表示",
"details_view_in_browser": "Block Explorer で表示",
"details_title": "取引",
"incoming_transaction": "受取りトランザクション",
"outgoing_transaction": "支払いトランザクション",

View File

@ -273,7 +273,7 @@
"details_inputs": "입력",
"details_outputs": "출력",
"details_received": "받기 완료",
"details_show_in_block_explorer": "블록 익스플로러에서 보기",
"details_view_in_browser": "블록 익스플로러에서 보기",
"details_title": "트랜잭션",
"details_to": "출력",
"enable_offline_signing": "이 지갑은 오프라인 서명과 함께 쓸수 없습니다. 지금 가능하도록 할까요?",

View File

@ -255,7 +255,7 @@
"details_inputs": "Masukan",
"details_outputs": "Keluaran",
"details_received": "Diterima",
"details_show_in_block_explorer": "Lihat di Penjelajah Bongkah.",
"details_view_in_browser": "Lihat di Penjelajah Bongkah.",
"details_title": "Urus niaga",
"details_to": "Keluaran",
"enable_offline_signing": "Dompet ini tidak digunakan bersama dengan penandatanganan luar talian. Adakah anda mahu membolehkan ciri ini sekarang?",

View File

@ -275,7 +275,7 @@
"details_inputs": "Inndata",
"details_outputs": "Utdata",
"details_received": "Mottatt",
"details_show_in_block_explorer": "Vis i Block Explorer",
"details_view_in_browser": "Vis i Block Explorer",
"details_title": "Transaksjon",
"details_to": "Utdata",
"enable_offline_signing": "Denne lommeboken brukes ikke i forbindelse med en offline-signering. Vil du aktivere det nå?",

View File

@ -280,7 +280,7 @@
"details_inputs": "Inputs",
"details_outputs": "Outputs",
"details_received": "Ontvangen",
"details_show_in_block_explorer": "Weergeven in block explorer",
"details_view_in_browser": "Weergeven in block explorer",
"details_title": "Transacties",
"details_to": "Uitvoer",
"enable_offline_signing": "Deze wallet wordt niet gebruikt in combinatie met een hardware wallet. Wilt u het gebruik inschakelen?",

View File

@ -333,7 +333,7 @@
"details_outputs": "Wyjścia",
"date": "Data",
"details_received": "Otrzymano",
"details_show_in_block_explorer": "Zobacz w eksploratorze bloków",
"details_view_in_browser": "Zobacz w eksploratorze bloków",
"details_title": "Transakcja",
"incoming_transaction": "Transakcja przychodząca",
"outgoing_transaction": "Transakcja wychodząca",

View File

@ -333,7 +333,7 @@
"details_outputs": "Saídas",
"date": "Data",
"details_received": "Recebido",
"details_show_in_block_explorer": "Ver no Explorador de Blocos",
"details_view_in_browser": "Ver no Explorador de Blocos",
"details_title": "Transação",
"incoming_transaction": "Transação de Entrada",
"outgoing_transaction": "Transação de Saída",

View File

@ -244,7 +244,7 @@
"details_outputs": "Outputs",
"date": "Data",
"details_received": "Recebido",
"details_show_in_block_explorer": "Mostrar no block explorer",
"details_view_in_browser": "Mostrar no block explorer",
"details_title": "detalhes",
"details_to": "Para",
"enable_offline_signing": "Esta carteira não está a ser utilizada em conjunto com uma assinatura offline. Deseja activá-la agora?",

View File

@ -262,7 +262,7 @@
"details_inputs": "Input-uri",
"details_outputs": "Output-uri",
"details_received": "Primit",
"details_show_in_block_explorer": "Afișează în Block Explorer",
"details_view_in_browser": "Afișează în Block Explorer",
"details_title": "Tranzacție",
"details_to": "Output",
"enable_offline_signing": "Acest portofel nu este folosit în legătură cu o semnare offline. Ai vrea să activezi asta acum?",

View File

@ -344,7 +344,7 @@
"details_outputs": "Выходы",
"date": "Дата",
"details_received": "Получена",
"details_show_in_block_explorer": "Показать в блокчейне",
"details_view_in_browser": "Показать в блокчейне",
"details_title": "Детали транзакции",
"incoming_transaction": "Входящая транзакция",
"outgoing_transaction": "Исходящая транзакция",

View File

@ -271,7 +271,7 @@
"details_inputs": "යෙදවුම්",
"details_outputs": "ප්‍රතිදාන",
"details_received": "ලැබුණි",
"details_show_in_block_explorer": "බ්ලොක් එක්ස්ප්ලෝරර් හි බලන්න",
"details_view_in_browser": "බ්ලොක් එක්ස්ප්ලෝරර් හි බලන්න",
"details_title": "ගනුදෙනුව",
"details_to": "ප්‍රතිදානය",
"enable_offline_signing": "මෙම පසුම්බිය නොබැඳි අත්සන් කිරීම සමඟ එක්ව භාවිතා නොකෙරේ. ඔබ දැන් එය සක්‍රීය කිරීමට කැමතිද?",

View File

@ -156,7 +156,7 @@
"details_inputs": "Vstupy",
"details_outputs": "Výstupy",
"details_received": "Prijaté",
"details_show_in_block_explorer": "Ukázať v block exploreri",
"details_view_in_browser": "Ukázať v block exploreri",
"details_title": "Transakcia",
"details_to": "Výstup",
"pending": "Čaká...",

View File

@ -280,7 +280,7 @@
"details_outputs": "Izhodi",
"date": "Datum",
"details_received": "Prejeto",
"details_show_in_block_explorer": "Prikaži v raziskovalcu blokov",
"details_view_in_browser": "Prikaži v raziskovalcu blokov",
"details_title": "Transakcija",
"details_to": "Izhod",
"enable_offline_signing": "Ta denarnica se ne uporablja skupaj s podpisovanjem brez povezave (offline). Ali ga želite omogočiti?",

View File

@ -289,7 +289,7 @@
"details_outputs": "Outputs",
"date": "Datum",
"details_received": "Mottaget",
"details_show_in_block_explorer": "Visa i block explorer",
"details_view_in_browser": "Visa i block explorer",
"details_title": "Transaktion",
"details_to": "Output",
"enable_offline_signing": "Den här plånboken används inte i samband med en offlinesignering. Skulle du vilja aktivera det nu?",

View File

@ -201,7 +201,7 @@
"details_inputs": "อินพุท",
"details_outputs": "เอ้าพุท",
"details_received": "ได้รับแล้ว",
"details_show_in_block_explorer": "แสดงด้วย block explorer",
"details_view_in_browser": "แสดงด้วย block explorer",
"details_title": "ธุรกรรม",
"details_to": "เอ้าพุท",
"pending": "รอดำเนินการ",

View File

@ -159,7 +159,7 @@
"cpfp_create": "Oluştur",
"details_copy": "Kopya",
"details_from": "Girdi",
"details_show_in_block_explorer": "Blok gezgininde göster",
"details_view_in_browser": "Blok gezgininde göster",
"details_title": "İşlem",
"details_to": ıktı",
"pending": "Beklemede",

View File

@ -210,7 +210,7 @@
"details_copy_amount": "Копіювати Суму",
"details_copy_note": "Копіювати Нотатку",
"details_from": "Від",
"details_show_in_block_explorer": "Show in block explorer",
"details_view_in_browser": "Show in block explorer",
"details_title": "Деталі транзакції",
"details_to": "Кому",
"pending": "Очікування",

View File

@ -283,7 +283,7 @@
"details_outputs": "Các đầu ra",
"date": "Ngày",
"details_received": "Đã nhận",
"details_show_in_block_explorer": "Xem trong Block Explorer",
"details_view_in_browser": "Xem trong Block Explorer",
"details_title": "Giao dịch",
"details_to": "Đầu ra",
"enable_offline_signing": "Ví này không được sử dụng cùng với một bản ký ngoại tuyến. Bạn có muốn kích hoạt nó ngay bây giờ không?",

View File

@ -154,7 +154,7 @@
"cpfp_create": "Skep",
"details_copy": "Kopieer",
"details_from": "Inset",
"details_show_in_block_explorer": "Wys in blok verkenner",
"details_view_in_browser": "Wys in blok verkenner",
"details_title": "Transaksie",
"details_to": "Resultaat",
"list_title": "transaksies",

View File

@ -93,7 +93,7 @@
"cpfp_create": "Yakha",
"details_copy": "Ikopi",
"details_from": "Negalelo",
"details_show_in_block_explorer": "Bonisa ibhloko umhloi",
"details_view_in_browser": "Bonisa ibhloko umhloi",
"details_title": "Ngeniswa",
"details_to": "Mveliso",
"list_title": "ngeniswa",

View File

@ -241,7 +241,7 @@
"details_inputs": "输入",
"details_outputs": "输出",
"details_received": "已收到",
"details_show_in_block_explorer": "在区块浏览器查看",
"details_view_in_browser": "在区块浏览器查看",
"details_title": "转账",
"details_to": "输出",
"enable_offline_signing": "此钱包未与线下签名结合使用。您想立即启用它吗?",

View File

@ -237,7 +237,7 @@
"details_inputs": "輸入",
"details_outputs": "輸出",
"details_received": "已收到",
"details_show_in_block_explorer": "區塊瀏覽器展示",
"details_view_in_browser": "區塊瀏覽器展示",
"details_title": "轉賬",
"details_to": "輸出",
"enable_offline_signing": "此錢包未與線下簽名結合使用。您想立即啟用它嗎?",

View File

@ -7,7 +7,7 @@ import navigationStyle, { CloseButtonPosition } from '../components/navigationSt
import { useTheme } from '../components/themes';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
import loc from '../loc';
import LNDViewAdditionalInvoiceInformation from '../screen/lnd/lndViewAdditionalInvoiceInformation';
import LNDViewAdditionalInvoiceInformation from '../screen/lnd/LNDViewAdditionalInvoiceInformation';
import LNDViewAdditionalInvoicePreImage from '../screen/lnd/lndViewAdditionalInvoicePreImage';
import LNDViewInvoice from '../screen/lnd/lndViewInvoice';
import LnurlAuth from '../screen/lnd/lnurlAuth';
@ -321,6 +321,7 @@ const DetailViewStackScreensStack = () => {
name="ElectrumSettings"
component={ElectrumSettingsComponent}
options={navigationStyle({ title: loc.settings.electrum_settings_server })(theme)}
initialParams={{ server: undefined }}
/>
<DetailViewStack.Screen
name="EncryptStorage"

View File

@ -1,4 +1,5 @@
import { LightningTransaction } from '../class/wallets/types';
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
import { SendDetailsParams } from './SendDetailsStackParamList';
export type DetailViewStackParamList = {
@ -53,7 +54,7 @@ export type DetailViewStackParamList = {
NetworkSettings: undefined;
About: undefined;
DefaultView: undefined;
ElectrumSettings: undefined;
ElectrumSettings: { server?: ElectrumServerItem };
SettingsBlockExplorer: undefined;
EncryptStorage: undefined;
Language: undefined;

View File

@ -5,7 +5,7 @@ import { LazyLoadingIndicator } from './LazyLoadingIndicator';
const LNDCreateInvoice = lazy(() => import('../screen/lnd/lndCreateInvoice'));
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
const LNDViewInvoice = lazy(() => import('../screen/lnd/lndViewInvoice'));
const LNDViewAdditionalInvoiceInformation = lazy(() => import('../screen/lnd/lndViewAdditionalInvoiceInformation'));
const LNDViewAdditionalInvoiceInformation = lazy(() => import('../screen/lnd/LNDViewAdditionalInvoiceInformation'));
const LNDViewAdditionalInvoicePreImage = lazy(() => import('../screen/lnd/lndViewAdditionalInvoicePreImage'));
export const LNDCreateInvoiceComponent = () => (

View File

@ -11,7 +11,7 @@ const Licensing = lazy(() => import('../screen/settings/Licensing'));
const NetworkSettings = lazy(() => import('../screen/settings/NetworkSettings'));
const About = lazy(() => import('../screen/settings/About'));
const DefaultView = lazy(() => import('../screen/settings/DefaultView'));
const ElectrumSettings = lazy(() => import('../screen/settings/electrumSettings'));
const ElectrumSettings = lazy(() => import('../screen/settings/ElectrumSettings'));
const EncryptStorage = lazy(() => import('../screen/settings/EncryptStorage'));
const LightningSettings = lazy(() => import('../screen/settings/LightningSettings'));
const NotificationSettings = lazy(() => import('../screen/settings/NotificationSettings'));

View File

@ -9,7 +9,7 @@ export type PaymentCodeStackParamList = {
amount: number;
amountSats: number;
unit: BitcoinUnit;
noRbf: boolean;
isTransactionReplaceable: boolean;
launchedBy: string;
isEditable: boolean;
uri: string /* payjoin uri */;

View File

@ -17,6 +17,7 @@ import {
} from './LazyLoadSendDetailsStack';
import { SendDetailsStackParamList } from './SendDetailsStackParamList';
import HeaderRightButton from '../components/HeaderRightButton';
import { BitcoinUnit } from '../models/bitcoinUnits';
const Stack = createNativeStackNavigator<SendDetailsStackParamList>();
@ -37,7 +38,7 @@ const SendDetailsStack = () => {
statusBarStyle: 'light',
closeButtonPosition: CloseButtonPosition.Left,
})(theme)}
initialParams={{ isEditable: true }} // Correctly typed now
initialParams={{ isEditable: true, feeUnit: BitcoinUnit.BTC, amountUnit: BitcoinUnit.BTC }} // Correctly typed now
/>
<Stack.Screen
name="Confirm"

View File

@ -3,7 +3,12 @@ import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../clas
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
export type SendDetailsParams = {
memo?: string;
transactionMemo?: string;
isTransactionReplaceable?: boolean;
payjoinUrl?: string;
feeUnit?: BitcoinUnit;
frozenBalance?: number;
amountUnit?: BitcoinUnit;
address?: string;
amount?: number;
amountSats?: number;
@ -11,6 +16,7 @@ export type SendDetailsParams = {
noRbf?: boolean;
walletID: string;
launchedBy?: string;
utxos?: CreateTransactionUtxo[] | null;
isEditable?: boolean;
uri?: string;
addRecipientParams?: {
@ -74,7 +80,6 @@ export type SendDetailsStackParamList = {
};
CoinControl: {
walletID: string;
onUTXOChoose: (u: CreateTransactionUtxo[]) => void;
};
PaymentCodeList: {
walletID: string;

85
package-lock.json generated
View File

@ -17,11 +17,11 @@
"@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#839f2966cee77c0ad99d09609dadb61a338e7f54",
"@ngraveio/bc-ur": "1.1.13",
"@noble/secp256k1": "1.6.3",
"@react-native-async-storage/async-storage": "1.24.0",
"@react-native-clipboard/clipboard": "1.14.2",
"@react-native-async-storage/async-storage": "2.0.0",
"@react-native-clipboard/clipboard": "1.14.3",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#a33379d",
"@react-native/gradle-plugin": "^0.75.4",
"@react-native/gradle-plugin": "0.75.4",
"@react-native/metro-config": "0.75.4",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
@ -53,7 +53,7 @@
"electrum-mnemonic": "2.0.0",
"events": "3.3.0",
"junderw-crc32c": "1.2.0",
"lottie-react-native": "6.7.2",
"lottie-react-native": "7.0.0",
"path-browserify": "1.0.1",
"payjoin-client": "1.0.1",
"process": "0.11.10",
@ -66,7 +66,7 @@
"react-native-camera-kit": "13.0.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "11.1.0",
"react-native-device-info": "13.2.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-fs": "2.20.0",
@ -75,10 +75,10 @@
"react-native-haptic-feedback": "2.3.3",
"react-native-image-picker": "7.1.2",
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e",
"react-native-keychain": "8.2.0",
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.2.1",
"react-native-permissions": "4.1.5",
"react-native-permissions": "5.0.2",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.2",
@ -86,11 +86,11 @@
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.16.0",
"react-native-safe-area-context": "4.11.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f",
"react-native-screens": "3.34.0",
"react-native-screens": "3.35.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
"react-native-share": "10.2.1",
"react-native-share": "11.0.4",
"react-native-svg": "15.8.0",
"react-native-tcp-socket": "6.2.0",
"react-native-vector-icons": "10.2.0",
@ -4507,21 +4507,21 @@
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
"integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.0.0.tgz",
"integrity": "sha512-af6H9JjfL6G/PktBfUivvexoiFKQTJGQCtSWxMdivLzNIY94mu9DdiY0JqCSg/LyPCLGKhHPUlRQhNvpu3/KVA==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.60 <1.0"
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native-clipboard/clipboard": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.14.2.tgz",
"integrity": "sha512-Mb58f3neB6sM9oOtKYVGLvN8KVByea67OA9ekJ0c9FwdH24INu8RJoA7/fq+PRk+7oxbeamAcEoQPRv0uwbbMw==",
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.14.3.tgz",
"integrity": "sha512-EVWxJfCSyBN2SH5b3JrA/w1qlYu3vihQOfdD7fs/BYp63xL6qy93CvbFDHzF8ooFpGM6f67hkAN+gxl1RfOKuw==",
"license": "MIT",
"workspaces": [
"example"
@ -17929,9 +17929,9 @@
}
},
"node_modules/lottie-react-native": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-6.7.2.tgz",
"integrity": "sha512-MZVx6N1EeO/EaSx8T44mJ0aHc5Mqee+xIfWwszni0oz8U2wlHdaWGjES44dHxaxgAp/0dRaFt3PkpZ6egTzcBg==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-7.0.0.tgz",
"integrity": "sha512-RnwacxdB1MKDS/WSX8XFyXw5nxEKF+aLYRzbkQBQY0pZTRF2XYg8zd25D1su1M0TEP0sgWutwN5rweSeCsf8qQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@dotlottie/react-player": "^1.6.1",
@ -20593,9 +20593,9 @@
}
},
"node_modules/react-native-device-info": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-11.1.0.tgz",
"integrity": "sha512-hzXJSObJdezEz0hF7MAJ3tGeoesuQWenXXt9mrQR9Mjb8kXpZ09rqSsZ/quNpJdZpQ3rYiFa3/0GFG5KNn9PBg==",
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-13.2.0.tgz",
"integrity": "sha512-VpTxHZsEZ7kes2alaZkB31278KuSPXfTZ4TmCCN77+bYxNnaHUDiBiQ1TSoKAOp51b7gZ/7EvM4McfgHofcTBQ==",
"license": "MIT",
"peerDependencies": {
"react-native": "*"
@ -20713,10 +20713,17 @@
}
},
"node_modules/react-native-keychain": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-8.2.0.tgz",
"integrity": "sha512-SkRtd9McIl1Ss2XSWNLorG+KMEbgeVqX+gV+t3u1EAAqT8q2/OpRmRbxpneT2vnb/dMhiU7g6K/pf3nxLUXRvA==",
"license": "MIT"
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-9.1.0.tgz",
"integrity": "sha512-txBVnhO/AVg+j6lrKe9cL/HPpb5BNHYYBRg/6lzbxKe8AsNqp639wq0nN8/IdkUCPjSezN8Pgp142HNomgqk4Q==",
"license": "MIT",
"workspaces": [
"KeychainExample",
"website"
],
"engines": {
"node": ">=18"
}
},
"node_modules/react-native-linear-gradient": {
"version": "2.8.3",
@ -20745,9 +20752,9 @@
}
},
"node_modules/react-native-permissions": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-4.1.5.tgz",
"integrity": "sha512-r6VMRacASmtRHS+GZ+5HQCp9p9kiE+UU9magHOZCXZLTJitdTuVHWZRrb4v4oqZGU+zAp3mZhTQftuMMv+WLUg==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.0.2.tgz",
"integrity": "sha512-CsL0jSGTJZET/Mq5l14ZcemQtPSj1Ru6LLO/J/T5RUQKwu9PavJqun7LX2te6yQ0tUoi+lA/etRHpggRVW+o0A==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.1.0",
@ -20869,9 +20876,9 @@
}
},
"node_modules/react-native-safe-area-context": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.11.1.tgz",
"integrity": "sha512-urF1m4nFiZFaWjsv2zj8J/hKvo4b2tJW+6CYU1mY4lKv1RwhG2eV8J/EHKuNlLhATZx3+6j7szrpHrQW2ZcAaQ==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@ -20888,9 +20895,9 @@
}
},
"node_modules/react-native-screens": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.34.0.tgz",
"integrity": "sha512-8ri3Pd9QcpfXnVckOe/Lnto+BXmSPHV/Q0RB0XW0gDKsCv5wi5k7ez7g1SzgiYHl29MSdiqgjH30zUyOOowOaw==",
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.35.0.tgz",
"integrity": "sha512-rmkqb/M/SQIrXwygk6pXcOhgHltYAhidf1WceO7ujAxkr6XtwmgFyd1HIztsrJa568GrAuwPdQ11I7TpVk+XsA==",
"license": "MIT",
"dependencies": {
"react-freeze": "^1.0.0",
@ -20908,9 +20915,9 @@
"license": "ISC"
},
"node_modules/react-native-share": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.2.1.tgz",
"integrity": "sha512-Z2LWGYWH7raM4H6Oauttv1tEhaB43XSWJAN8iS6oaSG9CnyrUBeYFF4QpU1AH5RgNeylXQdN8CtbizCHHt6coQ==",
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-11.0.4.tgz",
"integrity": "sha512-i91n1Pcdaxxr39uxR5KduXjqi5FSEXuEO6rmeHl8OPs5rqo4No36qJXUU6du4TZUI6tFSENdxnzZMh3OsMF+ug==",
"license": "MIT",
"engines": {
"node": ">=16"

View File

@ -81,11 +81,11 @@
"@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#839f2966cee77c0ad99d09609dadb61a338e7f54",
"@ngraveio/bc-ur": "1.1.13",
"@noble/secp256k1": "1.6.3",
"@react-native-async-storage/async-storage": "1.24.0",
"@react-native-clipboard/clipboard": "1.14.2",
"@react-native-async-storage/async-storage": "2.0.0",
"@react-native-clipboard/clipboard": "1.14.3",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#a33379d",
"@react-native/gradle-plugin": "^0.75.4",
"@react-native/gradle-plugin": "0.75.4",
"@react-native/metro-config": "0.75.4",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
@ -117,7 +117,7 @@
"electrum-mnemonic": "2.0.0",
"events": "3.3.0",
"junderw-crc32c": "1.2.0",
"lottie-react-native": "6.7.2",
"lottie-react-native": "7.0.0",
"path-browserify": "1.0.1",
"payjoin-client": "1.0.1",
"process": "0.11.10",
@ -130,7 +130,7 @@
"react-native-camera-kit": "13.0.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "11.1.0",
"react-native-device-info": "13.2.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-fs": "2.20.0",
@ -139,10 +139,10 @@
"react-native-haptic-feedback": "2.3.3",
"react-native-image-picker": "7.1.2",
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e",
"react-native-keychain": "8.2.0",
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.2.1",
"react-native-permissions": "4.1.5",
"react-native-permissions": "5.0.2",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.2",
@ -150,11 +150,11 @@
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.16.0",
"react-native-safe-area-context": "4.11.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f",
"react-native-screens": "3.34.0",
"react-native-screens": "3.35.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
"react-native-share": "10.2.1",
"react-native-share": "11.0.4",
"react-native-svg": "15.8.0",
"react-native-tcp-socket": "6.2.0",
"react-native-vector-icons": "10.2.0",

View File

@ -0,0 +1,110 @@
import React, { useEffect, useState } from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import { Share, StyleSheet, View } from 'react-native';
import { BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import QRCodeComponent from '../../components/QRCodeComponent';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { LightningCustodianWallet } from '../../class';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
type RouteParams = {
params: {
walletID: string;
};
};
const LNDViewAdditionalInvoiceInformation: React.FC = () => {
const { walletID } = useRoute<RouteProp<RouteParams>>().params;
const { wallets } = useStorage();
const wallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet;
const [walletInfo, setWalletInfo] = useState<{ uris?: string[] } | undefined>();
const { colors } = useTheme();
const { goBack } = useExtendedNavigation();
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
});
useEffect(() => {
if (wallet) {
wallet
.fetchInfo()
.then(() => {
const info = wallet.info_raw;
// @ts-ignore: idk
if (info?.uris?.[0]) {
// @ts-ignore: idk
setWalletInfo(info);
} else {
presentAlert({ message: loc.errors.network });
goBack();
}
})
.catch((error: Error) => {
console.error(error);
presentAlert({ title: loc.errors.network, message: error.message });
goBack();
});
}
}, [wallet, goBack]);
return (
<SafeArea style={[styles.loading, stylesHook.root]}>
{!walletInfo ? (
<BlueLoading />
) : (
<View style={styles.wrapper}>
<View style={styles.qrcode}>
<QRCodeComponent value={walletInfo.uris?.[0] ?? ''} size={300} />
</View>
<BlueSpacing20 />
<BlueText>{loc.lndViewInvoice.open_direct_channel}</BlueText>
<CopyTextToClipboard text={walletInfo.uris?.[0] ?? ''} />
<View style={styles.share}>
<Button
icon={{
name: 'share-alternative',
type: 'entypo',
color: colors.buttonTextColor,
}}
onPress={async () => {
Share.share({
message: walletInfo.uris?.[0] ?? '',
});
}}
title={loc.receive.details_share}
/>
</View>
</View>
)}
</SafeArea>
);
};
const styles = StyleSheet.create({
loading: {
justifyContent: 'space-between',
alignItems: 'center',
},
wrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
qrcode: {
justifyContent: 'center',
alignItems: 'center',
},
share: {
marginBottom: 25,
},
});
export default LNDViewAdditionalInvoiceInformation;

View File

@ -1,103 +0,0 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { Share, StyleSheet, View } from 'react-native';
import { BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import QRCodeComponent from '../../components/QRCodeComponent';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
const LNDViewAdditionalInvoiceInformation = () => {
const { walletID } = useRoute().params;
const { wallets } = useStorage();
const wallet = wallets.find(w => w.getID() === walletID);
const [walletInfo, setWalletInfo] = useState();
const { colors } = useTheme();
const { goBack } = useNavigation();
const stylesHook = StyleSheet.create({
loading: {
backgroundColor: colors.elevated,
},
root: {
backgroundColor: colors.elevated,
},
});
useEffect(() => {
if (wallet) {
wallet
.fetchInfo()
.then(_ => {
setWalletInfo(wallet.info_raw);
})
.catch(error => {
console.log(error);
presentAlert({ message: loc.errors.network });
goBack();
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]);
if (walletInfo === undefined) {
return (
<SafeArea style={[styles.loading, stylesHook.loading]}>
<BlueLoading />
</SafeArea>
);
}
return (
<SafeArea style={stylesHook.root}>
<View style={styles.wrapper}>
<View style={styles.qrcode}>
<QRCodeComponent value={walletInfo.uris[0]} size={300} />
</View>
<BlueSpacing20 />
<BlueText>{loc.lndViewInvoice.open_direct_channel}</BlueText>
<CopyTextToClipboard text={walletInfo.uris[0]} />
<View style={styles.share}>
<Button
icon={{
name: 'share-alternative',
type: 'entypo',
color: colors.buttonTextColor,
}}
onPress={async () => {
Share.share({
message: walletInfo.uris[0],
});
}}
title={loc.receive.details_share}
/>
</View>
</View>
</SafeArea>
);
};
const styles = StyleSheet.create({
loading: {
justifyContent: 'space-between',
alignItems: 'center',
},
wrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
qrcode: {
justifyContent: 'center',
alignItems: 'center',
},
share: {
marginBottom: 25,
},
});
export default LNDViewAdditionalInvoiceInformation;

View File

@ -165,7 +165,7 @@ const ReceiveDetails = () => {
}, [showConfirmedBalance]);
const toolTipActions = useMemo(() => {
const action = CommonToolTipActions.PaymentCode;
const action = CommonToolTipActions.PaymentsCode;
action.menuState = wallet?.isBIP47Enabled();
return [action];
}, [wallet]);

View File

@ -160,7 +160,7 @@ const Broadcast: React.FC = () => {
<BlueSpacing20 />
</BlueCard>
)}
{BROADCAST_RESULT.success === broadcastResult && tx && <SuccessScreen tx={tx} url={`${selectedBlockExplorer}/tx/${tx}`} />}
{BROADCAST_RESULT.success === broadcastResult && tx && <SuccessScreen tx={tx} url={`${selectedBlockExplorer.url}/tx/${tx}`} />}
</View>
</SafeArea>
);

View File

@ -51,13 +51,13 @@ import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackPara
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { ContactList } from '../../class/contact-list';
import { useStorage } from '../../hooks/context/useStorage';
import { Action } from '../../components/types';
import SelectFeeModal from '../../components/SelectFeeModal';
import { useKeyboard } from '../../hooks/useKeyboard';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import ActionSheet from '../ActionSheet';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { Action } from '../../components/types';
interface IPaymentDestinations {
address: string; // btc address or payment code
@ -81,6 +81,13 @@ const SendDetails = () => {
const setParams = navigation.setParams;
const route = useRoute<RouteProps>();
const name = route.name;
const feeUnit = route.params?.feeUnit ?? BitcoinUnit.BTC;
const amountUnit = route.params?.amountUnit ?? BitcoinUnit.BTC;
const frozenBalance = route.params?.frozenBalance ?? 0;
const transactionMemo = route.params?.transactionMemo;
const utxos = route.params?.utxos;
const payjoinUrl = route.params?.payjoinUrl;
const isTransactionReplaceable = route.params?.isTransactionReplaceable;
const routeParams = route.params;
const scrollView = useRef<FlatList<any>>(null);
const scrollIndex = useRef(0);
@ -92,26 +99,18 @@ const SendDetails = () => {
const [isLoading, setIsLoading] = useState(false);
const [wallet, setWallet] = useState<TWallet | null>(null);
const feeModalRef = useRef<BottomModalHandle>(null);
const [walletSelectionOrCoinsSelectedHidden, setWalletSelectionOrCoinsSelectedHidden] = useState(false);
const [isAmountToolbarVisibleForAndroid, setIsAmountToolbarVisibleForAndroid] = useState(false);
const [isTransactionReplaceable, setIsTransactionReplaceable] = useState<boolean | undefined>(false);
const { isVisible } = useKeyboard();
const [addresses, setAddresses] = useState<IPaymentDestinations[]>([]);
const [units, setUnits] = useState<BitcoinUnit[]>([]);
const [transactionMemo, setTransactionMemo] = useState<string>('');
const [networkTransactionFees, setNetworkTransactionFees] = useState(new NetworkTransactionFee(3, 2, 1));
const [networkTransactionFeesIsLoading, setNetworkTransactionFeesIsLoading] = useState(false);
const [customFee, setCustomFee] = useState<string | null>(null);
const [feePrecalc, setFeePrecalc] = useState<IFee>({ current: null, slowFee: null, mediumFee: null, fastestFee: null });
const [feeUnit, setFeeUnit] = useState<BitcoinUnit>();
const [amountUnit, setAmountUnit] = useState<BitcoinUnit>();
const [utxo, setUtxo] = useState<CreateTransactionUtxo[] | null>(null);
const [frozenBalance, setFrozenBlance] = useState<number>(0);
const [payjoinUrl, setPayjoinUrl] = useState<string | null>(null);
const [changeAddress, setChangeAddress] = useState<string | null>(null);
const [dumb, setDumb] = useState(false);
const { isEditable } = routeParams;
// if utxo is limited we use it to calculate available balance
const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : (wallet?.getBalance() ?? 0);
const balance: number = utxos ? utxos.reduce((prev, curr) => prev + curr.value, 0) : (wallet?.getBalance() ?? 0);
const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true);
// if cutomFee is not set, we need to choose highest possible fee for wallet balance
@ -138,17 +137,6 @@ const SendDetails = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [colors, wallet, isTransactionReplaceable, balance, addresses, isEditable, isLoading]);
useKeyboard({
onKeyboardDidShow: () => {
setWalletSelectionOrCoinsSelectedHidden(true);
setIsAmountToolbarVisibleForAndroid(true);
},
onKeyboardDidHide: () => {
setWalletSelectionOrCoinsSelectedHidden(false);
setIsAmountToolbarVisibleForAndroid(false);
},
});
useEffect(() => {
// decode route params
const currentAddress = addresses[scrollIndex.current];
@ -176,10 +164,9 @@ const SendDetails = () => {
});
if (memo?.trim().length > 0) {
setTransactionMemo(memo);
setParams({ transactionMemo: memo });
}
setAmountUnit(BitcoinUnit.BTC);
setPayjoinUrl(pjUrl);
setParams({ payjoinUrl: pjUrl, amountUnit: BitcoinUnit.BTC });
} catch (error) {
console.log(error);
presentAlert({ title: loc.errors.error, message: loc.send.details_error_decode });
@ -196,9 +183,6 @@ const SendDetails = () => {
return [...value, { address: routeParams.address, key: String(Math.random()), amount, amountSats }];
}
});
if (routeParams.memo && routeParams.memo?.trim().length > 0) {
setTransactionMemo(routeParams.memo);
}
setUnits(u => {
u[scrollIndex.current] = unit;
return [...u];
@ -238,8 +222,7 @@ const SendDetails = () => {
}
const newWallet = (routeParams.walletID && wallets.find(w => w.getID() === routeParams.walletID)) || suitable[0];
setWallet(newWallet);
setFeeUnit(newWallet.getPreferredBalanceUnit());
setAmountUnit(newWallet.preferredBalanceUnit); // default for whole screen
setParams({ feeUnit: newWallet.getPreferredBalanceUnit(), amountUnit: newWallet.getPreferredBalanceUnit() });
// we are ready!
setIsLoading(false);
@ -276,9 +259,11 @@ const SendDetails = () => {
setSelectedWalletID(wallet.getID());
// reset other values
setUtxo(null);
setChangeAddress(null);
setIsTransactionReplaceable(wallet.type === HDSegwitBech32Wallet.type && !routeParams.noRbf ? true : undefined);
setParams({
utxos: null,
isTransactionReplaceable: wallet.type === HDSegwitBech32Wallet.type && !routeParams.isTransactionReplaceable ? true : undefined,
});
// update wallet UTXO
wallet
.fetchUtxo()
@ -294,9 +279,9 @@ const SendDetails = () => {
if (!wallet) return; // wait for it
const fees = networkTransactionFees;
const requestedSatPerByte = Number(feeRate);
const lutxo = utxo || wallet.getUtxo();
const lutxo = utxos || wallet.getUtxo();
let frozen = 0;
if (!utxo) {
if (!utxos) {
// if utxo is not limited search for frozen outputs and calc it's balance
frozen = wallet
.getUtxo(true)
@ -369,8 +354,8 @@ const SendDetails = () => {
}
setFeePrecalc(newFeePrecalc);
setFrozenBlance(frozen);
}, [wallet, networkTransactionFees, utxo, addresses, feeRate, dumb]); // eslint-disable-line react-hooks/exhaustive-deps
setParams({ frozenBalance: frozen });
}, [wallet, networkTransactionFees, utxos, addresses, feeRate, dumb]); // eslint-disable-line react-hooks/exhaustive-deps
// we need to re-calculate fees if user opens-closes coin control
useFocusEffect(
@ -470,9 +455,7 @@ const SendDetails = () => {
u[scrollIndex.current] = BitcoinUnit.BTC; // also resetting current unit to BTC
return [...u];
});
setTransactionMemo(options.label || ''); // there used to be `options.message` here as well. bug?
setAmountUnit(BitcoinUnit.BTC);
setPayjoinUrl(options.pj || '');
setParams({ transactionMemo: options.label || '', amountUnit: BitcoinUnit.BTC, payjoinUrl: options.pj || '' }); // there used to be `options.message` here as well. bug?
// RN Bug: contentOffset gets reset to 0 when state changes. Remove code once this bug is resolved.
setTimeout(() => scrollView.current?.scrollToIndex({ index: currentIndex, animated: false }), 50);
}
@ -565,7 +548,7 @@ const SendDetails = () => {
const change = await getChangeAddressAsync();
assert(change, 'Could not get change address');
const requestedSatPerByte = Number(feeRate);
const lutxo: CreateTransactionUtxo[] = utxo || (wallet?.getUtxo() ?? []);
const lutxo: CreateTransactionUtxo[] = utxos || (wallet?.getUtxo() ?? []);
console.log({ requestedSatPerByte, lutxo: lutxo.length });
const targets: CreateTransactionTarget[] = [];
@ -669,6 +652,10 @@ const SendDetails = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [routeParams.walletID]);
const setTransactionMemo = (memo: string) => {
setParams({ transactionMemo: memo });
};
/**
* same as `importTransaction`, but opens camera instead.
*
@ -898,7 +885,6 @@ const SendDetails = () => {
if (!wallet) return;
navigation.navigate('CoinControl', {
walletID: wallet?.getID(),
onUTXOChoose: (u: CreateTransactionUtxo[]) => setUtxo(u),
});
};
@ -956,23 +942,23 @@ const SendDetails = () => {
handleAddRecipient();
} else if (id === CommonToolTipActions.RemoveRecipient.id) {
handleRemoveRecipient();
} else if (id === SendDetails.actionKeys.SignPSBT) {
} else if (id === CommonToolTipActions.SignPSBT.id) {
handlePsbtSign();
} else if (id === SendDetails.actionKeys.SendMax) {
} else if (id === CommonToolTipActions.SendMax.id) {
onUseAllPressed();
} else if (id === SendDetails.actionKeys.AllowRBF) {
} else if (id === CommonToolTipActions.AllowRBF.id) {
onReplaceableFeeSwitchValueChanged(!isTransactionReplaceable);
} else if (id === SendDetails.actionKeys.ImportTransaction) {
} else if (id === CommonToolTipActions.ImportTransaction.id) {
importTransaction();
} else if (id === SendDetails.actionKeys.ImportTransactionQR) {
} else if (id === CommonToolTipActions.ImportTransactionQR.id) {
importQrTransaction();
} else if (id === SendDetails.actionKeys.ImportTransactionMultsig) {
} else if (id === CommonToolTipActions.ImportTransactionMultsig.id) {
importTransactionMultisig();
} else if (id === SendDetails.actionKeys.CoSignTransaction) {
} else if (id === CommonToolTipActions.CoSignTransaction.id) {
importTransactionMultisigScanQr();
} else if (id === SendDetails.actionKeys.CoinControl) {
} else if (id === CommonToolTipActions.CoinControl.id) {
handleCoinControl();
} else if (id === SendDetails.actionKeys.InsertContact) {
} else if (id === CommonToolTipActions.InsertContact.id) {
handleInsertContact();
} else if (id === CommonToolTipActions.RemoveAllRecipients.id) {
handleRemoveAllRecipients();
@ -980,66 +966,73 @@ const SendDetails = () => {
};
const headerRightActions = () => {
const actions: Action[] & Action[][] = [];
if (isEditable) {
if (wallet?.allowBIP47() && wallet?.isBIP47Enabled()) {
actions.push([
{ id: SendDetails.actionKeys.InsertContact, text: loc.send.details_insert_contact, icon: SendDetails.actionIcons.InsertContact },
]);
}
if (!wallet) return [];
if (Number(wallet?.getBalance()) > 0) {
const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX);
const walletActions: Action[][] = [];
actions.push([{ id: SendDetails.actionKeys.SendMax, text: loc.send.details_adv_full, disabled: balance === 0 || isSendMaxUsed }]);
}
if (wallet?.type === HDSegwitBech32Wallet.type && isTransactionReplaceable !== undefined) {
actions.push([{ id: SendDetails.actionKeys.AllowRBF, text: loc.send.details_adv_fee_bump, menuState: !!isTransactionReplaceable }]);
}
const transactionActions = [];
if (wallet?.type === WatchOnlyWallet.type && wallet.isHd()) {
transactionActions.push(
{
id: SendDetails.actionKeys.ImportTransaction,
text: loc.send.details_adv_import,
icon: SendDetails.actionIcons.ImportTransaction,
},
{
id: SendDetails.actionKeys.ImportTransactionQR,
text: loc.send.details_adv_import_qr,
icon: SendDetails.actionIcons.ImportTransactionQR,
},
);
}
if (wallet?.type === MultisigHDWallet.type) {
transactionActions.push({
id: SendDetails.actionKeys.ImportTransactionMultsig,
text: loc.send.details_adv_import,
icon: SendDetails.actionIcons.ImportTransactionMultsig,
});
}
if (wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0) {
transactionActions.push({
id: SendDetails.actionKeys.CoSignTransaction,
text: loc.multisig.co_sign_transaction,
icon: SendDetails.actionIcons.SignPSBT,
});
}
if ((wallet as MultisigHDWallet)?.allowCosignPsbt()) {
transactionActions.push({ id: SendDetails.actionKeys.SignPSBT, text: loc.send.psbt_sign, icon: SendDetails.actionIcons.SignPSBT });
}
actions.push(transactionActions);
const recipientActions: Action[] = [
CommonToolTipActions.AddRecipient,
CommonToolTipActions.RemoveRecipient,
{
...CommonToolTipActions.RemoveAllRecipients,
hidden: !(addresses.length > 1),
},
];
walletActions.push(recipientActions);
const recipientActions: Action[] = [CommonToolTipActions.AddRecipient, CommonToolTipActions.RemoveRecipient];
if (addresses.length > 1) {
recipientActions.push(CommonToolTipActions.RemoveAllRecipients);
}
actions.push(recipientActions);
}
const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX);
const sendMaxAction: Action[] = [
{
...CommonToolTipActions.SendMax,
disabled: wallet.getBalance() === 0 || isSendMaxUsed,
hidden: !isEditable || !(Number(wallet.getBalance()) > 0),
},
];
walletActions.push(sendMaxAction);
actions.push({ id: SendDetails.actionKeys.CoinControl, text: loc.cc.header, icon: SendDetails.actionIcons.CoinControl });
const rbfAction: Action[] = [
{
...CommonToolTipActions.AllowRBF,
menuState: isTransactionReplaceable,
hidden: !(wallet.type === HDSegwitBech32Wallet.type && isTransactionReplaceable !== undefined),
},
];
walletActions.push(rbfAction);
return actions;
const transactionActions: Action[] = [
{
...CommonToolTipActions.ImportTransaction,
hidden: !(wallet.type === WatchOnlyWallet.type && wallet.isHd()),
},
{
...CommonToolTipActions.ImportTransactionQR,
hidden: !(wallet.type === WatchOnlyWallet.type && wallet.isHd()),
},
{
...CommonToolTipActions.ImportTransactionMultsig,
hidden: !(wallet.type === MultisigHDWallet.type),
},
{
...CommonToolTipActions.CoSignTransaction,
hidden: !(wallet.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0),
},
{
...CommonToolTipActions.SignPSBT,
hidden: !(wallet as MultisigHDWallet)?.allowCosignPsbt(),
},
];
walletActions.push(transactionActions);
const specificWalletActions: Action[] = [
{
...CommonToolTipActions.InsertContact,
hidden: !(isEditable && wallet.allowBIP47() && wallet.isBIP47Enabled()),
},
CommonToolTipActions.CoinControl,
];
walletActions.push(specificWalletActions);
return walletActions;
};
const setHeaderRightOptions = () => {
@ -1050,7 +1043,7 @@ const SendDetails = () => {
};
const onReplaceableFeeSwitchValueChanged = (value: boolean) => {
setIsTransactionReplaceable(value);
setParams({ isTransactionReplaceable: value });
};
const handleRecipientsScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
@ -1141,16 +1134,16 @@ const SendDetails = () => {
};
const renderWalletSelectionOrCoinsSelected = () => {
if (walletSelectionOrCoinsSelectedHidden) return null;
if (utxo !== null) {
if (isVisible) return null;
if (utxos !== null) {
return (
<View style={styles.select}>
<CoinsSelected
number={utxo.length}
number={utxos?.length || 0}
onContainerPress={handleCoinControl}
onClose={() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setUtxo(null);
setParams({ utxos: null });
}}
/>
</View>
@ -1262,9 +1255,11 @@ const SendDetails = () => {
addrs[index] = item;
return [...addrs];
});
setTransactionMemo(memo || transactionMemo);
if (memo) {
setParams({ transactionMemo: memo });
}
setIsLoading(false);
setPayjoinUrl(pjUrl);
setParams({ payjoinUrl: pjUrl });
}}
onBarScanned={processAddressData}
address={item.address}
@ -1354,7 +1349,7 @@ const SendDetails = () => {
<DismissKeyboardInputAccessory />
{Platform.select({
ios: <InputAccessoryAllFunds canUseAll={balance > 0} onUseAllPressed={onUseAllPressed} balance={String(allBalance)} />,
android: isAmountToolbarVisibleForAndroid && (
android: isVisible && (
<InputAccessoryAllFunds canUseAll={balance > 0} onUseAllPressed={onUseAllPressed} balance={String(allBalance)} />
),
})}
@ -1366,29 +1361,6 @@ const SendDetails = () => {
export default SendDetails;
SendDetails.actionKeys = {
InsertContact: 'InsertContact',
SignPSBT: 'SignPSBT',
SendMax: 'SendMax',
AllowRBF: 'AllowRBF',
ImportTransaction: 'ImportTransaction',
ImportTransactionMultsig: 'ImportTransactionMultisig',
ImportTransactionQR: 'ImportTransactionQR',
CoinControl: 'CoinControl',
CoSignTransaction: 'CoSignTransaction',
};
SendDetails.actionIcons = {
InsertContact: { iconValue: 'at.badge.plus' },
SignPSBT: { iconValue: 'signature' },
SendMax: 'SendMax',
AllowRBF: 'AllowRBF',
ImportTransaction: { iconValue: 'square.and.arrow.down' },
ImportTransactionMultsig: { iconValue: 'square.and.arrow.down.on.square' },
ImportTransactionQR: { iconValue: 'qrcode.viewfinder' },
CoinControl: { iconValue: 'switch.2' },
};
const styles = StyleSheet.create({
root: {
flex: 1,

View File

@ -260,7 +260,7 @@ const CoinControl = () => {
const navigation = useExtendedNavigation();
const { width } = useWindowDimensions();
const bottomModalRef = useRef(null);
const { walletID, onUTXOChoose } = useRoute().params;
const { walletID } = useRoute().params;
const { wallets, saveToDisk, sleep } = useStorage();
const wallet = wallets.find(w => w.getID() === walletID);
// sort by height ascending, txid , vout ascending
@ -329,8 +329,13 @@ const CoinControl = () => {
const handleUseCoin = async u => {
setOutput(null);
navigation.pop();
onUTXOChoose(u);
navigation.navigate('SendDetailsRoot', {
screen: 'SendDetails',
params: {
utxos: u,
},
merge: true,
});
};
const handleMassFreeze = () => {
@ -476,7 +481,7 @@ const styles = StyleSheet.create({
padding: {
padding: 16,
},
modalMinHeight: Platform.OS === 'android' ? { minHeight: 490 } : {},
modalMinHeight: Platform.OS === 'android' ? { minHeight: 530 } : {},
empty: {
flex: 1,
justifyContent: 'center',

View File

@ -0,0 +1,536 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { Alert, Keyboard, LayoutAnimation, Platform, ScrollView, StyleSheet, Switch, TextInput, View } from 'react-native';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import {
DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID,
} from '../../components/DoneAndDismissKeyboardInputAccessory';
import DefaultPreference from 'react-native-default-preference';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import { useTheme } from '../../components/themes';
import { RouteProp, useRoute } from '@react-navigation/native';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { Divider } from '@rneui/themed';
import { Header } from '../../components/Header';
import AddressInput from '../../components/AddressInput';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency';
import { Action } from '../../components/types';
import { useStorage } from '../../hooks/context/useStorage';
import ListItem, { PressableWrapper } from '../../components/ListItem';
import HeaderMenuButton from '../../components/HeaderMenuButton';
type RouteProps = RouteProp<DetailViewStackParamList, 'ElectrumSettings'>;
export interface ElectrumServerItem {
host: string;
port?: number;
sslPort?: number;
}
const ElectrumSettings: React.FC = () => {
const { colors } = useTheme();
const { server } = useRoute<RouteProps>().params;
const { setOptions } = useExtendedNavigation();
const [isLoading, setIsLoading] = useState(true);
const [isOfflineMode, setIsOfflineMode] = useState(true);
const [serverHistory, setServerHistory] = useState<ElectrumServerItem[]>([]);
const [config, setConfig] = useState<{ connected?: number; host?: string; port?: string }>({});
const [host, setHost] = useState<string>('');
const [port, setPort] = useState<number | undefined>();
const [sslPort, setSslPort] = useState<number | undefined>(undefined);
const [isAndroidNumericKeyboardFocused, setIsAndroidNumericKeyboardFocused] = useState(false);
const [isAndroidAddressKeyboardVisible, setIsAndroidAddressKeyboardVisible] = useState(false);
const { setIsElectrumDisabled } = useStorage();
const stylesHook = StyleSheet.create({
inputWrap: {
borderColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
containerConnected: {
backgroundColor: colors.feeLabel,
},
containerDisconnected: {
backgroundColor: colors.redBG,
},
textConnected: {
color: colors.feeValue,
},
textDisconnected: {
color: colors.redText,
},
hostname: {
color: colors.foregroundColor,
},
inputText: {
color: colors.foregroundColor,
},
usePort: {
color: colors.foregroundColor,
},
});
useEffect(() => {
let configInterval: NodeJS.Timeout | null = null;
const fetchData = async () => {
const savedHost = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_HOST);
const savedPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_TCP_PORT);
const savedSslPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SSL_PORT);
const serverHistoryStr = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SERVER_HISTORY);
const offlineMode = await BlueElectrum.isDisabled();
const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : [];
setHost(savedHost || '');
setPort(savedPort ? Number(savedPort) : undefined);
setSslPort(savedSslPort ? Number(savedSslPort) : undefined);
setServerHistory(parsedServerHistory);
setIsOfflineMode(offlineMode);
setConfig(await BlueElectrum.getConfig());
configInterval = setInterval(async () => {
setConfig(await BlueElectrum.getConfig());
}, 500);
setIsLoading(false);
};
fetchData();
return () => {
if (configInterval) clearInterval(configInterval);
};
}, []);
useEffect(() => {
if (server) {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
Alert.alert(
loc.formatString(loc.settings.set_electrum_server_as_default, { server: (server as ElectrumServerItem).host }),
'',
[
{
text: loc._.ok,
onPress: () => {
onBarScanned(JSON.stringify(server));
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
}
}, [server]);
const clearHistory = useCallback(async () => {
setIsLoading(true);
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify([]));
setServerHistory([]);
setIsLoading(false);
}, []);
const serverExists = useCallback(
(value: ElectrumServerItem) => {
return serverHistory.some(s => `${s.host}:${s.port}:${s.sslPort}` === `${value.host}:${value.port}:${value.sslPort}`);
},
[serverHistory],
);
const save = useCallback(async () => {
Keyboard.dismiss();
setIsLoading(true);
try {
if (!host && !port && !sslPort) {
await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_HOST);
await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_TCP_PORT);
await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_SSL_PORT);
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_HOST);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_TCP_PORT);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_SSL_PORT);
} else {
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_HOST, host);
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || '');
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || '');
if (!serverExists({ host, port, sslPort })) {
const newServerHistory = [...serverHistory, { host, port, sslPort }];
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(newServerHistory));
setServerHistory(newServerHistory);
}
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, host);
await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || '');
await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || '');
}
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.settings.electrum_saved });
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: (error as Error).message });
}
setIsLoading(false);
}, [host, port, sslPort, serverExists, serverHistory]);
const resetToDefault = useCallback(() => {
Alert.alert(loc.settings.electrum_reset, loc.settings.electrum_reset_to_default, [
{
text: loc._.cancel,
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{
text: loc._.ok,
style: 'destructive',
onPress: async () => {
setHost('');
setPort(undefined);
setSslPort(undefined);
await save();
},
},
]);
}, [save]);
const selectServer = useCallback(
(value: string) => {
const parsedServer = JSON.parse(value) as ElectrumServerItem;
setHost(parsedServer.host);
setPort(parsedServer.port);
setSslPort(parsedServer.sslPort);
save();
},
[save],
);
const clearHistoryAlert = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
Alert.alert(loc.settings.electrum_clear_alert_title, loc.settings.electrum_clear_alert_message, [
{ text: loc.settings.electrum_clear_alert_cancel, onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{ text: loc.settings.electrum_clear_alert_ok, onPress: () => clearHistory() },
]);
}, [clearHistory]);
const onPressMenuItem = useCallback(
(id: string) => {
switch (id) {
case CommonToolTipActions.ResetToDefault.id:
resetToDefault();
break;
case CommonToolTipActions.ClearHistory.id:
clearHistoryAlert();
break;
default:
try {
selectServer(id);
} catch (error) {
console.warn('Unknown menu item selected:', id);
}
break;
}
},
[clearHistoryAlert, resetToDefault, selectServer],
);
const toolTipActions = useMemo(() => {
const actions: Action[] = [CommonToolTipActions.ResetToDefault];
if (serverHistory.length > 0) {
const serverSubactions: Action[] = serverHistory.map(value => ({
id: JSON.stringify(value),
text: `${value.host}`,
subtitle: `${value.port || value.sslPort}`,
menuState: `${host}:${port}:${sslPort}` === `${value.host}:${value.port}:${value.sslPort}`,
disabled: isLoading || (host === value.host && (port === value.port || sslPort === value.sslPort)),
}));
actions.push({
id: 'server_history',
text: loc.settings.electrum_history,
subactions: [CommonToolTipActions.ClearHistory, ...serverSubactions],
});
}
return actions;
}, [host, isLoading, port, serverHistory, sslPort]);
const HeaderRight = useMemo(
() => <HeaderMenuButton actions={toolTipActions} onPressMenuItem={onPressMenuItem} />,
[onPressMenuItem, toolTipActions],
);
useEffect(() => {
setOptions({
headerRight: isOfflineMode ? null : () => HeaderRight,
});
}, [HeaderRight, isOfflineMode, setOptions]);
const checkServer = async () => {
setIsLoading(true);
try {
const features = await BlueElectrum.serverFeatures();
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
presentAlert({ message: JSON.stringify(features, null, 2) });
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: (error as Error).message });
}
setIsLoading(false);
};
const onBarScanned = (value: string) => {
let v = value;
if (value && DeeplinkSchemaMatch.getServerFromSetElectrumServerAction(value)) {
v = DeeplinkSchemaMatch.getServerFromSetElectrumServerAction(value) as string;
}
const [scannedHost, scannedPort, type] = v?.split(':') ?? [];
setHost(scannedHost);
if (type === 's') {
setSslPort(Number(scannedPort));
setPort(undefined);
} else {
setPort(Number(scannedPort));
setSslPort(undefined);
}
};
const importScan = async () => {
const scanned = await scanQrHelper('ElectrumSettings', true);
if (scanned) {
onBarScanned(scanned);
}
};
const onSSLPortChange = (value: boolean) => {
Keyboard.dismiss();
if (value) {
setPort(undefined);
setSslPort(port);
} else {
setPort(sslPort);
setSslPort(undefined);
}
};
const onElectrumConnectionEnabledSwitchChange = async (value: boolean) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (value) {
await BlueElectrum.setDisabled(true);
setIsElectrumDisabled(true);
BlueElectrum.forceDisconnect();
} else {
await BlueElectrum.setDisabled(false);
setIsElectrumDisabled(false);
BlueElectrum.connectMain();
}
setIsOfflineMode(value);
};
const renderElectrumSettings = () => {
return (
<>
<Divider />
<BlueSpacing20 />
<Header leftText={loc.settings.electrum_status} />
<BlueSpacing20 />
<BlueCard>
<View style={styles.connectWrap}>
<View style={[styles.container, config.connected === 1 ? stylesHook.containerConnected : stylesHook.containerDisconnected]}>
<BlueText
style={[styles.textConnectionStatus, config.connected === 1 ? stylesHook.textConnected : stylesHook.textDisconnected]}
>
{config.connected === 1 ? loc.settings.electrum_connected : loc.settings.electrum_connected_not}
</BlueText>
</View>
</View>
<BlueSpacing10 />
<BlueText style={[styles.hostname, stylesHook.hostname]} onPress={checkServer} selectable>
{config.host}:{config.port}
</BlueText>
</BlueCard>
<BlueSpacing20 />
<Divider />
<BlueSpacing10 />
<BlueSpacing20 />
<Header leftText={loc.settings.electrum_preferred_server} />
<BlueCard>
<BlueText>{loc.settings.electrum_preferred_server_description}</BlueText>
<BlueSpacing20 />
<AddressInput
testID="HostInput"
placeholder={loc.formatString(loc.settings.electrum_host, { example: '10.20.30.40' })}
address={host}
onChangeText={text => setHost(text.trim())}
editable={!isLoading}
onBarScanned={importScan}
keyboardType="default"
onBlur={() => setIsAndroidAddressKeyboardVisible(false)}
onFocus={() => setIsAndroidAddressKeyboardVisible(true)}
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
isLoading={isLoading}
/>
<BlueSpacing20 />
<View style={styles.portWrap}>
<View style={[styles.inputWrap, stylesHook.inputWrap]}>
<TextInput
placeholder={loc.formatString(loc.settings.electrum_port, { example: '50001' })}
value={sslPort?.toString() === '' || sslPort === undefined ? port?.toString() || '' : sslPort?.toString() || ''}
onChangeText={text => {
const parsed = Number(text.trim());
if (Number.isNaN(parsed)) {
// Handle invalid input
sslPort === undefined ? setPort(undefined) : setSslPort(undefined);
return;
}
sslPort === undefined ? setPort(parsed) : setSslPort(parsed);
}}
numberOfLines={1}
style={[styles.inputText, stylesHook.inputText]}
editable={!isLoading}
placeholderTextColor="#81868e"
underlineColorAndroid="transparent"
autoCorrect={false}
autoCapitalize="none"
keyboardType="number-pad"
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
testID="PortInput"
onFocus={() => setIsAndroidNumericKeyboardFocused(true)}
onBlur={() => setIsAndroidNumericKeyboardFocused(false)}
/>
</View>
<BlueText style={[styles.usePort, stylesHook.usePort]}>{loc.settings.use_ssl}</BlueText>
<Switch
testID="SSLPortInput"
value={sslPort !== undefined}
onValueChange={onSSLPortChange}
disabled={host?.endsWith('.onion') ?? false}
/>
</View>
</BlueCard>
<BlueCard>
<BlueSpacing20 />
<Button showActivityIndicator={isLoading} disabled={isLoading} testID="Save" onPress={save} title={loc.settings.save} />
</BlueCard>
{Platform.select({
ios: <DismissKeyboardInputAccessory />,
android: isAndroidNumericKeyboardFocused && <DismissKeyboardInputAccessory />,
})}
{Platform.select({
ios: (
<DoneAndDismissKeyboardInputAccessory
onClearTapped={() => setHost('')}
onPasteTapped={text => {
setHost(text);
Keyboard.dismiss();
}}
/>
),
android: isAndroidAddressKeyboardVisible && (
<DoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
setHost('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setHost(text);
Keyboard.dismiss();
}}
/>
),
})}
</>
);
};
return (
<ScrollView
keyboardShouldPersistTaps="always"
automaticallyAdjustContentInsets
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustKeyboardInsets
testID="ElectrumSettingsScrollView"
>
<ListItem
Component={PressableWrapper}
title={loc.settings.electrum_offline_mode}
switch={{
onValueChange: onElectrumConnectionEnabledSwitchChange,
value: isOfflineMode,
testID: 'ElectrumConnectionEnabledSwitch',
}}
disabled={isLoading}
bottomDivider={false}
subtitle={loc.settings.electrum_offline_description}
/>
{!isOfflineMode && renderElectrumSettings()}
</ScrollView>
);
};
const styles = StyleSheet.create({
connectWrap: {
width: 'auto',
height: 34,
flexWrap: 'wrap',
justifyContent: 'center',
flexDirection: 'row',
},
hostname: {
textAlign: 'center',
},
container: {
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 16,
paddingRight: 16,
borderRadius: 20,
},
inputWrap: {
flex: 1,
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
},
portWrap: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
inputText: {
flex: 1,
marginHorizontal: 8,
minHeight: 36,
height: 36,
},
textConnectionStatus: {
fontWeight: 'bold',
},
usePort: {
marginHorizontal: 16,
},
});
export default ElectrumSettings;

View File

@ -3,7 +3,7 @@ import { I18nManager, Linking, ScrollView, StyleSheet, TextInput, View, Pressabl
import { Button as ButtonRNElements } from '@rneui/themed';
// @ts-ignore: no declaration file
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueSpacing20, BlueText } from '../../BlueComponents';
import { BlueCard, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import { Button } from '../../components/Button';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
@ -123,6 +123,10 @@ const NotificationSettings: React.FC = () => {
setIsLoading(false);
}, [URI]);
const onSystemSettings = () => {
Linking.openSettings();
};
return (
<ScrollView style={stylesWithThemeHook.scroll} automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="automatic">
<ListItem
@ -193,6 +197,8 @@ const NotificationSettings: React.FC = () => {
</BlueCard>
</>
)}
<BlueSpacing40 />
<ListItem title={loc.settings.privacy_system_settings} onPress={onSystemSettings} chevron />
</ScrollView>
);
};

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Platform, Pressable, ScrollView, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
import { Platform, ScrollView, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
import { openSettings } from 'react-native-permissions';
import A from '../../blue_modules/analytics';
import { Header } from '../../components/Header';
@ -127,11 +127,7 @@ const SettingsPrivacy: React.FC = () => {
disabled: isLoading === SettingsPrivacySection.All,
testID: 'ClipboardSwitch',
}}
subtitle={
<Pressable accessibilityRole="button">
<Text style={styles.subtitleText}>{loc.settings.privacy_clipboard_explanation}</Text>
</Pressable>
}
subtitle={loc.settings.privacy_clipboard_explanation}
/>
<ListItem

View File

@ -1,542 +0,0 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
Alert,
Keyboard,
Platform,
Pressable,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import DefaultPreference from 'react-native-default-preference';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueButtonLink, BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import presentAlert, { AlertType } from '../../components/Alert';
import Button from '../../components/Button';
import ListItem from '../../components/ListItem';
import { BlueCurrentTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { StorageContext } from '../../components/Context/StorageProvider';
import {
DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID,
} from '../../components/DoneAndDismissKeyboardInputAccessory';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
export default class ElectrumSettings extends Component {
static contextType = StorageContext;
constructor(props) {
super(props);
const server = props?.route?.params?.server;
this.state = {
isLoading: true,
isOfflineMode: false,
serverHistory: [],
config: {},
server,
sslPort: '',
port: '',
};
}
componentWillUnmount() {
clearInterval(this.state.inverval);
}
async componentDidMount() {
const host = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_HOST);
const port = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_TCP_PORT);
const sslPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SSL_PORT);
const serverHistoryStr = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SERVER_HISTORY);
const isOfflineMode = await BlueElectrum.isDisabled();
const serverHistory = JSON.parse(serverHistoryStr) || [];
this.setState({
isLoading: false,
host,
port,
sslPort,
serverHistory,
isOfflineMode,
isAndroidNumericKeyboardFocused: false,
isAndroidAddressKeyboardVisible: false,
});
const inverval = setInterval(async () => {
this.setState({
config: await BlueElectrum.getConfig(),
});
}, 500);
this.setState({
config: await BlueElectrum.getConfig(),
inverval,
});
if (this.state.server) {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
Alert.alert(
loc.formatString(loc.settings.set_electrum_server_as_default, { server: this.state.server }),
'',
[
{
text: loc._.ok,
onPress: () => {
this.onBarScanned(this.state.server);
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
}
}
checkServer = async () => {
this.setState({ isLoading: true }, async () => {
const features = await BlueElectrum.serverFeatures();
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
presentAlert({ message: JSON.stringify(features, null, 2) });
this.setState({ isLoading: false });
});
};
selectServer = async server => {
this.setState({ host: server.host, port: server.port, sslPort: server.sslPort }, () => {
this.save();
});
};
clearHistoryAlert() {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
Alert.alert(loc.settings.electrum_clear_alert_title, loc.settings.electrum_clear_alert_message, [
{ text: loc.settings.electrum_clear_alert_cancel, onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{ text: loc.settings.electrum_clear_alert_ok, onPress: () => this.clearHistory() },
]);
}
clearHistory = async () => {
this.setState({ isLoading: true }, async () => {
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify([]));
this.setState({
serverHistory: [],
isLoading: false,
});
});
};
resetToDefault = async () => {
this.setState({ port: '', host: '', sslPort: '' }, () => {
this.save();
});
};
serverExists = server => {
const { serverHistory } = this.state;
return serverHistory.some(s => {
return `${s.host}${s.port}${s.sslPort}` === `${server.host}${server.port}${server.sslPort}`;
});
};
save = () => {
const host = this.state.host ? this.state.host : '';
const port = this.state.port ? this.state.port : '';
const sslPort = this.state.sslPort ? this.state.sslPort : '';
const serverHistory = this.state.serverHistory || [];
this.setState({ isLoading: true }, async () => {
try {
if (!host && !port && !sslPort) {
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_HOST, '');
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_TCP_PORT, '');
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SSL_PORT, '');
try {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.clear(BlueElectrum.ELECTRUM_HOST);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_SSL_PORT);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_TCP_PORT);
} catch (e) {
// Must be running on Android
console.log(e);
}
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.settings.electrum_saved });
} else if (!(await BlueElectrum.testConnection(host, port, sslPort))) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.electrum_error_connect });
} else {
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_HOST, host);
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_TCP_PORT, port);
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SSL_PORT, sslPort);
if (!this.serverExists({ host, port, sslPort })) {
serverHistory.push({
host,
port,
sslPort,
});
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(serverHistory));
}
try {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, host);
await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, port);
await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, sslPort);
} catch (e) {
// Must be running on Android
console.log(e);
}
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.settings.electrum_saved });
}
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: error, type: AlertType.Toast });
}
this.setState({ isLoading: false });
});
};
onBarScanned = value => {
if (DeeplinkSchemaMatch.getServerFromSetElectrumServerAction(value)) {
// in case user scans a QR with a deeplink like `bluewallet:setelectrumserver?server=electrum1.bluewallet.io%3A443%3As`
value = DeeplinkSchemaMatch.getServerFromSetElectrumServerAction(value);
}
const [host, port, type] = value.split(':');
this.setState({ host, sslPort: '', port: '' }, () => {
type === 's' ? this.setState({ sslPort: port }) : this.setState({ port });
});
};
importScan = async () => {
const scanned = await scanQrHelper('ElectrumSettings', true);
this.onBarScanned(scanned);
};
useSSLPortToggled = value => {
switch (value) {
case true:
this.setState(prevState => {
return { port: '', sslPort: prevState.port };
});
break;
case false:
this.setState(prevState => {
return { port: prevState.sslPort, sslPort: '' };
});
break;
}
};
onElectrumConnectionEnabledSwitchValueChanged = async value => {
if (value === true) {
await BlueElectrum.setDisabled(true);
this.context.setIsElectrumDisabled(true);
BlueElectrum.forceDisconnect();
} else {
await BlueElectrum.setDisabled(false);
this.context.setIsElectrumDisabled(false);
BlueElectrum.connectMain();
}
this.setState({ isOfflineMode: value });
};
renderElectrumSettings = () => {
const serverHistoryItems = this.state.serverHistory.map((server, i) => {
return (
<View key={i} style={styles.serverHistoryItem}>
<Text style={styles.serverRow} numberOfLines={1} ellipsizeMode="middle">{`${server.host}:${server.port || server.sslPort}`}</Text>
<TouchableOpacity accessibilityRole="button" style={styles.selectButton} onPress={() => this.selectServer(server)}>
<BlueText>{loc.settings.electrum_select}</BlueText>
</TouchableOpacity>
</View>
);
});
return (
<>
<BlueCard>
<BlueText style={styles.status}>{loc.settings.electrum_status}</BlueText>
<View style={styles.connectWrap}>
<View style={[styles.container, this.state.config.connected === 1 ? styles.containerConnected : styles.containerDisconnected]}>
<BlueText style={this.state.config.connected === 1 ? styles.textConnected : styles.textDisconnected}>
{this.state.config.connected === 1 ? loc.settings.electrum_connected : loc.settings.electrum_connected_not}
</BlueText>
</View>
</View>
<BlueSpacing20 />
<BlueText style={styles.hostname} onPress={this.checkServer}>
{this.state.config.host}:{this.state.config.port}
</BlueText>
</BlueCard>
<BlueCard>
<View style={styles.inputWrap}>
<TextInput
placeholder={loc.formatString(loc.settings.electrum_host, { example: '10.20.30.40' })}
value={this.state.host}
onChangeText={text => {
const host = text.trim();
this.setState({ host }, () => {
if (host.endsWith('.onion')) {
this.useSSLPortToggled(false);
}
});
}}
numberOfLines={1}
style={styles.inputText}
editable={!this.state.isLoading}
placeholderTextColor="#81868e"
autoCorrect={false}
autoCapitalize="none"
underlineColorAndroid="transparent"
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
testID="HostInput"
onFocus={() => this.setState({ isAndroidAddressKeyboardVisible: true })}
onBlur={() => this.setState({ isAndroidAddressKeyboardVisible: false })}
/>
</View>
<BlueSpacing20 />
<View style={styles.portWrap}>
<View style={styles.inputWrap}>
<TextInput
placeholder={loc.formatString(loc.settings.electrum_port, { example: '50001' })}
value={this.state.sslPort?.trim() === '' || this.state.sslPort === null ? this.state.port : this.state.sslPort}
onChangeText={text =>
this.setState(prevState => {
if (prevState.sslPort?.trim() === '') {
return { port: text.trim(), sslPort: '' };
} else {
return { port: '', sslPort: text.trim() };
}
})
}
numberOfLines={1}
style={styles.inputText}
editable={!this.state.isLoading}
placeholderTextColor="#81868e"
underlineColorAndroid="transparent"
autoCorrect={false}
autoCapitalize="none"
keyboardType="number-pad"
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
testID="PortInput"
onFocus={() => this.setState({ isAndroidNumericKeyboardFocused: true })}
onBlur={() => this.setState({ isAndroidNumericKeyboardFocused: false })}
/>
</View>
<BlueText style={styles.usePort}>{loc.settings.use_ssl}</BlueText>
<Switch
testID="SSLPortInput"
value={this.state.sslPort?.trim() > 0}
onValueChange={this.useSSLPortToggled}
disabled={this.state.host?.endsWith('.onion') ?? false}
/>
</View>
<BlueSpacing20 />
<View style={styles.serverAddTitle}>
<BlueText style={styles.explain}>{loc.settings.electrum_settings_explain}</BlueText>
<TouchableOpacity accessibilityRole="button" testID="ResetToDefault" onPress={() => this.resetToDefault()}>
<BlueText>{loc.settings.electrum_reset}</BlueText>
</TouchableOpacity>
</View>
<BlueSpacing20 />
{this.state.isLoading ? <BlueLoading /> : <Button testID="Save" onPress={this.save} title={loc.settings.save} />}
<BlueSpacing20 />
<BlueButtonLink title={loc.wallets.import_scan_qr} onPress={this.importScan} />
<BlueSpacing20 />
</BlueCard>
{Platform.select({
ios: <DismissKeyboardInputAccessory />,
android: this.state.isAndroidNumericKeyboardFocused && <DismissKeyboardInputAccessory />,
})}
{Platform.select({
ios: (
<DoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ host: '' })}
onPasteTapped={text => {
this.setState({ host: text });
Keyboard.dismiss();
}}
/>
),
android: this.state.isAndroidAddressKeyboardVisible && (
<DoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
this.setState({ host: '' });
Keyboard.dismiss();
}}
onPasteTapped={text => {
this.setState({ host: text });
Keyboard.dismiss();
}}
/>
),
})}
{serverHistoryItems.length > 0 && !this.state.isLoading && (
<BlueCard>
<View style={styles.serverHistoryTitle}>
<BlueText style={styles.explain}>{loc.settings.electrum_history}</BlueText>
<TouchableOpacity accessibilityRole="button" onPress={() => this.clearHistoryAlert()}>
<BlueText>{loc.settings.electrum_clear}</BlueText>
</TouchableOpacity>
</View>
{serverHistoryItems}
</BlueCard>
)}
</>
);
};
render() {
return (
<ScrollView
keyboardShouldPersistTaps="always"
automaticallyAdjustContentInsets
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustKeyboardInsets
>
<ListItem
Component={Pressable}
title={loc.settings.electrum_offline_mode}
switch={{
onValueChange: this.onElectrumConnectionEnabledSwitchValueChanged,
value: this.state.isOfflineMode,
testID: 'ElectrumConnectionEnabledSwitch',
}}
subtitle={loc.settings.electrum_offline_description}
/>
{!this.state.isOfflineMode && this.renderElectrumSettings()}
</ScrollView>
);
}
}
ElectrumSettings.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
}),
route: PropTypes.shape({
name: PropTypes.string,
params: PropTypes.shape({
server: PropTypes.string,
}),
}),
};
const styles = StyleSheet.create({
status: {
textAlign: 'center',
color: BlueCurrentTheme.colors.feeText,
marginBottom: 4,
},
connectWrap: {
width: 'auto',
height: 34,
flexWrap: 'wrap',
justifyContent: 'center',
flexDirection: 'row',
},
container: {
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 16,
paddingRight: 16,
borderRadius: 20,
},
containerConnected: {
backgroundColor: BlueCurrentTheme.colors.feeLabel,
},
containerDisconnected: {
backgroundColor: BlueCurrentTheme.colors.redBG,
},
textConnected: {
color: BlueCurrentTheme.colors.feeValue,
fontWeight: 'bold',
},
textDisconnected: {
color: BlueCurrentTheme.colors.redText,
fontWeight: 'bold',
},
hostname: {
textAlign: 'center',
color: BlueCurrentTheme.colors.foregroundColor,
},
usePort: {
textAlign: 'center',
color: BlueCurrentTheme.colors.foregroundColor,
marginHorizontal: 8,
},
explain: {
color: BlueCurrentTheme.colors.feeText,
marginBottom: -24,
flexShrink: 1,
},
inputWrap: {
flex: 1,
flexDirection: 'row',
borderColor: BlueCurrentTheme.colors.formBorder,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderWidth: 1,
borderBottomWidth: 0.5,
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
},
portWrap: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
inputText: {
flex: 1,
marginHorizontal: 8,
minHeight: 36,
color: '#81868e',
height: 36,
},
serverAddTitle: {
flexDirection: 'row',
justifyContent: 'space-between',
marginVertical: 16,
},
serverHistoryTitle: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 10,
},
serverHistoryItem: {
flexDirection: 'row',
paddingVertical: 20,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderBottomWidth: 0.5,
flexWrap: 'nowrap',
},
serverRow: {
flexGrow: 2,
maxWidth: '80%',
color: BlueCurrentTheme.colors.foregroundColor,
},
selectButton: {
flexGrow: 1,
marginLeft: 16,
alignItems: 'flex-end',
},
});

View File

@ -347,7 +347,7 @@ const TransactionDetails = () => {
onPress={handleOnOpenTransactionOnBlockExplorerTapped}
buttonStyle={StyleSheet.flatten([styles.greyButton, stylesHooks.greyButton])}
>
<Text style={[styles.Link, stylesHooks.Link]}>{loc.transactions.details_show_in_block_explorer}</Text>
<Text style={[styles.Link, stylesHooks.Link]}>{loc.transactions.details_view_in_browser}</Text>
</ToolTipMenu>
</BlueCard>
</ScrollView>

View File

@ -61,7 +61,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
// change currency to ARS ($) and switch it back to USD ($)
await element(by.id('Currency')).tap();
await element(by.text('ARS ($)')).tap();
await expect(element(by.text('Price is obtained from Yadio'))).toBeVisible();
await expect(element(by.text('Rate is obtained from Yadio'))).toBeVisible();
await element(by.text('USD ($)')).tap();
await device.pressBack();
@ -85,14 +85,20 @@ describe('BlueWallet UI Tests - no wallets', () => {
// network -> electrum server
// change electrum server to electrum.blockstream.info and revert it back
await element(by.id('ElectrumSettings')).tap();
await element(by.id('ElectrumSettingsScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await element(by.id('HostInput')).replaceText('electrum.blockstream.info\n');
await element(by.id('PortInput')).replaceText('50001\n');
await element(by.id('ElectrumSettingsScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await element(by.id('Save')).tap();
await sup('OK');
await element(by.text('OK')).tap();
await element(by.id('ResetToDefault')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Reset to default')).tap();
await sup('OK');
await element(by.text('OK')).tap();
await sup('OK');
await element(by.text('OK')).tap();
await element(by.id('ElectrumSettingsScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await expect(element(by.id('HostInput'))).toHaveText('');
await expect(element(by.id('PortInput'))).toHaveText('');
await expect(element(by.id('SSLPortInput'))).toHaveToggleValue(false);

View File

@ -164,7 +164,7 @@ jest.mock('react-native-ios-context-menu', () => {
});
jest.mock('rn-qr-generator', () => ({
detect: jest.fn((uri) => {
detect: jest.fn(uri => {
if (uri === 'invalid-image') {
return Promise.reject(new Error('Failed to decode QR code'));
}

View File

@ -1,4 +1,6 @@
import { Platform } from 'react-native';
import loc from '../loc';
import { Action } from '../components/types';
const keys = {
CopyTXID: 'copyTX_ID',
@ -20,68 +22,71 @@ const keys = {
SaveChanges: 'saveChanges',
ClearClipboard: 'clearClipboard',
PaymentsCode: 'paymentsCode',
ResetToDefault: 'resetToDefault',
ClearHistory: 'clearHistory',
ScanQR: 'scan_qr',
RemoveAllRecipients: 'RemoveAllRecipients',
AddRecipient: 'AddRecipient',
RemoveRecipient: 'RemoveRecipient',
ChoosePhoto: 'choose_photo',
ImportFile: 'import_file',
InsertContact: 'insert_contact',
SignPSBT: 'sign_psbt',
SendMax: 'send_max',
AllowRBF: 'allow_rbf',
ImportTransaction: 'import_transaction',
ImportTransactionMultsig: 'import_transaction_multisig',
ImportTransactionQR: 'import_transaction_qr',
CoinControl: 'coin_control',
CoSignTransaction: 'co_sign_transaction',
CopyToClipboard: 'copyToClipboard',
Share: 'share',
SignVerify: 'signVerify',
ExportPrivateKey: 'exportPrivateKey',
PasteFromClipboard: 'pasteFromClipboard',
};
const icons = {
Eye: {
iconValue: 'eye',
},
EyeSlash: {
iconValue: 'eye.slash',
},
Clipboard: {
iconValue: 'doc.on.doc',
},
ClearClipboard: {
iconValue: 'clipboard',
},
Link: {
iconValue: 'link',
},
Note: {
iconValue: 'note.text',
},
ManageWallets: {
iconValue: 'slider.horizontal.3',
},
ImportWallet: {
iconValue: 'square.and.arrow.down.on.square',
},
ViewInBitcoin: {
iconValue: 'bitcoinsign.circle',
},
ViewInFiat: {
iconValue: 'coloncurrencysign.circle',
},
Entropy: {
iconValue: 'dice',
},
SearchAccount: {
iconValue: 'magnifyingglass',
},
Passphrase: {
iconValue: 'rectangle.and.pencil.and.ellipsis',
},
MoreInfo: {
iconValue: 'info.circle',
},
SaveChanges: {
iconValue: 'checkmark',
},
PaymentsCode: {
iconValue: 'qrcode',
const icons: { [key: string]: { iconValue: string } } = {
Eye: { iconValue: 'eye' },
EyeSlash: { iconValue: 'eye.slash' },
Link: { iconValue: 'link' },
Note: { iconValue: 'note.text' },
ManageWallets: { iconValue: 'slider.horizontal.3' },
ImportWallet: { iconValue: 'square.and.arrow.down.on.square' },
ViewInBitcoin: { iconValue: 'bitcoinsign.circle' },
ViewInFiat: { iconValue: 'coloncurrencysign.circle' },
Entropy: { iconValue: 'dice' },
SearchAccount: { iconValue: 'magnifyingglass' },
Passphrase: { iconValue: 'rectangle.and.pencil.and.ellipsis' },
MoreInfo: { iconValue: 'info.circle' },
SaveChanges: { iconValue: 'checkmark' },
InsertContact: { iconValue: 'at.badge.plus' },
SignPSBT: { iconValue: 'signature' },
SendMax: { iconValue: 'dial.high' },
AllowRBF: { iconValue: 'arrowshape.up.circle' },
ImportTransaction: { iconValue: 'square.and.arrow.down' },
ImportTransactionMultsig: { iconValue: 'square.and.arrow.down.on.square' },
ImportTransactionQR: { iconValue: 'qrcode.viewfinder' },
CoinControl: { iconValue: 'switch.2' },
CoSignTransaction: { iconValue: 'signature' },
PaymentsCode: { iconValue: 'qrcode.viewfinder' },
ClearHistory: {
iconValue: 'trash',
},
RemoveAllRecipients: { iconValue: 'person.2.slash' },
AddRecipient: { iconValue: 'person.badge.plus' },
RemoveRecipient: { iconValue: 'person.badge.minus' },
ScanQR: { iconValue: Platform.OS === 'ios' ? 'qrcode.viewfinder' : 'ic_menu_camera' },
ChoosePhoto: { iconValue: Platform.OS === 'ios' ? 'photo.on.rectangle' : 'ic_menu_gallery' },
Clipboard: { iconValue: Platform.OS === 'ios' ? 'document.on.clipboard' : 'ic_menu_file' },
ExportPrivateKey: { iconValue: 'key' },
Share: { iconValue: 'square.and.arrow.up' },
Signature: { iconValue: 'signature' },
PasteFromClipboard: { iconValue: 'document.on.clipboard' },
ImportFile: { iconValue: 'document.viewfinder' },
};
export const CommonToolTipActions = {
export const CommonToolTipActions: { [key: string]: Action } = {
CopyTXID: {
id: keys.CopyTXID,
text: loc.transactions.details_copy_txid,
@ -94,7 +99,7 @@ export const CommonToolTipActions = {
},
OpenInBlockExplorer: {
id: keys.OpenInBlockExplorer,
text: loc.transactions.details_show_in_block_explorer,
text: loc.transactions.details_view_in_browser,
icon: icons.Link,
},
ExpandNote: {
@ -141,16 +146,19 @@ export const CommonToolTipActions = {
id: keys.ViewInFiat,
text: loc.total_balance_view.view_in_fiat,
icon: icons.ViewInFiat,
hidden: false,
},
ViewInSats: {
id: keys.ViewInSats,
text: loc.total_balance_view.view_in_sats,
icon: icons.ViewInBitcoin,
hidden: false,
},
ViewInBitcoin: {
id: keys.ViewInBitcoin,
text: loc.total_balance_view.view_in_bitcoin,
icon: icons.ViewInBitcoin,
hidden: false,
},
Entropy: {
id: keys.Entropy,
@ -185,15 +193,114 @@ export const CommonToolTipActions = {
text: loc._.save,
icon: icons.SaveChanges,
},
PaymentCode: {
PaymentsCode: {
id: keys.PaymentsCode,
text: loc.bip47.purpose,
icon: icons.PaymentsCode,
menuState: false,
},
ScanQR: {
id: keys.ScanQR,
text: loc.wallets.list_long_scan,
icon: icons.ScanQR,
},
ChoosePhoto: {
id: keys.ChoosePhoto,
text: loc.wallets.list_long_choose,
icon: icons.ChoosePhoto,
},
ImportFile: {
id: keys.ImportFile,
text: loc.wallets.import_file,
icon: icons.ImportFile,
},
InsertContact: {
id: keys.InsertContact,
text: loc.send.details_insert_contact,
icon: icons.InsertContact,
hidden: true,
},
SignPSBT: {
id: keys.SignPSBT,
text: loc.send.psbt_sign,
icon: icons.SignPSBT,
hidden: true,
},
SendMax: {
id: keys.SendMax,
text: loc.send.details_adv_full,
icon: icons.SendMax,
hidden: true,
},
AllowRBF: {
id: keys.AllowRBF,
text: loc.send.details_adv_fee_bump,
icon: icons.AllowRBF,
hidden: true,
menuState: false,
},
ImportTransaction: {
id: keys.ImportTransaction,
text: loc.send.details_adv_import,
icon: icons.ImportTransaction,
hidden: true,
},
ImportTransactionQR: {
id: keys.ImportTransactionQR,
text: loc.send.details_adv_import_qr,
icon: icons.ImportTransactionQR,
hidden: true,
},
ImportTransactionMultsig: {
id: keys.ImportTransactionMultsig,
text: loc.send.details_adv_import,
icon: icons.ImportTransactionMultsig,
hidden: true,
},
CoSignTransaction: {
id: keys.CoSignTransaction,
text: loc.multisig.co_sign_transaction,
icon: icons.CoSignTransaction,
hidden: true,
},
CoinControl: {
id: keys.CoinControl,
text: loc.cc.header,
icon: icons.CoinControl,
hidden: false,
},
CopyToClipboard: {
id: keys.CopyToClipboard,
text: loc.transactions.details_copy,
icon: icons.Clipboard,
},
Share: {
id: keys.Share,
text: loc.receive.details_share,
icon: icons.Share,
},
SignVerify: {
id: keys.SignVerify,
text: loc.addresses.sign_title,
icon: icons.Signature,
},
ExportPrivateKey: {
id: keys.ExportPrivateKey,
text: loc.addresses.copy_private_key,
icon: icons.ExportPrivateKey,
},
ResetToDefault: {
id: keys.ResetToDefault,
text: loc.settings.electrum_reset,
},
ClearHistory: {
id: keys.ClearHistory,
text: loc.settings.electrum_clear,
icon: icons.ClearHistory,
},
PasteFromClipboard: {
id: keys.PasteFromClipboard,
text: loc.transactions.details_copy_amount,
text: loc.wallets.paste_from_clipboard,
icon: icons.PasteFromClipboard,
},
};