mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 23:08:07 +01:00
REF: navigation styles
This commit is contained in:
commit
f6aeb13636
13 changed files with 462 additions and 216 deletions
|
@ -747,48 +747,6 @@ export const BlueHeader = props => {
|
||||||
|
|
||||||
export const BlueHeaderDefaultSub = props => {
|
export const BlueHeaderDefaultSub = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return (
|
|
||||||
<SafeAreaView style={{ backgroundColor: colors.brandingColor }}>
|
|
||||||
<Header
|
|
||||||
backgroundColor={colors.background}
|
|
||||||
leftContainerStyle={{ minWidth: '100%' }}
|
|
||||||
outerContainerStyles={{
|
|
||||||
borderBottomColor: 'transparent',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
}}
|
|
||||||
leftComponent={
|
|
||||||
<Text
|
|
||||||
adjustsFontSizeToFit
|
|
||||||
style={{
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 30,
|
|
||||||
color: colors.foregroundColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.leftText}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
rightComponent={
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
if (props.onClose) props.onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={stylesBlueIcon.box}>
|
|
||||||
<View style={stylesBlueIcon.ballTransparrent}>
|
|
||||||
<Image source={require('./img/close.png')} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BlueHeaderDefaultSubHooks = props => {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
|
@ -878,15 +836,8 @@ export const BlueSpacing10 = props => {
|
||||||
return <View {...props} style={{ height: 10, opacity: 0 }} />;
|
return <View {...props} style={{ height: 10, opacity: 0 }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BlueUseAllFundsButton extends Component {
|
export const BlueUseAllFundsButton = ({ balance, canUseAll, onUseAllPressed }) => {
|
||||||
static InputAccessoryViewID = 'useMaxInputAccessoryViewID';
|
const { colors } = useTheme();
|
||||||
static propTypes = {
|
|
||||||
balance: PropTypes.string.isRequired,
|
|
||||||
canUseAll: PropTypes.bool.isRequired,
|
|
||||||
onUseAllPressed: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputView = (
|
const inputView = (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
@ -895,13 +846,13 @@ export class BlueUseAllFundsButton extends Component {
|
||||||
maxHeight: 44,
|
maxHeight: 44,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
|
backgroundColor: colors.inputBackgroundColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'flex-start' }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'flex-start' }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
color: BlueCurrentTheme.colors.alternativeTextColor,
|
color: colors.alternativeTextColor,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
marginRight: 0,
|
marginRight: 0,
|
||||||
|
@ -913,16 +864,16 @@ export class BlueUseAllFundsButton extends Component {
|
||||||
>
|
>
|
||||||
{loc.send.input_total}
|
{loc.send.input_total}
|
||||||
</Text>
|
</Text>
|
||||||
{this.props.canUseAll ? (
|
{canUseAll ? (
|
||||||
<BlueButtonLink
|
<BlueButtonLink
|
||||||
onPress={this.props.onUseAllPressed}
|
onPress={onUseAllPressed}
|
||||||
style={{ marginLeft: 8, paddingRight: 0, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
style={{ marginLeft: 8, paddingRight: 0, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
||||||
title={`${this.props.balance} ${BitcoinUnit.BTC}`}
|
title={`${balance} ${BitcoinUnit.BTC}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
color: BlueCurrentTheme.colors.alternativeTextColor,
|
color: colors.alternativeTextColor,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
marginRight: 0,
|
marginRight: 0,
|
||||||
|
@ -932,7 +883,7 @@ export class BlueUseAllFundsButton extends Component {
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.props.balance} {BitcoinUnit.BTC}
|
{balance} {BitcoinUnit.BTC}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
@ -940,7 +891,7 @@ export class BlueUseAllFundsButton extends Component {
|
||||||
<BlueButtonLink
|
<BlueButtonLink
|
||||||
style={{ paddingRight: 8, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
style={{ paddingRight: 8, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
||||||
title={loc.send.input_done}
|
title={loc.send.input_done}
|
||||||
onPress={() => Keyboard.dismiss()}
|
onPress={Keyboard.dismiss}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -951,8 +902,13 @@ export class BlueUseAllFundsButton extends Component {
|
||||||
} else {
|
} else {
|
||||||
return <KeyboardAvoidingView style={{ height: 44 }}>{inputView}</KeyboardAvoidingView>;
|
return <KeyboardAvoidingView style={{ height: 44 }}>{inputView}</KeyboardAvoidingView>;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
BlueUseAllFundsButton.InputAccessoryViewID = 'useMaxInputAccessoryViewID';
|
||||||
|
BlueUseAllFundsButton.propTypes = {
|
||||||
|
balance: PropTypes.string.isRequired,
|
||||||
|
canUseAll: PropTypes.bool.isRequired,
|
||||||
|
onUseAllPressed: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export const BlueDismissKeyboardInputAccessory = () => {
|
export const BlueDismissKeyboardInputAccessory = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
|
@ -7,17 +7,22 @@ import { BlueStorageContext } from '../blue_modules/storage-context';
|
||||||
|
|
||||||
function DeviceQuickActions() {
|
function DeviceQuickActions() {
|
||||||
DeviceQuickActions.STORAGE_KEY = 'DeviceQuickActionsEnabled';
|
DeviceQuickActions.STORAGE_KEY = 'DeviceQuickActionsEnabled';
|
||||||
const { wallets, walletsInitialized, isStorageEncryted } = useContext(BlueStorageContext);
|
const { wallets, walletsInitialized, isStorageEncrypted } = useContext(BlueStorageContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (walletsInitialized) {
|
if (walletsInitialized) {
|
||||||
if (isStorageEncryted) {
|
isStorageEncrypted()
|
||||||
|
.then(value => {
|
||||||
|
if (value) {
|
||||||
QuickActions.clearShortcutItems();
|
QuickActions.clearShortcutItems();
|
||||||
} else {
|
} else {
|
||||||
setQuickActions();
|
setQuickActions();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(() => QuickActions.clearShortcutItems());
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [wallets, walletsInitialized, isStorageEncryted]);
|
}, [wallets, walletsInitialized]);
|
||||||
|
|
||||||
DeviceQuickActions.setEnabled = (enabled = true) => {
|
DeviceQuickActions.setEnabled = (enabled = true) => {
|
||||||
return AsyncStorage.setItem(DeviceQuickActions.STORAGE_KEY, JSON.stringify(enabled)).then(() => {
|
return AsyncStorage.setItem(DeviceQuickActions.STORAGE_KEY, JSON.stringify(enabled)).then(() => {
|
||||||
|
|
|
@ -554,15 +554,17 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||||
// is it wallet descriptor?
|
// is it wallet descriptor?
|
||||||
// @see https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
// @see https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
||||||
// @see https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Wallets/Wallet-Export-Spec.md
|
// @see https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Wallets/Wallet-Export-Spec.md
|
||||||
|
if (!json && secret.indexOf('sortedmulti(')) {
|
||||||
|
// provided secret was NOT json but plain wallet descriptor text. lets mock json
|
||||||
|
json = { descriptor: secret, label: 'Multisig vault' };
|
||||||
|
}
|
||||||
if (secret.indexOf('sortedmulti(') !== -1 && json.descriptor) {
|
if (secret.indexOf('sortedmulti(') !== -1 && json.descriptor) {
|
||||||
if (json.label) this.setLabel(json.label);
|
if (json.label) this.setLabel(json.label);
|
||||||
if (json.descriptor.startsWith('wsh(')) {
|
if (json.descriptor.startsWith('wsh(')) {
|
||||||
this.setNativeSegwit();
|
this.setNativeSegwit();
|
||||||
}
|
} else if (json.descriptor.startsWith('sh(wsh(')) {
|
||||||
if (json.descriptor.startsWith('sh(')) {
|
this.setWrappedSegwit();
|
||||||
this.setLegacy();
|
} else if (json.descriptor.startsWith('sh(')) {
|
||||||
}
|
|
||||||
if (json.descriptor.startsWith('sh(wsh(')) {
|
|
||||||
this.setLegacy();
|
this.setLegacy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -459,7 +459,9 @@
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"manage_keys": "Manage Keys",
|
"manage_keys": "Manage Keys",
|
||||||
"how_many_signatures_can_bluewallet_make": "How Many Signatures Can BlueWallet Make",
|
"how_many_signatures_can_bluewallet_make": "how many signatures can bluewallet make",
|
||||||
|
"signatures_required_to_spend": "Signatures required {number}",
|
||||||
|
"signatures_we_can_make": "can make {number}",
|
||||||
"scan_or_import_file": "Scan or import file",
|
"scan_or_import_file": "Scan or import file",
|
||||||
"export_coordination_setup": "Export Coordination Setup",
|
"export_coordination_setup": "Export Coordination Setup",
|
||||||
"cosign_this_transaction": "Co-sign this transaction?",
|
"cosign_this_transaction": "Co-sign this transaction?",
|
||||||
|
@ -531,8 +533,10 @@
|
||||||
"empty": "This wallet doesn't have any coins at the moment",
|
"empty": "This wallet doesn't have any coins at the moment",
|
||||||
"freeze": "Freeze",
|
"freeze": "Freeze",
|
||||||
"freezeLabel": "Freeze",
|
"freezeLabel": "Freeze",
|
||||||
|
"freezeLabel_un": "Unfreeze",
|
||||||
"header": "Coin Control",
|
"header": "Coin Control",
|
||||||
"use_coin": "Use Coin",
|
"use_coin": "Use Coin",
|
||||||
|
"use_coins": "Use Coins",
|
||||||
"tip": "Allows you to see, label, freeze or select coins for improved wallet management."
|
"tip": "Allows you to see, label, freeze or select coins for improved wallet management."
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Localization from 'react-localization';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
import * as RNLocalize from 'react-native-localize';
|
import * as RNLocalize from 'react-native-localize';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ import { AvailableLanguages } from './languages';
|
||||||
const currency = require('../blue_modules/currency');
|
const currency = require('../blue_modules/currency');
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
// first-time loading sequence
|
// first-time loading sequence
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import React, { useMemo, useState, useContext, useEffect, useRef } from 'react';
|
import React, { useMemo, useState, useContext, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ListItem, Avatar, Badge } from 'react-native-elements';
|
import { Avatar, Badge, Icon, ListItem } from 'react-native-elements';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
LayoutAnimation,
|
||||||
|
PixelRatio,
|
||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
useColorScheme,
|
useWindowDimensions,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useRoute, useTheme, useNavigation } from '@react-navigation/native';
|
import { useRoute, useTheme, useNavigation } from '@react-navigation/native';
|
||||||
|
@ -21,6 +23,7 @@ import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||||
import { SafeBlueArea, BlueSpacing10, BlueSpacing20, BlueButton, BlueListItem } from '../../BlueComponents';
|
import { SafeBlueArea, BlueSpacing10, BlueSpacing20, BlueButton, BlueListItem } from '../../BlueComponents';
|
||||||
import navigationStyle from '../../components/navigationStyle';
|
import navigationStyle from '../../components/navigationStyle';
|
||||||
import BottomModal from '../../components/BottomModal';
|
import BottomModal from '../../components/BottomModal';
|
||||||
|
import { FContainer, FButton } from '../../components/FloatButtons';
|
||||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||||
|
|
||||||
// https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086
|
// https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086
|
||||||
|
@ -36,87 +39,151 @@ const debounce = (func, wait) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const Output = ({
|
const FrozenBadge = () => {
|
||||||
item: { address, txid, value, vout },
|
const { colors } = useTheme();
|
||||||
|
const oStyles = StyleSheet.create({
|
||||||
|
freeze: { backgroundColor: colors.redBG, borderWidth: 0 },
|
||||||
|
freezeText: { color: colors.redText },
|
||||||
|
});
|
||||||
|
return <Badge value={loc.cc.freeze} badgeStyle={oStyles.freeze} textStyle={oStyles.freezeText} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeBadge = () => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const oStyles = StyleSheet.create({
|
||||||
|
change: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0 },
|
||||||
|
changeText: { color: colors.alternativeTextColor },
|
||||||
|
});
|
||||||
|
return <Badge value={loc.cc.change} badgeStyle={oStyles.change} textStyle={oStyles.changeText} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutputList = ({
|
||||||
|
item: { address, txid, value, vout, confirmations },
|
||||||
balanceUnit = BitcoinUnit.BTC,
|
balanceUnit = BitcoinUnit.BTC,
|
||||||
oMemo,
|
oMemo,
|
||||||
frozen,
|
frozen,
|
||||||
change = false,
|
change,
|
||||||
full = false,
|
onOpen,
|
||||||
onPress,
|
selected,
|
||||||
|
selectionStarted,
|
||||||
|
onSelect,
|
||||||
|
onDeSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { txMetadata } = useContext(BlueStorageContext);
|
const { txMetadata } = useContext(BlueStorageContext);
|
||||||
const cs = useColorScheme();
|
|
||||||
const memo = oMemo || txMetadata[txid]?.memo || '';
|
const memo = oMemo || txMetadata[txid]?.memo || '';
|
||||||
const fullId = `${txid}:${vout}`;
|
|
||||||
const shortId = `${address.substring(0, 9)}...${address.substr(address.length - 9)}`;
|
const shortId = `${address.substring(0, 9)}...${address.substr(address.length - 9)}`;
|
||||||
const color = `#${txid.substring(0, 6)}`;
|
const color = `#${txid.substring(0, 6)}`;
|
||||||
const amount = formatBalance(value, balanceUnit, true);
|
const amount = formatBalance(value, balanceUnit, true);
|
||||||
|
|
||||||
const oStyles = StyleSheet.create({
|
const oStyles = StyleSheet.create({
|
||||||
containerFull: { paddingHorizontal: 0 },
|
container: { borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated },
|
||||||
avatar: { borderColor: 'white', borderWidth: 1 },
|
containerSelected: {
|
||||||
amount: { fontWeight: 'bold' },
|
borderBottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
memo: { fontSize: 13, marginTop: 3 },
|
backgroundColor: colors.ballOutgoingExpired,
|
||||||
changeLight: { backgroundColor: colors.buttonDisabledBackgroundColor },
|
borderTopLeftRadius: 10,
|
||||||
changeDark: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0 },
|
borderBottomLeftRadius: 10,
|
||||||
changeText: { color: colors.alternativeTextColor },
|
},
|
||||||
freezeLight: { backgroundColor: colors.redBG },
|
avatar: { borderColor: 'white', borderWidth: 1, backgroundColor: color },
|
||||||
freezeDark: { backgroundColor: colors.redBG, borderWidth: 0 },
|
amount: { fontWeight: 'bold', color: colors.foregroundColor },
|
||||||
freezeText: { color: colors.redText },
|
memo: { fontSize: 13, marginTop: 3, color: colors.alternativeTextColor },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let onPress = onOpen;
|
||||||
|
if (selectionStarted) {
|
||||||
|
onPress = selected ? onDeSelect : onSelect;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem bottomDivider onPress={onPress} containerStyle={selected ? oStyles.containerSelected : oStyles.container}>
|
||||||
bottomDivider
|
<Avatar
|
||||||
onPress={onPress}
|
rounded
|
||||||
containerStyle={[{ borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated }, full && oStyles.containerFull]}
|
overlayContainerStyle={oStyles.avatar}
|
||||||
>
|
onPress={selected ? onDeSelect : onSelect}
|
||||||
<Avatar rounded overlayContainerStyle={[oStyles.avatar, { backgroundColor: color }]} />
|
icon={selected ? { name: 'check' } : undefined}
|
||||||
|
/>
|
||||||
<ListItem.Content>
|
<ListItem.Content>
|
||||||
<ListItem.Title style={[oStyles.amount, { color: colors.foregroundColor }]}>{amount}</ListItem.Title>
|
<ListItem.Title style={oStyles.amount}>{amount}</ListItem.Title>
|
||||||
{full ? (
|
<ListItem.Subtitle style={oStyles.memo} numberOfLines={1}>
|
||||||
<>
|
|
||||||
{memo ? (
|
|
||||||
<>
|
|
||||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]}>{memo}</ListItem.Subtitle>
|
|
||||||
<BlueSpacing10 />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]}>{address}</ListItem.Subtitle>
|
|
||||||
<BlueSpacing10 />
|
|
||||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]}>{fullId}</ListItem.Subtitle>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]} numberOfLines={1}>
|
|
||||||
{memo || shortId}
|
{memo || shortId}
|
||||||
</ListItem.Subtitle>
|
</ListItem.Subtitle>
|
||||||
)}
|
|
||||||
</ListItem.Content>
|
</ListItem.Content>
|
||||||
{change && (
|
{change && <ChangeBadge />}
|
||||||
<Badge value={loc.cc.change} badgeStyle={oStyles[cs === 'dark' ? 'changeDark' : 'changeLight']} textStyle={oStyles.changeText} />
|
{frozen && <FrozenBadge />}
|
||||||
)}
|
|
||||||
{frozen && (
|
|
||||||
<Badge value={loc.cc.freeze} badgeStyle={oStyles[cs === 'dark' ? 'freezeDark' : 'freezeLight']} textStyle={oStyles.freezeText} />
|
|
||||||
)}
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Output.propTypes = {
|
OutputList.propTypes = {
|
||||||
item: PropTypes.shape({
|
item: PropTypes.shape({
|
||||||
address: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
txid: PropTypes.string.isRequired,
|
txid: PropTypes.string.isRequired,
|
||||||
value: PropTypes.number.isRequired,
|
value: PropTypes.number.isRequired,
|
||||||
vout: PropTypes.number.isRequired,
|
vout: PropTypes.number.isRequired,
|
||||||
|
confirmations: PropTypes.number.isRequired,
|
||||||
}),
|
}),
|
||||||
balanceUnit: PropTypes.string,
|
balanceUnit: PropTypes.string,
|
||||||
oMemo: PropTypes.string,
|
oMemo: PropTypes.string,
|
||||||
frozen: PropTypes.bool,
|
frozen: PropTypes.bool,
|
||||||
change: PropTypes.bool,
|
change: PropTypes.bool,
|
||||||
full: PropTypes.bool,
|
onOpen: PropTypes.func,
|
||||||
onPress: PropTypes.func,
|
selected: PropTypes.bool,
|
||||||
|
selectionStarted: PropTypes.bool,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
|
onDeSelect: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutputModal = ({ item: { address, txid, value, vout, confirmations }, balanceUnit = BitcoinUnit.BTC, oMemo }) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { txMetadata } = useContext(BlueStorageContext);
|
||||||
|
const memo = oMemo || txMetadata[txid]?.memo || '';
|
||||||
|
const fullId = `${txid}:${vout}`;
|
||||||
|
const color = `#${txid.substring(0, 6)}`;
|
||||||
|
const amount = formatBalance(value, balanceUnit, true);
|
||||||
|
|
||||||
|
const oStyles = StyleSheet.create({
|
||||||
|
container: { paddingHorizontal: 0, borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated },
|
||||||
|
avatar: { borderColor: 'white', borderWidth: 1, backgroundColor: color },
|
||||||
|
amount: { fontWeight: 'bold', color: colors.foregroundColor },
|
||||||
|
tranContainer: { paddingLeft: 20 },
|
||||||
|
tranText: { fontWeight: 'normal', fontSize: 13, color: colors.alternativeTextColor },
|
||||||
|
memo: { fontSize: 13, marginTop: 3, color: colors.alternativeTextColor },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem bottomDivider containerStyle={oStyles.container}>
|
||||||
|
<Avatar rounded overlayContainerStyle={oStyles.avatar} />
|
||||||
|
<ListItem.Content>
|
||||||
|
<ListItem.Title style={oStyles.amount}>
|
||||||
|
{amount}
|
||||||
|
<View style={oStyles.tranContainer}>
|
||||||
|
<Text style={oStyles.tranText}>{loc.formatString(loc.transactions.list_conf, { number: confirmations })}</Text>
|
||||||
|
</View>
|
||||||
|
</ListItem.Title>
|
||||||
|
{memo ? (
|
||||||
|
<>
|
||||||
|
<ListItem.Subtitle style={oStyles.memo}>{memo}</ListItem.Subtitle>
|
||||||
|
<BlueSpacing10 />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<ListItem.Subtitle style={oStyles.memo}>{address}</ListItem.Subtitle>
|
||||||
|
<BlueSpacing10 />
|
||||||
|
<ListItem.Subtitle style={oStyles.memo}>{fullId}</ListItem.Subtitle>
|
||||||
|
</ListItem.Content>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OutputModal.propTypes = {
|
||||||
|
item: PropTypes.shape({
|
||||||
|
address: PropTypes.string.isRequired,
|
||||||
|
txid: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
vout: PropTypes.number.isRequired,
|
||||||
|
confirmations: PropTypes.number.isRequired,
|
||||||
|
}),
|
||||||
|
balanceUnit: PropTypes.string,
|
||||||
|
oMemo: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mStyles = StyleSheet.create({
|
const mStyles = StyleSheet.create({
|
||||||
|
@ -137,28 +204,27 @@ const mStyles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const OutputModalContent = ({ output, wallet, onUseCoin }) => {
|
const OutputModalContent = ({ output, wallet, onUseCoin, frozen, setFrozen }) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { txMetadata, saveToDisk } = useContext(BlueStorageContext);
|
const { txMetadata, saveToDisk } = useContext(BlueStorageContext);
|
||||||
const [frozen, setFrozen] = useState(wallet.getUTXOMetadata(output.txid, output.vout).frozen || false);
|
|
||||||
const [memo, setMemo] = useState(wallet.getUTXOMetadata(output.txid, output.vout).memo || txMetadata[output.txid]?.memo || '');
|
const [memo, setMemo] = useState(wallet.getUTXOMetadata(output.txid, output.vout).memo || txMetadata[output.txid]?.memo || '');
|
||||||
const onMemoChange = value => setMemo(value);
|
const onMemoChange = value => setMemo(value);
|
||||||
const switchValue = useMemo(() => ({ value: frozen, onValueChange: value => setFrozen(value) }), [frozen, setFrozen]);
|
const switchValue = useMemo(() => ({ value: frozen, onValueChange: value => setFrozen(value) }), [frozen, setFrozen]);
|
||||||
|
|
||||||
// save on form change. Because effect called on each event, debounce it.
|
// save on form change. Because effect called on each event, debounce it.
|
||||||
const debouncedSave = useRef(
|
const debouncedSaveMemo = useRef(
|
||||||
debounce(async (frozen, memo) => {
|
debounce(async memo => {
|
||||||
wallet.setUTXOMetadata(output.txid, output.vout, { frozen, memo });
|
wallet.setUTXOMetadata(output.txid, output.vout, { memo });
|
||||||
await saveToDisk();
|
await saveToDisk();
|
||||||
}, 500),
|
}, 500),
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debouncedSave.current(frozen, memo);
|
debouncedSaveMemo.current(memo);
|
||||||
}, [frozen, memo]);
|
}, [memo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Output item={output} balanceUnit={wallet.getPreferredBalanceUnit()} full />
|
<OutputModal item={output} balanceUnit={wallet.getPreferredBalanceUnit()} />
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="OutputMemo"
|
testID="OutputMemo"
|
||||||
|
@ -189,18 +255,48 @@ OutputModalContent.propTypes = {
|
||||||
output: PropTypes.object,
|
output: PropTypes.object,
|
||||||
wallet: PropTypes.object,
|
wallet: PropTypes.object,
|
||||||
onUseCoin: PropTypes.func.isRequired,
|
onUseCoin: PropTypes.func.isRequired,
|
||||||
|
frozen: PropTypes.bool.isRequired,
|
||||||
|
setFrozen: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CoinControl = () => {
|
const CoinControl = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
const { walletId, onUTXOChoose } = useRoute().params;
|
const { walletId, onUTXOChoose } = useRoute().params;
|
||||||
const { wallets } = useContext(BlueStorageContext);
|
const { wallets, saveToDisk } = useContext(BlueStorageContext);
|
||||||
const wallet = wallets.find(w => w.getID() === walletId);
|
const wallet = wallets.find(w => w.getID() === walletId);
|
||||||
// sort by height ascending, txid , vout ascending
|
// sort by height ascending, txid , vout ascending
|
||||||
const utxo = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout);
|
const utxo = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout);
|
||||||
const [output, setOutput] = useState();
|
const [output, setOutput] = useState();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [frozen, setFrozen] = useState(
|
||||||
|
utxo.filter(output => wallet.getUTXOMetadata(output.txid, output.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// save frozen status. Because effect called on each event, debounce it.
|
||||||
|
const debouncedSaveFronen = useRef(
|
||||||
|
debounce(async frozen => {
|
||||||
|
utxo.forEach(({ txid, vout }) => {
|
||||||
|
wallet.setUTXOMetadata(txid, vout, { frozen: frozen.includes(`${txid}:${vout}`) });
|
||||||
|
});
|
||||||
|
await saveToDisk();
|
||||||
|
}, 500),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedSaveFronen.current(frozen);
|
||||||
|
}, [frozen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
wallet.fetchUtxo().then(() => {
|
||||||
|
const freshUtxo = wallet.getUtxo(true);
|
||||||
|
setFrozen(
|
||||||
|
freshUtxo.filter(output => wallet.getUTXOMetadata(output.txid, output.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`),
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [wallet, setLoading]);
|
||||||
|
|
||||||
const stylesHook = StyleSheet.create({
|
const stylesHook = StyleSheet.create({
|
||||||
tip: {
|
tip: {
|
||||||
|
@ -218,10 +314,6 @@ const CoinControl = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
wallet.fetchUtxo().then(() => setLoading(false));
|
|
||||||
}, [wallet, setLoading]);
|
|
||||||
|
|
||||||
const handleChoose = item => setOutput(item);
|
const handleChoose = item => setOutput(item);
|
||||||
|
|
||||||
const handleUseCoin = utxo => {
|
const handleUseCoin = utxo => {
|
||||||
|
@ -230,21 +322,60 @@ const CoinControl = () => {
|
||||||
onUTXOChoose(utxo);
|
onUTXOChoose(utxo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMassFreeze = () => {
|
||||||
|
if (allFrozen) {
|
||||||
|
setFrozen(f => f.filter(i => !selected.includes(i))); // unfreeze
|
||||||
|
} else {
|
||||||
|
setFrozen(f => [...new Set([...f, ...selected])]); // freeze
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMassUse = () => {
|
||||||
|
const fUtxo = utxo.filter(({ txid, vout }) => selected.includes(`${txid}:${vout}`));
|
||||||
|
handleUseCoin(fUtxo);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if any outputs are selected
|
||||||
|
const selectionStarted = selected.length > 0;
|
||||||
|
// check if all selected items are frozen
|
||||||
|
const allFrozen = selectionStarted && selected.reduce((prev, curr) => (prev ? frozen.includes(curr) : false), true);
|
||||||
|
const buttonFontSize = PixelRatio.roundToNearestPixel(width / 26) > 22 ? 22 : PixelRatio.roundToNearestPixel(width / 26);
|
||||||
|
|
||||||
const renderItem = p => {
|
const renderItem = p => {
|
||||||
const { memo, frozen } = wallet.getUTXOMetadata(p.item.txid, p.item.vout);
|
const { memo } = wallet.getUTXOMetadata(p.item.txid, p.item.vout);
|
||||||
const change = wallet.addressIsChange(p.item.address);
|
const change = wallet.addressIsChange(p.item.address);
|
||||||
|
const oFrozen = frozen.includes(`${p.item.txid}:${p.item.vout}`);
|
||||||
return (
|
return (
|
||||||
<Output
|
<OutputList
|
||||||
balanceUnit={wallet.getPreferredBalanceUnit()}
|
balanceUnit={wallet.getPreferredBalanceUnit()}
|
||||||
item={p.item}
|
item={p.item}
|
||||||
oMemo={memo}
|
oMemo={memo}
|
||||||
frozen={frozen}
|
frozen={oFrozen}
|
||||||
change={change}
|
change={change}
|
||||||
onPress={() => handleChoose(p.item)}
|
onOpen={() => handleChoose(p.item)}
|
||||||
|
selected={selected.includes(`${p.item.txid}:${p.item.vout}`)}
|
||||||
|
selectionStarted={selectionStarted}
|
||||||
|
onSelect={() => {
|
||||||
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // animate buttons show
|
||||||
|
setSelected(selected => [...selected, `${p.item.txid}:${p.item.vout}`]);
|
||||||
|
}}
|
||||||
|
onDeSelect={() => setSelected(selected => selected.filter(i => i !== `${p.item.txid}:${p.item.vout}`))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderOutputModalContent = () => {
|
||||||
|
const oFrozen = frozen.includes(`${output.txid}:${output.vout}`);
|
||||||
|
const setOFrozen = value => {
|
||||||
|
if (value) {
|
||||||
|
setFrozen(f => [...f, `${output.txid}:${output.vout}`]);
|
||||||
|
} else {
|
||||||
|
setFrozen(f => f.filter(i => i !== `${output.txid}:${output.vout}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <OutputModalContent output={output} wallet={wallet} onUseCoin={handleUseCoin} frozen={oFrozen} setFrozen={setOFrozen} />;
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={[styles.root, styles.center, { backgroundColor: colors.elevated }]}>
|
<SafeBlueArea style={[styles.root, styles.center, { backgroundColor: colors.elevated }]}>
|
||||||
|
@ -254,7 +385,7 @@ const CoinControl = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={[styles.root, { backgroundColor: colors.elevated }]}>
|
<View style={[styles.root, { backgroundColor: colors.elevated }]}>
|
||||||
{utxo.length === 0 && (
|
{utxo.length === 0 && (
|
||||||
<View style={styles.empty}>
|
<View style={styles.empty}>
|
||||||
<Text style={{ color: colors.foregroundColor }}>{loc.cc.empty}</Text>
|
<Text style={{ color: colors.foregroundColor }}>{loc.cc.empty}</Text>
|
||||||
|
@ -269,14 +400,37 @@ const CoinControl = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
|
||||||
<View style={[styles.modalContent, { backgroundColor: colors.elevated }]}>
|
<View style={[styles.modalContent, { backgroundColor: colors.elevated }]}>{output && renderOutputModalContent()}</View>
|
||||||
{output && <OutputModalContent output={output} wallet={wallet} onUseCoin={handleUseCoin} />}
|
|
||||||
</View>
|
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</BottomModal>
|
</BottomModal>
|
||||||
|
|
||||||
<FlatList ListHeaderComponent={tipCoins} data={utxo} renderItem={renderItem} keyExtractor={item => `${item.txid}:${item.vout}`} />
|
<FlatList
|
||||||
</SafeBlueArea>
|
ListHeaderComponent={tipCoins}
|
||||||
|
data={utxo}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={item => `${item.txid}:${item.vout}`}
|
||||||
|
contentInset={{ top: 0, left: 0, bottom: 70, right: 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectionStarted && (
|
||||||
|
<FContainer>
|
||||||
|
<FButton
|
||||||
|
onPress={handleMassFreeze}
|
||||||
|
text={allFrozen ? loc.cc.freezeLabel_un : loc.cc.freezeLabel}
|
||||||
|
icon={<Icon name="snowflake" size={buttonFontSize} type="font-awesome-5" color={colors.buttonAlternativeTextColor} />}
|
||||||
|
/>
|
||||||
|
<FButton
|
||||||
|
onPress={handleMassUse}
|
||||||
|
text={selected.length > 1 ? loc.cc.use_coins : loc.cc.use_coin}
|
||||||
|
icon={
|
||||||
|
<View style={styles.sendIcon}>
|
||||||
|
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FContainer>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -307,6 +461,9 @@ const styles = StyleSheet.create({
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginVertical: 24,
|
marginVertical: 24,
|
||||||
},
|
},
|
||||||
|
sendIcon: {
|
||||||
|
transform: [{ rotate: '225deg' }],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
CoinControl.navigationOptions = navigationStyle({
|
CoinControl.navigationOptions = navigationStyle({
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
BlueSpacing20,
|
BlueSpacing20,
|
||||||
BlueCard,
|
BlueCard,
|
||||||
BlueListItem,
|
BlueListItem,
|
||||||
BlueHeaderDefaultSubHooks,
|
BlueHeaderDefaultSub,
|
||||||
BlueText,
|
BlueText,
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import Biometric from '../../class/biometrics';
|
import Biometric from '../../class/biometrics';
|
||||||
|
@ -158,7 +158,7 @@ const EncryptStorage = () => {
|
||||||
<ScrollView contentContainerStyle={styles.root}>
|
<ScrollView contentContainerStyle={styles.root}>
|
||||||
{biometrics.isDeviceBiometricCapable && (
|
{biometrics.isDeviceBiometricCapable && (
|
||||||
<>
|
<>
|
||||||
<BlueHeaderDefaultSubHooks leftText={loc.settings.biometrics} rightComponent={null} />
|
<BlueHeaderDefaultSub leftText={loc.settings.biometrics} rightComponent={null} />
|
||||||
<BlueListItem
|
<BlueListItem
|
||||||
title={loc.formatString(loc.settings.encrypt_use, { type: biometrics.biometricsType })}
|
title={loc.formatString(loc.settings.encrypt_use, { type: biometrics.biometricsType })}
|
||||||
Component={TouchableWithoutFeedback}
|
Component={TouchableWithoutFeedback}
|
||||||
|
@ -170,7 +170,7 @@ const EncryptStorage = () => {
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<BlueHeaderDefaultSubHooks leftText={loc.settings.encrypt_tstorage} rightComponent={null} />
|
<BlueHeaderDefaultSub leftText={loc.settings.encrypt_tstorage} rightComponent={null} />
|
||||||
<BlueListItem
|
<BlueListItem
|
||||||
testID="EncyptedAndPasswordProtected"
|
testID="EncyptedAndPasswordProtected"
|
||||||
hideChevron
|
hideChevron
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ScrollView, StyleSheet, StatusBar } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
import navigationStyle from '../../components/navigationStyle';
|
import navigationStyle from '../../components/navigationStyle';
|
||||||
import { BlueListItem, BlueHeaderDefaultSubHooks } from '../../BlueComponents';
|
import { BlueListItem, BlueHeaderDefaultSub } from '../../BlueComponents';
|
||||||
import loc from '../../loc';
|
import loc from '../../loc';
|
||||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const Settings = () => {
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.root}>
|
<ScrollView style={styles.root}>
|
||||||
<StatusBar barStyle="default" />
|
<StatusBar barStyle="default" />
|
||||||
<BlueHeaderDefaultSubHooks leftText={loc.settings.header} rightComponent={null} />
|
<BlueHeaderDefaultSub leftText={loc.settings.header} />
|
||||||
<BlueListItem title={loc.settings.general} onPress={() => navigate('GeneralSettings')} chevron />
|
<BlueListItem title={loc.settings.general} onPress={() => navigate('GeneralSettings')} chevron />
|
||||||
<BlueListItem title={loc.settings.currency} onPress={() => navigate('Currency')} chevron />
|
<BlueListItem title={loc.settings.currency} onPress={() => navigate('Currency')} chevron />
|
||||||
<BlueListItem title={loc.settings.language} onPress={() => navigate('Language')} chevron />
|
<BlueListItem title={loc.settings.language} onPress={() => navigate('Language')} chevron />
|
||||||
|
|
|
@ -196,7 +196,7 @@ const TransactionsDetails = () => {
|
||||||
{tx.received && (
|
{tx.received && (
|
||||||
<>
|
<>
|
||||||
<BlueText style={[styles.rowCaption, stylesHooks.rowCaption]}>{loc.transactions.details_received}</BlueText>
|
<BlueText style={[styles.rowCaption, stylesHooks.rowCaption]}>{loc.transactions.details_received}</BlueText>
|
||||||
<BlueText style={styles.rowValue}>{dayjs(tx.received).format('MM/DD/YYYY h:mm A')}</BlueText>
|
<BlueText style={styles.rowValue}>{dayjs(tx.received).format('LLL')}</BlueText>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -72,15 +72,16 @@ const WalletExport = () => {
|
||||||
return goBack();
|
return goBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!wallet.current.getUserHasSavedExport()) {
|
||||||
|
wallet.current.setUserHasSavedExport(true);
|
||||||
|
saveToDisk();
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
task.cancel();
|
task.cancel();
|
||||||
Privacy.disableBlur();
|
Privacy.disableBlur();
|
||||||
wallet.current.setUserHasSavedExport(true);
|
|
||||||
saveToDisk();
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [goBack, walletID]),
|
}, [goBack, walletID]),
|
||||||
|
|
|
@ -24,15 +24,14 @@ import ActionSheet from '../ActionSheet';
|
||||||
import Clipboard from '@react-native-community/clipboard';
|
import Clipboard from '@react-native-community/clipboard';
|
||||||
import loc from '../../loc';
|
import loc from '../../loc';
|
||||||
import { FContainer, FButton } from '../../components/FloatButtons';
|
import { FContainer, FButton } from '../../components/FloatButtons';
|
||||||
import { getSystemName, isTablet } from 'react-native-device-info';
|
import { isTablet } from 'react-native-device-info';
|
||||||
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
||||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||||
import isCatalyst from 'react-native-is-catalyst';
|
import { isCatalyst, isMacCatalina } from '../../blue_modules/environment';
|
||||||
|
|
||||||
const A = require('../../blue_modules/analytics');
|
const A = require('../../blue_modules/analytics');
|
||||||
const fs = require('../../blue_modules/fs');
|
const fs = require('../../blue_modules/fs');
|
||||||
const WalletsListSections = { CAROUSEL: 'CAROUSEL', LOCALTRADER: 'LOCALTRADER', TRANSACTIONS: 'TRANSACTIONS' };
|
const WalletsListSections = { CAROUSEL: 'CAROUSEL', LOCALTRADER: 'LOCALTRADER', TRANSACTIONS: 'TRANSACTIONS' };
|
||||||
const isDesktop = getSystemName() === 'Mac OS X';
|
|
||||||
|
|
||||||
const WalletsList = () => {
|
const WalletsList = () => {
|
||||||
const walletsCarousel = useRef();
|
const walletsCarousel = useRef();
|
||||||
|
@ -234,7 +233,7 @@ const WalletsList = () => {
|
||||||
const renderLocalTrader = () => {
|
const renderLocalTrader = () => {
|
||||||
if (carouselData.every(wallet => wallet === false)) return null;
|
if (carouselData.every(wallet => wallet === false)) return null;
|
||||||
if (carouselData.length > 0 && !carouselData.some(wallet => wallet.type === PlaceholderWallet.type)) {
|
if (carouselData.length > 0 && !carouselData.some(wallet => wallet.type === PlaceholderWallet.type)) {
|
||||||
return (
|
const button = (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigate('HodlHodl', { screen: 'HodlHodl' });
|
navigate('HodlHodl', { screen: 'HodlHodl' });
|
||||||
|
@ -250,6 +249,7 @@ const WalletsList = () => {
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
return isLargeScreen ? <SafeAreaView>{button}</SafeAreaView> : button;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -324,7 +324,7 @@ const WalletsList = () => {
|
||||||
<FContainer>
|
<FContainer>
|
||||||
<FButton
|
<FButton
|
||||||
onPress={onScanButtonPressed}
|
onPress={onScanButtonPressed}
|
||||||
onLongPress={isDesktop ? undefined : sendButtonLongPress}
|
onLongPress={isMacCatalina ? undefined : sendButtonLongPress}
|
||||||
icon={<Image resizeMode="stretch" source={scanImage} />}
|
icon={<Image resizeMode="stretch" source={scanImage} />}
|
||||||
text={loc.send.details_scan}
|
text={loc.send.details_scan}
|
||||||
/>
|
/>
|
||||||
|
@ -340,7 +340,7 @@ const WalletsList = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScanButtonPressed = () => {
|
const onScanButtonPressed = () => {
|
||||||
if (isDesktop) {
|
if (isMacCatalina) {
|
||||||
fs.showActionSheet().then(onBarScanned);
|
fs.showActionSheet().then(onBarScanned);
|
||||||
} else {
|
} else {
|
||||||
navigate('ScanQRCodeRoot', {
|
navigate('ScanQRCodeRoot', {
|
||||||
|
@ -368,7 +368,7 @@ const WalletsList = () => {
|
||||||
const sendButtonLongPress = async () => {
|
const sendButtonLongPress = async () => {
|
||||||
const isClipboardEmpty = (await Clipboard.getString()).replace(' ', '').length === 0;
|
const isClipboardEmpty = (await Clipboard.getString()).replace(' ', '').length === 0;
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
if (isDesktop) {
|
if (isMacCatalina) {
|
||||||
fs.showActionSheet().then(onBarScanned);
|
fs.showActionSheet().then(onBarScanned);
|
||||||
} else {
|
} else {
|
||||||
const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan];
|
const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan];
|
||||||
|
@ -436,7 +436,7 @@ const WalletsList = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.root} onLayout={onLayout}>
|
<View style={styles.root} onLayout={onLayout}>
|
||||||
<StatusBar barStyle="default" />
|
<StatusBar barStyle="default" />
|
||||||
<View style={[styles.walletsListWrapper, stylesHook.walletsListWrapper]}>
|
<View style={[styles.walletsListWrapper, stylesHook.walletsListWrapper]}>
|
||||||
<SectionList
|
<SectionList
|
||||||
|
@ -456,7 +456,7 @@ const WalletsList = () => {
|
||||||
/>
|
/>
|
||||||
{renderScanButton()}
|
{renderScanButton()}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,8 @@ import {
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon, Badge } from 'react-native-elements';
|
||||||
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { getSystemName } from 'react-native-device-info';
|
import { getSystemName } from 'react-native-device-info';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -116,6 +115,16 @@ const ViewEditMultisigCosigners = () => {
|
||||||
wordText: {
|
wordText: {
|
||||||
color: colors.labelText,
|
color: colors.labelText,
|
||||||
},
|
},
|
||||||
|
tipKeys: {
|
||||||
|
color: colors.alternativeTextColor,
|
||||||
|
},
|
||||||
|
tipLabel: {
|
||||||
|
backgroundColor: colors.inputBackgroundColor,
|
||||||
|
borderColor: colors.inputBackgroundColor,
|
||||||
|
},
|
||||||
|
tipLabelText: {
|
||||||
|
color: colors.buttonTextColor,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
|
@ -449,31 +458,68 @@ const ViewEditMultisigCosigners = () => {
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.root, stylesHook.root]}>
|
<View style={[styles.root, stylesHook.root]}>
|
||||||
<BlueLoading />
|
<BlueLoading />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const howMany = (
|
||||||
|
<Badge
|
||||||
|
value={wallet.getM()}
|
||||||
|
badgeStyle={[styles.tipLabel, stylesHook.tipLabel]}
|
||||||
|
textStyle={[styles.tipLabelText, stylesHook.tipLabelText]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const andHere = (
|
||||||
|
<Badge
|
||||||
|
value={wallet.howManySignaturesCanWeMake()}
|
||||||
|
badgeStyle={[styles.tipLabel, stylesHook.tipLabel]}
|
||||||
|
textStyle={[styles.tipLabelText, stylesHook.tipLabelText]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tipKeys = () => {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<BlueSpacing20 />
|
||||||
|
<Text style={[styles.tipKeys, stylesHook.tipKeys]}>
|
||||||
|
{loc.formatString(loc.multisig.signatures_required_to_spend, { number: howMany })}
|
||||||
|
{loc.formatString(loc.multisig.signatures_we_can_make, { number: andHere })}
|
||||||
|
</Text>
|
||||||
|
<BlueSpacing10 />
|
||||||
|
<BlueSpacing20 />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const footer = <BlueButton disabled={vaultKeyData.isLoading || isSaveButtonDisabled} title={loc._.save} onPress={onSave} />;
|
const footer = <BlueButton disabled={vaultKeyData.isLoading || isSaveButtonDisabled} title={loc._.save} onPress={onSave} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.root, stylesHook.root]}>
|
<View style={[styles.root, stylesHook.root]}>
|
||||||
<StatusBar barStyle="default" />
|
<StatusBar barStyle="light-content" />
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
enabled
|
enabled
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : null}
|
behavior={Platform.OS === 'ios' ? 'padding' : null}
|
||||||
keyboardVerticalOffset={62}
|
keyboardVerticalOffset={62}
|
||||||
style={[styles.mainBlock, styles.root]}
|
style={[styles.mainBlock, styles.root]}
|
||||||
>
|
>
|
||||||
<FlatList data={data.current} extraData={vaultKeyData} renderItem={_renderKeyItem} keyExtractor={(_item, index) => `${index}`} />
|
<FlatList
|
||||||
|
ListHeaderComponent={tipKeys}
|
||||||
|
data={data.current}
|
||||||
|
extraData={vaultKeyData}
|
||||||
|
renderItem={_renderKeyItem}
|
||||||
|
keyExtractor={(_item, index) => `${index}`}
|
||||||
|
/>
|
||||||
<BlueSpacing10 />
|
<BlueSpacing10 />
|
||||||
{footer}
|
{footer}
|
||||||
|
<BlueSpacing40 />
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
{renderProvideMnemonicsModal()}
|
{renderProvideMnemonicsModal()}
|
||||||
|
|
||||||
{renderMnemonicsModal()}
|
{renderMnemonicsModal()}
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -571,6 +617,20 @@ const styles = StyleSheet.create({
|
||||||
header2Text: { color: '#9AA0AA', fontSize: 14, paddingBottom: 20 },
|
header2Text: { color: '#9AA0AA', fontSize: 14, paddingBottom: 20 },
|
||||||
alignItemsCenter: { alignItems: 'center' },
|
alignItemsCenter: { alignItems: 'center' },
|
||||||
squareButtonWrapper: { height: 50, width: 250 },
|
squareButtonWrapper: { height: 50, width: 250 },
|
||||||
|
tipKeys: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '600',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
tipLabel: {
|
||||||
|
width: 30,
|
||||||
|
marginRight: 6,
|
||||||
|
position: 'relative',
|
||||||
|
bottom: -3,
|
||||||
|
},
|
||||||
|
tipLabelText: {
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ViewEditMultisigCosigners.navigationOptions = navigationStyle({
|
ViewEditMultisigCosigners.navigationOptions = navigationStyle({
|
||||||
|
|
|
@ -1510,6 +1510,65 @@ describe('multisig-wallet (native segwit)', () => {
|
||||||
assert.strictEqual(w.getDerivationPath(), '');
|
assert.strictEqual(w.getDerivationPath(), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can import from specter-desktop/fullynoded (p2sh-p2wsh)', () => {
|
||||||
|
// @see https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Wallets/Wallet-Export-Spec.md
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
JSON.stringify({
|
||||||
|
label: 'nested2of3',
|
||||||
|
blockheight: 481824,
|
||||||
|
descriptor:
|
||||||
|
'sh(wsh(sortedmulti(2,[99fe7770/48h/0h/0h/1h]xpub6FEEbEaYM9pmY8rxz4g6AuaJVszwKt8g6cFg9nFWeE85EdBrGBcnhHqXAaPbQ4Hi3Xu9vijtYdYnNjERw9eSniF3235Vjde11GieeHjv7XT/0/*,[636bdad0/48h/0h/0h/1h]xpub6F67TyyWngU5rkVPxHTdmuYkaXHXeRwwVg5SsDeiPPjt6Mithh4Qzpu2yHjNa5W7nhcTbV6QaJMvppYMDSnB3SxArCkp9GvHQqpr5P17yFv/0/*,[99c90b2f/48h/0h/0h/1h]xpub6E6FyTrwmuUYeRMULXSAGvUKeP5ba6pQKVhWNuvVZFmGPnDYb9m5vP2XsSEQ4gKUGfXtLcKs4AV31vpfx2P5KuWm9co4HM3FtGov8enmJ6f/0/*)))#wy7xtlnw',
|
||||||
|
}),
|
||||||
|
'sh(wsh(sortedmulti(2,[99fe7770/48h/0h/0h/1h]xpub6FEEbEaYM9pmY8rxz4g6AuaJVszwKt8g6cFg9nFWeE85EdBrGBcnhHqXAaPbQ4Hi3Xu9vijtYdYnNjERw9eSniF3235Vjde11GieeHjv7XT/0/*,[636bdad0/48h/0h/0h/1h]xpub6F67TyyWngU5rkVPxHTdmuYkaXHXeRwwVg5SsDeiPPjt6Mithh4Qzpu2yHjNa5W7nhcTbV6QaJMvppYMDSnB3SxArCkp9GvHQqpr5P17yFv/0/*,[99c90b2f/48h/0h/0h/1h]xpub6E6FyTrwmuUYeRMULXSAGvUKeP5ba6pQKVhWNuvVZFmGPnDYb9m5vP2XsSEQ4gKUGfXtLcKs4AV31vpfx2P5KuWm9co4HM3FtGov8enmJ6f/0/*)))#wy7xtlnw',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const secret of secrets) {
|
||||||
|
const w = new MultisigHDWallet();
|
||||||
|
w.setSecret(secret);
|
||||||
|
assert.strictEqual(w.getM(), 2);
|
||||||
|
assert.strictEqual(w.getN(), 3);
|
||||||
|
assert.strictEqual(w._getExternalAddressByIndex(0), '3GSZaKT3LujScx6JeWejc6xjZsCDRzptsA');
|
||||||
|
assert.strictEqual(w._getExternalAddressByIndex(1), '3GT11kStn8W6q2kj257uZqW9xEKJwPMDkw');
|
||||||
|
assert.ok(w.getLabel() === 'nested2of3' || w.getLabel() === 'Multisig vault');
|
||||||
|
assert.ok(w.isWrappedSegwit());
|
||||||
|
assert.ok(!w.isNativeSegwit());
|
||||||
|
assert.ok(!w.isLegacy());
|
||||||
|
|
||||||
|
assert.strictEqual(w.getFingerprint(1), '99FE7770');
|
||||||
|
assert.strictEqual(w.getFingerprint(2), '636BDAD0');
|
||||||
|
assert.strictEqual(w.getFingerprint(3), '99C90B2F');
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
w.getCosigner(1),
|
||||||
|
'xpub6FEEbEaYM9pmY8rxz4g6AuaJVszwKt8g6cFg9nFWeE85EdBrGBcnhHqXAaPbQ4Hi3Xu9vijtYdYnNjERw9eSniF3235Vjde11GieeHjv7XT',
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
w.getCosigner(2),
|
||||||
|
'xpub6F67TyyWngU5rkVPxHTdmuYkaXHXeRwwVg5SsDeiPPjt6Mithh4Qzpu2yHjNa5W7nhcTbV6QaJMvppYMDSnB3SxArCkp9GvHQqpr5P17yFv',
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
w.getCosigner(3),
|
||||||
|
'xpub6E6FyTrwmuUYeRMULXSAGvUKeP5ba6pQKVhWNuvVZFmGPnDYb9m5vP2XsSEQ4gKUGfXtLcKs4AV31vpfx2P5KuWm9co4HM3FtGov8enmJ6f',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(w.getCustomDerivationPathForCosigner(1), "m/48'/0'/0'/1'");
|
||||||
|
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), "m/48'/0'/0'/1'");
|
||||||
|
assert.strictEqual(w.getCustomDerivationPathForCosigner(3), "m/48'/0'/0'/1'");
|
||||||
|
assert.strictEqual(w.getDerivationPath(), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ww = new MultisigHDWallet();
|
||||||
|
ww.addCosigner('equal emotion skin exchange scale inflict half expose awkward deliver series broken');
|
||||||
|
ww.addCosigner('spatial road snack luggage buddy media seek charge people pool neither family');
|
||||||
|
ww.addCosigner('sing author lyrics expand ladder embody frost rapid survey similar flight unknown');
|
||||||
|
ww.setM(2);
|
||||||
|
ww.setDerivationPath("m/48'/0'/0'/1'");
|
||||||
|
ww.setWrappedSegwit();
|
||||||
|
assert.strictEqual(ww._getExternalAddressByIndex(0), '3GSZaKT3LujScx6JeWejc6xjZsCDRzptsA');
|
||||||
|
assert.strictEqual(ww.getFingerprint(1), '99FE7770');
|
||||||
|
});
|
||||||
|
|
||||||
it('can edit cosigners', () => {
|
it('can edit cosigners', () => {
|
||||||
const path = "m/48'/0'/0'/2'";
|
const path = "m/48'/0'/0'/2'";
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue