|
@ -1,6 +1,7 @@ |
|
|
import React, { Component } from 'react'; |
|
|
import React, { useEffect, useState, useCallback } from 'react'; |
|
|
import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native'; |
|
|
import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native'; |
|
|
import QRCode from 'react-native-qrcode-svg'; |
|
|
import QRCode from 'react-native-qrcode-svg'; |
|
|
|
|
|
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; |
|
|
import bip21 from 'bip21'; |
|
|
import bip21 from 'bip21'; |
|
|
import { |
|
|
import { |
|
|
BlueLoading, |
|
|
BlueLoading, |
|
@ -15,7 +16,6 @@ import { |
|
|
BlueSpacing20, |
|
|
BlueSpacing20, |
|
|
BlueAlertWalletExportReminder, |
|
|
BlueAlertWalletExportReminder, |
|
|
} from '../../BlueComponents'; |
|
|
} from '../../BlueComponents'; |
|
|
import PropTypes from 'prop-types'; |
|
|
|
|
|
import Privacy from '../../Privacy'; |
|
|
import Privacy from '../../Privacy'; |
|
|
import Share from 'react-native-share'; |
|
|
import Share from 'react-native-share'; |
|
|
import { Chain, BitcoinUnit } from '../../models/bitcoinUnits'; |
|
|
import { Chain, BitcoinUnit } from '../../models/bitcoinUnits'; |
|
@ -26,120 +26,113 @@ import Handoff from 'react-native-handoff'; |
|
|
const BlueApp = require('../../BlueApp'); |
|
|
const BlueApp = require('../../BlueApp'); |
|
|
const loc = require('../../loc'); |
|
|
const loc = require('../../loc'); |
|
|
|
|
|
|
|
|
export default class ReceiveDetails extends Component { |
|
|
const ReceiveDetails = () => { |
|
|
static navigationOptions = ({ navigation }) => ({ |
|
|
const secret = useNavigationParam('secret'); |
|
|
...BlueNavigationStyle(navigation, true), |
|
|
const [wallet, setWallet] = useState(); |
|
|
title: loc.receive.header, |
|
|
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false); |
|
|
headerLeft: null, |
|
|
const [address, setAddress] = useState(''); |
|
|
}); |
|
|
const [customLabel, setCustomLabel] = useState(); |
|
|
|
|
|
const [customAmount, setCustomAmount] = useState(0); |
|
|
constructor(props) { |
|
|
const [bip21encoded, setBip21encoded] = useState(); |
|
|
super(props); |
|
|
const [qrCodeSVG, setQrCodeSVG] = useState(); |
|
|
let secret = props.navigation.state.params.secret || ''; |
|
|
const [isCustom, setIsCustom] = useState(false); |
|
|
|
|
|
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false); |
|
|
|
|
|
const { navigate, goBack } = useNavigation(); |
|
|
|
|
|
|
|
|
this.state = { |
|
|
const renderReceiveDetails = useCallback(async () => { |
|
|
secret: secret, |
|
|
console.log('receive/details - componentDidMount'); |
|
|
isHandOffUseEnabled: false, |
|
|
wallet.setUserHasSavedExport(true); |
|
|
address: '', |
|
|
|
|
|
customLabel: '', |
|
|
|
|
|
customAmount: 0, |
|
|
|
|
|
bip21encoded: undefined, |
|
|
|
|
|
isCustom: false, |
|
|
|
|
|
isCustomModalVisible: false, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
renderReceiveDetails = async () => { |
|
|
|
|
|
this.wallet.setUserHasSavedExport(true); |
|
|
|
|
|
await BlueApp.saveToDisk(); |
|
|
await BlueApp.saveToDisk(); |
|
|
let address; |
|
|
let address; |
|
|
if (this.wallet.getAddressAsync) { |
|
|
if (wallet.getAddressAsync) { |
|
|
if (this.wallet.chain === Chain.ONCHAIN) { |
|
|
if (wallet.chain === Chain.ONCHAIN) { |
|
|
try { |
|
|
try { |
|
|
address = await Promise.race([this.wallet.getAddressAsync(), BlueApp.sleep(1000)]); |
|
|
address = await Promise.race([wallet.getAddressAsync(), BlueApp.sleep(1000)]); |
|
|
} catch (_) {} |
|
|
} catch (_) {} |
|
|
if (!address) { |
|
|
if (!address) { |
|
|
// either sleep expired or getAddressAsync threw an exception
|
|
|
// either sleep expired or getAddressAsync threw an exception
|
|
|
console.warn('either sleep expired or getAddressAsync threw an exception'); |
|
|
console.warn('either sleep expired or getAddressAsync threw an exception'); |
|
|
address = this.wallet._getExternalAddressByIndex(this.wallet.next_free_address_index); |
|
|
address = wallet._getExternalAddressByIndex(wallet.next_free_address_index); |
|
|
} else { |
|
|
} else { |
|
|
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
|
|
|
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
|
|
|
} |
|
|
} |
|
|
this.setState({ |
|
|
setAddress(address); |
|
|
address: address, |
|
|
} else if (wallet.chain === Chain.OFFCHAIN) { |
|
|
}); |
|
|
|
|
|
} else if (this.wallet.chain === Chain.OFFCHAIN) { |
|
|
|
|
|
try { |
|
|
try { |
|
|
await Promise.race([this.wallet.getAddressAsync(), BlueApp.sleep(1000)]); |
|
|
await Promise.race([wallet.getAddressAsync(), BlueApp.sleep(1000)]); |
|
|
address = this.wallet.getAddress(); |
|
|
address = wallet.getAddress(); |
|
|
} catch (_) {} |
|
|
} catch (_) {} |
|
|
if (!address) { |
|
|
if (!address) { |
|
|
// either sleep expired or getAddressAsync threw an exception
|
|
|
// either sleep expired or getAddressAsync threw an exception
|
|
|
console.warn('either sleep expired or getAddressAsync threw an exception'); |
|
|
console.warn('either sleep expired or getAddressAsync threw an exception'); |
|
|
address = this.wallet.getAddress(); |
|
|
address = wallet.getAddress(); |
|
|
} else { |
|
|
} else { |
|
|
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
|
|
|
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
this.setState({ |
|
|
setAddress(address); |
|
|
address: address, |
|
|
} else if (wallet.getAddress) { |
|
|
}); |
|
|
setAddress(wallet.getAddress()); |
|
|
} else if (this.wallet.getAddress) { |
|
|
|
|
|
this.setState({ |
|
|
|
|
|
address: this.wallet.getAddress(), |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
} |
|
|
InteractionManager.runAfterInteractions(async () => { |
|
|
InteractionManager.runAfterInteractions(async () => { |
|
|
const bip21encoded = bip21.encode(this.state.address); |
|
|
const bip21encoded = bip21.encode(address); |
|
|
this.setState({ bip21encoded }); |
|
|
setBip21encoded(bip21encoded); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}, [wallet]); |
|
|
|
|
|
|
|
|
componentDidMount() { |
|
|
useEffect(() => { |
|
|
Privacy.enableBlur(); |
|
|
Privacy.enableBlur(); |
|
|
console.log('receive/details - componentDidMount'); |
|
|
|
|
|
|
|
|
|
|
|
for (let w of BlueApp.getWallets()) { |
|
|
setWallet(BlueApp.getWallets().find(w => w.getSecret() === secret)); |
|
|
if (w.getSecret() === this.state.secret) { |
|
|
|
|
|
// found our wallet
|
|
|
if (wallet) { |
|
|
this.wallet = w; |
|
|
if (!wallet.getUserHasSavedExport()) { |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (this.wallet) { |
|
|
|
|
|
if (!this.wallet.getUserHasSavedExport()) { |
|
|
|
|
|
BlueAlertWalletExportReminder({ |
|
|
BlueAlertWalletExportReminder({ |
|
|
onSuccess: this.renderReceiveDetails, |
|
|
onSuccess: renderReceiveDetails, |
|
|
onFailure: () => { |
|
|
onFailure: () => { |
|
|
this.props.navigation.goBack(); |
|
|
goBack(); |
|
|
this.props.navigation.navigate('WalletExport', { |
|
|
navigate('WalletExport', { |
|
|
wallet: this.wallet, |
|
|
wallet: wallet, |
|
|
}); |
|
|
}); |
|
|
}, |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
} else { |
|
|
this.renderReceiveDetails(); |
|
|
renderReceiveDetails(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
HandoffSettings.isHandoffUseEnabled().then(value => this.setState({ isHandOffUseEnabled: value })); |
|
|
HandoffSettings.isHandoffUseEnabled().then(setIsHandOffUseEnabled); |
|
|
} |
|
|
return () => Privacy.disableBlur(); |
|
|
|
|
|
}, [goBack, navigate, renderReceiveDetails, secret, wallet]); |
|
|
|
|
|
|
|
|
componentWillUnmount() { |
|
|
const dismissCustomAmountModal = () => { |
|
|
Privacy.disableBlur(); |
|
|
Keyboard.dismiss(); |
|
|
} |
|
|
setIsCustomModalVisible(false); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
renderCustomAmountModal = () => { |
|
|
const showCustomAmountModal = () => { |
|
|
|
|
|
setIsCustomModalVisible(true); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const createCustomAmountAddress = () => { |
|
|
|
|
|
setIsCustom(true); |
|
|
|
|
|
setIsCustomModalVisible(false); |
|
|
|
|
|
setBip21encoded(bip21.encode(address, { amount: customAmount, label: customLabel })); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const clearCustomAmount = () => { |
|
|
|
|
|
setIsCustom(false); |
|
|
|
|
|
setIsCustomModalVisible(false); |
|
|
|
|
|
setCustomAmount(''); |
|
|
|
|
|
setCustomLabel(''); |
|
|
|
|
|
setBip21encoded(bip21.encode(address)); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const renderCustomAmountModal = () => { |
|
|
return ( |
|
|
return ( |
|
|
<Modal |
|
|
<Modal isVisible={isCustomModalVisible} style={styles.bottomModal} onBackdropPress={dismissCustomAmountModal}> |
|
|
isVisible={this.state.isCustomModalVisible} |
|
|
|
|
|
style={styles.bottomModal} |
|
|
|
|
|
onBackdropPress={() => { |
|
|
|
|
|
Keyboard.dismiss(); |
|
|
|
|
|
this.setState({ isCustomModalVisible: false }); |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}> |
|
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}> |
|
|
<View style={styles.modalContent}> |
|
|
<View style={styles.modalContent}> |
|
|
<BlueBitcoinAmount amount={this.state.customAmount || ''} onChangeText={text => this.setState({ customAmount: text })} /> |
|
|
<BlueBitcoinAmount amount={customAmount || ''} onChangeText={setCustomAmount} /> |
|
|
<View |
|
|
<View |
|
|
style={{ |
|
|
style={{ |
|
|
flexDirection: 'row', |
|
|
flexDirection: 'row', |
|
@ -157,38 +150,18 @@ export default class ReceiveDetails extends Component { |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
<TextInput |
|
|
<TextInput |
|
|
onChangeText={text => this.setState({ customLabel: text })} |
|
|
onChangeText={setCustomLabel} |
|
|
placeholder={loc.receive.details.label} |
|
|
placeholder={loc.receive.details.label} |
|
|
value={this.state.customLabel || ''} |
|
|
value={customLabel || ''} |
|
|
numberOfLines={1} |
|
|
numberOfLines={1} |
|
|
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} |
|
|
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} |
|
|
/> |
|
|
/> |
|
|
</View> |
|
|
</View> |
|
|
<BlueSpacing20 /> |
|
|
<BlueSpacing20 /> |
|
|
<View> |
|
|
<View> |
|
|
<BlueButton |
|
|
<BlueButton title={loc.receive.details.create} onPress={createCustomAmountAddress} /> |
|
|
title={loc.receive.details.create} |
|
|
|
|
|
onPress={() => { |
|
|
|
|
|
this.setState({ |
|
|
|
|
|
isCustom: true, |
|
|
|
|
|
isCustomModalVisible: false, |
|
|
|
|
|
bip21encoded: bip21.encode(this.state.address, { amount: this.state.customAmount, label: this.state.customLabel }), |
|
|
|
|
|
}); |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
<BlueSpacing20 /> |
|
|
<BlueSpacing20 /> |
|
|
<BlueButtonLink |
|
|
<BlueButtonLink title="Reset" onPress={clearCustomAmount} /> |
|
|
title="Reset" |
|
|
|
|
|
onPress={() => { |
|
|
|
|
|
this.setState({ |
|
|
|
|
|
isCustom: false, |
|
|
|
|
|
isCustomModalVisible: false, |
|
|
|
|
|
customAmount: '', |
|
|
|
|
|
customLabel: '', |
|
|
|
|
|
bip21encoded: bip21.encode(this.state.addresss), |
|
|
|
|
|
}); |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
</View> |
|
|
</View> |
|
|
<BlueSpacing20 /> |
|
|
<BlueSpacing20 /> |
|
|
</View> |
|
|
</View> |
|
@ -197,55 +170,66 @@ export default class ReceiveDetails extends Component { |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
showCustomAmountModal = () => { |
|
|
const handleShareButtonPressed = () => { |
|
|
this.setState({ isCustomModalVisible: true }); |
|
|
if (qrCodeSVG === undefined) { |
|
|
|
|
|
Share.open({ message: bip21encoded }).catch(error => console.log(error)); |
|
|
|
|
|
} else { |
|
|
|
|
|
InteractionManager.runAfterInteractions(async () => { |
|
|
|
|
|
qrCodeSVG.toDataURL(data => { |
|
|
|
|
|
let shareImageBase64 = { |
|
|
|
|
|
message: bip21encoded, |
|
|
|
|
|
url: `data:image/png;base64,${data}`, |
|
|
|
|
|
}; |
|
|
|
|
|
Share.open(shareImageBase64).catch(error => console.log(error)); |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
render() { |
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<SafeBlueArea style={{ flex: 1 }}> |
|
|
<SafeBlueArea style={{ flex: 1 }}> |
|
|
{this.state.isHandOffUseEnabled && this.state.address !== undefined && ( |
|
|
{isHandOffUseEnabled && address !== undefined && ( |
|
|
<Handoff |
|
|
<Handoff |
|
|
title={`Bitcoin Transaction ${this.state.address}`} |
|
|
title={`Bitcoin Transaction ${address}`} |
|
|
type="io.bluewallet.bluewallet" |
|
|
type="io.bluewallet.bluewallet" |
|
|
url={`https://blockstream.info/address/${this.state.address}`} |
|
|
url={`https://blockstream.info/address/${address}`} |
|
|
/> |
|
|
/> |
|
|
)} |
|
|
)} |
|
|
<ScrollView contentContainerStyle={{ justifyContent: 'space-between' }}> |
|
|
<ScrollView contentContainerStyle={{ justifyContent: 'space-between' }}> |
|
|
<View style={{ marginTop: 32, alignItems: 'center', paddingHorizontal: 16 }}> |
|
|
<View style={{ marginTop: 32, alignItems: 'center', paddingHorizontal: 16 }}> |
|
|
{this.state.isCustom && ( |
|
|
{isCustom && ( |
|
|
<> |
|
|
<> |
|
|
<BlueText |
|
|
<BlueText |
|
|
style={{ color: '#0c2550', fontWeight: '600', fontSize: 36, textAlign: 'center', paddingBottom: 24 }} |
|
|
style={{ color: '#0c2550', fontWeight: '600', fontSize: 36, textAlign: 'center', paddingBottom: 24 }} |
|
|
numberOfLines={1} |
|
|
numberOfLines={1} |
|
|
> |
|
|
> |
|
|
{this.state.customAmount} {BitcoinUnit.BTC} |
|
|
{customAmount} {BitcoinUnit.BTC} |
|
|
</BlueText> |
|
|
</BlueText> |
|
|
<BlueText style={{ color: '#0c2550', fontWeight: '600', textAlign: 'center', paddingBottom: 24 }} numberOfLines={1}> |
|
|
<BlueText style={{ color: '#0c2550', fontWeight: '600', textAlign: 'center', paddingBottom: 24 }} numberOfLines={1}> |
|
|
{this.state.customLabel} |
|
|
{customLabel} |
|
|
</BlueText> |
|
|
</BlueText> |
|
|
</> |
|
|
</> |
|
|
)} |
|
|
)} |
|
|
{this.state.bip21encoded === undefined ? ( |
|
|
{bip21encoded === undefined ? ( |
|
|
<View style={{ alignItems: 'center', width: 300, height: 300 }}> |
|
|
<View style={{ alignItems: 'center', width: 300, height: 300 }}> |
|
|
<BlueLoading /> |
|
|
<BlueLoading /> |
|
|
</View> |
|
|
</View> |
|
|
) : ( |
|
|
) : ( |
|
|
<QRCode |
|
|
<QRCode |
|
|
value={this.state.bip21encoded} |
|
|
value={bip21encoded} |
|
|
logo={require('../../img/qr-code.png')} |
|
|
logo={require('../../img/qr-code.png')} |
|
|
size={(is.ipad() && 300) || 300} |
|
|
size={(is.ipad() && 300) || 300} |
|
|
logoSize={90} |
|
|
logoSize={90} |
|
|
color={BlueApp.settings.foregroundColor} |
|
|
color={BlueApp.settings.foregroundColor} |
|
|
logoBackgroundColor={BlueApp.settings.brandingColor} |
|
|
logoBackgroundColor={BlueApp.settings.brandingColor} |
|
|
ecl={'H'} |
|
|
ecl={'H'} |
|
|
getRef={c => (this.qrCodeSVG = c)} |
|
|
getRef={setQrCodeSVG} |
|
|
/> |
|
|
/> |
|
|
)} |
|
|
)} |
|
|
<BlueCopyTextToClipboard text={this.state.isCustom ? this.state.bip21encoded : this.state.address} /> |
|
|
<BlueCopyTextToClipboard text={isCustom ? bip21encoded : address} /> |
|
|
</View> |
|
|
</View> |
|
|
<View style={{ alignItems: 'center', alignContent: 'flex-end', marginBottom: 24 }}> |
|
|
<View style={{ alignItems: 'center', alignContent: 'flex-end', marginBottom: 24 }}> |
|
|
<BlueButtonLink title={loc.receive.details.setAmount} onPress={this.showCustomAmountModal} /> |
|
|
<BlueButtonLink title={loc.receive.details.setAmount} onPress={showCustomAmountModal} /> |
|
|
<View> |
|
|
<View> |
|
|
<BlueButton |
|
|
<BlueButton |
|
|
icon={{ |
|
|
icon={{ |
|
@ -253,31 +237,24 @@ export default class ReceiveDetails extends Component { |
|
|
type: 'entypo', |
|
|
type: 'entypo', |
|
|
color: BlueApp.settings.buttonTextColor, |
|
|
color: BlueApp.settings.buttonTextColor, |
|
|
}} |
|
|
}} |
|
|
onPress={async () => { |
|
|
onPress={handleShareButtonPressed} |
|
|
if (this.qrCodeSVG === undefined) { |
|
|
|
|
|
Share.open({ message: this.state.bip21encoded }).catch(error => console.log(error)); |
|
|
|
|
|
} else { |
|
|
|
|
|
InteractionManager.runAfterInteractions(async () => { |
|
|
|
|
|
this.qrCodeSVG.toDataURL(data => { |
|
|
|
|
|
let shareImageBase64 = { |
|
|
|
|
|
message: this.state.bip21encoded, |
|
|
|
|
|
url: `data:image/png;base64,${data}`, |
|
|
|
|
|
}; |
|
|
|
|
|
Share.open(shareImageBase64).catch(error => console.log(error)); |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
title={loc.receive.details.share} |
|
|
title={loc.receive.details.share} |
|
|
/> |
|
|
/> |
|
|
</View> |
|
|
</View> |
|
|
</View> |
|
|
</View> |
|
|
{this.renderCustomAmountModal()} |
|
|
{renderCustomAmountModal()} |
|
|
</ScrollView> |
|
|
</ScrollView> |
|
|
</SafeBlueArea> |
|
|
</SafeBlueArea> |
|
|
); |
|
|
); |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
ReceiveDetails.navigationOptions = ({ navigation }) => ({ |
|
|
|
|
|
...BlueNavigationStyle(navigation, true), |
|
|
|
|
|
title: loc.receive.header, |
|
|
|
|
|
headerLeft: null, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
export default ReceiveDetails; |
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({ |
|
|
const styles = StyleSheet.create({ |
|
|
modalContent: { |
|
|
modalContent: { |
|
@ -296,15 +273,3 @@ const styles = StyleSheet.create({ |
|
|
margin: 0, |
|
|
margin: 0, |
|
|
}, |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
ReceiveDetails.propTypes = { |
|
|
|
|
|
navigation: PropTypes.shape({ |
|
|
|
|
|
goBack: PropTypes.func, |
|
|
|
|
|
navigate: PropTypes.func, |
|
|
|
|
|
state: PropTypes.shape({ |
|
|
|
|
|
params: PropTypes.shape({ |
|
|
|
|
|
secret: PropTypes.string, |
|
|
|
|
|
}), |
|
|
|
|
|
}), |
|
|
|
|
|
}), |
|
|
|
|
|
}; |
|
|
|
|
|