From 21f3c12b39533c36521654475aa78b9766db3871 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sun, 25 Aug 2024 14:21:45 -0400 Subject: [PATCH 01/35] FIX: Disable autoinit of notifications prior to user autorizaiton --- android/app/build.gradle | 2 ++ android/app/src/main/AndroidManifest.xml | 6 ++++++ android/build.gradle | 2 ++ blue_modules/notifications.js | 2 +- ios/BlueWallet/Info.plist | 4 ++++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 16b614d25..6f193baba 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -83,6 +83,8 @@ android { versionName "7.0.3" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + firebaseMessagingAutoInitEnabled false + firebaseAnalyticsCollectionEnabled false } lintOptions { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ade4b6cb1..7f0afb58f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -52,6 +52,12 @@ + + diff --git a/android/build.gradle b/android/build.gradle index 1f8d0f1ca..40c92e632 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -68,6 +68,8 @@ subprojects { defaultConfig { minSdkVersion 24 } + firebaseMessagingAutoInitEnabled false + firebaseAnalyticsCollectionEnabled false } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { diff --git a/blue_modules/notifications.js b/blue_modules/notifications.js index dc97ad4db..5aef08be3 100644 --- a/blue_modules/notifications.js +++ b/blue_modules/notifications.js @@ -109,7 +109,7 @@ function Notifications(props) { * - if you are not using remote notification or do not have Firebase installed, use this: * requestPermissions: Platform.OS === 'ios' */ - requestPermissions: true, + requestPermissions: Platform.OS === 'ios', }); } }); diff --git a/ios/BlueWallet/Info.plist b/ios/BlueWallet/Info.plist index 07d085561..a1e090e99 100644 --- a/ios/BlueWallet/Info.plist +++ b/ios/BlueWallet/Info.plist @@ -384,5 +384,9 @@ apiKey 17ba9059f676f1cc4f45d98182388b01 + FIREBASE_ANALYTICS_COLLECTION_ENABLED + +FIREBASE_MESSAGING_AUTO_INIT_ENABLED + From 2b0d1df559b3113c80c6cf9dd1bb6df6375c9ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Sun, 25 Aug 2024 14:56:22 -0400 Subject: [PATCH 02/35] Update build.gradle --- android/app/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6f193baba..16b614d25 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -83,8 +83,6 @@ android { versionName "7.0.3" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' - firebaseMessagingAutoInitEnabled false - firebaseAnalyticsCollectionEnabled false } lintOptions { From 42fabd8f9732c4a9646dd9e7b597dcca1ee28875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Sun, 25 Aug 2024 14:56:42 -0400 Subject: [PATCH 03/35] Update build.gradle --- android/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 40c92e632..1f8d0f1ca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -68,8 +68,6 @@ subprojects { defaultConfig { minSdkVersion 24 } - firebaseMessagingAutoInitEnabled false - firebaseAnalyticsCollectionEnabled false } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { From e111b42ee0674a0c4370f99a20980c74c167026b Mon Sep 17 00:00:00 2001 From: overtorment Date: Sun, 8 Sep 2024 13:30:00 +0100 Subject: [PATCH 04/35] REF: lnurl-pay to ts --- screen/lnd/{lnurlPay.js => lnurlPay.tsx} | 103 ++++++++++++----------- 1 file changed, 53 insertions(+), 50 deletions(-) rename screen/lnd/{lnurlPay.js => lnurlPay.tsx} (73%) diff --git a/screen/lnd/lnurlPay.js b/screen/lnd/lnurlPay.tsx similarity index 73% rename from screen/lnd/lnurlPay.js rename to screen/lnd/lnurlPay.tsx index d127bc573..12cff6524 100644 --- a/screen/lnd/lnurlPay.js +++ b/screen/lnd/lnurlPay.tsx @@ -1,5 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useRoute } from '@react-navigation/native'; +import { RouteProp, useRoute } from '@react-navigation/native'; import React, { useEffect, useState } from 'react'; import { I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Icon } from '@rneui/themed'; @@ -13,38 +13,40 @@ import Button from '../../components/Button'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; -import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics'; +import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { useStorage } from '../../hooks/context/useStorage'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; +import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet'; +import { TWallet } from '../../class/wallets/types'; +import { pop } from '../../NavigationService'; -/** - * if user has default currency - fiat, attempting to pay will trigger conversion from entered in input field fiat value - * to satoshi, and attempt to pay this satoshi value, which might be a little bit off from `min` & `max` values - * provided by LnUrl. thats why we cache initial precise conversion rate so the reverse conversion wont be off. - */ -const _cacheFiatToSat = {}; +type RouteParams = { + walletID: string; + lnurl: string; +}; -const LnurlPay = () => { +const _cacheFiatToSat: Record = {}; + +const LnurlPay: React.FC = () => { const { wallets } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); - const { walletID, lnurl } = useRoute().params; - /** @type {LightningCustodianWallet} */ - const wallet = wallets.find(w => w.getID() === walletID); - const [unit, setUnit] = useState(wallet.getPreferredBalanceUnit()); - const [isLoading, setIsLoading] = useState(true); - const [_LN, setLN] = useState(); - const [payButtonDisabled, setPayButtonDisabled] = useState(true); - const [payload, setPayload] = useState(); - const { setParams, pop, navigate } = useExtendedNavigation(); - const [amount, setAmount] = useState(); + const route = useRoute, string>>(); + const { walletID, lnurl } = route.params; + const wallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet; + const [unit, setUnit] = useState(wallet.getPreferredBalanceUnit()); + const [isLoading, setIsLoading] = useState(true); + const [_LN, setLN] = useState(); + const [payButtonDisabled, setPayButtonDisabled] = useState(true); + const [payload, setPayload] = useState(); + const { setParams, navigate } = useExtendedNavigation(); + const [amount, setAmount] = useState(); const { colors } = useTheme(); const stylesHook = StyleSheet.create({ root: { backgroundColor: colors.background, }, - walletWrapLabel: { color: colors.buttonAlternativeTextColor, }, @@ -68,18 +70,16 @@ const LnurlPay = () => { setLN(ln); setIsLoading(false); } - }, [lnurl, pop]); + }, [lnurl]); useEffect(() => { setPayButtonDisabled(isLoading); }, [isLoading]); useEffect(() => { - if (payload) { - /** @type {Lnurl} */ - const LN = _LN; - let originalSatAmount; - let newAmount = (originalSatAmount = LN.getMin()); + if (payload && _LN) { + let originalSatAmount: number | false; + let newAmount: number | boolean | string = (originalSatAmount = _LN.getMin()); if (!newAmount) { presentAlert({ message: 'Internal error: incorrect LNURL amount' }); return; @@ -90,22 +90,21 @@ const LnurlPay = () => { break; case BitcoinUnit.LOCAL_CURRENCY: newAmount = satoshiToLocalCurrency(newAmount, false); - _cacheFiatToSat[newAmount] = originalSatAmount; + _cacheFiatToSat[newAmount] = String(originalSatAmount); break; } - setAmount(newAmount); + setAmount(newAmount.toString()); } - }, [payload]); // eslint-disable-line react-hooks/exhaustive-deps + }, [payload, _LN, unit]); - const onWalletSelect = w => { + const onWalletSelect = (w: TWallet) => { setParams({ walletID: w.getID() }); pop(); }; const pay = async () => { setPayButtonDisabled(true); - /** @type {Lnurl} */ - const LN = _LN; + if (!_LN || !amount) return; const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { @@ -114,39 +113,41 @@ const LnurlPay = () => { } } - let amountSats = amount; + let amountSats: number | false; switch (unit) { case BitcoinUnit.SATS: - amountSats = parseInt(amountSats, 10); // nop + amountSats = parseInt(amount, 10); break; case BitcoinUnit.BTC: - amountSats = btcToSatoshi(amountSats); + amountSats = btcToSatoshi(amount); break; case BitcoinUnit.LOCAL_CURRENCY: - if (_cacheFiatToSat[amount]) { - amountSats = _cacheFiatToSat[amount]; + if (_cacheFiatToSat[String(amount)]) { + amountSats = parseInt(_cacheFiatToSat[amount], 10); } else { - amountSats = btcToSatoshi(fiatToBTC(amountSats)); + amountSats = btcToSatoshi(fiatToBTC(parseFloat(amount))); } break; + default: + throw new Error('Unknown unit type'); } - let bolt11payload; try { - let comment; - if (LN.getCommentAllowed()) { + let comment: string | undefined; + if (_LN.getCommentAllowed()) { comment = await prompt('Comment', '', false, 'plain-text'); } - bolt11payload = await LN.requestBolt11FromLnurlPayService(amountSats, comment); + const bolt11payload = await _LN.requestBolt11FromLnurlPayService(amountSats, comment); + // @ts-ignore fixme after lnurl.js converted to ts await wallet.payInvoice(bolt11payload.pr); + // @ts-ignore fixme after lnurl.js converted to ts const decoded = wallet.decodeInvoice(bolt11payload.pr); setPayButtonDisabled(false); - // success, probably triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); if (wallet.last_paid_invoice_result && wallet.last_paid_invoice_result.payment_preimage) { - await LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage); + await _LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage); } navigate('ScanLndInvoiceRoot', { @@ -158,12 +159,12 @@ const LnurlPay = () => { }, }); setIsLoading(false); - } catch (Err) { - console.log(Err.message); + } catch (err) { + console.log((err as Error).message); setIsLoading(false); setPayButtonDisabled(false); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - return presentAlert({ message: Err.message }); + return presentAlert({ message: (err as Error).message }); } }; @@ -198,15 +199,16 @@ const LnurlPay = () => { const renderGotPayload = () => { return ( - + @@ -234,7 +236,7 @@ const LnurlPay = () => { ); }; - return isLoading || wallet === undefined || amount === undefined ? ( + return isLoading || !wallet || amount === undefined ? ( @@ -246,6 +248,7 @@ const LnurlPay = () => { export default LnurlPay; const styles = StyleSheet.create({ + scrollviewContainer: { justifyContent: 'space-around' }, img: { width: 200, height: 200, alignSelf: 'center' }, alignSelfCenter: { alignSelf: 'center', From a61f8fcb297d97bac63f7d10c9ee5e521128cfe2 Mon Sep 17 00:00:00 2001 From: overtorment Date: Sun, 8 Sep 2024 14:37:52 +0100 Subject: [PATCH 05/35] REF: lnurl class to ts --- class/{lnurl.js => lnurl.ts} | 161 +++++++++++++++++++++++------------ 1 file changed, 107 insertions(+), 54 deletions(-) rename class/{lnurl.js => lnurl.ts} (64%) diff --git a/class/lnurl.js b/class/lnurl.ts similarity index 64% rename from class/lnurl.js rename to class/lnurl.ts index 75972c65d..0ee2401ed 100644 --- a/class/lnurl.js +++ b/class/lnurl.ts @@ -3,11 +3,51 @@ import bolt11 from 'bolt11'; import createHash from 'create-hash'; import { createHmac } from 'crypto'; import CryptoJS from 'crypto-js'; +// @ts-ignore import secp256k1 from 'secp256k1'; import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL +interface LnurlPayServicePayload { + callback: string; + fixed: boolean; + min: number; + max: number; + domain: string; + metadata: string; + description?: string; + image?: string; + amount: number; + commentAllowed?: number; +} + +interface LnurlPayServiceBolt11Payload { + pr: string; + successAction?: any; + disposable?: boolean; + tag: string; + metadata: any; + minSendable: number; + maxSendable: number; + callback: string; + commentAllowed: number; +} + +interface DecodedInvoice { + destination: string; + num_satoshis: string; + num_millisatoshis: string; + timestamp: string; + fallback_addr: string; + route_hints: any[]; + payment_hash?: string; + description_hash?: string; + cltv_expiry?: string; + expiry?: string; + description?: string; +} + /** * @see https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-pay.md */ @@ -16,7 +56,13 @@ export default class Lnurl { static TAG_WITHDRAW_REQUEST = 'withdrawRequest'; // type of LNURL static TAG_LOGIN_REQUEST = 'login'; // type of LNURL - constructor(url, AsyncStorage) { + private _lnurl: string; + private _lnurlPayServiceBolt11Payload: LnurlPayServiceBolt11Payload | false; + private _lnurlPayServicePayload: LnurlPayServicePayload | false; + private _AsyncStorage: any; + private _preimage: string | false; + + constructor(url: string, AsyncStorage?: any) { this._lnurl = url; this._lnurlPayServiceBolt11Payload = false; this._lnurlPayServicePayload = false; @@ -24,7 +70,7 @@ export default class Lnurl { this._preimage = false; } - static findlnurl(bodyOfText) { + static findlnurl(bodyOfText: string): string | null { const res = /^(?:http.*[&?]lightning=|lightning:)?(lnurl1[02-9ac-hj-np-z]+)/.exec(bodyOfText.toLowerCase()); if (res) { return res[1]; @@ -32,7 +78,7 @@ export default class Lnurl { return null; } - static getUrlFromLnurl(lnurlExample) { + static getUrlFromLnurl(lnurlExample: string): string | false { const found = Lnurl.findlnurl(lnurlExample); if (!found) { if (Lnurl.isLightningAddress(lnurlExample)) { @@ -49,22 +95,22 @@ export default class Lnurl { return Buffer.from(bech32.fromWords(decoded.words)).toString(); } - static isLnurl(url) { + static isLnurl(url: string): boolean { return Lnurl.findlnurl(url) !== null; } - static isOnionUrl(url) { + static isOnionUrl(url: string): boolean { return Lnurl.parseOnionUrl(url) !== null; } - static parseOnionUrl(url) { + static parseOnionUrl(url: string): [string, string] | null { const match = url.match(ONION_REGEX); if (match === null) return null; const [, baseURI, path] = match; return [baseURI, path]; } - async fetchGet(url) { + async fetchGet(url: string): Promise { const resp = await fetch(url, { method: 'GET' }); if (resp.status >= 300) { throw new Error('Bad response from server'); @@ -76,14 +122,14 @@ export default class Lnurl { return reply; } - decodeInvoice(invoice) { + decodeInvoice(invoice: string): DecodedInvoice { const { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice); - const decoded = { - destination: payeeNodeKey, + const decoded: DecodedInvoice = { + destination: payeeNodeKey ?? '', num_satoshis: satoshis ? satoshis.toString() : '0', num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0', - timestamp: timestamp.toString(), + timestamp: timestamp?.toString() ?? '', fallback_addr: '', route_hints: [], }; @@ -92,10 +138,10 @@ export default class Lnurl { const { tagName, data } = tags[i]; switch (tagName) { case 'payment_hash': - decoded.payment_hash = data; + decoded.payment_hash = String(data); break; case 'purpose_commit_hash': - decoded.description_hash = data; + decoded.description_hash = String(data); break; case 'min_final_cltv_expiry': decoded.cltv_expiry = data.toString(); @@ -104,21 +150,21 @@ export default class Lnurl { decoded.expiry = data.toString(); break; case 'description': - decoded.description = data; + decoded.description = String(data); break; } } if (!decoded.expiry) decoded.expiry = '3600'; // default - if (parseInt(decoded.num_satoshis, 10) === 0 && decoded.num_millisatoshis > 0) { - decoded.num_satoshis = (decoded.num_millisatoshis / 1000).toString(); + if (parseInt(decoded.num_satoshis, 10) === 0 && parseInt(decoded.num_millisatoshis, 10) > 0) { + decoded.num_satoshis = (parseInt(decoded.num_millisatoshis, 10) / 1000).toString(); } return decoded; } - async requestBolt11FromLnurlPayService(amountSat, comment = '') { + async requestBolt11FromLnurlPayService(amountSat: number, comment: string = ''): Promise { if (!this._lnurlPayServicePayload) throw new Error('this._lnurlPayServicePayload is not set'); if (!this._lnurlPayServicePayload.callback) throw new Error('this._lnurlPayServicePayload.callback is not set'); if (amountSat < this._lnurlPayServicePayload.min || amountSat > this._lnurlPayServicePayload.max) @@ -132,15 +178,13 @@ export default class Lnurl { ); const nonce = Math.floor(Math.random() * 2e16).toString(16); const separator = this._lnurlPayServicePayload.callback.indexOf('?') === -1 ? '?' : '&'; - if (this.getCommentAllowed() && comment && comment.length > this.getCommentAllowed()) { - comment = comment.substr(0, this.getCommentAllowed()); + if (this.getCommentAllowed() && comment && comment.length > (this.getCommentAllowed() as number)) { + comment = comment.substr(0, this.getCommentAllowed() as number); } if (comment) comment = `&comment=${encodeURIComponent(comment)}`; const urlToFetch = this._lnurlPayServicePayload.callback + separator + 'amount=' + Math.floor(amountSat * 1000) + '&nonce=' + nonce + comment; - this._lnurlPayServiceBolt11Payload = await this.fetchGet(urlToFetch); - if (this._lnurlPayServiceBolt11Payload.status === 'ERROR') - throw new Error(this._lnurlPayServiceBolt11Payload.reason || 'requestBolt11FromLnurlPayService() error'); + this._lnurlPayServiceBolt11Payload = (await this.fetchGet(urlToFetch)) as LnurlPayServiceBolt11Payload; // check pr description_hash, amount etc: const decoded = this.decodeInvoice(this._lnurlPayServiceBolt11Payload.pr); @@ -155,11 +199,12 @@ export default class Lnurl { return this._lnurlPayServiceBolt11Payload; } - async callLnurlPayService() { + async callLnurlPayService(): Promise { if (!this._lnurl) throw new Error('this._lnurl is not set'); const url = Lnurl.getUrlFromLnurl(this._lnurl); + if (!url) throw new Error('Invalid LNURL'); // calling the url - const reply = await this.fetchGet(url); + const reply = (await this.fetchGet(url)) as LnurlPayServiceBolt11Payload; if (reply.tag !== Lnurl.TAG_PAY_REQUEST) { throw new Error('lnurl-pay expected, found tag ' + reply.tag); @@ -168,8 +213,8 @@ export default class Lnurl { const data = reply; // parse metadata and extract things from it - let image; - let description; + let image: string | undefined; + let description: string | undefined; const kvs = JSON.parse(data.metadata); for (let i = 0; i < kvs.length; i++) { const [k, v] = kvs[i]; @@ -185,14 +230,15 @@ export default class Lnurl { } // setting the payment screen with the parameters - const min = Math.ceil((data.minSendable || 0) / 1000); - const max = Math.floor(data.maxSendable / 1000); + const min = Math.ceil((data?.minSendable ?? 0) / 1000); + const max = Math.floor((data?.maxSendable ?? 0) / 1000); this._lnurlPayServicePayload = { callback: data.callback, fixed: min === max, min, max, + // @ts-ignore idk domain: data.callback.match(/^(https|http):\/\/([^/]+)\//)[2], metadata: data.metadata, description, @@ -204,7 +250,7 @@ export default class Lnurl { return this._lnurlPayServicePayload; } - async loadSuccessfulPayment(paymentHash) { + async loadSuccessfulPayment(paymentHash: string): Promise { if (!paymentHash) throw new Error('No paymentHash provided'); let data; try { @@ -224,7 +270,7 @@ export default class Lnurl { return true; } - async storeSuccess(paymentHash, preimage) { + async storeSuccess(paymentHash: string, preimage: string | { data: Buffer }): Promise { if (typeof preimage === 'object') { preimage = Buffer.from(preimage.data).toString('hex'); } @@ -241,35 +287,39 @@ export default class Lnurl { ); } - getSuccessAction() { - return this._lnurlPayServiceBolt11Payload.successAction; + getSuccessAction(): any | undefined { + return this._lnurlPayServiceBolt11Payload && 'successAction' in this._lnurlPayServiceBolt11Payload + ? this._lnurlPayServiceBolt11Payload.successAction + : undefined; } - getDomain() { - return this._lnurlPayServicePayload.domain; + getDomain(): string | undefined { + return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.domain : undefined; } - getDescription() { - return this._lnurlPayServicePayload.description; + getDescription(): string | undefined { + return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.description : undefined; } - getImage() { - return this._lnurlPayServicePayload.image; + getImage(): string | undefined { + return this._lnurlPayServicePayload ? this._lnurlPayServicePayload.image : undefined; } - getLnurl() { + getLnurl(): string { return this._lnurl; } - getDisposable() { - return this._lnurlPayServiceBolt11Payload.disposable; + getDisposable(): boolean | undefined { + return this._lnurlPayServiceBolt11Payload && 'disposable' in this._lnurlPayServiceBolt11Payload + ? this._lnurlPayServiceBolt11Payload.disposable + : undefined; } - getPreimage() { + getPreimage(): string | false { return this._preimage; } - static decipherAES(ciphertextBase64, preimageHex, ivBase64) { + static decipherAES(ciphertextBase64: string, preimageHex: string, ivBase64: string): string { const iv = CryptoJS.enc.Base64.parse(ivBase64); const key = CryptoJS.enc.Hex.parse(preimageHex); return CryptoJS.AES.decrypt(Buffer.from(ciphertextBase64, 'base64').toString('hex'), key, { @@ -279,27 +329,30 @@ export default class Lnurl { }).toString(CryptoJS.enc.Utf8); } - getCommentAllowed() { - return this?._lnurlPayServicePayload?.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed, 10) : false; + getCommentAllowed(): number | false { + if (!this._lnurlPayServicePayload) return false; + return this._lnurlPayServicePayload.commentAllowed ? parseInt(this._lnurlPayServicePayload.commentAllowed.toString(), 10) : false; } - getMin() { - return this?._lnurlPayServicePayload?.min ? parseInt(this._lnurlPayServicePayload.min, 10) : false; + getMin(): number | false { + if (!this._lnurlPayServicePayload) return false; + return this._lnurlPayServicePayload.min ? parseInt(this._lnurlPayServicePayload.min.toString(), 10) : false; } - getMax() { - return this?._lnurlPayServicePayload?.max ? parseInt(this._lnurlPayServicePayload.max, 10) : false; + getMax(): number | false { + if (!this._lnurlPayServicePayload) return false; + return this._lnurlPayServicePayload.max ? parseInt(this._lnurlPayServicePayload.max.toString(), 10) : false; } - getAmount() { + getAmount(): number | false { return this.getMin(); } - authenticate(secret) { + authenticate(secret: string): Promise { return new Promise((resolve, reject) => { if (!this._lnurl) throw new Error('this._lnurl is not set'); - const url = parse(Lnurl.getUrlFromLnurl(this._lnurl), true); + const url = parse(Lnurl.getUrlFromLnurl(this._lnurl) || '', true); const hmac = createHmac('sha256', secret); hmac.on('readable', async () => { @@ -308,7 +361,7 @@ export default class Lnurl { if (!privateKey) return; const privateKeyBuf = Buffer.from(privateKey, 'hex'); const publicKey = secp256k1.publicKeyCreate(privateKeyBuf); - const signatureObj = secp256k1.sign(Buffer.from(url.query.k1, 'hex'), privateKeyBuf); + const signatureObj = secp256k1.sign(Buffer.from(url.query.k1 as string, 'hex'), privateKeyBuf); const derSignature = secp256k1.signatureExport(signatureObj.signature); const reply = await this.fetchGet(`${url.href}&sig=${derSignature.toString('hex')}&key=${publicKey.toString('hex')}`); @@ -326,7 +379,7 @@ export default class Lnurl { }); } - static isLightningAddress(address) { + static isLightningAddress(address: string) { // ensure only 1 `@` present: if (address.split('@').length !== 2) return false; const splitted = address.split('@'); From ff6b412219508d0a051bc5587026eb416db69a3f Mon Sep 17 00:00:00 2001 From: overtorment Date: Sun, 8 Sep 2024 15:32:55 +0100 Subject: [PATCH 06/35] REF: lnurl class to ts --- class/lnurl.ts | 6 +++--- screen/lnd/lnurlPaySuccess.tsx | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/class/lnurl.ts b/class/lnurl.ts index 0ee2401ed..677e378b9 100644 --- a/class/lnurl.ts +++ b/class/lnurl.ts @@ -3,7 +3,7 @@ import bolt11 from 'bolt11'; import createHash from 'create-hash'; import { createHmac } from 'crypto'; import CryptoJS from 'crypto-js'; -// @ts-ignore +// @ts-ignore theres no types for secp256k1 import secp256k1 from 'secp256k1'; import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api @@ -62,8 +62,8 @@ export default class Lnurl { private _AsyncStorage: any; private _preimage: string | false; - constructor(url: string, AsyncStorage?: any) { - this._lnurl = url; + constructor(url: string | false, AsyncStorage?: any) { + this._lnurl = url || ''; this._lnurlPayServiceBolt11Payload = false; this._lnurlPayServicePayload = false; this._AsyncStorage = AsyncStorage; diff --git a/screen/lnd/lnurlPaySuccess.tsx b/screen/lnd/lnurlPaySuccess.tsx index e6d5dd466..dde822d1a 100644 --- a/screen/lnd/lnurlPaySuccess.tsx +++ b/screen/lnd/lnurlPaySuccess.tsx @@ -43,6 +43,9 @@ const LnurlPaySuccess: React.FC = () => { switch (successAction.tag) { case 'aes': { const preimage = lnurl.getPreimage(); + if (!preimage) { + break; + } setMessage(Lnurl.decipherAES(successAction.ciphertext, preimage, successAction.iv)); setPreamble(successAction.description); break; From 5b662004dbef416aa19ee817becc76ca26ca3b18 Mon Sep 17 00:00:00 2001 From: overtorment Date: Sun, 8 Sep 2024 15:35:20 +0100 Subject: [PATCH 07/35] REF: auto linter fix --- components/TransactionsNavigationHeader.tsx | 2 +- components/WalletsCarousel.tsx | 48 +++++++++++---------- screen/send/SendDetails.tsx | 2 +- screen/transactions/TransactionDetails.tsx | 2 +- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/components/TransactionsNavigationHeader.tsx b/components/TransactionsNavigationHeader.tsx index 244d29c6b..a4ee6da7c 100644 --- a/components/TransactionsNavigationHeader.tsx +++ b/components/TransactionsNavigationHeader.tsx @@ -218,7 +218,7 @@ const TransactionsNavigationHeader: React.FC {wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY - ? (preferredFiatCurrency?.endPointKey ?? FiatUnit.USD) + ? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD : wallet.getPreferredBalanceUnit()} diff --git a/components/WalletsCarousel.tsx b/components/WalletsCarousel.tsx index c55b91eb4..489ebb5a3 100644 --- a/components/WalletsCarousel.tsx +++ b/components/WalletsCarousel.tsx @@ -248,7 +248,7 @@ export const WalletCarouselItem: React.FC = React.memo( return ( @@ -374,27 +374,31 @@ const WalletsCarousel = forwardRef((props const flatListRef = useRef>(null); - useImperativeHandle(ref, (): any => { - return { - scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params), - scrollToIndex: (params: { - animated?: boolean | null | undefined; - index: number; - viewOffset?: number | undefined; - viewPosition?: number | undefined; - }) => flatListRef.current?.scrollToIndex(params), - scrollToItem: (params: { - animated?: boolean | null | undefined; - item: any; - viewOffset?: number | undefined; - viewPosition?: number | undefined; - }) => flatListRef.current?.scrollToItem(params), - scrollToOffset: (params: { animated?: boolean | null | undefined; offset: number }) => flatListRef.current?.scrollToOffset(params), - recordInteraction: () => flatListRef.current?.recordInteraction(), - flashScrollIndicators: () => flatListRef.current?.flashScrollIndicators(), - getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(), - }; - }, []); + useImperativeHandle( + ref, + (): any => { + return { + scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params), + scrollToIndex: (params: { + animated?: boolean | null | undefined; + index: number; + viewOffset?: number | undefined; + viewPosition?: number | undefined; + }) => flatListRef.current?.scrollToIndex(params), + scrollToItem: (params: { + animated?: boolean | null | undefined; + item: any; + viewOffset?: number | undefined; + viewPosition?: number | undefined; + }) => flatListRef.current?.scrollToItem(params), + scrollToOffset: (params: { animated?: boolean | null | undefined; offset: number }) => flatListRef.current?.scrollToOffset(params), + recordInteraction: () => flatListRef.current?.recordInteraction(), + flashScrollIndicators: () => flatListRef.current?.flashScrollIndicators(), + getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(), + }; + }, + [], + ); const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => { console.debug('onScrollToIndexFailed'); diff --git a/screen/send/SendDetails.tsx b/screen/send/SendDetails.tsx index 225c574e2..b9b6899fb 100644 --- a/screen/send/SendDetails.tsx +++ b/screen/send/SendDetails.tsx @@ -110,7 +110,7 @@ const SendDetails = () => { 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 = utxo ? utxo.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 diff --git a/screen/transactions/TransactionDetails.tsx b/screen/transactions/TransactionDetails.tsx index 9374b519e..045e3e479 100644 --- a/screen/transactions/TransactionDetails.tsx +++ b/screen/transactions/TransactionDetails.tsx @@ -139,7 +139,7 @@ const TransactionDetails = () => { // okay, this txid _was_ with someone using payment codes, so we show the label edit dialog // and load user-defined alias for the pc if any - setCounterpartyLabel(counterpartyMetadata ? (counterpartyMetadata[foundPaymentCode]?.label ?? '') : ''); + setCounterpartyLabel(counterpartyMetadata ? counterpartyMetadata[foundPaymentCode]?.label ?? '' : ''); setIsCounterpartyLabelVisible(true); setPaymentCode(foundPaymentCode); } From 4a3068308458daabc2eff2f9075c07024925feaf Mon Sep 17 00:00:00 2001 From: overtorment Date: Sun, 8 Sep 2024 16:02:32 +0100 Subject: [PATCH 08/35] REF: deepscan --- class/lnurl.ts | 4 ++-- screen/lnd/lnurlPay.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/class/lnurl.ts b/class/lnurl.ts index 677e378b9..1435d66b3 100644 --- a/class/lnurl.ts +++ b/class/lnurl.ts @@ -230,8 +230,8 @@ export default class Lnurl { } // setting the payment screen with the parameters - const min = Math.ceil((data?.minSendable ?? 0) / 1000); - const max = Math.floor((data?.maxSendable ?? 0) / 1000); + const min = Math.ceil((data.minSendable ?? 0) / 1000); + const max = Math.floor((data.maxSendable ?? 0) / 1000); this._lnurlPayServicePayload = { callback: data.callback, diff --git a/screen/lnd/lnurlPay.tsx b/screen/lnd/lnurlPay.tsx index 12cff6524..019d8f884 100644 --- a/screen/lnd/lnurlPay.tsx +++ b/screen/lnd/lnurlPay.tsx @@ -35,7 +35,7 @@ const LnurlPay: React.FC = () => { const route = useRoute, string>>(); const { walletID, lnurl } = route.params; const wallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet; - const [unit, setUnit] = useState(wallet.getPreferredBalanceUnit()); + const [unit, setUnit] = useState(wallet?.getPreferredBalanceUnit() ?? BitcoinUnit.BTC); const [isLoading, setIsLoading] = useState(true); const [_LN, setLN] = useState(); const [payButtonDisabled, setPayButtonDisabled] = useState(true); From 24e160d03e11983de9c5b2bf4e41ec0baa39134d Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Mon, 9 Sep 2024 21:39:35 -0400 Subject: [PATCH 09/35] FIX: Support importing JSON but not opening from outside --- blue_modules/fs.ts | 7 +++++-- ios/BlueWallet/Info.plist | 22 ---------------------- screen/send/SendDetails.tsx | 2 +- screen/send/psbtWithHardwareWallet.js | 5 ++++- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/blue_modules/fs.ts b/blue_modules/fs.ts index 292f4b202..6869a8375 100644 --- a/blue_modules/fs.ts +++ b/blue_modules/fs.ts @@ -93,7 +93,10 @@ export const writeFileAndExport = async function (fileName: string, contents: st export const openSignedTransaction = async function (): Promise { try { const res = await DocumentPicker.pickSingle({ - type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles], + type: + Platform.OS === 'ios' + ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.json] + : [DocumentPicker.types.allFiles], }); return await _readPsbtFileIntoBase64(res.uri); @@ -160,7 +163,7 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri 'io.bluewallet.psbt.txn', 'io.bluewallet.backup', DocumentPicker.types.plainText, - 'public.json', + DocumentPicker.types.json, DocumentPicker.types.images, ] : [DocumentPicker.types.allFiles], diff --git a/ios/BlueWallet/Info.plist b/ios/BlueWallet/Info.plist index a1e090e99..d813525db 100644 --- a/ios/BlueWallet/Info.plist +++ b/ios/BlueWallet/Info.plist @@ -58,18 +58,6 @@ io.bluewallet.backup - - CFBundleTypeName - JSON File - CFBundleTypeRole - Editor - LSHandlerRank - Owner - LSItemContentTypes - - public.json - - CFBundleTypeIconFiles @@ -239,10 +227,6 @@ - UTTypeConformsTo - - public.json - UTTypeDescription BW COSIGNER UTTypeIconFiles @@ -288,8 +272,6 @@ JSON File UTTypeIconFiles - UTTypeIdentifier - public.json UTTypeTagSpecification public.filename-extension @@ -360,10 +342,6 @@ - UTTypeConformsTo - - public.json - UTTypeDescription BW COSIGNER UTTypeIconFiles diff --git a/screen/send/SendDetails.tsx b/screen/send/SendDetails.tsx index b9b6899fb..6c3544b9e 100644 --- a/screen/send/SendDetails.tsx +++ b/screen/send/SendDetails.tsx @@ -741,7 +741,7 @@ const SendDetails = () => { const res = await DocumentPicker.pickSingle({ type: Platform.OS === 'ios' - ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, 'public.json'] + ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, DocumentPicker.types.json] : [DocumentPicker.types.allFiles], }); diff --git a/screen/send/psbtWithHardwareWallet.js b/screen/send/psbtWithHardwareWallet.js index 35c6f8a63..b49a52c61 100644 --- a/screen/send/psbtWithHardwareWallet.js +++ b/screen/send/psbtWithHardwareWallet.js @@ -196,7 +196,10 @@ const PsbtWithHardwareWallet = () => { const openSignedTransaction = async () => { try { const res = await DocumentPicker.pickSingle({ - type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles], + type: + Platform.OS === 'ios' + ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.json] + : [DocumentPicker.types.allFiles], }); const file = await RNFS.readFile(res.uri); if (file) { From 3dfedf1bd0c4c7495fcc11545335db38c7d970a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Mon, 9 Sep 2024 23:53:00 -0400 Subject: [PATCH 10/35] Update build-ios-release-pullrequest.yml --- .github/workflows/build-ios-release-pullrequest.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-ios-release-pullrequest.yml b/.github/workflows/build-ios-release-pullrequest.yml index 11d17eddb..e150cccab 100644 --- a/.github/workflows/build-ios-release-pullrequest.yml +++ b/.github/workflows/build-ios-release-pullrequest.yml @@ -54,7 +54,7 @@ jobs: run: | bundle exec fastlane ios install_pods - name: Cache CocoaPods Pods - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} @@ -88,7 +88,7 @@ jobs: bundle exec fastlane ios setup_provisioning_profiles - name: Cache Provisioning Profiles id: cache_provisioning_profiles - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/Library/MobileDevice/Provisioning Profiles key: ${{ runner.os }}-provisioning-profiles-${{ github.sha }} @@ -131,7 +131,7 @@ jobs: - name: Build App run: bundle exec fastlane ios build_app_lane - name: Upload IPA as Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa @@ -154,7 +154,7 @@ jobs: ruby-version: 3.1.6 bundler-cache: true - name: Cache Ruby Gems - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} @@ -165,7 +165,7 @@ jobs: bundle config path vendor/bundle bundle install --jobs 4 --retry 3 --quiet - name: Download IPA from Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa path: ./ios/build From 4bc7e53a6ec5bb077d8c178b978f26e4f3ca92cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Mon, 9 Sep 2024 23:53:30 -0400 Subject: [PATCH 11/35] Update build-release-apk.yml --- .github/workflows/build-release-apk.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index a831c5ae9..3b2b0d164 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -25,7 +25,7 @@ jobs: node-version: 20 - name: Use npm caches - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} @@ -49,7 +49,7 @@ jobs: bundler-cache: true - name: Cache Ruby Gems - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} @@ -90,7 +90,7 @@ jobs: echo "APK_PATH=${APK_PATH}" >> $GITHUB_ENV - name: Upload APK as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: signed-apk path: ${{ env.APK_PATH }} @@ -114,7 +114,7 @@ jobs: run: bundle install --jobs 4 --retry 3 - name: Download APK artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: signed-apk From 29f82040775e38dab3c9a57691a05113a2b72f41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:25:47 +0000 Subject: [PATCH 12/35] Update dependency react-native-safe-area-context to v4.11.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b338d520..eac58ab07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ "react-native-randombytes": "3.6.1", "react-native-rate": "1.2.12", "react-native-reanimated": "3.15.1", - "react-native-safe-area-context": "4.10.9", + "react-native-safe-area-context": "4.11.0", "react-native-screens": "3.34.0", "react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc", "react-native-share": "10.2.1", @@ -13367,9 +13367,9 @@ } }, "node_modules/react-native-safe-area-context": { - "version": "4.10.9", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.10.9.tgz", - "integrity": "sha512-wz/JXV1kARWyP5q93PFNKQP03StVBimOK7rRYEJjM+blZdXbM6H7EP3XhQUb6OK620+0M1AzpcGgyTHvgSJNAw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.11.0.tgz", + "integrity": "sha512-Bg7bozxEB+ZS+H3tVYs5yY1cvxNXgR6nRQwpSMkYR9IN5CbxohLnSprrOPG/ostTCd4F6iCk0c51pExEhifSKQ==", "license": "MIT", "peerDependencies": { "react": "*", diff --git a/package.json b/package.json index c94eebb40..5a83c4717 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "react-native-rate": "1.2.12", "react-native-reanimated": "3.15.1", "@react-native/metro-config": "0.75.2", - "react-native-safe-area-context": "4.10.9", + "react-native-safe-area-context": "4.11.0", "react-native-screens": "3.34.0", "react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc", "react-native-share": "10.2.1", From fdfb6d11cfae187db971ca64ec9294afbe6a6c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Tue, 10 Sep 2024 17:19:31 -0400 Subject: [PATCH 13/35] REF: Add Wallet advanced options (#7023) --- .../build-ios-release-pullrequest.yml | 10 +- class/blue-app.ts | 14 +- components/Context/SettingsProvider.tsx | 27 -- components/TooltipMenu.tsx | 25 +- components/TransactionListItem.tsx | 4 +- components/addresses/AddressItem.tsx | 2 +- components/types.ts | 12 +- fastlane/Fastfile | 21 +- loc/en.json | 8 +- navigation/AddWalletStack.tsx | 4 +- navigation/DetailViewScreensStack.tsx | 8 +- screen/receive/details.js | 184 +++++++---- screen/settings/GeneralSettings.tsx | 17 +- screen/wallets/Add.tsx | 298 +++++++++--------- screen/wallets/ViewEditMultisigCosigners.tsx | 63 ++-- screen/wallets/WalletsAddMultisig.tsx | 23 +- screen/wallets/addMultisigStep2.js | 56 ++-- screen/wallets/details.js | 52 ++- screen/wallets/import.js | 111 ++++--- tests/e2e/bluewallet.spec.js | 32 +- tests/e2e/bluewallet2.spec.js | 1 + tests/e2e/helperz.js | 8 - typings/CommonToolTipActions.ts | 60 +++- 23 files changed, 586 insertions(+), 454 deletions(-) diff --git a/.github/workflows/build-ios-release-pullrequest.yml b/.github/workflows/build-ios-release-pullrequest.yml index e150cccab..1055e1040 100644 --- a/.github/workflows/build-ios-release-pullrequest.yml +++ b/.github/workflows/build-ios-release-pullrequest.yml @@ -134,11 +134,11 @@ jobs: uses: actions/upload-artifact@v4 with: name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa - path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa + path: ./build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa testflight-upload: needs: build - runs-on: macos-14 + runs-on: macos-latest if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight') env: APPLE_ID: ${{ secrets.APPLE_ID }} @@ -168,12 +168,12 @@ jobs: uses: actions/download-artifact@v4 with: name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa - path: ./ios/build + path: ./ - name: Create App Store Connect API Key JSON - run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./ios/appstore_api_key.json + run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json - name: Upload to TestFlight env: - APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/ios/appstore_api_key.p8 + APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8 MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }} GIT_URL: ${{ secrets.GIT_URL }} diff --git a/class/blue-app.ts b/class/blue-app.ts index eb40b7581..e77b7451a 100644 --- a/class/blue-app.ts +++ b/class/blue-app.ts @@ -67,13 +67,12 @@ const isReactNative = typeof navigator !== 'undefined' && navigator?.product === export class BlueApp { static FLAG_ENCRYPTED = 'data_encrypted'; static LNDHUB = 'lndhub'; - static ADVANCED_MODE_ENABLED = 'advancedmodeenabled'; static DO_NOT_TRACK = 'donottrack'; static HANDOFF_STORAGE_KEY = 'HandOff'; private static _instance: BlueApp | null = null; - static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK, BlueApp.ADVANCED_MODE_ENABLED]; + static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK]; public cachedPassword?: false | string; public tx_metadata: TTXMetadata; @@ -882,17 +881,6 @@ export class BlueApp { return finalBalance; }; - isAdvancedModeEnabled = async (): Promise => { - try { - return !!(await AsyncStorage.getItem(BlueApp.ADVANCED_MODE_ENABLED)); - } catch (_) {} - return false; - }; - - setIsAdvancedModeEnabled = async (value: boolean) => { - await AsyncStorage.setItem(BlueApp.ADVANCED_MODE_ENABLED, value ? '1' : ''); - }; - isHandoffEnabled = async (): Promise => { try { return !!(await AsyncStorage.getItem(BlueApp.HANDOFF_STORAGE_KEY)); diff --git a/components/Context/SettingsProvider.tsx b/components/Context/SettingsProvider.tsx index 7e9e5203b..37ccafd8d 100644 --- a/components/Context/SettingsProvider.tsx +++ b/components/Context/SettingsProvider.tsx @@ -69,8 +69,6 @@ interface SettingsContextType { setIsHandOffUseEnabledAsyncStorage: (value: boolean) => Promise; isPrivacyBlurEnabled: boolean; setIsPrivacyBlurEnabledState: (value: boolean) => void; - isAdvancedModeEnabled: boolean; - setIsAdvancedModeEnabledStorage: (value: boolean) => Promise; isDoNotTrackEnabled: boolean; setDoNotTrackStorage: (value: boolean) => Promise; isWidgetBalanceDisplayAllowed: boolean; @@ -96,8 +94,6 @@ const defaultSettingsContext: SettingsContextType = { setIsHandOffUseEnabledAsyncStorage: async () => {}, isPrivacyBlurEnabled: true, setIsPrivacyBlurEnabledState: () => {}, - isAdvancedModeEnabled: false, - setIsAdvancedModeEnabledStorage: async () => {}, isDoNotTrackEnabled: false, setDoNotTrackStorage: async () => {}, isWidgetBalanceDisplayAllowed: true, @@ -125,8 +121,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil const [isHandOffUseEnabled, setHandOffUseEnabled] = useState(false); // PrivacyBlur const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState(true); - // AdvancedMode - const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useState(false); // DoNotTrack const [isDoNotTrackEnabled, setIsDoNotTrackEnabled] = useState(false); // WidgetCommunication @@ -141,19 +135,10 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil const [isTotalBalanceEnabled, setIsTotalBalanceEnabled] = useState(true); const [totalBalancePreferredUnit, setTotalBalancePreferredUnitState] = useState(BitcoinUnit.BTC); - const advancedModeStorage = useAsyncStorage(BlueApp.ADVANCED_MODE_ENABLED); const languageStorage = useAsyncStorage(STORAGE_KEY); const { walletsInitialized } = useStorage(); useEffect(() => { - advancedModeStorage - .getItem() - .then(advMode => { - console.debug('SettingsContext advMode:', advMode); - setIsAdvancedModeEnabled(advMode ? JSON.parse(advMode) : false); - }) - .catch(error => console.error('Error fetching advanced mode settings:', error)); - getIsHandOffUseEnabled() .then(handOff => { console.debug('SettingsContext handOff:', handOff); @@ -243,14 +228,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setLanguage(newLanguage); }, []); - const setIsAdvancedModeEnabledStorage = useCallback( - async (value: boolean) => { - await advancedModeStorage.setItem(JSON.stringify(value)); - setIsAdvancedModeEnabled(value); - }, - [advancedModeStorage], - ); - const setDoNotTrackStorage = useCallback(async (value: boolean) => { await DefaultPreference.setName(GROUP_IO_BLUEWALLET); if (value) { @@ -321,8 +298,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setIsHandOffUseEnabledAsyncStorage, isPrivacyBlurEnabled, setIsPrivacyBlurEnabledState, - isAdvancedModeEnabled, - setIsAdvancedModeEnabledStorage, isDoNotTrackEnabled, setDoNotTrackStorage, isWidgetBalanceDisplayAllowed, @@ -347,8 +322,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setIsHandOffUseEnabledAsyncStorage, isPrivacyBlurEnabled, setIsPrivacyBlurEnabledState, - isAdvancedModeEnabled, - setIsAdvancedModeEnabledStorage, isDoNotTrackEnabled, setDoNotTrackStorage, isWidgetBalanceDisplayAllowed, diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index 3023fae7f..39a5be982 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -1,5 +1,6 @@ import React, { Ref, useCallback, useMemo } from 'react'; import { Platform, Pressable, TouchableOpacity } from 'react-native'; +import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; import { ContextMenuView, RenderItem, @@ -8,7 +9,6 @@ import { IconConfig, MenuElementConfig, } from 'react-native-ios-context-menu'; -import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; import { ToolTipMenuProps, Action } from './types'; import { useSettings } from '../hooks/context/useSettings'; @@ -30,6 +30,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { const { language } = useSettings(); + // Map Menu Items for iOS Context Menu const mapMenuItemForContextMenuView = useCallback((action: Action) => { if (!action.id) return null; return { @@ -41,14 +42,30 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { }; }, []); + // Map Menu Items for RN Menu (supports subactions and displayInline) const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => { if (!action.id) return null; + + // Check for subactions + const subactions = + action.subactions?.map(subaction => ({ + id: subaction.id.toString(), + title: subaction.text, + subtitle: subaction.subtitle, + image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined, + state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState), + attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden }, + })) || []; + return { id: action.id.toString(), title: action.text, + subtitle: action.subtitle, image: action.icon?.iconValue ? action.icon.iconValue : undefined, state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState), - attributes: { disabled: action.disabled }, + attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden }, + subactions: subactions.length > 0 ? subactions : undefined, + displayInline: action.displayInline || false, }; }, []); @@ -98,7 +115,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { ); const renderContextMenuView = () => { - console.debug('ToolTipMenu.tsx rendering: renderContextMenuView'); return ( ) => { }; const renderMenuView = () => { - console.debug('ToolTipMenu.tsx rendering: renderMenuView'); return ( ) => { onPressAction={handlePressMenuItemForMenuView} actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid} shouldOpenOnLongPress={!isMenuPrimaryAction} - // @ts-ignore: its not in the types but it works + // @ts-ignore: Not exposed in types accessibilityLabel={props.accessibilityLabel} accessibilityHint={props.accessibilityHint} accessibilityRole={props.accessibilityRole} diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index 13d9a9a54..239980f4f 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -289,7 +289,7 @@ export const TransactionListItem: React.FC = React.mem handleOnViewOnBlockExplorer, ], ); - const toolTipActions = useMemo((): Action[] | Action[][] => { + const toolTipActions = useMemo((): Action[] => { const actions: (Action | Action[])[] = []; if (rowTitle !== loc.lnd.expired) { @@ -308,7 +308,7 @@ export const TransactionListItem: React.FC = React.mem actions.push([CommonToolTipActions.ExpandNote]); } - return actions as Action[] | Action[][]; + return actions as Action[]; }, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]); const accessibilityState = useMemo(() => { diff --git a/components/addresses/AddressItem.tsx b/components/addresses/AddressItem.tsx index dd93d5dd5..d9ac2fca5 100644 --- a/components/addresses/AddressItem.tsx +++ b/components/addresses/AddressItem.tsx @@ -209,7 +209,7 @@ const styles = StyleSheet.create({ }, }); -const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] | Action[][] => { +const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] => { const actions = [ { id: actionKeys.CopyToClipboard, diff --git a/components/types.ts b/components/types.ts index e3c4c8087..dc39a2ee8 100644 --- a/components/types.ts +++ b/components/types.ts @@ -1,4 +1,4 @@ -import { AccessibilityRole, ViewStyle } from 'react-native'; +import { AccessibilityRole, ViewStyle, ColorValue } from 'react-native'; export interface Action { id: string | number; @@ -7,13 +7,19 @@ export interface Action { iconValue: string; }; menuTitle?: string; + subtitle?: string; menuState?: 'mixed' | boolean | undefined; + displayInline?: boolean; // Indicates if subactions should be displayed inline or nested (iOS only) + image?: string; + imageColor?: ColorValue; + destructive?: boolean; + hidden?: boolean; disabled?: boolean; - displayInline?: boolean; + subactions?: Action[]; // Nested/Inline actions (subactions) within an action } export interface ToolTipMenuProps { - actions: Action[] | Action[][]; + actions: Action[]; children: React.ReactNode; enableAndroidRipple?: boolean; dismissMenu?: () => void; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8c7be5660..7e0a9fb59 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -167,7 +167,8 @@ platform :ios do type: "development", app_identifier: app_identifier, readonly: false, # This will regenerate the provisioning profile if needed - force_for_new_devices: true # This forces match to add new devices to the profile + force_for_new_devices: true, + clone_branch_directly: true ) end @@ -211,6 +212,7 @@ platform :ios do git_basic_authorization: ENV["GIT_ACCESS_TOKEN"], git_url: ENV["GIT_URL"], type: "appstore", + clone_branch_directly: true, # Skip if the branch already exists (Exit 128 error) platform: platform, app_identifier: app_identifier, team_id: ENV["ITC_TEAM_ID"], @@ -228,7 +230,8 @@ platform :ios do type: "development", platform: "catalyst", app_identifier: app_identifiers, - readonly: true + readonly: true, + clone_branch_directly: true ) end @@ -238,7 +241,9 @@ platform :ios do type: "appstore", platform: "catalyst", app_identifier: app_identifiers, - readonly: true + readonly: true, + clone_branch_directly: true + ) end @@ -250,14 +255,16 @@ platform :ios do platform: "catalyst", app_identifier: app_identifier, readonly: false, - force_for_new_devices: true + force_for_new_devices: true, + clone_branch_directly: true ) match( type: "appstore", platform: "catalyst", app_identifier: app_identifier, - readonly: false + readonly: false, + clone_branch_directly: true ) end end @@ -326,8 +333,8 @@ platform :ios do changelog = ENV["LATEST_COMMIT_MESSAGE"] upload_to_testflight( - api_key_path: "appstore_api_key.json", - ipa: "./build/BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa", + api_key_path: "./appstore_api_key.json", + ipa: "./BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa", skip_waiting_for_build_processing: true, # Do not wait for processing changelog: changelog ) diff --git a/loc/en.json b/loc/en.json index 290a6326e..7de756eb1 100644 --- a/loc/en.json +++ b/loc/en.json @@ -219,7 +219,6 @@ "about_sm_twitter": "Follow us on Twitter", "privacy_temporary_screenshots": "Allow Screenshots", "privacy_temporary_screenshots_instructions": "Screen capture protection will be turned off for this session, allowing you to take screenshots. Once you close and reopen the app, the protection will be automatically turned back on.", - "advanced_options": "Advanced Options", "biometrics": "Biometrics", "biometrics_no_longer_available": "Your device settings have changed and no longer match the selected security settings in the app. Please re-enable biometrics or passcode, then restart the app to apply these changes.", "biom_10times": "You have attempted to enter your password 10 times. Would you like to reset your storage? This will remove all wallets and decrypt your storage.", @@ -272,8 +271,6 @@ "encrypt_use_expl": "{type} will be used to confirm your identity before making a transaction, unlocking, exporting, or deleting a wallet. {type} will not be used to unlock encrypted storage.", "biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.", "general": "General", - "general_adv_mode": "Advanced Mode", - "general_adv_mode_e": "When enabled, you will see advanced options such as different wallet types, the ability to specify the LNDHub instance you wish to connect to, and custom entropy during wallet creation.", "general_continuity": "Continuity", "general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.", "groundcontrol_explanation": "GroundControl is a free, open-source push notifications server for Bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallet’s infrastructure. Leave blank to use GroundControl’s default server.", @@ -479,7 +476,8 @@ "add_ln_wallet_first": "You must first add a Lightning wallet.", "identity_pubkey": "Identity Pubkey", "xpub_title": "Wallet XPUB", - "manage_wallets_search_placeholder": "Search wallets, memos" + "manage_wallets_search_placeholder": "Search wallets, memos", + "more_info": "More Info" }, "total_balance_view": { "view_in_bitcoin": "View in Bitcoin", @@ -489,7 +487,7 @@ "explanation": "View the total balance of all your wallets in the overview screen." }, "multisig": { - "multisig_vault": "Vault", + "multisig_vault": "Multisig Vault", "default_label": "Multisig Vault", "multisig_vault_explain": "Best security for large amounts", "provide_signature": "Provide signature", diff --git a/navigation/AddWalletStack.tsx b/navigation/AddWalletStack.tsx index 998aa67a2..61d6eda8c 100644 --- a/navigation/AddWalletStack.tsx +++ b/navigation/AddWalletStack.tsx @@ -1,7 +1,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; -import navigationStyle from '../components/navigationStyle'; +import navigationStyle, { CloseButtonPosition } from '../components/navigationStyle'; import { useTheme } from '../components/themes'; import loc from '../loc'; import { @@ -49,7 +49,7 @@ const AddWalletStack = () => { name="AddWallet" component={AddComponent} options={navigationStyle({ - headerBackVisible: false, + closeButtonPosition: CloseButtonPosition.Left, title: loc.wallets.add_title, })(theme)} /> diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 8e40245a8..8aa25fafb 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -73,7 +73,6 @@ const DetailViewStackScreensStack = () => { const { wallets } = useStorage(); const { isTotalBalanceEnabled } = useSettings(); - const SaveButton = useMemo(() => , []); const DetailButton = useMemo(() => , []); const navigateToAddWallet = useCallback(() => { @@ -122,7 +121,6 @@ const DetailViewStackScreensStack = () => { options={navigationStyle({ headerTitle: loc.wallets.details_title, statusBarStyle: 'auto', - headerRight: () => SaveButton, })(theme)} /> { options={navigationStyle({ title: loc.addresses.addresses_title, statusBarStyle: 'auto' })(theme)} /> - + diff --git a/screen/receive/details.js b/screen/receive/details.js index 98a497a58..b0c868b97 100644 --- a/screen/receive/details.js +++ b/screen/receive/details.js @@ -1,6 +1,17 @@ import { useFocusEffect, useRoute } from '@react-navigation/native'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { BackHandler, InteractionManager, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + BackHandler, + Image, + InteractionManager, + LayoutAnimation, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; import Share from 'react-native-share'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; @@ -24,6 +35,9 @@ import { SuccessView } from '../send/success'; import { useStorage } from '../../hooks/context/useStorage'; import { HandOffActivityType } from '../../components/types'; import SegmentedControl from '../../components/SegmentControl'; +import ToolTipMenu from '../../components/TooltipMenu'; +import { Icon } from '@rneui/themed'; +import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; const segmentControlValues = [loc.wallets.details_address, loc.bip47.payment_code]; @@ -43,9 +57,9 @@ const ReceiveDetails = () => { const [showConfirmedBalance, setShowConfirmedBalance] = useState(false); const [showAddress, setShowAddress] = useState(false); const [currentTab, setCurrentTab] = useState(segmentControlValues[0]); - const { goBack, setParams } = useExtendedNavigation(); + const { goBack, setParams, setOptions } = useExtendedNavigation(); const bottomModalRef = useRef(null); - const { colors } = useTheme(); + const { colors, closeImage } = useTheme(); const [intervalMs, setIntervalMs] = useState(5000); const [eta, setEta] = useState(''); const [initialConfirmed, setInitialConfirmed] = useState(0); @@ -79,15 +93,119 @@ const ReceiveDetails = () => { }, }); + const setAddressBIP21Encoded = useCallback( + addr => { + const newBip21encoded = DeeplinkSchemaMatch.bip21encode(addr); + setParams({ address: addr }); + setBip21encoded(newBip21encoded); + setShowAddress(true); + }, + [setParams], + ); + + const obtainWalletAddress = useCallback(async () => { + console.debug('receive/details - componentDidMount'); + let newAddress; + if (address) { + setAddressBIP21Encoded(address); + await Notifications.tryToObtainPermissions(receiveAddressButton); + Notifications.majorTomToGroundControl([address], [], []); + } else { + if (wallet.chain === Chain.ONCHAIN) { + try { + if (!isElectrumDisabled) newAddress = await Promise.race([wallet.getAddressAsync(), sleep(1000)]); + } catch (_) {} + if (newAddress === undefined) { + // either sleep expired or getAddressAsync threw an exception + console.warn('either sleep expired or getAddressAsync threw an exception'); + newAddress = wallet._getExternalAddressByIndex(wallet.getNextFreeAddressIndex()); + } else { + saveToDisk(); // caching whatever getAddressAsync() generated internally + } + } else if (wallet.chain === Chain.OFFCHAIN) { + try { + await Promise.race([wallet.getAddressAsync(), sleep(1000)]); + newAddress = wallet.getAddress(); + } catch (_) {} + if (newAddress === undefined) { + // either sleep expired or getAddressAsync threw an exception + console.warn('either sleep expired or getAddressAsync threw an exception'); + newAddress = wallet.getAddress(); + } else { + saveToDisk(); // caching whatever getAddressAsync() generated internally + } + } + setAddressBIP21Encoded(newAddress); + await Notifications.tryToObtainPermissions(receiveAddressButton); + Notifications.majorTomToGroundControl([newAddress], [], []); + } + }, [wallet, saveToDisk, address, setAddressBIP21Encoded, isElectrumDisabled, sleep]); + + const onEnablePaymentsCodeSwitchValue = useCallback(() => { + if (wallet.allowBIP47()) { + wallet.switchBIP47(!wallet.isBIP47Enabled()); + } + saveToDisk(); + obtainWalletAddress(); + }, [wallet, saveToDisk, obtainWalletAddress]); + useEffect(() => { if (showConfirmedBalance) { triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); } }, [showConfirmedBalance]); + const toolTipActions = useMemo(() => { + const action = CommonToolTipActions.PaymentCode; + action.menuState = wallet.isBIP47Enabled(); + return [action]; + }, [wallet]); + + const onPressMenuItem = useCallback(() => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + onEnablePaymentsCodeSwitchValue(); + }, [onEnablePaymentsCodeSwitchValue]); + + const HeaderRight = useMemo( + () => ( + + + + ), + [colors.foregroundColor, onPressMenuItem, toolTipActions], + ); + + const handleClose = useCallback(() => { + goBack(); + }, [goBack]); + + const HeaderLeft = useMemo( + () => ( + + + + ), + [closeImage, handleClose], + ); + + useEffect(() => { + wallet.allowBIP47() && + !wallet.isBIP47Enabled() && + setOptions({ + headerLeft: () => (wallet.isBIP47Enabled() ? null : HeaderLeft), + headerRight: () => (wallet.isBIP47Enabled() ? HeaderLeft : HeaderRight), + }); + }, [HeaderLeft, HeaderRight, colors.foregroundColor, setOptions, wallet]); + // re-fetching address balance periodically useEffect(() => { - console.log('receive/details - useEffect'); + console.debug('receive/details - useEffect'); const intervalId = setInterval(async () => { try { @@ -95,9 +213,9 @@ const ReceiveDetails = () => { const addressToUse = address || decoded.address; if (!addressToUse) return; - console.log('checking address', addressToUse, 'for balance...'); + console.debug('checking address', addressToUse, 'for balance...'); const balance = await BlueElectrum.getBalanceByAddress(addressToUse); - console.log('...got', balance); + console.debug('...got', balance); if (balance.unconfirmed > 0) { if (initialConfirmed === 0 && initialUnconfirmed === 0) { @@ -157,7 +275,7 @@ const ReceiveDetails = () => { } } } catch (error) { - console.log(error); + console.debug(error); } }, intervalMs); @@ -209,16 +327,6 @@ const ReceiveDetails = () => { return true; }; - const setAddressBIP21Encoded = useCallback( - addr => { - const newBip21encoded = DeeplinkSchemaMatch.bip21encode(addr); - setParams({ address: addr }); - setBip21encoded(newBip21encoded); - setShowAddress(true); - }, - [setParams], - ); - useEffect(() => { BackHandler.addEventListener('hardwareBackPress', handleBackButton); @@ -256,44 +364,6 @@ const ReceiveDetails = () => { ); }; - const obtainWalletAddress = useCallback(async () => { - console.log('receive/details - componentDidMount'); - let newAddress; - if (address) { - setAddressBIP21Encoded(address); - await Notifications.tryToObtainPermissions(receiveAddressButton); - Notifications.majorTomToGroundControl([address], [], []); - } else { - if (wallet.chain === Chain.ONCHAIN) { - try { - if (!isElectrumDisabled) newAddress = await Promise.race([wallet.getAddressAsync(), sleep(1000)]); - } catch (_) {} - if (newAddress === undefined) { - // either sleep expired or getAddressAsync threw an exception - console.warn('either sleep expired or getAddressAsync threw an exception'); - newAddress = wallet._getExternalAddressByIndex(wallet.getNextFreeAddressIndex()); - } else { - saveToDisk(); // caching whatever getAddressAsync() generated internally - } - } else if (wallet.chain === Chain.OFFCHAIN) { - try { - await Promise.race([wallet.getAddressAsync(), sleep(1000)]); - newAddress = wallet.getAddress(); - } catch (_) {} - if (newAddress === undefined) { - // either sleep expired or getAddressAsync threw an exception - console.warn('either sleep expired or getAddressAsync threw an exception'); - newAddress = wallet.getAddress(); - } else { - saveToDisk(); // caching whatever getAddressAsync() generated internally - } - } - setAddressBIP21Encoded(newAddress); - await Notifications.tryToObtainPermissions(receiveAddressButton); - Notifications.majorTomToGroundControl([newAddress], [], []); - } - }, [wallet, saveToDisk, address, setAddressBIP21Encoded, isElectrumDisabled, sleep]); - useFocusEffect( useCallback(() => { const task = InteractionManager.runAfterInteractions(async () => { @@ -357,7 +427,7 @@ const ReceiveDetails = () => { const handleShareButtonPressed = () => { Share.open({ message: currentTab === loc.wallets.details_address ? bip21encoded : wallet.getBIP47PaymentCode() }).catch(error => - console.log(error), + console.debug(error), ); }; diff --git a/screen/settings/GeneralSettings.tsx b/screen/settings/GeneralSettings.tsx index 39fbdcabd..8d52e7aa3 100644 --- a/screen/settings/GeneralSettings.tsx +++ b/screen/settings/GeneralSettings.tsx @@ -16,14 +16,7 @@ const styles = StyleSheet.create({ const GeneralSettings: React.FC = () => { const { wallets } = useStorage(); - const { - isAdvancedModeEnabled, - setIsAdvancedModeEnabledStorage, - isHandOffUseEnabled, - setIsHandOffUseEnabledAsyncStorage, - isLegacyURv1Enabled, - setIsLegacyURv1EnabledStorage, - } = useSettings(); + const { isHandOffUseEnabled, setIsHandOffUseEnabledAsyncStorage, isLegacyURv1Enabled, setIsLegacyURv1EnabledStorage } = useSettings(); const { navigate } = useNavigation(); const { colors } = useTheme(); @@ -64,14 +57,6 @@ const GeneralSettings: React.FC = () => { ) : null} - - - {loc.settings.general_adv_mode_e} - { +const walletReducer = (state: State, action: TAction): State => { switch (action.type) { case ActionTypes.SET_LOADING: return { ...state, isLoading: action.payload }; case ActionTypes.SET_WALLET_BASE_URI: return { ...state, walletBaseURI: action.payload }; case ActionTypes.SET_SELECTED_INDEX: - return { ...state, selectedIndex: action.payload }; + return { ...state, selectedIndex: action.payload, selectedWalletType: ButtonSelected.ONCHAIN }; case ActionTypes.SET_LABEL: return { ...state, label: action.payload }; case ActionTypes.SET_SELECTED_WALLET_TYPE: return { ...state, selectedWalletType: action.payload }; - case ActionTypes.INCREMENT_BACKDOOR_PRESSED: - return { ...state, backdoorPressed: state.backdoorPressed + 1 }; case ActionTypes.SET_ENTROPY: return { ...state, entropy: action.payload }; case ActionTypes.SET_ENTROPY_BUTTON_TEXT: @@ -111,10 +107,9 @@ const WalletsAdd: React.FC = () => { const selectedWalletType = state.selectedWalletType; const entropy = state.entropy; const entropyButtonText = state.entropyButtonText; - // const colorScheme = useColorScheme(); + // const { addWallet, saveToDisk } = useStorage(); - const { isAdvancedModeEnabled } = useSettings(); const { navigate, goBack, setOptions } = useNavigation(); const stylesHook = { advancedText: { @@ -138,20 +133,7 @@ const WalletsAdd: React.FC = () => { }, }; - useEffect(() => { - AsyncStorage.getItem(BlueApp.LNDHUB) - .then(url => (url ? setWalletBaseURI(url) : setWalletBaseURI(''))) - .catch(() => setWalletBaseURI('')) - .finally(() => setIsLoading(false)); - }, []); - - useEffect(() => { - setOptions({ - statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }), - }); - }, [colorScheme, setOptions]); - - const entropyGenerated = (newEntropy: Buffer) => { + const entropyGenerated = useCallback((newEntropy: Buffer) => { let entropyTitle; if (!newEntropy) { entropyTitle = loc.wallets.add_entropy_provide; @@ -162,7 +144,127 @@ const WalletsAdd: React.FC = () => { } setEntropy(newEntropy); setEntropyButtonText(entropyTitle); - }; + }, []); + + const navigateToEntropy = useCallback(() => { + Alert.alert( + loc.wallets.add_wallet_seed_length, + loc.wallets.add_wallet_seed_length_message, + [ + { + text: loc._.cancel, + onPress: () => {}, + style: 'default', + }, + { + text: loc.wallets.add_wallet_seed_length_12, + onPress: () => { + // @ts-ignore: Return later to update + navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 12 }); + }, + style: 'default', + }, + { + text: loc.wallets.add_wallet_seed_length_24, + onPress: () => { + // @ts-ignore: Return later to update + navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 24 }); + }, + style: 'default', + }, + ], + { cancelable: true }, + ); + }, [entropyGenerated, navigate]); + + const toolTipActions = useMemo(() => { + const walletSubactions: Action[] = [ + { + id: HDSegwitBech32Wallet.type, + text: `${loc.multisig.native_segwit_title}`, + subtitle: 'p2wsh/HD', + menuState: selectedIndex === 0 && selectedWalletType === ButtonSelected.ONCHAIN, + }, + { + id: SegwitP2SHWallet.type, + text: `${loc.multisig.wrapped_segwit_title}`, + subtitle: 'p2sh-p2wsh/HD', + menuState: selectedIndex === 1 && selectedWalletType === ButtonSelected.ONCHAIN, + }, + { + id: HDSegwitP2SHWallet.type, + text: `${loc.multisig.legacy_title}`, + subtitle: 'p2sh/non-HD', + menuState: selectedIndex === 2 && selectedWalletType === ButtonSelected.ONCHAIN, + }, + { + id: LightningCustodianWallet.type, + text: LightningCustodianWallet.typeReadable, + menuState: selectedWalletType === ButtonSelected.OFFCHAIN, + }, + ]; + + const walletAction: Action = { + id: 'wallets', + text: loc.multisig.wallet_type, + subactions: walletSubactions, + displayInline: true, + }; + + const entropyAction = { + ...CommonToolTipActions.Entropy, + text: entropyButtonText, + menuState: false, + }; + + return [walletAction, entropyAction]; + }, [entropyButtonText, selectedIndex, selectedWalletType]); + + const handleOnLightningButtonPressed = useCallback(() => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setSelectedWalletType(ButtonSelected.OFFCHAIN); + }, []); + + const HeaderRight = useMemo( + () => ( + { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + if (id === HDSegwitBech32Wallet.type) { + setSelectedIndex(0); + } else if (id === SegwitP2SHWallet.type) { + setSelectedIndex(1); + } else if (id === HDSegwitP2SHWallet.type) { + setSelectedIndex(2); + } else if (id === LightningCustodianWallet.type) { + handleOnLightningButtonPressed(); + } else if (id === CommonToolTipActions.Entropy.id) { + navigateToEntropy(); + } + }} + actions={toolTipActions} + > + + + ), + [colors.foregroundColor, handleOnLightningButtonPressed, navigateToEntropy, toolTipActions], + ); + + useEffect(() => { + setOptions({ + headerRight: () => HeaderRight, + statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }), + }); + }, [HeaderRight, colorScheme, colors.foregroundColor, navigateToEntropy, setOptions, toolTipActions]); + + useEffect(() => { + AsyncStorage.getItem(BlueApp.LNDHUB) + .then(url => (url ? setWalletBaseURI(url) : setWalletBaseURI(''))) + .catch(() => setWalletBaseURI('')) + .finally(() => setIsLoading(false)); + }, []); const setIsLoading = (value: boolean) => { dispatch({ type: 'SET_LOADING', payload: value }); @@ -184,10 +286,6 @@ const WalletsAdd: React.FC = () => { dispatch({ type: 'SET_SELECTED_WALLET_TYPE', payload: value }); }; - const setBackdoorPressed = (value: number) => { - dispatch({ type: 'INCREMENT_BACKDOOR_PRESSED', payload: value }); - }; - const setEntropy = (value: Buffer) => { dispatch({ type: 'SET_ENTROPY', payload: value }); }; @@ -225,7 +323,6 @@ const WalletsAdd: React.FC = () => { } catch (e: any) { console.log(e.toString()); presentAlert({ message: e.toString() }); - goBack(); return; } } else { @@ -291,37 +388,6 @@ const WalletsAdd: React.FC = () => { }); }; - const navigateToEntropy = () => { - Alert.alert( - loc.wallets.add_wallet_seed_length, - loc.wallets.add_wallet_seed_length_message, - [ - { - text: loc._.cancel, - onPress: () => {}, - style: 'default', - }, - { - text: loc.wallets.add_wallet_seed_length_12, - onPress: () => { - // @ts-ignore: Return later to update - navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 12 }); - }, - style: 'default', - }, - { - text: loc.wallets.add_wallet_seed_length_24, - onPress: () => { - // @ts-ignore: Return later to update - navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 24 }); - }, - style: 'default', - }, - ], - { cancelable: true }, - ); - }; - const navigateToImportWallet = () => { // @ts-ignore: Return later to update navigate('ImportWallet'); @@ -339,16 +405,6 @@ const WalletsAdd: React.FC = () => { setSelectedWalletType(ButtonSelected.ONCHAIN); }; - const handleOnLightningButtonPressed = () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - // @ts-ignore: Return later to update - setBackdoorPressed((prevState: number) => { - return prevState + 1; - }); - Keyboard.dismiss(); - setSelectedWalletType(ButtonSelected.OFFCHAIN); - }; - return ( @@ -374,12 +430,6 @@ const WalletsAdd: React.FC = () => { onPress={handleOnBitcoinButtonPressed} size={styles.button} /> - { - {(() => { - if (selectedWalletType === ButtonSelected.ONCHAIN && isAdvancedModeEnabled) { - return ( - - - {loc.settings.advanced_options} - setSelectedIndex(0)} - title={HDSegwitBech32Wallet.typeReadable} - checkmark={selectedIndex === 0} - /> - setSelectedIndex(1)} - title={SegwitP2SHWallet.typeReadable} - checkmark={selectedIndex === 1} - /> - setSelectedIndex(2)} - title={HDSegwitP2SHWallet.typeReadable} - checkmark={selectedIndex === 2} - /> - - ); - } else if (selectedWalletType === ButtonSelected.OFFCHAIN) { - return ( - <> - - {loc.settings.advanced_options} - - {loc.wallets.add_lndhub} - - - - - ); - } - })()} - {isAdvancedModeEnabled === true && selectedWalletType === ButtonSelected.ONCHAIN && !isLoading && ( - + {selectedWalletType === ButtonSelected.OFFCHAIN && ( + <> + + {loc.wallets.add_lndhub} + + + + )} + {!isLoading ? ( <> @@ -508,9 +522,6 @@ const styles = StyleSheet.create({ advanced: { marginHorizontal: 20, }, - advancedText: { - fontWeight: '500', - }, lndUri: { flexDirection: 'row', borderWidth: 1, @@ -524,9 +535,6 @@ const styles = StyleSheet.create({ import: { marginVertical: 24, }, - noPadding: { - paddingHorizontal: 0, - }, }); export default WalletsAdd; diff --git a/screen/wallets/ViewEditMultisigCosigners.tsx b/screen/wallets/ViewEditMultisigCosigners.tsx index c1d801aa3..e39b90534 100644 --- a/screen/wallets/ViewEditMultisigCosigners.tsx +++ b/screen/wallets/ViewEditMultisigCosigners.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useFocusEffect, useRoute } from '@react-navigation/native'; import { ActivityIndicator, @@ -11,7 +11,6 @@ import { ListRenderItemInfo, Platform, StyleSheet, - Switch, Text, View, } from 'react-native'; @@ -20,12 +19,11 @@ import { isDesktop } from '../../blue_modules/environment'; import { encodeUR } from '../../blue_modules/ur'; import { BlueButtonLink, + BlueCard, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, - BlueSpacing40, - BlueText, BlueTextCentered, } from '../../BlueComponents'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; @@ -49,14 +47,14 @@ import usePrivacy from '../../hooks/usePrivacy'; import loc from '../../loc'; import ActionSheet from '../ActionSheet'; import { useStorage } from '../../hooks/context/useStorage'; -import { useSettings } from '../../hooks/context/useSettings'; +import ToolTipMenu from '../../components/TooltipMenu'; +import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; const ViewEditMultisigCosigners: React.FC = () => { const hasLoaded = useRef(false); const { colors } = useTheme(); const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); - const { isAdvancedModeEnabled } = useSettings(); const { navigate, dispatch, addListener } = useExtendedNavigation(); const openScannerButtonRef = useRef(); const route = useRoute(); @@ -97,6 +95,9 @@ const ViewEditMultisigCosigners: React.FC = () => { vaultKeyText: { color: colors.alternativeTextColor, }, + askPassphrase: { + backgroundColor: colors.lightButton, + }, vaultKeyCircleSuccess: { backgroundColor: colors.msSuccessBG, }, @@ -523,6 +524,12 @@ const ViewEditMultisigCosigners: React.FC = () => { const hideShareModal = () => {}; + const toolTipActions = useMemo(() => { + const passphrase = CommonToolTipActions.Passphrase; + passphrase.menuState = askPassphrase; + return [passphrase]; + }, [askPassphrase]); + const renderProvideMnemonicsModal = () => { return ( { } > - {loc.multisig.type_your_mnemonics} - - - {isAdvancedModeEnabled && ( - <> - - - {loc.wallets.import_passphrase} - - - - )} + <> + { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setAskPassphrase(!askPassphrase); + }} + actions={toolTipActions} + style={[styles.askPassprase, stylesHook.askPassphrase]} + > + + + + {loc.multisig.type_your_mnemonics} + + + ); }; @@ -639,10 +652,11 @@ const ViewEditMultisigCosigners: React.FC = () => { contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets keyExtractor={(_item, index) => `${index}`} + contentContainerStyle={styles.contentContainerStyle} /> - {footer} - + {footer} + {renderProvideMnemonicsModal()} @@ -665,6 +679,7 @@ const styles = StyleSheet.create({ paddingTop: 32, minHeight: 370, }, + contentContainerStyle: { padding: 16 }, modalContent: { padding: 22, justifyContent: 'center', @@ -700,12 +715,8 @@ const styles = StyleSheet.create({ tipLabelText: { fontWeight: '500', }, - row: { - flexDirection: 'row', - alignItems: 'center', - marginHorizontal: 16, - justifyContent: 'space-between', - }, + + askPassprase: { top: 0, left: 0, justifyContent: 'center', width: 33, height: 33, borderRadius: 33 / 2 }, }); export default ViewEditMultisigCosigners; diff --git a/screen/wallets/WalletsAddMultisig.tsx b/screen/wallets/WalletsAddMultisig.tsx index b02356d68..d1512ef2f 100644 --- a/screen/wallets/WalletsAddMultisig.tsx +++ b/screen/wallets/WalletsAddMultisig.tsx @@ -11,7 +11,6 @@ import ListItem from '../../components/ListItem'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; import loc from '../../loc'; -import { useSettings } from '../../hooks/context/useSettings'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; @@ -27,7 +26,6 @@ const WalletsAddMultisig: React.FC = () => { const [m, setM] = useState(2); const [n, setN] = useState(3); const [format, setFormat] = useState(MultisigHDWallet.FORMAT_P2WSH); - const { isAdvancedModeEnabled } = useSettings(); const stylesHook = StyleSheet.create({ root: { @@ -202,17 +200,16 @@ const WalletsAddMultisig: React.FC = () => { - {isAdvancedModeEnabled && ( - - - - )} + + + +