diff --git a/BlueComponents.js b/BlueComponents.js index b7879481d..b0f169471 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -163,19 +163,32 @@ export class BlueWalletNavigationHeader extends Component { onWalletUnitChange: PropTypes.func, }; - static getDerivedStateFromProps(props, _state) { - return { wallet: props.wallet, onWalletUnitChange: props.onWalletUnitChange }; + static getDerivedStateFromProps(props, state) { + return { wallet: props.wallet, onWalletUnitChange: props.onWalletUnitChange, allowOnchainAddress: state.allowOnchainAddress }; } constructor(props) { super(props); - this.state = { wallet: props.wallet, walletPreviousPreferredUnit: props.wallet.getPreferredBalanceUnit() }; + this.state = { + wallet: props.wallet, + walletPreviousPreferredUnit: props.wallet.getPreferredBalanceUnit(), + showManageFundsButton: false, + }; } handleCopyPress = _item => { Clipboard.setString(loc.formatBalance(this.state.wallet.getBalance(), this.state.wallet.getPreferredBalanceUnit()).toString()); }; + componentDidMount() { + if (this.state.wallet.type === LightningCustodianWallet.type) { + this.state.wallet + .allowOnchainAddress() + .then(value => this.setState({ allowOnchainAddress: value })) + .catch(e => console.log('This Lndhub wallet does not have an onchain address API.')); + } + } + handleBalanceVisibility = async _item => { const wallet = this.state.wallet; @@ -311,7 +324,7 @@ export class BlueWalletNavigationHeader extends Component { )} - {this.state.wallet.type === LightningCustodianWallet.type && ( + {this.state.wallet.type === LightningCustodianWallet.type && this.state.allowOnchainAddress && ( { + this.walletsCarousel.current.snapToItem(item); + }; + render() { return ( { - WalletsCarousel.carousel = c; - }} + ref={this.walletsCarousel} renderItem={this._renderItem} sliderWidth={sliderWidth} sliderHeight={sliderHeight} diff --git a/android/app/build.gradle b/android/app/build.gradle index f8e209d51..77257b908 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -119,7 +119,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "4.8.2" + versionName "4.9.0" multiDexEnabled true missingDimensionStrategy 'react-native-camera', 'general' } diff --git a/class/lightning-custodian-wallet.js b/class/lightning-custodian-wallet.js index fcac7ca79..f4b0fad5c 100644 --- a/class/lightning-custodian-wallet.js +++ b/class/lightning-custodian-wallet.js @@ -366,6 +366,15 @@ export class LightningCustodianWallet extends LegacyWallet { return this.fetchBtcAddress(); } + async allowOnchainAddress() { + if (this.getAddress() !== undefined) { + return true; + } else { + await this.fetchBtcAddress(); + return this.getAddress() !== undefined; + } + } + getTransactions() { let txs = []; this.pending_transactions_raw = this.pending_transactions_raw || []; diff --git a/ios/BlueWallet/Info.plist b/ios/BlueWallet/Info.plist index 0363ef3c7..b3534f6ee 100644 --- a/ios/BlueWallet/Info.plist +++ b/ios/BlueWallet/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.8.2 + 4.9.0 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/BlueWalletWatch Extension/Info.plist b/ios/BlueWalletWatch Extension/Info.plist index a50252f59..b9b8ee2ad 100644 --- a/ios/BlueWalletWatch Extension/Info.plist +++ b/ios/BlueWalletWatch Extension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 4.8.2 + 4.9.0 CFBundleVersion 239 CLKComplicationPrincipalClass diff --git a/ios/BlueWalletWatch/Info.plist b/ios/BlueWalletWatch/Info.plist index e8bc70dce..26f0c6790 100644 --- a/ios/BlueWalletWatch/Info.plist +++ b/ios/BlueWalletWatch/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.8.2 + 4.9.0 CFBundleVersion 239 UISupportedInterfaceOrientations diff --git a/ios/TodayExtension/Info.plist b/ios/TodayExtension/Info.plist index 685983291..eebccd34a 100644 --- a/ios/TodayExtension/Info.plist +++ b/ios/TodayExtension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.8.2 + 4.9.0 CFBundleVersion 1 NSExtension diff --git a/ios/fastlane/metadata/en-US/release_notes.txt b/ios/fastlane/metadata/en-US/release_notes.txt index 46aa75559..960466d0e 100644 --- a/ios/fastlane/metadata/en-US/release_notes.txt +++ b/ios/fastlane/metadata/en-US/release_notes.txt @@ -1,3 +1,17 @@ +v4.9.0 +====== + +* ADD: Native segwit (BIP84) are now default wallets +* ADD: Toggle to turn off RBF when creating a transaction +* ADD: Scroll to end of wallets list when adding a wallet +* FIX: Default LN invoice expiry is now 24h instead of 1h +* FIX: Speeded up lighnint wallets +* FIX: Force Light theme mode even if system is in dark mode +* FIX: Hide Manage Funds button if wallet doesn't allow onchain refill. +* FIX: LN Scan to receive is more visible +* FIX: Quick actions not appearing on non-3d touch devices. +* FIX: Dont show clipboard modal when biometrics is dismissed + v4.8.1 ====== @@ -36,20 +50,4 @@ v4.7.0 * FIX: electrum connection * FIX: Now able to use biometrics with encrypted storage (not for unlocking) * FIX: LApp marketplace address is now editable -* FIX: single address watch-only wallet Receive button crash - -v4.6.0 -====== - -* ADD: Optional biometrics unlock (this does not exclude full encryption) -* ADD: Cryptoadvance HW wallet support (for BIP84) via PSBT and QR codes -* ADD: LN Refill with external wallet -* ADD: Default into wallet on launch -* FIX: NaN when sending onchain -* FIX: zero on send success screen -* FIX: Time shown for top-most transaction -* FIX: minor issue with scanQrWif -* FIX: typo on NL language -* FIX: better wallet export QR readability -* FIX: RBF tx memo porting -* REF: better initial HD rescan \ No newline at end of file +* FIX: single address watch-only wallet Receive button crash \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 631b427d5..e3fe32f6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "BlueWallet", - "version": "4.8.2", + "version": "4.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4ae3d32d3..8a3617722 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BlueWallet", - "version": "4.8.2", + "version": "4.9.0", "devDependencies": { "@babel/core": "^7.5.0", "@babel/runtime": "^7.5.1", diff --git a/screen/wallets/add.js b/screen/wallets/add.js index 1f610a43e..7b3c7ff3e 100644 --- a/screen/wallets/add.js +++ b/screen/wallets/add.js @@ -173,14 +173,14 @@ export default class WalletsAdd extends Component { {loc.settings.advanced_options} this.onSelect(index, value)} selectedIndex={0}> - - {HDSegwitP2SHWallet.typeReadable} - Multiple addresses + + {HDSegwitBech32Wallet.typeReadable} - Multiple addresses {SegwitP2SHWallet.typeReadable} - Single address - - {HDSegwitBech32Wallet.typeReadable} - Multiple addresses + + {HDSegwitP2SHWallet.typeReadable} - Multiple addresses @@ -220,108 +220,104 @@ export default class WalletsAdd extends Component { title={loc.wallets.add.create} disabled={this.state.activeBitcoin === undefined} onPress={() => { - this.setState( - { isLoading: true }, - async () => { - let w; + this.setState({ isLoading: true }, async () => { + let w; - if (this.state.activeLightning) { - // eslint-disable-next-line + if (this.state.activeLightning) { + // eslint-disable-next-line - this.createLightningWallet = async () => { - w = new LightningCustodianWallet(); - w.setLabel(this.state.label || loc.wallets.details.title); + this.createLightningWallet = async () => { + w = new LightningCustodianWallet(); + w.setLabel(this.state.label || loc.wallets.details.title); - try { - let lndhub = - this.state.walletBaseURI.trim().length > 0 - ? this.state.walletBaseURI - : LightningCustodianWallet.defaultBaseUri; - if (lndhub) { - const isValidNodeAddress = await LightningCustodianWallet.isValidNodeAddress(lndhub); - if (isValidNodeAddress) { - w.setBaseURI(lndhub); - w.init(); - } else { - throw new Error('The provided node address is not valid LNDHub node.'); - } + try { + let lndhub = + this.state.walletBaseURI.trim().length > 0 + ? this.state.walletBaseURI + : LightningCustodianWallet.defaultBaseUri; + if (lndhub) { + const isValidNodeAddress = await LightningCustodianWallet.isValidNodeAddress(lndhub); + if (isValidNodeAddress) { + w.setBaseURI(lndhub); + w.init(); + } else { + throw new Error('The provided node address is not valid LNDHub node.'); } - await w.createAccount(); - await w.authorize(); - } catch (Err) { - this.setState({ isLoading: false }); - console.warn('lnd create failure', Err); - return alert(Err); - // giving app, not adding anything } - A(A.ENUM.CREATED_LIGHTNING_WALLET); - await w.generate(); - BlueApp.wallets.push(w); - await BlueApp.saveToDisk(); - EV(EV.enum.WALLETS_COUNT_CHANGED); - A(A.ENUM.CREATED_WALLET); - ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); - this.props.navigation.dismiss(); - }; - - if (!BlueApp.getWallets().some(wallet => wallet.type !== LightningCustodianWallet.type)) { - Alert.alert( - loc.wallets.add.lightning, - loc.wallets.createBitcoinWallet, - [ - { - text: loc.send.details.cancel, - style: 'cancel', - onPress: () => { - this.setState({ isLoading: false }); - }, - }, - { - text: loc._.ok, - style: 'default', - onPress: () => { - this.createLightningWallet(); - }, - }, - ], - { cancelable: false }, - ); - } else { - this.createLightningWallet(); + await w.createAccount(); + await w.authorize(); + } catch (Err) { + this.setState({ isLoading: false }); + console.warn('lnd create failure', Err); + return alert(Err); + // giving app, not adding anything } - } else if (this.state.selectedIndex === 2) { - // btc was selected - // index 2 radio - hd bip84 - w = new HDSegwitBech32Wallet(); - w.setLabel(this.state.label || loc.wallets.details.title); - } else if (this.state.selectedIndex === 1) { - // btc was selected - // index 1 radio - segwit single address - w = new SegwitP2SHWallet(); - w.setLabel(this.state.label || loc.wallets.details.title); - } else { - // zero index radio - HD segwit - w = new HDSegwitP2SHWallet(); - w.setLabel(this.state.label || loc.wallets.details.title); - } - if (this.state.activeBitcoin) { + A(A.ENUM.CREATED_LIGHTNING_WALLET); await w.generate(); BlueApp.wallets.push(w); await BlueApp.saveToDisk(); EV(EV.enum.WALLETS_COUNT_CHANGED); A(A.ENUM.CREATED_WALLET); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); - if (w.type === HDSegwitP2SHWallet.type || w.type === HDSegwitBech32Wallet.type) { - this.props.navigation.navigate('PleaseBackup', { - secret: w.getSecret(), - }); - } else { - this.props.navigation.dismiss(); - } + this.props.navigation.dismiss(); + }; + + if (!BlueApp.getWallets().some(wallet => wallet.type !== LightningCustodianWallet.type)) { + Alert.alert( + loc.wallets.add.lightning, + loc.wallets.createBitcoinWallet, + [ + { + text: loc.send.details.cancel, + style: 'cancel', + onPress: () => { + this.setState({ isLoading: false }); + }, + }, + { + text: loc._.ok, + style: 'default', + onPress: () => { + this.createLightningWallet(); + }, + }, + ], + { cancelable: false }, + ); + } else { + this.createLightningWallet(); } - }, - 1, - ); + } else if (this.state.selectedIndex === 2) { + // zero index radio - HD segwit + w = new HDSegwitP2SHWallet(); + w.setLabel(this.state.label || loc.wallets.details.title); + } else if (this.state.selectedIndex === 1) { + // btc was selected + // index 1 radio - segwit single address + w = new SegwitP2SHWallet(); + w.setLabel(this.state.label || loc.wallets.details.title); + } else { + // btc was selected + // index 2 radio - hd bip84 + w = new HDSegwitBech32Wallet(); + w.setLabel(this.state.label || loc.wallets.details.title); + } + if (this.state.activeBitcoin) { + await w.generate(); + BlueApp.wallets.push(w); + await BlueApp.saveToDisk(); + EV(EV.enum.WALLETS_COUNT_CHANGED); + A(A.ENUM.CREATED_WALLET); + ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); + if (w.type === HDSegwitP2SHWallet.type || w.type === HDSegwitBech32Wallet.type) { + this.props.navigation.navigate('PleaseBackup', { + secret: w.getSecret(), + }); + } else { + this.props.navigation.dismiss(); + } + } + }); }} /> ) : ( diff --git a/screen/wallets/import.js b/screen/wallets/import.js index 5ff344f52..e952200a5 100644 --- a/screen/wallets/import.js +++ b/screen/wallets/import.js @@ -97,6 +97,16 @@ export default class WalletsImport extends Component { // trying other wallet types + let hd4 = new HDSegwitBech32Wallet(); + hd4.setSecret(text); + if (hd4.validateMnemonic()) { + await hd4.fetchBalance(); + if (hd4.getBalance() > 0) { + await hd4.fetchTransactions(); + return this._saveWallet(hd4); + } + } + let segwitWallet = new SegwitP2SHWallet(); segwitWallet.setSecret(text); if (segwitWallet.getAddress()) { @@ -130,16 +140,6 @@ export default class WalletsImport extends Component { // if we're here - nope, its not a valid WIF - let hd4 = new HDSegwitBech32Wallet(); - hd4.setSecret(text); - if (hd4.validateMnemonic()) { - await hd4.fetchBalance(); - if (hd4.getBalance() > 0) { - await hd4.fetchTransactions(); - return this._saveWallet(hd4); - } - } - let hd1 = new HDLegacyBreadwalletWallet(); hd1.setSecret(text); if (hd1.validateMnemonic()) { diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 5b61a538c..2a6ad3750 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -29,6 +29,8 @@ export default class WalletsList extends Component { ), }); + walletsCarousel = React.createRef(); + constructor(props) { super(props); this.state = { @@ -37,11 +39,12 @@ export default class WalletsList extends Component { wallets: BlueApp.getWallets().concat(false), lastSnappedTo: 0, }; - EV(EV.enum.WALLETS_COUNT_CHANGED, this.redrawScreen.bind(this)); + + EV(EV.enum.WALLETS_COUNT_CHANGED, () => this.redrawScreen(true)); // here, when we receive TRANSACTIONS_COUNT_CHANGED we do not query // remote server, we just redraw the screen - EV(EV.enum.TRANSACTIONS_COUNT_CHANGED, this.redrawScreen.bind(this)); + EV(EV.enum.TRANSACTIONS_COUNT_CHANGED, this.redrawScreen); } componentDidMount() { @@ -107,7 +110,7 @@ export default class WalletsList extends Component { ); } - redrawScreen() { + redrawScreen = (scrollToEnd = false) => { console.log('wallets/list redrawScreen()'); if (BlueApp.getBalance() !== 0) { A(A.ENUM.GOT_NONZERO_BALANCE); @@ -115,13 +118,25 @@ export default class WalletsList extends Component { A(A.ENUM.GOT_ZERO_BALANCE); } - this.setState({ - isLoading: false, - isFlatListRefreshControlHidden: true, - dataSource: BlueApp.getTransactions(null, 10), - wallets: BlueApp.getWallets().concat(false), - }); - } + const wallets = BlueApp.getWallets().concat(false); + if (scrollToEnd) { + scrollToEnd = wallets.length > this.state.wallets.length; + } + + this.setState( + { + isLoading: false, + isFlatListRefreshControlHidden: true, + dataSource: BlueApp.getTransactions(null, 10), + wallets: BlueApp.getWallets().concat(false), + }, + () => { + if (scrollToEnd) { + this.walletsCarousel.snapToItem(this.state.wallets.length - 1); + } + }, + ); + }; txMemo(hash) { if (BlueApp.tx_metadata[hash] && BlueApp.tx_metadata[hash]['memo']) { @@ -268,6 +283,7 @@ export default class WalletsList extends Component { onSnapToItem={index => { this.onSnapToItem(index); }} + ref={c => this.walletsCarousel = c} /> EV(EV.enum.WALLETS_COUNT_CHANGED), 500); this.setState({ isLoading: false }); - this.props.navigation.dismiss(); return; } } @@ -117,16 +117,16 @@ export default class ScanQrWif extends React.Component { BlueApp.wallets.push(hd); await BlueApp.saveToDisk(); alert(loc.wallets.import.success); + this.props.navigation.popToTop(); setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); this.setState({ isLoading: false }); - this.props.navigation.dismiss(); return; } } // nope - // is it HD mnemonic? - hd = new HDSegwitP2SHWallet(); + // is it HD BIP49 mnemonic? + hd = new HDSegwitBech32Wallet(); hd.setSecret(ret.data); if (hd.validateMnemonic()) { for (let w of BlueApp.wallets) { @@ -144,9 +144,9 @@ export default class ScanQrWif extends React.Component { await hd.fetchTransactions(); await BlueApp.saveToDisk(); alert(loc.wallets.import.success); + this.props.navigation.popToTop(); setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); this.setState({ isLoading: false }); - this.props.navigation.dismiss(); return; } // nope @@ -179,11 +179,11 @@ export default class ScanQrWif extends React.Component { BlueApp.wallets.push(lnd); lnd.setLabel(loc.wallets.import.imported + ' ' + lnd.typeReadable); + this.props.navigation.popToTop(); alert(loc.wallets.import.success); await BlueApp.saveToDisk(); setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); this.setState({ isLoading: false }); - this.props.navigation.dismiss(); return; } // nope @@ -208,9 +208,9 @@ export default class ScanQrWif extends React.Component { await watchOnly.fetchBalance(); await watchOnly.fetchTransactions(); await BlueApp.saveToDisk(); + this.props.navigation.popToTop(); setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); this.setState({ isLoading: false }); - this.props.navigation.dismiss(); return; } // nope @@ -235,8 +235,8 @@ export default class ScanQrWif extends React.Component { await newLegacyWallet.fetchBalance(); await newLegacyWallet.fetchTransactions(); await BlueApp.saveToDisk(); + this.props.navigation.popToTop(); setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); - this.props.navigation.dismiss(); return; } @@ -257,7 +257,7 @@ export default class ScanQrWif extends React.Component { alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newWallet.getAddress()); } await BlueApp.saveToDisk(); - this.props.navigation.dismiss(); + this.props.navigation.popToTop(); setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); }; // end @@ -337,8 +337,8 @@ export default class ScanQrWif extends React.Component { ScanQrWif.propTypes = { navigation: PropTypes.shape({ - dismiss: PropTypes.func, goBack: PropTypes.func, + popToTop: PropTypes.func, navigate: PropTypes.func, }), }; diff --git a/tests/integration/hd-segwit-bech32-wallet.test.js b/tests/integration/hd-segwit-bech32-wallet.test.js index d4f8828a4..5b2c0ee98 100644 --- a/tests/integration/hd-segwit-bech32-wallet.test.js +++ b/tests/integration/hd-segwit-bech32-wallet.test.js @@ -52,7 +52,7 @@ describe('Bech32 Segwit HD (BIP84)', () => { // checking that internal pointer and async address getter return the same address let freeAddress = await hd.getAddressAsync(); - assert.strictEqual(hd.next_free_address_index, 0); + assert.strictEqual(hd.next_free_address_index, 1); // someone ACTUALLY used this example mnemonic lol assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress); let freeChangeAddress = await hd.getChangeAddressAsync(); assert.strictEqual(hd.next_free_change_address_index, 0);