mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-13 19:16:52 +01:00
Compare commits
40 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c7909049dc | ||
|
b5270d0a07 | ||
|
4f3b828990 | ||
|
26720e8284 | ||
|
a80bacc0f4 | ||
|
5f18540ca7 | ||
|
c14cb3508c | ||
|
751c7d6f45 | ||
|
0b1c3dd9f7 | ||
|
ae89a59794 | ||
|
10b3432e0e | ||
|
c67eea8155 | ||
|
9421511f74 | ||
|
9ec0ef51e4 | ||
|
1cada11c50 | ||
|
d2cebde6ad | ||
|
1a940971bc | ||
|
28316b4d73 | ||
|
4670eea38a | ||
|
b2552bdc71 | ||
|
dbd4066f7e | ||
|
4cdd952f90 | ||
|
ddee4cdaaf | ||
|
0aa6b96e4b | ||
|
8d49aff279 | ||
|
18a187b120 | ||
|
1f77a852a8 | ||
|
9d899d672d | ||
|
e7b81e5517 | ||
|
f8af06e2ae | ||
|
8b81472fa4 | ||
|
4ad2b15070 | ||
|
dd118af993 | ||
|
d375bd9780 | ||
|
040f91028a | ||
|
1c8aa08de8 | ||
|
d7743a740f | ||
|
88be0332e4 | ||
|
ef5887f28b | ||
|
136dd20f9e |
42 changed files with 847 additions and 584 deletions
|
@ -87,7 +87,7 @@ android {
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "7.1.3"
|
versionName "7.1.5"
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ dependencies {
|
||||||
androidTestImplementation('com.wix:detox:0.1.1')
|
androidTestImplementation('com.wix:detox:0.1.1')
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||||
}
|
}
|
||||||
apply plugin: 'com.google.gms.google-services' // Google Services plugin
|
apply plugin: 'com.google.gms.google-services' // Google Services plugin
|
||||||
apply plugin: "com.bugsnag.android.gradle"
|
apply plugin: "com.bugsnag.android.gradle"
|
||||||
|
|
|
@ -56,7 +56,8 @@ object MarketAPI {
|
||||||
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
|
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
|
||||||
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
||||||
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
|
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
|
||||||
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
|
"CoinDesk" -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
|
||||||
|
else -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +74,10 @@ object MarketAPI {
|
||||||
"coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
|
"coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
|
||||||
"Coinbase" -> json.getJSONObject("data").getString("amount")
|
"Coinbase" -> json.getJSONObject("data").getString("amount")
|
||||||
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
|
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
|
||||||
|
"CoinDesk" -> {
|
||||||
|
val rate = json.optDouble(endPointKey.uppercase(), -1.0)
|
||||||
|
if (rate < 0) null else rate.toString()
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/price_value"
|
android:id="@+id/price_value"
|
||||||
style="@style/WidgetTextPrimary"
|
style="@style/WidgetTextPrimary"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
|
@ -67,6 +67,7 @@
|
||||||
android:autoSizeTextType="uniform"
|
android:autoSizeTextType="uniform"
|
||||||
android:duplicateParentState="false"
|
android:duplicateParentState="false"
|
||||||
android:editable="false"
|
android:editable="false"
|
||||||
|
android:gravity="end"
|
||||||
android:lines="1"
|
android:lines="1"
|
||||||
android:text="Loading..."
|
android:text="Loading..."
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:initialLayout="@layout/widget_layout"
|
android:initialLayout="@layout/widget_layout"
|
||||||
android:minWidth="160dp"
|
android:minWidth="170dp"
|
||||||
android:minHeight="100dp"
|
android:minHeight="100dp"
|
||||||
android:updatePeriodMillis="0"
|
android:updatePeriodMillis="0"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen"
|
||||||
|
|
|
@ -86,9 +86,9 @@ class DeeplinkSchemaMatch {
|
||||||
} else if (wallet.chain === Chain.OFFCHAIN) {
|
} else if (wallet.chain === Chain.OFFCHAIN) {
|
||||||
if (action === 'openSend') {
|
if (action === 'openSend') {
|
||||||
completionHandler([
|
completionHandler([
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
walletID: wallet.getID(),
|
walletID: wallet.getID(),
|
||||||
},
|
},
|
||||||
|
@ -156,9 +156,9 @@ class DeeplinkSchemaMatch {
|
||||||
]);
|
]);
|
||||||
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
|
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
|
||||||
completionHandler([
|
completionHandler([
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
uri: event.url.replace('://', ':'),
|
uri: event.url.replace('://', ':'),
|
||||||
},
|
},
|
||||||
|
@ -181,9 +181,9 @@ class DeeplinkSchemaMatch {
|
||||||
// this might be not just an email but a lightning address
|
// this might be not just an email but a lightning address
|
||||||
// @see https://lightningaddress.com
|
// @see https://lightningaddress.com
|
||||||
completionHandler([
|
completionHandler([
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
uri: event.url,
|
uri: event.url,
|
||||||
},
|
},
|
||||||
|
@ -305,9 +305,9 @@ class DeeplinkSchemaMatch {
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
uri: uri.lndInvoice,
|
uri: uri.lndInvoice,
|
||||||
walletID: wallet.getID(),
|
walletID: wallet.getID(),
|
||||||
|
|
|
@ -79,6 +79,20 @@ export type TransactionOutput = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DecodedInvoice {
|
||||||
|
destination: string;
|
||||||
|
payment_hash: string;
|
||||||
|
num_satoshis: number;
|
||||||
|
timestamp: number;
|
||||||
|
expiry: number;
|
||||||
|
description: string;
|
||||||
|
description_hash: string;
|
||||||
|
fallback_addr: string;
|
||||||
|
cltv_expiry: string;
|
||||||
|
route_hints: any[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export type LightningTransaction = {
|
export type LightningTransaction = {
|
||||||
memo?: string;
|
memo?: string;
|
||||||
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
|
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import React, { useCallback } from 'react';
|
import React from 'react';
|
||||||
import { Keyboard, StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
||||||
import loc from '../loc';
|
import loc from '../loc';
|
||||||
import { AddressInputScanButton } from './AddressInputScanButton';
|
import { AddressInputScanButton } from './AddressInputScanButton';
|
||||||
import { useTheme } from './themes';
|
import { useTheme } from './themes';
|
||||||
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
|
|
||||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
|
||||||
|
|
||||||
interface AddressInputProps {
|
interface AddressInputProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
@ -31,7 +29,6 @@ interface AddressInputProps {
|
||||||
| 'twitter'
|
| 'twitter'
|
||||||
| 'web-search'
|
| 'web-search'
|
||||||
| 'visible-password';
|
| 'visible-password';
|
||||||
skipValidation?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddressInput = ({
|
const AddressInput = ({
|
||||||
|
@ -46,7 +43,6 @@ const AddressInput = ({
|
||||||
onBlur = () => {},
|
onBlur = () => {},
|
||||||
keyboardType = 'default',
|
keyboardType = 'default',
|
||||||
style,
|
style,
|
||||||
skipValidation = false,
|
|
||||||
}: AddressInputProps) => {
|
}: AddressInputProps) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const stylesHook = StyleSheet.create({
|
const stylesHook = StyleSheet.create({
|
||||||
|
@ -60,29 +56,6 @@ const AddressInput = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateAddressWithFeedback = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
if (skipValidation) return;
|
|
||||||
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(value);
|
|
||||||
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(value);
|
|
||||||
const isValid = isBitcoinAddress || isLightningInvoice;
|
|
||||||
|
|
||||||
triggerHapticFeedback(isValid ? HapticFeedbackTypes.NotificationSuccess : HapticFeedbackTypes.NotificationError);
|
|
||||||
return {
|
|
||||||
isValid,
|
|
||||||
type: isBitcoinAddress ? 'bitcoin' : isLightningInvoice ? 'lightning' : 'invalid',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[skipValidation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlurEditing = () => {
|
|
||||||
if (!skipValidation) {
|
|
||||||
validateAddressWithFeedback(address);
|
|
||||||
}
|
|
||||||
Keyboard.dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.root, stylesHook.root, style]}>
|
<View style={[styles.root, stylesHook.root, style]}>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -100,7 +73,7 @@ const AddressInput = ({
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
keyboardType={keyboardType}
|
keyboardType={keyboardType}
|
||||||
{...(skipValidation ? { onBlur } : { onBlur: onBlurEditing })}
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
|
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -89,6 +89,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSwipeActive, setIsSwipeActive] = useState(false);
|
||||||
|
const resetFunctionRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const CARD_SORT_ACTIVE = 1.06;
|
const CARD_SORT_ACTIVE = 1.06;
|
||||||
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
|
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
|
||||||
|
@ -125,24 +127,39 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const leftContent = (reset: () => void) => (
|
const leftContent = (reset: () => void) => {
|
||||||
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
|
resetFunctionRef.current = reset;
|
||||||
);
|
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
|
||||||
|
|
||||||
const handleRightPress = (reset: () => void) => {
|
|
||||||
handleDeleteWallet(item.data as TWallet);
|
|
||||||
reset();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rightContent = (reset: () => void) => <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
const handleRightPress = (reset: () => void) => {
|
||||||
|
reset();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
handleDeleteWallet(item.data as TWallet);
|
||||||
|
}, 100); // short delay to allow swipe reset animation to complete
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightContent = (reset: () => void) => {
|
||||||
|
resetFunctionRef.current = reset;
|
||||||
|
return <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
||||||
|
};
|
||||||
|
|
||||||
const startDrag = useCallback(() => {
|
const startDrag = useCallback(() => {
|
||||||
|
if (isSwipeActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetFunctionRef.current) {
|
||||||
|
resetFunctionRef.current();
|
||||||
|
}
|
||||||
|
|
||||||
scaleValue.setValue(CARD_SORT_ACTIVE);
|
scaleValue.setValue(CARD_SORT_ACTIVE);
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||||
if (drag) {
|
if (drag) {
|
||||||
drag();
|
drag();
|
||||||
}
|
}
|
||||||
}, [CARD_SORT_ACTIVE, drag, scaleValue]);
|
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <ActivityIndicator size="large" color={colors.brandingColor} />;
|
return <ActivityIndicator size="large" color={colors.brandingColor} />;
|
||||||
|
@ -155,23 +172,38 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
|
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
|
||||||
|
|
||||||
|
const swipeDisabled = isActive || globalDragActive;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={animatedStyle}>
|
<Animated.View style={animatedStyle}>
|
||||||
<ListItem.Swipeable
|
<ListItem.Swipeable
|
||||||
leftWidth={80}
|
leftWidth={swipeDisabled ? 0 : 80}
|
||||||
rightWidth={90}
|
rightWidth={swipeDisabled ? 0 : 90}
|
||||||
containerStyle={[style, { backgroundColor }, isActive || globalDragActive ? styles.transparentBackground : {}]}
|
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
|
||||||
leftContent={globalDragActive ? null : isActive ? null : leftContent}
|
leftContent={swipeDisabled ? null : leftContent}
|
||||||
rightContent={globalDragActive ? null : isActive ? null : rightContent}
|
rightContent={swipeDisabled ? null : rightContent}
|
||||||
onPressOut={onPressOut}
|
onPressOut={onPressOut}
|
||||||
minSlideWidth={80}
|
minSlideWidth={swipeDisabled ? 0 : 80}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
style={isActive || globalDragActive ? styles.transparentBackground : {}}
|
style={swipeDisabled ? styles.transparentBackground : {}}
|
||||||
|
onSwipeBegin={direction => {
|
||||||
|
if (!swipeDisabled) {
|
||||||
|
console.debug(`Swipe began: ${direction}`);
|
||||||
|
setIsSwipeActive(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSwipeEnd={() => {
|
||||||
|
if (!swipeDisabled) {
|
||||||
|
console.debug('Swipe ended');
|
||||||
|
setIsSwipeActive(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ListItem.Content>
|
<ListItem.Content>
|
||||||
<WalletCarouselItem
|
<WalletCarouselItem
|
||||||
item={item.data}
|
item={item.data}
|
||||||
handleLongPress={isDraggingDisabled ? undefined : startDrag}
|
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onPressOut={onPressOut}
|
onPressOut={onPressOut}
|
||||||
|
|
|
@ -220,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||||
}
|
}
|
||||||
const loaded = await LN.loadSuccessfulPayment(paymentHash);
|
const loaded = await LN.loadSuccessfulPayment(paymentHash);
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
navigate('ScanLndInvoiceRoot', {
|
navigate('ScanLNDInvoiceRoot', {
|
||||||
screen: 'LnurlPaySuccess',
|
screen: 'LnurlPaySuccess',
|
||||||
params: {
|
params: {
|
||||||
paymentHash,
|
paymentHash,
|
||||||
|
|
|
@ -15,7 +15,8 @@ const scanQrHelper = async (): Promise<string> => {
|
||||||
await requestCameraAuthorization();
|
await requestCameraAuthorization();
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (navigationRef.isReady()) {
|
if (navigationRef.isReady()) {
|
||||||
navigationRef.current?.navigate('ScanQRCode', {
|
navigationRef.navigate('ScanQRCode', {
|
||||||
|
showFileImportButton: true,
|
||||||
onBarScanned: (data: string) => {
|
onBarScanned: (data: string) => {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,25 +19,42 @@ import loc from '../loc';
|
||||||
import { Chain } from '../models/bitcoinUnits';
|
import { Chain } from '../models/bitcoinUnits';
|
||||||
import { navigationRef } from '../NavigationService';
|
import { navigationRef } from '../NavigationService';
|
||||||
import ActionSheet from '../screen/ActionSheet';
|
import ActionSheet from '../screen/ActionSheet';
|
||||||
import { useStorage } from '../hooks/context/useStorage';
|
import { useStorage } from './context/useStorage';
|
||||||
import RNQRGenerator from 'rn-qr-generator';
|
import RNQRGenerator from 'rn-qr-generator';
|
||||||
import presentAlert from './Alert';
|
import presentAlert from '../components/Alert';
|
||||||
import useMenuElements from '../hooks/useMenuElements';
|
import useWidgetCommunication from './useWidgetCommunication';
|
||||||
import useWidgetCommunication from '../hooks/useWidgetCommunication';
|
import useWatchConnectivity from './useWatchConnectivity';
|
||||||
import useWatchConnectivity from '../hooks/useWatchConnectivity';
|
import useDeviceQuickActions from './useDeviceQuickActions';
|
||||||
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
|
import useHandoffListener from './useHandoffListener';
|
||||||
import useHandoffListener from '../hooks/useHandoffListener';
|
import useMenuElements from './useMenuElements';
|
||||||
|
|
||||||
const ClipboardContentType = Object.freeze({
|
const ClipboardContentType = Object.freeze({
|
||||||
BITCOIN: 'BITCOIN',
|
BITCOIN: 'BITCOIN',
|
||||||
LIGHTNING: 'LIGHTNING',
|
LIGHTNING: 'LIGHTNING',
|
||||||
});
|
});
|
||||||
|
|
||||||
const CompanionDelegates = () => {
|
/**
|
||||||
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
|
* Hook that initializes all companion listeners and functionality without rendering a component
|
||||||
|
*/
|
||||||
|
const useCompanionListeners = (skipIfNotInitialized = true) => {
|
||||||
|
const {
|
||||||
|
wallets,
|
||||||
|
addWallet,
|
||||||
|
saveToDisk,
|
||||||
|
fetchAndSaveWalletTransactions,
|
||||||
|
refreshAllWalletTransactions,
|
||||||
|
setSharedCosigner,
|
||||||
|
walletsInitialized,
|
||||||
|
} = useStorage();
|
||||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||||
const clipboardContent = useRef<undefined | string>();
|
const clipboardContent = useRef<undefined | string>();
|
||||||
|
|
||||||
|
// We need to call hooks unconditionally before any conditional logic
|
||||||
|
// We'll use this check inside the effects to conditionally run logic
|
||||||
|
const shouldActivateListeners = !skipIfNotInitialized || walletsInitialized;
|
||||||
|
|
||||||
|
// Initialize other hooks regardless of activation status
|
||||||
|
// They'll handle their own conditional logic internally
|
||||||
useWatchConnectivity();
|
useWatchConnectivity();
|
||||||
useWidgetCommunication();
|
useWidgetCommunication();
|
||||||
useMenuElements();
|
useMenuElements();
|
||||||
|
@ -45,6 +62,8 @@ const CompanionDelegates = () => {
|
||||||
useHandoffListener();
|
useHandoffListener();
|
||||||
|
|
||||||
const processPushNotifications = useCallback(async () => {
|
const processPushNotifications = useCallback(async () => {
|
||||||
|
if (!shouldActivateListeners) return false;
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
try {
|
try {
|
||||||
const notifications2process = await getStoredNotifications();
|
const notifications2process = await getStoredNotifications();
|
||||||
|
@ -164,15 +183,19 @@ const CompanionDelegates = () => {
|
||||||
console.error('Failed to process push notifications:', error);
|
console.error('Failed to process push notifications:', error);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
|
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!shouldActivateListeners) return;
|
||||||
|
|
||||||
initializeNotifications(processPushNotifications);
|
initializeNotifications(processPushNotifications);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [shouldActivateListeners]);
|
||||||
|
|
||||||
const handleOpenURL = useCallback(
|
const handleOpenURL = useCallback(
|
||||||
async (event: { url: string }): Promise<void> => {
|
async (event: { url: string }): Promise<void> => {
|
||||||
|
if (!shouldActivateListeners) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!event.url) return;
|
if (!event.url) return;
|
||||||
let decodedUrl: string;
|
let decodedUrl: string;
|
||||||
|
@ -227,11 +250,13 @@ const CompanionDelegates = () => {
|
||||||
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
|
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[wallets, addWallet, saveToDisk, setSharedCosigner],
|
[wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
|
||||||
);
|
);
|
||||||
|
|
||||||
const showClipboardAlert = useCallback(
|
const showClipboardAlert = useCallback(
|
||||||
({ contentType }: { contentType: undefined | string }) => {
|
({ contentType }: { contentType: undefined | string }) => {
|
||||||
|
if (!shouldActivateListeners) return;
|
||||||
|
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
|
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
|
||||||
getClipboardContent().then(clipboard => {
|
getClipboardContent().then(clipboard => {
|
||||||
if (!clipboard) return;
|
if (!clipboard) return;
|
||||||
|
@ -254,12 +279,13 @@ const CompanionDelegates = () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[handleOpenURL],
|
[handleOpenURL, shouldActivateListeners],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAppStateChange = useCallback(
|
const handleAppStateChange = useCallback(
|
||||||
async (nextAppState: AppStateStatus | undefined) => {
|
async (nextAppState: AppStateStatus | undefined) => {
|
||||||
if (wallets.length === 0) return;
|
if (!shouldActivateListeners || wallets.length === 0) return;
|
||||||
|
|
||||||
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
|
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
|
||||||
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
|
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
|
||||||
updateExchangeRate();
|
updateExchangeRate();
|
||||||
|
@ -299,10 +325,12 @@ const CompanionDelegates = () => {
|
||||||
appState.current = nextAppState;
|
appState.current = nextAppState;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[processPushNotifications, showClipboardAlert, wallets],
|
[processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addListeners = useCallback(() => {
|
const addListeners = useCallback(() => {
|
||||||
|
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
|
||||||
|
|
||||||
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
|
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
|
||||||
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
|
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
|
||||||
|
|
||||||
|
@ -310,18 +338,16 @@ const CompanionDelegates = () => {
|
||||||
urlSubscription,
|
urlSubscription,
|
||||||
appStateSubscription,
|
appStateSubscription,
|
||||||
};
|
};
|
||||||
}, [handleOpenURL, handleAppStateChange]);
|
}, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscriptions = addListeners();
|
const subscriptions = addListeners();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
subscriptions.urlSubscription?.remove();
|
subscriptions.urlSubscription?.remove?.();
|
||||||
subscriptions.appStateSubscription?.remove();
|
subscriptions.appStateSubscription?.remove?.();
|
||||||
};
|
};
|
||||||
}, [addListeners]);
|
}, [addListeners]);
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CompanionDelegates;
|
export default useCompanionListeners;
|
|
@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
|
||||||
const requiresBiometrics = [
|
const requiresBiometrics = [
|
||||||
'WalletExportRoot',
|
'WalletExportRoot',
|
||||||
'WalletXpubRoot',
|
'WalletXpubRoot',
|
||||||
'ViewEditMultisigCosignersRoot',
|
'ViewEditMultisigCosigners',
|
||||||
'ExportMultisigCoordinationSetupRoot',
|
'ExportMultisigCoordinationSetupRoot',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,140 +1,167 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
||||||
|
import { navigationRef } from '../NavigationService';
|
||||||
import { CommonActions } from '@react-navigation/native';
|
import { CommonActions } from '@react-navigation/native';
|
||||||
import * as NavigationService from '../NavigationService';
|
|
||||||
import { useStorage } from './context/useStorage';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
|
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
|
||||||
Uses MenuElementsEmitter for event handling.
|
Uses MenuElementsEmitter for event handling and navigation state.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type MenuEventHandler = () => void;
|
type MenuActionHandler = () => void;
|
||||||
|
|
||||||
|
// Singleton setup - initialize once at module level
|
||||||
const { MenuElementsEmitter } = NativeModules;
|
const { MenuElementsEmitter } = NativeModules;
|
||||||
let eventEmitter: NativeEventEmitter | null = null;
|
let eventEmitter: NativeEventEmitter | null = null;
|
||||||
|
let listenersInitialized = false;
|
||||||
|
|
||||||
let globalReloadTransactionsFunction: MenuEventHandler | null = null;
|
// Registry for transaction handlers by screen ID
|
||||||
|
const handlerRegistry = new Map<string, MenuActionHandler>();
|
||||||
|
|
||||||
// Only create the emitter if the module exists and we're on iOS/macOS
|
// Store subscription references for proper cleanup
|
||||||
|
let subscriptions: { remove: () => void }[] = [];
|
||||||
|
|
||||||
|
// Create a more robust emitter with error handling
|
||||||
try {
|
try {
|
||||||
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) {
|
if (Platform.OS === 'ios' && MenuElementsEmitter) {
|
||||||
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
|
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
|
||||||
eventEmitter = null;
|
eventEmitter = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty function that does nothing - used as default
|
/**
|
||||||
const noop = () => {};
|
* Safely navigate using multiple fallback approaches
|
||||||
|
*/
|
||||||
const useMenuElements = () => {
|
function safeNavigate(routeName: string, params?: Record<string, any>): void {
|
||||||
const { walletsInitialized } = useStorage();
|
try {
|
||||||
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop);
|
if (navigationRef.current?.isReady()) {
|
||||||
// Track if listeners have been set up
|
navigationRef.current.navigate(routeName as never, params as never);
|
||||||
const listenersInitialized = useRef<boolean>(false);
|
|
||||||
const listenersRef = useRef<any[]>([]);
|
|
||||||
|
|
||||||
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
|
|
||||||
if (typeof handler !== 'function') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadTransactionsMenuActionRef.current = handler;
|
if (navigationRef.isReady()) {
|
||||||
globalReloadTransactionsFunction = handler;
|
navigationRef.dispatch(
|
||||||
}, []);
|
CommonActions.navigate({
|
||||||
|
name: routeName,
|
||||||
const clearReloadTransactionsMenuAction = useCallback(() => {
|
params,
|
||||||
reloadTransactionsMenuActionRef.current = noop;
|
}),
|
||||||
}, []);
|
);
|
||||||
|
|
||||||
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
|
|
||||||
try {
|
|
||||||
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
|
|
||||||
} catch (error) {
|
|
||||||
// Navigation failed silently
|
|
||||||
}
|
}
|
||||||
}, []);
|
} catch (error) {
|
||||||
|
console.error(`[MenuElements] Navigation error:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const eventActions = useMemo(
|
// Cleanup event listeners to prevent memory leaks
|
||||||
() => ({
|
function cleanupListeners(): void {
|
||||||
openSettings: () => {
|
if (subscriptions.length > 0) {
|
||||||
dispatchNavigate('Settings');
|
subscriptions.forEach(subscription => {
|
||||||
},
|
try {
|
||||||
addWallet: () => {
|
subscription.remove();
|
||||||
dispatchNavigate('AddWalletRoot');
|
} catch (e) {
|
||||||
},
|
console.warn('[MenuElements] Error removing subscription:', e);
|
||||||
importWallet: () => {
|
|
||||||
dispatchNavigate('AddWalletRoot', 'ImportWallet');
|
|
||||||
},
|
|
||||||
reloadTransactions: () => {
|
|
||||||
try {
|
|
||||||
const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop;
|
|
||||||
handler();
|
|
||||||
} catch (error) {
|
|
||||||
// Execution failed silently
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[dispatchNavigate],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Skip if emitter doesn't exist or wallets aren't initialized yet
|
|
||||||
if (!eventEmitter || !walletsInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listenersInitialized.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (listenersRef.current.length > 0) {
|
|
||||||
listenersRef.current.forEach(listener => listener?.remove?.());
|
|
||||||
listenersRef.current = [];
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
subscriptions = [];
|
||||||
|
listenersInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventEmitter.removeAllListeners('openSettings');
|
function initializeListeners(): void {
|
||||||
eventEmitter.removeAllListeners('addWalletMenuAction');
|
if (!eventEmitter || listenersInitialized) return;
|
||||||
eventEmitter.removeAllListeners('importWalletMenuAction');
|
|
||||||
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
|
|
||||||
} catch (error) {
|
|
||||||
// Error cleanup silently ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
cleanupListeners();
|
||||||
const listeners = [
|
|
||||||
eventEmitter.addListener('openSettings', eventActions.openSettings),
|
|
||||||
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
|
|
||||||
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
|
|
||||||
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
|
|
||||||
];
|
|
||||||
|
|
||||||
listenersRef.current = listeners;
|
// Navigation actions
|
||||||
listenersInitialized.current = true;
|
const globalActions = {
|
||||||
} catch (error) {
|
navigateToSettings: (): void => {
|
||||||
// Listener setup failed silently
|
safeNavigate('Settings');
|
||||||
}
|
},
|
||||||
|
|
||||||
|
navigateToAddWallet: (): void => {
|
||||||
|
safeNavigate('AddWalletRoot');
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateToImportWallet: (): void => {
|
||||||
|
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||||
|
},
|
||||||
|
|
||||||
|
executeReloadTransactions: (): void => {
|
||||||
|
const currentRoute = navigationRef.current?.getCurrentRoute();
|
||||||
|
if (!currentRoute) return;
|
||||||
|
|
||||||
|
const screenName = currentRoute.name;
|
||||||
|
const params = (currentRoute.params as { walletID?: string }) || {};
|
||||||
|
const walletID = params.walletID;
|
||||||
|
|
||||||
|
const specificKey = walletID ? `${screenName}-${walletID}` : null;
|
||||||
|
|
||||||
|
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
|
||||||
|
const genericHandler = handlerRegistry.get(screenName);
|
||||||
|
const handler = specificHandler || genericHandler;
|
||||||
|
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
|
||||||
|
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
|
||||||
|
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
|
||||||
|
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MenuElements] Error setting up event listeners:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
listenersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuElementsHook {
|
||||||
|
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||||
|
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||||
|
isMenuElementsSupported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountedComponents = new Set<string>();
|
||||||
|
|
||||||
|
const useMenuElements = (): MenuElementsHook => {
|
||||||
|
useEffect(() => {
|
||||||
|
initializeListeners();
|
||||||
|
|
||||||
|
const unsubscribe = navigationRef.addListener('state', () => {});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
unsubscribe();
|
||||||
listenersRef.current.forEach(listener => {
|
|
||||||
if (listener && typeof listener.remove === 'function') {
|
|
||||||
listener.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listenersRef.current = [];
|
|
||||||
listenersInitialized.current = false;
|
|
||||||
} catch (error) {
|
|
||||||
// Cleanup error silently ignored
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [walletsInitialized, eventActions]);
|
}, []);
|
||||||
|
|
||||||
|
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
|
||||||
|
if (typeof handler !== 'function') return false;
|
||||||
|
|
||||||
|
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
|
||||||
|
if (!key) return false;
|
||||||
|
|
||||||
|
mountedComponents.add(key);
|
||||||
|
|
||||||
|
handlerRegistry.set(key, handler);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
|
||||||
|
if (!screenKey) return;
|
||||||
|
|
||||||
|
handlerRegistry.delete(screenKey);
|
||||||
|
mountedComponents.delete(screenKey);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setReloadTransactionsMenuActionFunction,
|
registerTransactionsHandler,
|
||||||
clearReloadTransactionsMenuAction,
|
unregisterTransactionsHandler,
|
||||||
isMenuElementsSupported: !!eventEmitter,
|
isMenuElementsSupported: !!eventEmitter,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,28 @@
|
||||||
const useMenuElements = () => {
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
type MenuActionHandler = () => void;
|
||||||
|
|
||||||
|
interface MenuElementsHook {
|
||||||
|
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||||
|
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||||
|
isMenuElementsSupported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementation for platforms other than iOS
|
||||||
|
const useMenuElements = (): MenuElementsHook => {
|
||||||
|
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
|
||||||
|
// Non-functional stub for non-iOS platforms
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
|
||||||
|
// No-op for non-supported platforms
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setReloadTransactionsMenuActionFunction: (_func: any) => {},
|
registerTransactionsHandler,
|
||||||
clearReloadTransactionsMenuAction: () => {},
|
unregisterTransactionsHandler,
|
||||||
isMenuElementsSupported: true,
|
isMenuElementsSupported: false, // Not supported on platforms other than iOS
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1455,7 +1455,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||||
|
@ -1483,7 +1483,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
@ -1518,7 +1518,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||||
|
@ -1541,7 +1541,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
@ -1577,7 +1577,7 @@
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1590,7 +1590,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
|
@ -1620,7 +1620,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1633,7 +1633,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
|
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
|
||||||
|
@ -1664,7 +1664,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1683,7 +1683,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
|
@ -1720,7 +1720,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1739,7 +1739,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
|
||||||
|
@ -1907,7 +1907,7 @@
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1927,7 +1927,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
|
@ -1960,7 +1960,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1980,7 +1980,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
|
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
|
||||||
|
@ -2012,7 +2012,7 @@
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -2026,7 +2026,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
|
@ -2061,7 +2061,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1703157999;
|
CURRENT_PROJECT_VERSION = 1703169999;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -2075,7 +2075,7 @@
|
||||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 7.1.3;
|
MARKETING_VERSION = 7.1.5;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;
|
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;
|
||||||
|
|
|
@ -257,11 +257,13 @@
|
||||||
// Safely access the MenuElementsEmitter
|
// Safely access the MenuElementsEmitter
|
||||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||||
if (emitter) {
|
if (emitter) {
|
||||||
|
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
|
||||||
|
// Force on main thread for consistency
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[emitter openSettings];
|
[emitter openSettings];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"MenuElementsEmitter not available");
|
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,11 +271,13 @@
|
||||||
// Safely access the MenuElementsEmitter
|
// Safely access the MenuElementsEmitter
|
||||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||||
if (emitter) {
|
if (emitter) {
|
||||||
|
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
|
||||||
|
// Force on main thread for consistency
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[emitter addWalletMenuAction];
|
[emitter addWalletMenuAction];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"MenuElementsEmitter not available");
|
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,11 +285,13 @@
|
||||||
// Safely access the MenuElementsEmitter
|
// Safely access the MenuElementsEmitter
|
||||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||||
if (emitter) {
|
if (emitter) {
|
||||||
|
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
|
||||||
|
// Force on main thread for consistency
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[emitter importWalletMenuAction];
|
[emitter importWalletMenuAction];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"MenuElementsEmitter not available");
|
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,11 +299,13 @@
|
||||||
// Safely access the MenuElementsEmitter
|
// Safely access the MenuElementsEmitter
|
||||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||||
if (emitter) {
|
if (emitter) {
|
||||||
|
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
|
||||||
|
// Force on main thread for consistency
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[emitter reloadTransactionsMenuAction];
|
[emitter reloadTransactionsMenuAction];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"MenuElementsEmitter not available");
|
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,27 @@ import React
|
||||||
|
|
||||||
@objc(MenuElementsEmitter)
|
@objc(MenuElementsEmitter)
|
||||||
class MenuElementsEmitter: RCTEventEmitter {
|
class MenuElementsEmitter: RCTEventEmitter {
|
||||||
private static var _sharedInstance: MenuElementsEmitter?
|
// Use a weak reference for the singleton to prevent retain cycles
|
||||||
|
private static weak var sharedInstance: MenuElementsEmitter?
|
||||||
|
|
||||||
|
// Use LRU cache with a max size to prevent unbounded growth
|
||||||
|
private var lastEventTime: [String: TimeInterval] = [:]
|
||||||
|
private let throttleInterval: TimeInterval = 0.3 // 300ms throttle
|
||||||
|
private let maxCacheSize = 10 // Limit the cache size
|
||||||
|
|
||||||
|
// Track listener state without needing constant bridge access
|
||||||
private var hasListeners = false
|
private var hasListeners = false
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
MenuElementsEmitter._sharedInstance = self
|
MenuElementsEmitter.sharedInstance = self
|
||||||
NSLog("[MenuElements] Swift: Initialized MenuElementsEmitter instance")
|
NSLog("[MenuElements] MenuElementsEmitter initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NSLog("[MenuElements] MenuElementsEmitter deallocated")
|
||||||
|
// Ensure all event listeners are removed in deinit
|
||||||
|
self.removeAllListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
override class func requiresMainQueueSetup() -> Bool {
|
override class func requiresMainQueueSetup() -> Bool {
|
||||||
|
@ -22,54 +35,100 @@ class MenuElementsEmitter: RCTEventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc static func shared() -> MenuElementsEmitter? {
|
@objc static func shared() -> MenuElementsEmitter? {
|
||||||
return _sharedInstance
|
if sharedInstance == nil {
|
||||||
|
NSLog("[MenuElements] Warning: Attempting to use sharedInstance when it's nil")
|
||||||
|
}
|
||||||
|
return sharedInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
override func startObserving() {
|
override func startObserving() {
|
||||||
hasListeners = true
|
hasListeners = true
|
||||||
NSLog("[MenuElements] Swift: Started observing events")
|
NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func stopObserving() {
|
override func stopObserving() {
|
||||||
hasListeners = false
|
hasListeners = false
|
||||||
NSLog("[MenuElements] Swift: Stopped observing events")
|
NSLog("[MenuElements] Stopped observing events")
|
||||||
|
// Clear cache when stopping observation
|
||||||
|
lastEventTime.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func safelyEmitEvent(withName name: String) {
|
private func limitCacheSize() {
|
||||||
if hasListeners && self.bridge != nil {
|
if lastEventTime.count > maxCacheSize {
|
||||||
NSLog("[MenuElements] Swift: Emitting event: %@", name)
|
// Remove oldest entries if cache is too large
|
||||||
DispatchQueue.main.async { [weak self] in
|
let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
|
||||||
guard let self = self else { return }
|
for i in 0..<(lastEventTime.count - maxCacheSize) {
|
||||||
self.sendEvent(withName: name, body: nil)
|
lastEventTime.removeValue(forKey: sortedKeys[i].key)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
NSLog("[MenuElements] Swift: Cannot emit %@ event. %@", name, !hasListeners ? "No listeners" : "Bridge not ready")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canEmitEvent(named eventName: String) -> Bool {
|
||||||
|
let now = Date().timeIntervalSince1970
|
||||||
|
|
||||||
|
if let lastTime = lastEventTime[eventName], now - lastTime < throttleInterval {
|
||||||
|
NSLog("[MenuElements] Throttling event: \(eventName)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEventTime[eventName] = now
|
||||||
|
limitCacheSize() // Keep cache size in check
|
||||||
|
|
||||||
|
let canEmit = hasListeners && bridge != nil
|
||||||
|
if (!canEmit) {
|
||||||
|
NSLog("[MenuElements] Cannot emit event: \(eventName), hasListeners: \(hasListeners), bridge: \(bridge != nil ? "available" : "unavailable")")
|
||||||
|
}
|
||||||
|
|
||||||
|
return canEmit
|
||||||
|
}
|
||||||
|
|
||||||
|
private func safelyEmitEvent(withName name: String) {
|
||||||
|
guard canEmitEvent(named: name) else { return }
|
||||||
|
|
||||||
|
NSLog("[MenuElements] Emitting event: \(name)")
|
||||||
|
|
||||||
|
// Use weak self to avoid retain cycles
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self, self.bridge != nil, self.hasListeners else {
|
||||||
|
NSLog("[MenuElements] Failed to emit event: \(name) - bridge or listeners not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.sendEvent(withName: name, body: nil)
|
||||||
|
NSLog("[MenuElements] Event sent: \(name)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAllListeners() {
|
||||||
|
NSLog("[MenuElements] Removing all listeners")
|
||||||
|
// Clean up resources
|
||||||
|
lastEventTime.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openSettings() {
|
@objc func openSettings() {
|
||||||
NSLog("[MenuElements] Swift: openSettings called")
|
NSLog("[MenuElements] openSettings method called")
|
||||||
safelyEmitEvent(withName: "openSettings")
|
safelyEmitEvent(withName: "openSettings")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func addWalletMenuAction() {
|
@objc func addWalletMenuAction() {
|
||||||
NSLog("[MenuElements] Swift: addWalletMenuAction called")
|
NSLog("[MenuElements] addWalletMenuAction method called")
|
||||||
safelyEmitEvent(withName: "addWalletMenuAction")
|
safelyEmitEvent(withName: "addWalletMenuAction")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func importWalletMenuAction() {
|
@objc func importWalletMenuAction() {
|
||||||
NSLog("[MenuElements] Swift: importWalletMenuAction called")
|
NSLog("[MenuElements] importWalletMenuAction method called")
|
||||||
safelyEmitEvent(withName: "importWalletMenuAction")
|
safelyEmitEvent(withName: "importWalletMenuAction")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func reloadTransactionsMenuAction() {
|
@objc func reloadTransactionsMenuAction() {
|
||||||
NSLog("[MenuElements] Swift: reloadTransactionsMenuAction called")
|
|
||||||
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
|
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func invalidate() {
|
override func invalidate() {
|
||||||
NSLog("[MenuElements] Swift: Module invalidated")
|
NSLog("[MenuElements] Module invalidated")
|
||||||
MenuElementsEmitter._sharedInstance = nil
|
if MenuElementsEmitter.sharedInstance === self {
|
||||||
|
MenuElementsEmitter.sharedInstance = nil
|
||||||
|
}
|
||||||
|
removeAllListeners()
|
||||||
super.invalidate()
|
super.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@ class MarketAPI {
|
||||||
return "https://www.bnr.ro/nbrfxrates.xml"
|
return "https://www.bnr.ro/nbrfxrates.xml"
|
||||||
case "Kraken":
|
case "Kraken":
|
||||||
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
|
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
|
||||||
default:
|
default: // CoinDesk
|
||||||
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
|
return "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=\(endPointKey)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +131,14 @@ class MarketAPI {
|
||||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default: // CoinDesk
|
||||||
throw CurrencyError(errorDescription: "Unsupported data source \(source)")
|
if let rateDouble = json[endPointKey] as? Double {
|
||||||
|
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||||
|
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||||
|
return latestRateDataStore
|
||||||
|
} else {
|
||||||
|
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { fetch } from '../util/fetch';
|
||||||
import untypedFiatUnit from './fiatUnits.json';
|
import untypedFiatUnit from './fiatUnits.json';
|
||||||
|
|
||||||
export const FiatUnitSource = {
|
export const FiatUnitSource = {
|
||||||
|
@ -15,7 +16,8 @@ export const FiatUnitSource = {
|
||||||
|
|
||||||
const handleError = (source: string, ticker: string, error: Error) => {
|
const handleError = (source: string, ticker: string, error: Error) => {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not update rate for ${ticker} from ${source}: ${error.message}. ` + `Make sure the network you're on has access to ${source}.`,
|
`Could not update rate for ${ticker} from ${source}\n: ${error.message}. ` +
|
||||||
|
`\nMake sure the network you're on has access to ${source}.`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,11 +36,7 @@ interface CoinbaseResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CoinDeskResponse {
|
interface CoinDeskResponse {
|
||||||
bpi: {
|
[ticker: string]: number;
|
||||||
[ticker: string]: {
|
|
||||||
rate_float: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CoinGeckoResponse {
|
interface CoinGeckoResponse {
|
||||||
|
@ -96,8 +94,10 @@ const RateExtractors = {
|
||||||
|
|
||||||
CoinDesk: async (ticker: string): Promise<number> => {
|
CoinDesk: async (ticker: string): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
const json = (await fetchRate(`https://api.coindesk.com/v1/bpi/currentprice/${ticker}.json`)) as CoinDeskResponse;
|
const json = (await fetchRate(
|
||||||
const rate = Number(json?.bpi?.[ticker]?.rate_float);
|
`https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${ticker.toUpperCase()}`,
|
||||||
|
)) as CoinDeskResponse;
|
||||||
|
const rate = json?.[ticker.toUpperCase()];
|
||||||
if (!(rate >= 0)) throw new Error('Invalid data received');
|
if (!(rate >= 0)) throw new Error('Invalid data received');
|
||||||
return rate;
|
return rate;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
@ -32,10 +32,8 @@ import AztecoRedeemStackRoot from './AztecoRedeemStack';
|
||||||
import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
|
import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
|
||||||
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
|
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
|
||||||
import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
|
import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
|
||||||
import ScanLndInvoiceRoot from './ScanLndInvoiceStack';
|
|
||||||
import SendDetailsStack from './SendDetailsStack';
|
import SendDetailsStack from './SendDetailsStack';
|
||||||
import SignVerifyStackRoot from './SignVerifyStack';
|
import SignVerifyStackRoot from './SignVerifyStack';
|
||||||
import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack';
|
|
||||||
import WalletExportStack from './WalletExportStack';
|
import WalletExportStack from './WalletExportStack';
|
||||||
import WalletXpubStackRoot from './WalletXpubStack';
|
import WalletXpubStackRoot from './WalletXpubStack';
|
||||||
import SettingsButton from '../components/icons/SettingsButton';
|
import SettingsButton from '../components/icons/SettingsButton';
|
||||||
|
@ -66,6 +64,8 @@ import ToolsScreen from '../screen/settings/tools';
|
||||||
import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
|
import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
|
||||||
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
||||||
import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
|
import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
|
||||||
|
import ScanLNDInvoiceRoot from './ScanLNDInvoiceStack';
|
||||||
|
import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack';
|
||||||
|
|
||||||
const DetailViewStackScreensStack = () => {
|
const DetailViewStackScreensStack = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
@ -327,7 +327,7 @@ const DetailViewStackScreensStack = () => {
|
||||||
<DetailViewStack.Screen name="AddWalletRoot" component={AddWalletStack} options={NavigationDefaultOptions} />
|
<DetailViewStack.Screen name="AddWalletRoot" component={AddWalletStack} options={NavigationDefaultOptions} />
|
||||||
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} />
|
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} />
|
||||||
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
|
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
|
||||||
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />
|
<DetailViewStack.Screen name="ScanLNDInvoiceRoot" component={ScanLNDInvoiceRoot} options={NavigationDefaultOptions} />
|
||||||
<DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} />
|
<DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} />
|
||||||
{/* screens */}
|
{/* screens */}
|
||||||
<DetailViewStack.Screen
|
<DetailViewStack.Screen
|
||||||
|
@ -342,8 +342,8 @@ const DetailViewStackScreensStack = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DetailViewStack.Screen
|
<DetailViewStack.Screen
|
||||||
name="ViewEditMultisigCosignersRoot"
|
name="ViewEditMultisigCosigners"
|
||||||
component={ViewEditMultisigCosignersStackRoot}
|
component={ViewEditMultisigCosignersComponent}
|
||||||
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
|
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
|
||||||
initialParams={{ walletID: undefined, cosigners: undefined }}
|
initialParams={{ walletID: undefined, cosigners: undefined }}
|
||||||
/>
|
/>
|
||||||
|
@ -364,7 +364,7 @@ const DetailViewStackScreensStack = () => {
|
||||||
options={navigationStyle({
|
options={navigationStyle({
|
||||||
headerBackVisible: false,
|
headerBackVisible: false,
|
||||||
gestureEnabled: false,
|
gestureEnabled: false,
|
||||||
presentation: 'containedModal',
|
presentation: 'fullScreenModal',
|
||||||
title: loc.wallets.manage_title,
|
title: loc.wallets.manage_title,
|
||||||
statusBarStyle: 'auto',
|
statusBarStyle: 'auto',
|
||||||
})(theme)}
|
})(theme)}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export type DetailViewStackParamList = {
|
||||||
AddWalletRoot: undefined;
|
AddWalletRoot: undefined;
|
||||||
SendDetailsRoot: SendDetailsParams;
|
SendDetailsRoot: SendDetailsParams;
|
||||||
LNDCreateInvoiceRoot: undefined;
|
LNDCreateInvoiceRoot: undefined;
|
||||||
ScanLndInvoiceRoot: {
|
ScanLNDInvoiceRoot: {
|
||||||
screen: string;
|
screen: string;
|
||||||
params: {
|
params: {
|
||||||
paymentHash: string;
|
paymentHash: string;
|
||||||
|
@ -79,7 +79,7 @@ export type DetailViewStackParamList = {
|
||||||
ReleaseNotes: undefined;
|
ReleaseNotes: undefined;
|
||||||
ToolsScreen: undefined;
|
ToolsScreen: undefined;
|
||||||
SettingsPrivacy: undefined;
|
SettingsPrivacy: undefined;
|
||||||
ViewEditMultisigCosignersRoot: { walletID: string; cosigners: string[] };
|
ViewEditMultisigCosigners: { walletID: string; cosigners: string[]; onBarScanned?: string };
|
||||||
WalletXpubRoot: undefined;
|
WalletXpubRoot: undefined;
|
||||||
SignVerifyRoot: {
|
SignVerifyRoot: {
|
||||||
screen: 'SignVerify';
|
screen: 'SignVerify';
|
||||||
|
|
33
navigation/LNDStackParamsList.ts
Normal file
33
navigation/LNDStackParamsList.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { TWallet } from '../class/wallets/types';
|
||||||
|
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
|
||||||
|
import { ScanQRCodeParamList } from './DetailViewStackParamList';
|
||||||
|
import { TNavigationWrapper } from './SendDetailsStackParamList';
|
||||||
|
|
||||||
|
export type LNDStackParamsList = {
|
||||||
|
ScanLNDInvoice: {
|
||||||
|
walletID: string | undefined;
|
||||||
|
uri: string | undefined;
|
||||||
|
invoice: string | undefined;
|
||||||
|
onBarScanned: string | undefined;
|
||||||
|
};
|
||||||
|
LnurlPay: {
|
||||||
|
lnurl: string;
|
||||||
|
walletID: string;
|
||||||
|
};
|
||||||
|
LnurlPaySuccess: undefined;
|
||||||
|
ScanQRCode: ScanQRCodeParamList;
|
||||||
|
SelectWallet: {
|
||||||
|
chainType?: Chain;
|
||||||
|
onWalletSelect?: (wallet: TWallet, navigationWrapper: TNavigationWrapper) => void;
|
||||||
|
availableWallets?: TWallet[];
|
||||||
|
noWalletExplanationText?: string;
|
||||||
|
onChainRequireSend?: boolean;
|
||||||
|
};
|
||||||
|
Success: {
|
||||||
|
amount?: number;
|
||||||
|
fee?: number;
|
||||||
|
invoiceDescription?: string;
|
||||||
|
amountUnit: BitcoinUnit;
|
||||||
|
txid?: string;
|
||||||
|
};
|
||||||
|
};
|
|
@ -3,15 +3,15 @@ import React, { lazy, Suspense } from 'react';
|
||||||
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
|
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
|
||||||
|
|
||||||
// Lazy loading components for the navigation stack
|
// Lazy loading components for the navigation stack
|
||||||
const ScanLndInvoice = lazy(() => import('../screen/lnd/scanLndInvoice'));
|
const ScanLNDInvoice = lazy(() => import('../screen/lnd/ScanLNDInvoice'));
|
||||||
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
|
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
|
||||||
const Success = lazy(() => import('../screen/send/success'));
|
const Success = lazy(() => import('../screen/send/success'));
|
||||||
const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay'));
|
const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay'));
|
||||||
const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess'));
|
const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess'));
|
||||||
|
|
||||||
export const ScanLndInvoiceComponent = () => (
|
export const ScanLNDInvoiceComponent = () => (
|
||||||
<Suspense fallback={<LazyLoadingIndicator />}>
|
<Suspense fallback={<LazyLoadingIndicator />}>
|
||||||
<ScanLndInvoice />
|
<ScanLNDInvoice />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
import React, { lazy, Suspense } from 'react';
|
import React from 'react';
|
||||||
import { useStorage } from '../hooks/context/useStorage';
|
|
||||||
import DevMenu from '../components/DevMenu';
|
import DevMenu from '../components/DevMenu';
|
||||||
import MainRoot from './index';
|
import MainRoot from './index';
|
||||||
const CompanionDelegates = lazy(() => import('../components/CompanionDelegates'));
|
import useCompanionListeners from '../hooks/useCompanionListeners';
|
||||||
|
|
||||||
const MasterView = () => {
|
const MasterView = () => {
|
||||||
const { walletsInitialized } = useStorage();
|
// Initialize companion listeners only when wallets are initialized
|
||||||
|
// The hook checks walletsInitialized internally, so it won't run until ready
|
||||||
|
useCompanionListeners();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MainRoot />
|
<MainRoot />
|
||||||
{walletsInitialized && (
|
|
||||||
<Suspense>
|
|
||||||
<CompanionDelegates />
|
|
||||||
</Suspense>
|
|
||||||
)}
|
|
||||||
{__DEV__ && <DevMenu />}
|
{__DEV__ && <DevMenu />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,24 +4,19 @@ import React from 'react';
|
||||||
import navigationStyle from '../components/navigationStyle';
|
import navigationStyle from '../components/navigationStyle';
|
||||||
import { useTheme } from '../components/themes';
|
import { useTheme } from '../components/themes';
|
||||||
import loc from '../loc';
|
import loc from '../loc';
|
||||||
import {
|
|
||||||
LnurlPayComponent,
|
|
||||||
LnurlPaySuccessComponent,
|
|
||||||
ScanLndInvoiceComponent,
|
|
||||||
SelectWalletComponent,
|
|
||||||
SuccessComponent,
|
|
||||||
} from './LazyLoadScanLndInvoiceStack';
|
|
||||||
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
||||||
|
import { LnurlPayComponent, LnurlPaySuccessComponent, ScanLNDInvoiceComponent, SuccessComponent } from './LazyLoadScanLNDInvoiceStack';
|
||||||
|
import { SelectWalletComponent } from './LazyLoadLNDCreateInvoiceStack';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
const ScanLndInvoiceRoot = () => {
|
const ScanLNDInvoiceRoot = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ScanLndInvoice"
|
name="ScanLNDInvoice"
|
||||||
component={ScanLndInvoiceComponent}
|
component={ScanLNDInvoiceComponent}
|
||||||
options={navigationStyle({ headerBackVisible: false, title: loc.send.header, statusBarStyle: 'light' })(theme)}
|
options={navigationStyle({ headerBackVisible: false, title: loc.send.header, statusBarStyle: 'light' })(theme)}
|
||||||
initialParams={{ uri: undefined, walletID: undefined, invoice: undefined }}
|
initialParams={{ uri: undefined, walletID: undefined, invoice: undefined }}
|
||||||
/>
|
/>
|
||||||
|
@ -65,4 +60,4 @@ const ScanLndInvoiceRoot = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScanLndInvoiceRoot;
|
export default ScanLNDInvoiceRoot;
|
|
@ -82,9 +82,11 @@ export type SendDetailsStackParamList = {
|
||||||
launchedBy?: string;
|
launchedBy?: string;
|
||||||
};
|
};
|
||||||
Success: {
|
Success: {
|
||||||
fee: number;
|
fee?: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
amountUnit?: BitcoinUnit;
|
||||||
txid?: string;
|
txid?: string;
|
||||||
|
invoiceDescription?: string;
|
||||||
};
|
};
|
||||||
SelectWallet: {
|
SelectWallet: {
|
||||||
chainType?: Chain;
|
chainType?: Chain;
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import navigationStyle from '../components/navigationStyle';
|
|
||||||
import { useTheme } from '../components/themes';
|
|
||||||
import loc from '../loc';
|
|
||||||
import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack';
|
|
||||||
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
|
||||||
import { ScanQRCodeParamList } from './DetailViewStackParamList';
|
|
||||||
|
|
||||||
export type ViewEditMultisigCosignersStackParamList = {
|
|
||||||
ViewEditMultisigCosigners: {
|
|
||||||
walletID: string;
|
|
||||||
onBarScanned?: string;
|
|
||||||
};
|
|
||||||
ScanQRCode: ScanQRCodeParamList;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<ViewEditMultisigCosignersStackParamList>();
|
|
||||||
|
|
||||||
const ViewEditMultisigCosignersStackRoot = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
|
||||||
<Stack.Screen
|
|
||||||
name="ViewEditMultisigCosigners"
|
|
||||||
component={ViewEditMultisigCosignersComponent}
|
|
||||||
options={navigationStyle({
|
|
||||||
headerBackVisible: false,
|
|
||||||
title: loc.multisig.manage_keys,
|
|
||||||
})(theme)}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name="ScanQRCode"
|
|
||||||
component={ScanQRCodeComponent}
|
|
||||||
options={navigationStyle({
|
|
||||||
headerShown: false,
|
|
||||||
statusBarHidden: true,
|
|
||||||
presentation: 'fullScreenModal',
|
|
||||||
headerShadowVisible: false,
|
|
||||||
})(theme)}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewEditMultisigCosignersStackRoot;
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "bluewallet",
|
"name": "bluewallet",
|
||||||
"version": "7.1.3",
|
"version": "7.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bluewallet",
|
"name": "bluewallet",
|
||||||
"version": "7.1.3",
|
"version": "7.1.5",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
||||||
"react-native-device-info": "14.0.4",
|
"react-native-device-info": "14.0.4",
|
||||||
"react-native-document-picker": "9.3.1",
|
"react-native-document-picker": "9.3.1",
|
||||||
"react-native-draglist": "github:BlueWallet/react-native-draglist#8c52785",
|
"react-native-draglist": "github:BlueWallet/react-native-draglist#2fb0c1f",
|
||||||
"react-native-fs": "2.20.0",
|
"react-native-fs": "2.20.0",
|
||||||
"react-native-gesture-handler": "2.23.1",
|
"react-native-gesture-handler": "2.23.1",
|
||||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||||
|
@ -22087,8 +22087,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/react-native-camera-kit": {
|
"node_modules/react-native-camera-kit": {
|
||||||
"version": "14.2.0",
|
"version": "14.2.0",
|
||||||
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#1e1921223bc9da636f9889d96b03df5f77dc7bf1",
|
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#3193427143b73a6f304198b1123b2e8b90a90862",
|
||||||
"integrity": "sha512-jwVriBGZai7b4TCM0JXR0xqBY0HPtu2NSQQMETTNLyTjYYqkHEK2uaWkq/GY5B93gbAnTGJ5bRyQAqfWkPjDEw==",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bluewallet",
|
"name": "bluewallet",
|
||||||
"version": "7.1.3",
|
"version": "7.1.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -140,7 +140,7 @@
|
||||||
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
||||||
"react-native-device-info": "14.0.4",
|
"react-native-device-info": "14.0.4",
|
||||||
"react-native-document-picker": "9.3.1",
|
"react-native-document-picker": "9.3.1",
|
||||||
"react-native-draglist": "github:BlueWallet/react-native-draglist#8c52785",
|
"react-native-draglist": "github:BlueWallet/react-native-draglist#2fb0c1f",
|
||||||
"react-native-fs": "2.20.0",
|
"react-native-fs": "2.20.0",
|
||||||
"react-native-gesture-handler": "2.23.1",
|
"react-native-gesture-handler": "2.23.1",
|
||||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { ActivityIndicator, I18nManager, Keyboard, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
|
||||||
|
import { ActivityIndicator, I18nManager, Keyboard, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { Icon } from '@rneui/themed';
|
import { Icon } from '@rneui/themed';
|
||||||
|
|
||||||
import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
|
import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
|
||||||
|
@ -19,26 +19,35 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
import { useStorage } from '../../hooks/context/useStorage';
|
import { useStorage } from '../../hooks/context/useStorage';
|
||||||
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
|
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
|
||||||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||||
|
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||||
|
import { LNDStackParamsList } from '../../navigation/LNDStackParamsList';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
|
||||||
|
import { DecodedInvoice, TWallet } from '../../class/wallets/types';
|
||||||
|
import { useKeyboard } from '../../hooks/useKeyboard';
|
||||||
|
|
||||||
const ScanLndInvoice = () => {
|
type RouteProps = RouteProp<LNDStackParamsList, 'ScanLNDInvoice'>;
|
||||||
|
type NavigationProps = NativeStackNavigationProp<LNDStackParamsList, 'ScanLNDInvoice'>;
|
||||||
|
|
||||||
|
const ScanLNDInvoice = () => {
|
||||||
const { wallets, fetchAndSaveWalletTransactions } = useStorage();
|
const { wallets, fetchAndSaveWalletTransactions } = useStorage();
|
||||||
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const route = useRoute();
|
const route = useRoute<RouteProps>();
|
||||||
const { walletID, uri, invoice } = useRoute().params;
|
const { walletID, uri, invoice } = route.params || {};
|
||||||
/** @type {LightningCustodianWallet} */
|
const [wallet, setWallet] = useState<LightningCustodianWallet | undefined>(
|
||||||
const [wallet, setWallet] = useState(
|
(wallets.find(item => item.getID() === walletID) as LightningCustodianWallet) ||
|
||||||
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN),
|
(wallets.find(item => item.chain === Chain.OFFCHAIN) as LightningCustodianWallet),
|
||||||
);
|
);
|
||||||
const { navigate, setParams, goBack, pop } = useNavigation();
|
const { navigate, setParams, goBack, pop } = useExtendedNavigation<NavigationProps>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
|
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState<boolean>(false);
|
||||||
const [destination, setDestination] = useState('');
|
const [destination, setDestination] = useState<string>('');
|
||||||
const [unit, setUnit] = useState(BitcoinUnit.SATS);
|
const [unit, setUnit] = useState<BitcoinUnit>(BitcoinUnit.SATS);
|
||||||
const [decoded, setDecoded] = useState();
|
const [decoded, setDecoded] = useState<DecodedInvoice | undefined>();
|
||||||
const [amount, setAmount] = useState();
|
const [amount, setAmount] = useState<string | undefined>();
|
||||||
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState();
|
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState<boolean | undefined>();
|
||||||
const [expiresIn, setExpiresIn] = useState();
|
const [expiresIn, setExpiresIn] = useState<string | undefined>();
|
||||||
const stylesHook = StyleSheet.create({
|
const stylesHook = StyleSheet.create({
|
||||||
walletWrapLabel: {
|
walletWrapLabel: {
|
||||||
color: colors.buttonAlternativeTextColor,
|
color: colors.buttonAlternativeTextColor,
|
||||||
|
@ -54,18 +63,12 @@ const ScanLndInvoice = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow);
|
|
||||||
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide);
|
|
||||||
return () => {
|
|
||||||
showSubscription.remove();
|
|
||||||
hideSubscription.remove();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (walletID && wallet?.getID() !== walletID) {
|
if (walletID && wallet?.getID() !== walletID) {
|
||||||
setWallet(wallets.find(w => w.getID() === walletID));
|
const newWallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet;
|
||||||
|
if (newWallet) {
|
||||||
|
setWallet(newWallet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [walletID]);
|
}, [walletID]);
|
||||||
|
@ -75,7 +78,10 @@ const ScanLndInvoice = () => {
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||||
goBack();
|
goBack();
|
||||||
setTimeout(() => presentAlert({ message: loc.wallets.no_ln_wallet_error }), 500);
|
setTimeout(
|
||||||
|
() => presentAlert({ message: loc.wallets.no_ln_wallet_error, hapticFeedback: HapticFeedbackTypes.NotificationError }),
|
||||||
|
500,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [wallet]),
|
}, [wallet]),
|
||||||
|
@ -96,66 +102,68 @@ const ScanLndInvoice = () => {
|
||||||
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
|
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
let newDecoded;
|
let newDecoded: DecodedInvoice;
|
||||||
try {
|
try {
|
||||||
newDecoded = wallet.decodeInvoice(data);
|
newDecoded = wallet.decodeInvoice(data);
|
||||||
|
|
||||||
let newExpiresIn = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
|
const expiryTimeMs = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
|
||||||
if (+new Date() > newExpiresIn) {
|
let newExpiresIn: string;
|
||||||
|
|
||||||
|
if (+new Date() > expiryTimeMs) {
|
||||||
newExpiresIn = loc.lnd.expired;
|
newExpiresIn = loc.lnd.expired;
|
||||||
} else {
|
} else {
|
||||||
const time = Math.round((newExpiresIn - +new Date()) / (60 * 1000));
|
const time = Math.round((expiryTimeMs - +new Date()) / (60 * 1000));
|
||||||
newExpiresIn = loc.formatString(loc.lnd.expiresIn, { time });
|
newExpiresIn = loc.formatString(loc.lnd.expiresIn, { time });
|
||||||
}
|
}
|
||||||
|
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
setParams({ uri: undefined, invoice: data });
|
setParams({ uri: undefined, invoice: data });
|
||||||
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === '0');
|
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === 0);
|
||||||
setDestination(data);
|
setDestination(data);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setAmount(newDecoded.num_satoshis);
|
setAmount(newDecoded.num_satoshis.toString());
|
||||||
setExpiresIn(newExpiresIn);
|
setExpiresIn(newExpiresIn);
|
||||||
setDecoded(newDecoded);
|
setDecoded(newDecoded);
|
||||||
} catch (Err) {
|
} catch (Err: any) {
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
setParams({ uri: undefined });
|
setParams({ uri: undefined });
|
||||||
setTimeout(() => presentAlert({ message: Err.message }), 10);
|
setTimeout(() => presentAlert({ message: Err.message, hapticFeedback: HapticFeedbackTypes.NotificationError }), 10);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setAmount();
|
setAmount(undefined);
|
||||||
setDestination();
|
setDestination('');
|
||||||
setExpiresIn();
|
setExpiresIn(undefined);
|
||||||
setDecoded();
|
setDecoded(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [uri]);
|
}, [uri]);
|
||||||
|
|
||||||
const _keyboardDidShow = () => {
|
const _keyboardDidShow = (): void => {
|
||||||
setRenderWalletSelectionButtonHidden(true);
|
setRenderWalletSelectionButtonHidden(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _keyboardDidHide = () => {
|
const _keyboardDidHide = (): void => {
|
||||||
setRenderWalletSelectionButtonHidden(false);
|
setRenderWalletSelectionButtonHidden(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const processInvoice = data => {
|
useKeyboard({ onKeyboardDidShow: _keyboardDidShow, onKeyboardDidHide: _keyboardDidHide });
|
||||||
|
|
||||||
|
const processInvoice = (data: string): void => {
|
||||||
if (Lnurl.isLnurl(data)) return processLnurlPay(data);
|
if (Lnurl.isLnurl(data)) return processLnurlPay(data);
|
||||||
if (Lnurl.isLightningAddress(data)) return processLnurlPay(data);
|
if (Lnurl.isLightningAddress(data)) return processLnurlPay(data);
|
||||||
setParams({ uri: data });
|
setParams({ uri: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
const processLnurlPay = data => {
|
const processLnurlPay = (data: string): void => {
|
||||||
navigate('ScanLndInvoiceRoot', {
|
navigate('LnurlPay', {
|
||||||
screen: 'LnurlPay',
|
lnurl: data,
|
||||||
params: {
|
walletID: walletID || wallet?.getID() || '',
|
||||||
lnurl: data,
|
|
||||||
walletID: walletID || wallet.getID(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const pay = async () => {
|
const pay = async () => {
|
||||||
if (!decoded) {
|
if (!decoded || !wallet || !amount || !invoice) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,22 +175,22 @@ const ScanLndInvoice = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let amountSats = amount;
|
let amountSats: number = parseInt(amount, 10);
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case BitcoinUnit.SATS:
|
case BitcoinUnit.SATS:
|
||||||
amountSats = parseInt(amountSats, 10); // nop
|
// amount is already in sats
|
||||||
break;
|
break;
|
||||||
case BitcoinUnit.BTC:
|
case BitcoinUnit.BTC:
|
||||||
amountSats = btcToSatoshi(amountSats);
|
amountSats = btcToSatoshi(amount);
|
||||||
break;
|
break;
|
||||||
case BitcoinUnit.LOCAL_CURRENCY:
|
case BitcoinUnit.LOCAL_CURRENCY:
|
||||||
amountSats = btcToSatoshi(fiatToBTC(amountSats));
|
amountSats = btcToSatoshi(fiatToBTC(Number(amount)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const newExpiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
|
const expiryTimeMs = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
|
||||||
if (+new Date() > newExpiresIn) {
|
if (+new Date() > expiryTimeMs) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||||
return presentAlert({ message: loc.lnd.errorInvoiceExpired });
|
return presentAlert({ message: loc.lnd.errorInvoiceExpired });
|
||||||
|
@ -197,7 +205,7 @@ const ScanLndInvoice = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wallet.payInvoice(invoice, amountSats);
|
await wallet.payInvoice(invoice, amountSats);
|
||||||
} catch (Err) {
|
} catch (Err: any) {
|
||||||
console.log(Err.message);
|
console.log(Err.message);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||||
|
@ -212,7 +220,7 @@ const ScanLndInvoice = () => {
|
||||||
fetchAndSaveWalletTransactions(wallet.getID());
|
fetchAndSaveWalletTransactions(wallet.getID());
|
||||||
};
|
};
|
||||||
|
|
||||||
const processTextForInvoice = text => {
|
const processTextForInvoice = (text: string): void => {
|
||||||
if (
|
if (
|
||||||
(text && text.toLowerCase().startsWith('lnb')) ||
|
(text && text.toLowerCase().startsWith('lnb')) ||
|
||||||
text.toLowerCase().startsWith('lightning:lnb') ||
|
text.toLowerCase().startsWith('lightning:lnb') ||
|
||||||
|
@ -227,7 +235,7 @@ const ScanLndInvoice = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldDisablePayButton = () => {
|
const shouldDisablePayButton = (): boolean => {
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -235,16 +243,15 @@ const ScanLndInvoice = () => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return !(amount > 0);
|
return !(parseInt(amount, 10) > 0);
|
||||||
// return decoded.num_satoshis <= 0 || isLoading || isNaN(decoded.num_satoshis);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const naviageToSelectWallet = () => {
|
const naviageToSelectWallet = (): void => {
|
||||||
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
|
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderWalletSelectionButton = () => {
|
const renderWalletSelectionButton = (): JSX.Element | undefined => {
|
||||||
if (renderWalletSelectionButtonHidden) return;
|
if (renderWalletSelectionButtonHidden || !wallet) return;
|
||||||
const walletLabel = wallet.getLabel();
|
const walletLabel = wallet.getLabel();
|
||||||
return (
|
return (
|
||||||
<View style={styles.walletSelectRoot}>
|
<View style={styles.walletSelectRoot}>
|
||||||
|
@ -267,25 +274,27 @@ const ScanLndInvoice = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFees = () => {
|
const getFees = (): string => {
|
||||||
const min = Math.floor(decoded.num_satoshis * 0.003);
|
if (!decoded) return '';
|
||||||
const max = Math.floor(decoded.num_satoshis * 0.01) + 1;
|
const num_satoshis = parseInt(decoded.num_satoshis.toString(), 10);
|
||||||
|
const min = Math.floor(num_satoshis * 0.003);
|
||||||
|
const max = Math.floor(num_satoshis * 0.01) + 1;
|
||||||
return `${min} ${BitcoinUnit.SATS} - ${max} ${BitcoinUnit.SATS}`;
|
return `${min} ${BitcoinUnit.SATS} - ${max} ${BitcoinUnit.SATS}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBlur = () => {
|
const onBlur = (): void => {
|
||||||
processTextForInvoice(destination);
|
processTextForInvoice(destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onWalletSelect = selectedWallet => {
|
const onWalletSelect = (selectedWallet: TWallet): void => {
|
||||||
setParams({ walletID: selectedWallet.getID() });
|
setParams({ walletID: selectedWallet.getID() });
|
||||||
pop();
|
pop();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBarScanned = useCallback(
|
const onBarScanned = useCallback(
|
||||||
value => {
|
(value: string): void => {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
|
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, (completionValue: any) => {
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||||
navigate(...completionValue);
|
navigate(...completionValue);
|
||||||
});
|
});
|
||||||
|
@ -301,6 +310,12 @@ const ScanLndInvoice = () => {
|
||||||
}
|
}
|
||||||
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
|
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
|
||||||
|
|
||||||
|
const onChangeText = (text: string): void => {
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
setDestination(trimmedText);
|
||||||
|
processTextForInvoice(trimmedText);
|
||||||
|
};
|
||||||
|
|
||||||
if (wallet === undefined || !wallet) {
|
if (wallet === undefined || !wallet) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.loadingIndicator, stylesHook.root]}>
|
<View style={[styles.loadingIndicator, stylesHook.root]}>
|
||||||
|
@ -334,11 +349,7 @@ const ScanLndInvoice = () => {
|
||||||
|
|
||||||
<BlueCard>
|
<BlueCard>
|
||||||
<AddressInput
|
<AddressInput
|
||||||
onChangeText={text => {
|
onChangeText={onChangeText}
|
||||||
text = text.trim();
|
|
||||||
setDestination(text);
|
|
||||||
}}
|
|
||||||
onBarScanned={data => processTextForInvoice(data.data)}
|
|
||||||
address={destination}
|
address={destination}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
placeholder={loc.lnd.placeholder}
|
placeholder={loc.lnd.placeholder}
|
||||||
|
@ -381,7 +392,7 @@ const ScanLndInvoice = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScanLndInvoice;
|
export default ScanLNDInvoice;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
walletSelectRoot: {
|
walletSelectRoot: {
|
|
@ -109,7 +109,7 @@ const LNDCreateInvoice = () => {
|
||||||
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
|
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
|
||||||
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
|
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
|
||||||
// invoices (including through lnurl-withdraw)
|
// invoices (including through lnurl-withdraw)
|
||||||
navigate('ScanLndInvoiceRoot', {
|
navigate('ScanLNDInvoiceRoot', {
|
||||||
screen: 'LnurlPay',
|
screen: 'LnurlPay',
|
||||||
params: {
|
params: {
|
||||||
lnurl: data,
|
lnurl: data,
|
||||||
|
|
|
@ -149,7 +149,7 @@ const LnurlPay: React.FC = () => {
|
||||||
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', {
|
navigate('ScanLNDInvoiceRoot', {
|
||||||
screen: 'LnurlPaySuccess',
|
screen: 'LnurlPaySuccess',
|
||||||
params: {
|
params: {
|
||||||
paymentHash: decoded.payment_hash,
|
paymentHash: decoded.payment_hash,
|
||||||
|
|
|
@ -109,7 +109,7 @@ const LnurlPaySuccess: React.FC = () => {
|
||||||
{repeatable ? (
|
{repeatable ? (
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigate('ScanLndInvoiceRoot', {
|
navigate('ScanLNDInvoiceRoot', {
|
||||||
screen: 'LnurlPay',
|
screen: 'LnurlPay',
|
||||||
params: {
|
params: {
|
||||||
// @ts-ignore fixme
|
// @ts-ignore fixme
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { RouteProp, StackActions, useFocusEffect, useIsFocused, useRoute } from '@react-navigation/native';
|
import { RouteProp, StackActions, useIsFocused, useRoute } from '@react-navigation/native';
|
||||||
import * as bitcoin from 'bitcoinjs-lib';
|
import * as bitcoin from 'bitcoinjs-lib';
|
||||||
import createHash from 'create-hash';
|
import createHash from 'create-hash';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
|
import { Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
import Base43 from '../../blue_modules/base43';
|
import Base43 from '../../blue_modules/base43';
|
||||||
import * as fs from '../../blue_modules/fs';
|
import * as fs from '../../blue_modules/fs';
|
||||||
|
@ -12,7 +12,6 @@ import Button from '../../components/Button';
|
||||||
import { useTheme } from '../../components/themes';
|
import { useTheme } from '../../components/themes';
|
||||||
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
|
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
|
||||||
import loc from '../../loc';
|
import loc from '../../loc';
|
||||||
import { useSettings } from '../../hooks/context/useSettings';
|
|
||||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||||
import CameraScreen from '../../components/CameraScreen';
|
import CameraScreen from '../../components/CameraScreen';
|
||||||
import SafeArea from '../../components/SafeArea';
|
import SafeArea from '../../components/SafeArea';
|
||||||
|
@ -57,7 +56,6 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
const ScanQRCode = () => {
|
const ScanQRCode = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { setIsDrawerShouldHide } = useSettings();
|
|
||||||
const navigation = useExtendedNavigation();
|
const navigation = useExtendedNavigation();
|
||||||
const route = useRoute<RouteProps>();
|
const route = useRoute<RouteProps>();
|
||||||
const navigationState = navigation.getState();
|
const navigationState = navigation.getState();
|
||||||
|
@ -96,16 +94,6 @@ const ScanQRCode = () => {
|
||||||
return createHash('sha256').update(s).digest().toString('hex');
|
return createHash('sha256').update(s).digest().toString('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
setIsDrawerShouldHide(true);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setIsDrawerShouldHide(false);
|
|
||||||
};
|
|
||||||
}, [setIsDrawerShouldHide]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const _onReadUniformResourceV2 = (part: string) => {
|
const _onReadUniformResourceV2 = (part: string) => {
|
||||||
if (!decoder) decoder = new BlueURDecoder();
|
if (!decoder) decoder = new BlueURDecoder();
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -118,7 +118,8 @@ const Currency: React.FC = () => {
|
||||||
<FlatList
|
<FlatList
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
automaticallyAdjustContentInsets
|
automaticallyAdjustContentInsets
|
||||||
keyExtractor={(_item, index) => `${index}`}
|
automaticallyAdjustKeyboardInsets
|
||||||
|
keyExtractor={item => item.endPointKey}
|
||||||
data={data}
|
data={data}
|
||||||
initialNumToRender={30}
|
initialNumToRender={30}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
|
|
@ -515,7 +515,6 @@ const ElectrumSettings: React.FC = () => {
|
||||||
onChangeText={text => setHost(text.trim())}
|
onChangeText={text => setHost(text.trim())}
|
||||||
editable={!isLoading}
|
editable={!isLoading}
|
||||||
keyboardType="default"
|
keyboardType="default"
|
||||||
skipValidation
|
|
||||||
onBlur={() => setIsAndroidAddressKeyboardVisible(false)}
|
onBlur={() => setIsAndroidAddressKeyboardVisible(false)}
|
||||||
onFocus={() => setIsAndroidAddressKeyboardVisible(true)}
|
onFocus={() => setIsAndroidAddressKeyboardVisible(true)}
|
||||||
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
|
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
|
||||||
|
@ -611,9 +610,7 @@ const ElectrumSettings: React.FC = () => {
|
||||||
onValueChange: onElectrumConnectionEnabledSwitchChange,
|
onValueChange: onElectrumConnectionEnabledSwitchChange,
|
||||||
value: isElectrumDisabled,
|
value: isElectrumDisabled,
|
||||||
testID: 'ElectrumConnectionEnabledSwitch',
|
testID: 'ElectrumConnectionEnabledSwitch',
|
||||||
disabled: isLoading,
|
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
|
||||||
bottomDivider={false}
|
bottomDivider={false}
|
||||||
subtitle={loc.settings.electrum_offline_description}
|
subtitle={loc.settings.electrum_offline_description}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,9 +7,11 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
I18nManager,
|
I18nManager,
|
||||||
Animated,
|
Animated,
|
||||||
LayoutAnimation,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
LayoutAnimation,
|
||||||
|
UIManager,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
|
import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
|
||||||
|
@ -99,19 +101,21 @@ type Action =
|
||||||
interface State {
|
interface State {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
isSearchFocused: boolean;
|
isSearchFocused: boolean;
|
||||||
order: Item[];
|
originalWalletsOrder: Item[];
|
||||||
tempOrder: Item[];
|
currentWalletsOrder: Item[];
|
||||||
wallets: TWallet[];
|
availableWallets: TWallet[];
|
||||||
txMetadata: TTXMetadata;
|
txMetadata: TTXMetadata;
|
||||||
|
initialWalletsBackup: TWallet[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
isSearchFocused: false,
|
isSearchFocused: false,
|
||||||
order: [],
|
originalWalletsOrder: [],
|
||||||
tempOrder: [],
|
currentWalletsOrder: [],
|
||||||
wallets: [],
|
availableWallets: [],
|
||||||
txMetadata: {},
|
txMetadata: {},
|
||||||
|
initialWalletsBackup: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
|
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
|
||||||
|
@ -131,21 +135,22 @@ const reducer = (state: State, action: Action): State => {
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
wallets: action.payload.wallets,
|
availableWallets: action.payload.wallets,
|
||||||
txMetadata: action.payload.txMetadata,
|
txMetadata: action.payload.txMetadata,
|
||||||
order: initialWalletsOrder,
|
originalWalletsOrder: initialWalletsOrder,
|
||||||
tempOrder: initialWalletsOrder,
|
currentWalletsOrder: initialWalletsOrder,
|
||||||
|
initialWalletsBackup: deepCopyWallets(action.payload.wallets),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SET_FILTERED_ORDER: {
|
case SET_FILTERED_ORDER: {
|
||||||
const query = action.payload.toLowerCase();
|
const query = action.payload.toLowerCase();
|
||||||
const filteredWallets = state.wallets
|
const filteredWallets = state.availableWallets
|
||||||
.filter(wallet => wallet.getLabel()?.toLowerCase().includes(query))
|
.filter(wallet => wallet.getLabel()?.toLowerCase().includes(query))
|
||||||
.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
|
.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
|
||||||
|
|
||||||
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(query));
|
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(query));
|
||||||
|
|
||||||
const filteredTransactions = state.wallets.flatMap(wallet =>
|
const filteredTransactions = state.availableWallets.flatMap(wallet =>
|
||||||
wallet
|
wallet
|
||||||
.getTransactions()
|
.getTransactions()
|
||||||
.filter((tx: Transaction) =>
|
.filter((tx: Transaction) =>
|
||||||
|
@ -158,14 +163,16 @@ const reducer = (state: State, action: Action): State => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
tempOrder: filteredOrder,
|
currentWalletsOrder: filteredOrder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SAVE_CHANGES: {
|
case SAVE_CHANGES: {
|
||||||
|
const savedWallets = deepCopyWallets(action.payload);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
wallets: deepCopyWallets(action.payload),
|
availableWallets: savedWallets,
|
||||||
tempOrder: state.tempOrder.map(item =>
|
initialWalletsBackup: savedWallets,
|
||||||
|
currentWalletsOrder: state.currentWalletsOrder.map(item =>
|
||||||
item.type === ItemType.WalletSection
|
item.type === ItemType.WalletSection
|
||||||
? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! }
|
? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! }
|
||||||
: item,
|
: item,
|
||||||
|
@ -173,13 +180,15 @@ const reducer = (state: State, action: Action): State => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SET_TEMP_ORDER: {
|
case SET_TEMP_ORDER: {
|
||||||
return { ...state, tempOrder: action.payload };
|
return { ...state, currentWalletsOrder: action.payload };
|
||||||
}
|
}
|
||||||
case REMOVE_WALLET: {
|
case REMOVE_WALLET: {
|
||||||
const updatedOrder = state.tempOrder.filter(item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload);
|
const updatedOrder = state.currentWalletsOrder.filter(
|
||||||
|
item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
tempOrder: updatedOrder,
|
currentWalletsOrder: updatedOrder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -187,12 +196,16 @@ const reducer = (state: State, action: Action): State => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||||
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||||
|
}
|
||||||
|
|
||||||
const ManageWallets: React.FC = () => {
|
const ManageWallets: React.FC = () => {
|
||||||
const { colors, closeImage } = useTheme();
|
const { colors, closeImage } = useTheme();
|
||||||
const { wallets: storedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
|
const { wallets: persistedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
|
||||||
const { setIsDrawerShouldHide } = useSettings();
|
const { setIsDrawerShouldHide } = useSettings();
|
||||||
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList
|
const initialWalletsRef = useRef<TWallet[]>(deepCopyWallets(persistedWallets));
|
||||||
const { navigate, setOptions, goBack } = useExtendedNavigation();
|
const { navigate, setOptions, goBack, dispatch: navigationDispatch } = useExtendedNavigation();
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
|
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
|
||||||
const bounceAnim = useBounceAnimation(state.searchQuery);
|
const bounceAnim = useBounceAnimation(state.searchQuery);
|
||||||
|
@ -204,76 +217,123 @@ const ManageWallets: React.FC = () => {
|
||||||
color: colors.foregroundColor,
|
color: colors.foregroundColor,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [data, setData] = useState(state.tempOrder);
|
const [uiData, setUiData] = useState(state.currentWalletsOrder);
|
||||||
|
|
||||||
const listRef = useRef<FlatList<Item> | null>(null);
|
const listRef = useRef<FlatList<Item> | null>(null);
|
||||||
|
const [saveInProgress, setSaveInProgress] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(state.tempOrder);
|
setUiData(state.currentWalletsOrder);
|
||||||
}, [state.tempOrder]);
|
}, [state.currentWalletsOrder]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
|
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: initialWalletsRef.current, txMetadata } });
|
||||||
}, [txMetadata]);
|
}, [txMetadata]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedSearchQuery) {
|
if (debouncedSearchQuery) {
|
||||||
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
|
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: SET_TEMP_ORDER, payload: state.order });
|
dispatch({ type: SET_TEMP_ORDER, payload: state.originalWalletsOrder });
|
||||||
}
|
}
|
||||||
}, [debouncedSearchQuery, state.order]);
|
}, [debouncedSearchQuery, state.originalWalletsOrder]);
|
||||||
|
|
||||||
const hasUnsavedChanges = useMemo(() => {
|
const hasUnsavedChanges = useMemo(() => {
|
||||||
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
|
const currentWalletIds = state.currentWalletsOrder
|
||||||
}, [state.tempOrder]);
|
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||||
|
.map(item => item.data.getID());
|
||||||
|
|
||||||
usePreventRemove(hasUnsavedChanges, async () => {
|
const originalWalletIds = state.initialWalletsBackup.map(wallet => wallet.getID());
|
||||||
await new Promise<void>(resolve => {
|
|
||||||
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
if (currentWalletIds.length !== originalWalletIds.length) {
|
||||||
{ text: loc._.cancel, style: 'cancel', onPress: () => resolve() },
|
return true;
|
||||||
{ text: loc._.ok, style: 'default', onPress: () => resolve() },
|
}
|
||||||
]);
|
|
||||||
});
|
for (let i = 0; i < currentWalletIds.length; i++) {
|
||||||
|
if (currentWalletIds[i] !== originalWalletIds[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedWallets = state.currentWalletsOrder
|
||||||
|
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||||
|
.map(item => item.data);
|
||||||
|
|
||||||
|
for (const modifiedWallet of modifiedWallets) {
|
||||||
|
const originalWallet = state.initialWalletsBackup.find(w => w.getID() === modifiedWallet.getID());
|
||||||
|
if (originalWallet && originalWallet.hideBalance !== modifiedWallet.hideBalance) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, [state.currentWalletsOrder, state.initialWalletsBackup]);
|
||||||
|
|
||||||
|
usePreventRemove(hasUnsavedChanges && !saveInProgress, ({ data: preventRemoveData }) => {
|
||||||
|
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
||||||
|
{ text: loc._.cancel, style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: loc._.ok,
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => navigationDispatch(preventRemoveData.action),
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (saveInProgress) {
|
||||||
|
goBack();
|
||||||
|
setSaveInProgress(false);
|
||||||
|
}
|
||||||
|
}, [saveInProgress, goBack]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
||||||
const newWalletOrder = state.tempOrder
|
const reorderedWallets = state.currentWalletsOrder
|
||||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||||
.map(item => item.data);
|
.map(item => item.data);
|
||||||
|
|
||||||
setWalletsWithNewOrder(newWalletOrder);
|
const walletsToDelete = state.initialWalletsBackup.filter(
|
||||||
|
originalWallet => !reorderedWallets.some(wallet => wallet.getID() === originalWallet.getID()),
|
||||||
|
);
|
||||||
|
|
||||||
dispatch({ type: SAVE_CHANGES, payload: newWalletOrder });
|
setWalletsWithNewOrder(reorderedWallets);
|
||||||
|
dispatch({ type: SAVE_CHANGES, payload: reorderedWallets });
|
||||||
|
initialWalletsRef.current = deepCopyWallets(reorderedWallets);
|
||||||
|
|
||||||
walletsRef.current = deepCopyWallets(newWalletOrder);
|
walletsToDelete.forEach(wallet => {
|
||||||
|
handleWalletDeletion(wallet.getID());
|
||||||
state.tempOrder.forEach(item => {
|
|
||||||
if (item.type === ItemType.WalletSection && !newWalletOrder.some(wallet => wallet.getID() === item.data.getID())) {
|
|
||||||
handleWalletDeletion(item.data.getID());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
goBack();
|
setSaveInProgress(true);
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: SET_SEARCH_QUERY, payload: '' });
|
dispatch({ type: SET_SEARCH_QUERY, payload: '' });
|
||||||
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
|
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
|
||||||
}
|
}
|
||||||
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, handleWalletDeletion]);
|
}, [
|
||||||
|
setWalletsWithNewOrder,
|
||||||
|
state.searchQuery,
|
||||||
|
state.isSearchFocused,
|
||||||
|
state.currentWalletsOrder,
|
||||||
|
state.initialWalletsBackup,
|
||||||
|
handleWalletDeletion,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const buttonOpacity = useMemo(() => ({ opacity: saveInProgress ? 0.5 : 1 }), [saveInProgress]);
|
||||||
const HeaderLeftButton = useMemo(
|
const HeaderLeftButton = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={loc._.close}
|
accessibilityLabel={loc._.close}
|
||||||
style={styles.button}
|
style={[styles.button, buttonOpacity]}
|
||||||
onPress={goBack}
|
onPress={goBack}
|
||||||
|
disabled={saveInProgress}
|
||||||
testID="NavigationCloseButton"
|
testID="NavigationCloseButton"
|
||||||
>
|
>
|
||||||
<Image source={closeImage} />
|
<Image source={closeImage} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
[goBack, closeImage],
|
[buttonOpacity, goBack, saveInProgress, closeImage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const SaveButton = useMemo(
|
const SaveButton = useMemo(
|
||||||
|
@ -290,7 +350,6 @@ const ManageWallets: React.FC = () => {
|
||||||
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
|
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
|
||||||
placeholder: loc.wallets.manage_wallets_search_placeholder,
|
placeholder: loc.wallets.manage_wallets_search_placeholder,
|
||||||
};
|
};
|
||||||
|
|
||||||
setOptions({
|
setOptions({
|
||||||
headerLeft: () => HeaderLeftButton,
|
headerLeft: () => HeaderLeftButton,
|
||||||
headerRight: () => SaveButton,
|
headerRight: () => SaveButton,
|
||||||
|
@ -329,19 +388,30 @@ const ManageWallets: React.FC = () => {
|
||||||
[bounceAnim],
|
[bounceAnim],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteWallet = useCallback(
|
const handleDeleteWallet = useCallback(async (wallet: TWallet) => {
|
||||||
async (wallet: TWallet) => {
|
LayoutAnimation.configureNext({
|
||||||
const deletionSucceeded = await handleWalletDeletion(wallet.getID());
|
duration: 300,
|
||||||
if (deletionSucceeded) {
|
create: {
|
||||||
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
|
type: LayoutAnimation.Types.easeInEaseOut,
|
||||||
}
|
property: LayoutAnimation.Properties.opacity,
|
||||||
},
|
},
|
||||||
[handleWalletDeletion],
|
update: {
|
||||||
);
|
type: LayoutAnimation.Types.easeInEaseOut,
|
||||||
|
property: LayoutAnimation.Properties.opacity,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: LayoutAnimation.Types.easeInEaseOut,
|
||||||
|
property: LayoutAnimation.Properties.opacity,
|
||||||
|
duration: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleToggleHideBalance = useCallback(
|
const handleToggleHideBalance = useCallback(
|
||||||
(wallet: TWallet) => {
|
(wallet: TWallet) => {
|
||||||
const updatedOrder = state.tempOrder.map(item => {
|
const updatedOrder = state.currentWalletsOrder.map(item => {
|
||||||
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
|
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
|
||||||
item.data.hideBalance = !item.data.hideBalance;
|
item.data.hideBalance = !item.data.hideBalance;
|
||||||
return {
|
return {
|
||||||
|
@ -351,11 +421,10 @@ const ManageWallets: React.FC = () => {
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
||||||
|
|
||||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||||
},
|
},
|
||||||
[state.tempOrder],
|
[state.currentWalletsOrder],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToWallet = useCallback(
|
const navigateToWallet = useCallback(
|
||||||
|
@ -373,13 +442,19 @@ const ManageWallets: React.FC = () => {
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(info: DragListRenderItemInfo<Item>) => {
|
(info: DragListRenderItemInfo<Item>) => {
|
||||||
const { item, onDragStart, isActive } = info;
|
const { item, onDragStart, isActive } = info;
|
||||||
|
|
||||||
|
const compatibleState = {
|
||||||
|
wallets: state.availableWallets,
|
||||||
|
searchQuery: state.searchQuery,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageWalletsListItem
|
<ManageWalletsListItem
|
||||||
item={item}
|
item={item}
|
||||||
onPressIn={undefined}
|
onPressIn={undefined}
|
||||||
onPressOut={undefined}
|
onPressOut={undefined}
|
||||||
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
|
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
|
||||||
state={state}
|
state={compatibleState}
|
||||||
navigateToWallet={navigateToWallet}
|
navigateToWallet={navigateToWallet}
|
||||||
renderHighlightedText={renderHighlightedText}
|
renderHighlightedText={renderHighlightedText}
|
||||||
handleDeleteWallet={handleDeleteWallet}
|
handleDeleteWallet={handleDeleteWallet}
|
||||||
|
@ -389,33 +464,33 @@ const ManageWallets: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance],
|
[
|
||||||
|
state.availableWallets,
|
||||||
|
state.searchQuery,
|
||||||
|
state.isSearchFocused,
|
||||||
|
navigateToWallet,
|
||||||
|
renderHighlightedText,
|
||||||
|
handleDeleteWallet,
|
||||||
|
handleToggleHideBalance,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReordered = useCallback(
|
const onReordered = useCallback(
|
||||||
(fromIndex: number, toIndex: number) => {
|
(fromIndex: number, toIndex: number) => {
|
||||||
const copy = [...state.order];
|
const updatedOrder = [...state.currentWalletsOrder];
|
||||||
const removed = copy.splice(fromIndex, 1);
|
const removed = updatedOrder.splice(fromIndex, 1);
|
||||||
copy.splice(toIndex, 0, removed[0]);
|
updatedOrder.splice(toIndex, 0, removed[0]);
|
||||||
|
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||||
dispatch({ type: SET_TEMP_ORDER, payload: copy });
|
|
||||||
dispatch({
|
|
||||||
type: SET_INITIAL_ORDER,
|
|
||||||
payload: {
|
|
||||||
wallets: copy.filter(item => item.type === ItemType.WalletSection).map(item => item.data as TWallet),
|
|
||||||
txMetadata: state.txMetadata,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[state.order, state.txMetadata],
|
[state.currentWalletsOrder],
|
||||||
);
|
);
|
||||||
|
|
||||||
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
|
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
|
||||||
|
|
||||||
const renderHeader = useMemo(() => {
|
const renderHeader = useMemo(() => {
|
||||||
if (!state.searchQuery) return null;
|
if (!state.searchQuery) return null;
|
||||||
const hasWallets = state.wallets.length > 0;
|
const hasWallets = state.availableWallets.length > 0;
|
||||||
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) =>
|
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) =>
|
||||||
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
|
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
@ -425,7 +500,7 @@ const ManageWallets: React.FC = () => {
|
||||||
!hasWallets &&
|
!hasWallets &&
|
||||||
!hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text>
|
!hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text>
|
||||||
);
|
);
|
||||||
}, [state.searchQuery, state.wallets.length, state.txMetadata, stylesHook.noResultsText]);
|
}, [state.searchQuery, state.availableWallets.length, state.txMetadata, stylesHook.noResultsText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
|
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
|
||||||
|
@ -437,7 +512,7 @@ const ManageWallets: React.FC = () => {
|
||||||
automaticallyAdjustKeyboardInsets
|
automaticallyAdjustKeyboardInsets
|
||||||
automaticallyAdjustsScrollIndicatorInsets
|
automaticallyAdjustsScrollIndicatorInsets
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
data={data}
|
data={uiData}
|
||||||
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
|
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
onReordered={onReordered}
|
onReordered={onReordered}
|
||||||
|
@ -460,35 +535,37 @@ const styles = StyleSheet.create({
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
noResultsText: {
|
noResultsText: {
|
||||||
fontSize: 19,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 34,
|
marginTop: 34,
|
||||||
},
|
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||||
highlightedContainer: {
|
fontWeight: 'bold',
|
||||||
backgroundColor: 'white',
|
|
||||||
borderColor: 'black',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 5,
|
|
||||||
padding: 2,
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
textDecorationStyle: 'double',
|
|
||||||
textShadowColor: '#000',
|
|
||||||
textShadowOffset: { width: 1, height: 1 },
|
|
||||||
textShadowRadius: 1,
|
|
||||||
},
|
|
||||||
highlighted: {
|
|
||||||
color: 'black',
|
|
||||||
fontSize: 19,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
defaultText: {
|
|
||||||
fontSize: 19,
|
fontSize: 19,
|
||||||
},
|
},
|
||||||
dimmedText: {
|
dimmedText: {
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
|
defaultText: {
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
highlighted: {
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'black',
|
||||||
|
textShadowRadius: 1,
|
||||||
|
textShadowOffset: { width: 1, height: 1 },
|
||||||
|
textShadowColor: '#000',
|
||||||
|
textDecorationStyle: 'double',
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 5,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'black',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
highlightedContainer: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { RouteProp, useFocusEffect, useRoute, usePreventRemove, CommonActions } from '@react-navigation/native';
|
import { RouteProp, useFocusEffect, useRoute, usePreventRemove, StackActions } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
|
@ -18,7 +18,15 @@ import {
|
||||||
import { Badge, Icon } from '@rneui/themed';
|
import { Badge, Icon } from '@rneui/themed';
|
||||||
import { isDesktop } from '../../blue_modules/environment';
|
import { isDesktop } from '../../blue_modules/environment';
|
||||||
import { encodeUR } from '../../blue_modules/ur';
|
import { encodeUR } from '../../blue_modules/ur';
|
||||||
import { BlueCard, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../../BlueComponents';
|
import {
|
||||||
|
BlueCard,
|
||||||
|
BlueFormMultiInput,
|
||||||
|
BlueLoading,
|
||||||
|
BlueSpacing10,
|
||||||
|
BlueSpacing20,
|
||||||
|
BlueSpacing40,
|
||||||
|
BlueTextCentered,
|
||||||
|
} from '../../BlueComponents';
|
||||||
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
|
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
|
||||||
import presentAlert from '../../components/Alert';
|
import presentAlert from '../../components/Alert';
|
||||||
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
|
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
|
||||||
|
@ -40,14 +48,14 @@ import { useStorage } from '../../hooks/context/useStorage';
|
||||||
import ToolTipMenu from '../../components/TooltipMenu';
|
import ToolTipMenu from '../../components/TooltipMenu';
|
||||||
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
|
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
|
||||||
import { useSettings } from '../../hooks/context/useSettings';
|
import { useSettings } from '../../hooks/context/useSettings';
|
||||||
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
|
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import SafeArea from '../../components/SafeArea';
|
import SafeArea from '../../components/SafeArea';
|
||||||
import { TWallet } from '../../class/wallets/types';
|
import { TWallet } from '../../class/wallets/types';
|
||||||
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
|
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
|
||||||
|
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
|
||||||
|
|
||||||
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
|
type RouteParams = RouteProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
|
||||||
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
|
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
|
||||||
|
|
||||||
const ViewEditMultisigCosigners: React.FC = () => {
|
const ViewEditMultisigCosigners: React.FC = () => {
|
||||||
const hasLoaded = useRef(false);
|
const hasLoaded = useRef(false);
|
||||||
|
@ -169,9 +177,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
||||||
setIsSaveButtonDisabled(true);
|
setIsSaveButtonDisabled(true);
|
||||||
setWalletsWithNewOrder(newWallets);
|
setWalletsWithNewOrder(newWallets);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch(
|
const popTo = StackActions.popTo('WalletTransactions', {
|
||||||
CommonActions.navigate({ name: 'WalletTransactions', params: { walletID: wallet.getID(), walletType: MultisigHDWallet.type } }),
|
walletID,
|
||||||
);
|
walletType: wallet.type,
|
||||||
|
});
|
||||||
|
dispatch(popTo);
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
@ -560,6 +570,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
||||||
|
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<>
|
<>
|
||||||
|
<BlueSpacing40 />
|
||||||
<AddressInputScanButton
|
<AddressInputScanButton
|
||||||
beforePress={async () => {
|
beforePress={async () => {
|
||||||
await provideMnemonicsModalRef.current?.dismiss();
|
await provideMnemonicsModalRef.current?.dismiss();
|
||||||
|
@ -568,7 +579,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
||||||
type="link"
|
type="link"
|
||||||
onChangeText={setImportText}
|
onChangeText={setImportText}
|
||||||
/>
|
/>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing40 />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -95,7 +95,8 @@ const WalletDetails: React.FC = () => {
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [handleWalletDeletion, wallet]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const presentWalletHasBalanceAlert = useCallback(async () => {
|
const presentWalletHasBalanceAlert = useCallback(async () => {
|
||||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
|
||||||
|
@ -305,11 +306,8 @@ const WalletDetails: React.FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const navigateToViewEditCosigners = () => {
|
const navigateToViewEditCosigners = () => {
|
||||||
navigate('ViewEditMultisigCosignersRoot', {
|
navigate('ViewEditMultisigCosigners', {
|
||||||
screen: 'ViewEditMultisigCosigners',
|
walletID,
|
||||||
params: {
|
|
||||||
walletID,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const navigateToXPub = () =>
|
const navigateToXPub = () =>
|
||||||
|
@ -446,7 +444,7 @@ const WalletDetails: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_address.toLowerCase()}</Text>
|
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_address.toLowerCase()}</Text>
|
||||||
<Text style={[styles.textValue, stylesHook.textValue]}>
|
<Text style={[styles.textValue, stylesHook.textValue]} selectable>
|
||||||
{(() => {
|
{(() => {
|
||||||
// gracefully handling faulty wallets, so at least user has an option to delete the wallet
|
// gracefully handling faulty wallets, so at least user has an option to delete the wallet
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
findNodeHandle,
|
findNodeHandle,
|
||||||
FlatList,
|
FlatList,
|
||||||
I18nManager,
|
I18nManager,
|
||||||
InteractionManager,
|
|
||||||
LayoutAnimation,
|
LayoutAnimation,
|
||||||
PixelRatio,
|
PixelRatio,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
@ -53,12 +52,14 @@ const buttonFontSize =
|
||||||
? 22
|
? 22
|
||||||
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||||
|
|
||||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
|
||||||
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
||||||
|
|
||||||
|
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||||
|
|
||||||
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
|
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
|
||||||
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||||
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
|
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
|
||||||
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
|
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||||
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { params, name } = useRoute<RouteProps>();
|
const { params, name } = useRoute<RouteProps>();
|
||||||
|
@ -98,7 +99,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||||
if (wallet?.chain === Chain.ONCHAIN) {
|
if (wallet?.chain === Chain.ONCHAIN) {
|
||||||
navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters });
|
navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters });
|
||||||
} else {
|
} else {
|
||||||
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: parameters });
|
navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: parameters });
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -256,11 +257,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToViewEditCosigners = useCallback(() => {
|
const navigateToViewEditCosigners = useCallback(() => {
|
||||||
navigate('ViewEditMultisigCosignersRoot', {
|
navigate('ViewEditMultisigCosigners', {
|
||||||
screen: 'ViewEditMultisigCosigners',
|
walletID,
|
||||||
params: {
|
|
||||||
walletID,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, [navigate, walletID]);
|
}, [navigate, walletID]);
|
||||||
|
|
||||||
|
@ -321,7 +319,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||||
|
|
||||||
const sendButtonPress = () => {
|
const sendButtonPress = () => {
|
||||||
if (wallet?.chain === Chain.OFFCHAIN) {
|
if (wallet?.chain === Chain.OFFCHAIN) {
|
||||||
return navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID } });
|
return navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: { walletID } });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wallet?.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) {
|
if (wallet?.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) {
|
||||||
|
@ -387,17 +385,27 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wallet) {
|
||||||
|
const screenKey = `WalletTransactions-${walletID}`;
|
||||||
|
registerTransactionsHandler(() => refreshTransactions(true), screenKey);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterTransactionsHandler(screenKey);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [wallet, walletID, refreshTransactions, registerTransactionsHandler, unregisterTransactionsHandler]);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
if (wallet) {
|
||||||
setReloadTransactionsMenuActionFunction(() => refreshTransactions);
|
const screenKey = `WalletTransactions-${walletID}`;
|
||||||
});
|
|
||||||
return () => {
|
return () => {
|
||||||
task.cancel();
|
unregisterTransactionsHandler(screenKey);
|
||||||
console.debug('Next screen is focused, clearing reloadTransactionsMenuActionFunction');
|
};
|
||||||
setReloadTransactionsMenuActionFunction(() => {});
|
}
|
||||||
};
|
}, [wallet, walletID, unregisterTransactionsHandler]),
|
||||||
}, [setReloadTransactionsMenuActionFunction, refreshTransactions]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0);
|
const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0);
|
||||||
|
|
|
@ -98,7 +98,7 @@ const WalletsList: React.FC = () => {
|
||||||
const { isLargeScreen } = useIsLargeScreen();
|
const { isLargeScreen } = useIsLargeScreen();
|
||||||
const walletsCarousel = useRef<any>();
|
const walletsCarousel = useRef<any>();
|
||||||
const currentWalletIndex = useRef<number>(0);
|
const currentWalletIndex = useRef<number>(0);
|
||||||
const { setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction } = useMenuElements();
|
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||||
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
|
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
|
||||||
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
|
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
|
@ -159,19 +159,41 @@ const WalletsList: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [getBalance]);
|
}, [getBalance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const screenKey = route.name;
|
||||||
|
console.log(`[WalletsList] Registering handler with key: ${screenKey}`);
|
||||||
|
registerTransactionsHandler(onRefresh, screenKey);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log(`[WalletsList] Unmounting - cleaning up handler for: ${screenKey}`);
|
||||||
|
unregisterTransactionsHandler(screenKey);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [onRefresh, registerTransactionsHandler, unregisterTransactionsHandler]);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
const screenKey = route.name;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log(`[WalletsList] Blurred - cleaning up handler for: ${screenKey}`);
|
||||||
|
unregisterTransactionsHandler(screenKey);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [unregisterTransactionsHandler]),
|
||||||
|
);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
const task = InteractionManager.runAfterInteractions(() => {
|
||||||
setReloadTransactionsMenuActionFunction(onRefresh);
|
|
||||||
verifyBalance();
|
verifyBalance();
|
||||||
setSelectedWalletID(undefined);
|
setSelectedWalletID(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
task.cancel();
|
task.cancel();
|
||||||
clearReloadTransactionsMenuAction();
|
|
||||||
};
|
};
|
||||||
}, [onRefresh, setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction, verifyBalance, setSelectedWalletID]),
|
}, [verifyBalance, setSelectedWalletID]),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -207,6 +229,7 @@ const WalletsList: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshTransactions();
|
refreshTransactions();
|
||||||
|
// es-lint-disable-next-line react-hooks/exhaustive-deps
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -148,9 +148,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
||||||
url: `lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
|
url: `lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
|
||||||
},
|
},
|
||||||
expected: [
|
expected: [
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
||||||
},
|
},
|
||||||
|
@ -162,9 +162,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
||||||
url: `bluewallet:lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
|
url: `bluewallet:lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
|
||||||
},
|
},
|
||||||
expected: [
|
expected: [
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
||||||
},
|
},
|
||||||
|
@ -248,9 +248,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
||||||
url: 'lnaddress@zbd.gg',
|
url: 'lnaddress@zbd.gg',
|
||||||
},
|
},
|
||||||
expected: [
|
expected: [
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
params: {
|
params: {
|
||||||
uri: 'lnaddress@zbd.gg',
|
uri: 'lnaddress@zbd.gg',
|
||||||
},
|
},
|
||||||
|
@ -518,13 +518,13 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
||||||
navigate: (...args) => {
|
navigate: (...args) => {
|
||||||
navigateWasCalled = true;
|
navigateWasCalled = true;
|
||||||
assert.deepStrictEqual(args, [
|
assert.deepStrictEqual(args, [
|
||||||
'ScanLndInvoiceRoot',
|
'ScanLNDInvoiceRoot',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
uri: 'lightning:LNBC1855790N1PNUPWSFPP5P5RVQJA067PV6NJQ3EFKLP78TN6MHUK842ZFGDCTXRDSGNTY765QDZ62PSKJEPQW3HJQSNPD36XJCEQFPHKUETEVFSKGEM9WGSRYVPJXSSZSNMJV3JHYGZFGSAZQARFVD4K2AR5V95KCMMJ9YCQZPUXQZ6GSP53E4EX9YTD2MGDN2C2CFA0J0SM3E7PVLPJ208H5LMYPNJMGZ7RLGS9QXPQYSGQ6GQMEQXJKKF2DHXJK8XQ4WGLM5NTE3RKEXGYQC6HYGFKS9SHHA6HL9X4339MXHNNQFSH7TS62PU8T9RSWTK6HQ4LV4GW3DPD25DQ8UQQYC909N',
|
uri: 'lightning:LNBC1855790N1PNUPWSFPP5P5RVQJA067PV6NJQ3EFKLP78TN6MHUK842ZFGDCTXRDSGNTY765QDZ62PSKJEPQW3HJQSNPD36XJCEQFPHKUETEVFSKGEM9WGSRYVPJXSSZSNMJV3JHYGZFGSAZQARFVD4K2AR5V95KCMMJ9YCQZPUXQZ6GSP53E4EX9YTD2MGDN2C2CFA0J0SM3E7PVLPJ208H5LMYPNJMGZ7RLGS9QXPQYSGQ6GQMEQXJKKF2DHXJK8XQ4WGLM5NTE3RKEXGYQC6HYGFKS9SHHA6HL9X4339MXHNNQFSH7TS62PU8T9RSWTK6HQ4LV4GW3DPD25DQ8UQQYC909N',
|
||||||
walletID: 'bfcacb7288cf43c6c02a1154c432ec155b813798fa4e87cd2c1e5531d6363f71',
|
walletID: 'bfcacb7288cf43c6c02a1154c432ec155b813798fa4e87cd2c1e5531d6363f71',
|
||||||
},
|
},
|
||||||
screen: 'ScanLndInvoice',
|
screen: 'ScanLNDInvoice',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue