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
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "7.1.3"
|
||||
versionName "7.1.5"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ dependencies {
|
|||
androidTestImplementation('com.wix:detox:0.1.1')
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
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.bugsnag.android.gradle"
|
||||
|
|
|
@ -56,7 +56,8 @@ object MarketAPI {
|
|||
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
|
||||
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
||||
"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")
|
||||
"Coinbase" -> json.getJSONObject("data").getString("amount")
|
||||
"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
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<TextView
|
||||
android:id="@+id/price_value"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="8dp"
|
||||
|
@ -67,6 +67,7 @@
|
|||
android:autoSizeTextType="uniform"
|
||||
android:duplicateParentState="false"
|
||||
android:editable="false"
|
||||
android:gravity="end"
|
||||
android:lines="1"
|
||||
android:text="Loading..."
|
||||
android:textSize="24sp"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_layout"
|
||||
android:minWidth="160dp"
|
||||
android:minWidth="170dp"
|
||||
android:minHeight="100dp"
|
||||
android:updatePeriodMillis="0"
|
||||
android:widgetCategory="home_screen"
|
||||
|
|
|
@ -86,9 +86,9 @@ class DeeplinkSchemaMatch {
|
|||
} else if (wallet.chain === Chain.OFFCHAIN) {
|
||||
if (action === 'openSend') {
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
walletID: wallet.getID(),
|
||||
},
|
||||
|
@ -156,9 +156,9 @@ class DeeplinkSchemaMatch {
|
|||
]);
|
||||
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: event.url.replace('://', ':'),
|
||||
},
|
||||
|
@ -181,9 +181,9 @@ class DeeplinkSchemaMatch {
|
|||
// this might be not just an email but a lightning address
|
||||
// @see https://lightningaddress.com
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: event.url,
|
||||
},
|
||||
|
@ -305,9 +305,9 @@ class DeeplinkSchemaMatch {
|
|||
];
|
||||
} else {
|
||||
return [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: uri.lndInvoice,
|
||||
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 = {
|
||||
memo?: string;
|
||||
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { Keyboard, StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
||||
import React from 'react';
|
||||
import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
||||
import loc from '../loc';
|
||||
import { AddressInputScanButton } from './AddressInputScanButton';
|
||||
import { useTheme } from './themes';
|
||||
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
|
||||
interface AddressInputProps {
|
||||
isLoading?: boolean;
|
||||
|
@ -31,7 +29,6 @@ interface AddressInputProps {
|
|||
| 'twitter'
|
||||
| 'web-search'
|
||||
| 'visible-password';
|
||||
skipValidation?: boolean;
|
||||
}
|
||||
|
||||
const AddressInput = ({
|
||||
|
@ -46,7 +43,6 @@ const AddressInput = ({
|
|||
onBlur = () => {},
|
||||
keyboardType = 'default',
|
||||
style,
|
||||
skipValidation = false,
|
||||
}: AddressInputProps) => {
|
||||
const { colors } = useTheme();
|
||||
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 (
|
||||
<View style={[styles.root, stylesHook.root, style]}>
|
||||
<TextInput
|
||||
|
@ -100,7 +73,7 @@ const AddressInput = ({
|
|||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType={keyboardType}
|
||||
{...(skipValidation ? { onBlur } : { onBlur: onBlurEditing })}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
|
||||
</View>
|
||||
|
|
|
@ -89,6 +89,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSwipeActive, setIsSwipeActive] = useState(false);
|
||||
const resetFunctionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const CARD_SORT_ACTIVE = 1.06;
|
||||
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
|
||||
|
@ -125,24 +127,39 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
reset();
|
||||
};
|
||||
|
||||
const leftContent = (reset: () => void) => (
|
||||
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
|
||||
);
|
||||
|
||||
const handleRightPress = (reset: () => void) => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
reset();
|
||||
const leftContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
|
||||
};
|
||||
|
||||
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(() => {
|
||||
if (isSwipeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetFunctionRef.current) {
|
||||
resetFunctionRef.current();
|
||||
}
|
||||
|
||||
scaleValue.setValue(CARD_SORT_ACTIVE);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||
if (drag) {
|
||||
drag();
|
||||
}
|
||||
}, [CARD_SORT_ACTIVE, drag, scaleValue]);
|
||||
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
|
||||
|
||||
if (isLoading) {
|
||||
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 swipeDisabled = isActive || globalDragActive;
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ListItem.Swipeable
|
||||
leftWidth={80}
|
||||
rightWidth={90}
|
||||
containerStyle={[style, { backgroundColor }, isActive || globalDragActive ? styles.transparentBackground : {}]}
|
||||
leftContent={globalDragActive ? null : isActive ? null : leftContent}
|
||||
rightContent={globalDragActive ? null : isActive ? null : rightContent}
|
||||
leftWidth={swipeDisabled ? 0 : 80}
|
||||
rightWidth={swipeDisabled ? 0 : 90}
|
||||
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
|
||||
leftContent={swipeDisabled ? null : leftContent}
|
||||
rightContent={swipeDisabled ? null : rightContent}
|
||||
onPressOut={onPressOut}
|
||||
minSlideWidth={80}
|
||||
minSlideWidth={swipeDisabled ? 0 : 80}
|
||||
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>
|
||||
<WalletCarouselItem
|
||||
item={item.data}
|
||||
handleLongPress={isDraggingDisabled ? undefined : startDrag}
|
||||
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
|
|
|
@ -220,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
|||
}
|
||||
const loaded = await LN.loadSuccessfulPayment(paymentHash);
|
||||
if (loaded) {
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'LnurlPaySuccess',
|
||||
params: {
|
||||
paymentHash,
|
||||
|
|
|
@ -15,7 +15,8 @@ const scanQrHelper = async (): Promise<string> => {
|
|||
await requestCameraAuthorization();
|
||||
return new Promise(resolve => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.current?.navigate('ScanQRCode', {
|
||||
navigationRef.navigate('ScanQRCode', {
|
||||
showFileImportButton: true,
|
||||
onBarScanned: (data: string) => {
|
||||
resolve(data);
|
||||
},
|
||||
|
|
|
@ -19,25 +19,42 @@ import loc from '../loc';
|
|||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import ActionSheet from '../screen/ActionSheet';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { useStorage } from './context/useStorage';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import presentAlert from './Alert';
|
||||
import useMenuElements from '../hooks/useMenuElements';
|
||||
import useWidgetCommunication from '../hooks/useWidgetCommunication';
|
||||
import useWatchConnectivity from '../hooks/useWatchConnectivity';
|
||||
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
|
||||
import useHandoffListener from '../hooks/useHandoffListener';
|
||||
import presentAlert from '../components/Alert';
|
||||
import useWidgetCommunication from './useWidgetCommunication';
|
||||
import useWatchConnectivity from './useWatchConnectivity';
|
||||
import useDeviceQuickActions from './useDeviceQuickActions';
|
||||
import useHandoffListener from './useHandoffListener';
|
||||
import useMenuElements from './useMenuElements';
|
||||
|
||||
const ClipboardContentType = Object.freeze({
|
||||
BITCOIN: 'BITCOIN',
|
||||
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 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();
|
||||
useWidgetCommunication();
|
||||
useMenuElements();
|
||||
|
@ -45,6 +62,8 @@ const CompanionDelegates = () => {
|
|||
useHandoffListener();
|
||||
|
||||
const processPushNotifications = useCallback(async () => {
|
||||
if (!shouldActivateListeners) return false;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
try {
|
||||
const notifications2process = await getStoredNotifications();
|
||||
|
@ -164,15 +183,19 @@ const CompanionDelegates = () => {
|
|||
console.error('Failed to process push notifications:', error);
|
||||
}
|
||||
return false;
|
||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
|
||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
initializeNotifications(processPushNotifications);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [shouldActivateListeners]);
|
||||
|
||||
const handleOpenURL = useCallback(
|
||||
async (event: { url: string }): Promise<void> => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
try {
|
||||
if (!event.url) return;
|
||||
let decodedUrl: string;
|
||||
|
@ -227,11 +250,13 @@ const CompanionDelegates = () => {
|
|||
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
|
||||
}
|
||||
},
|
||||
[wallets, addWallet, saveToDisk, setSharedCosigner],
|
||||
[wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const showClipboardAlert = useCallback(
|
||||
({ contentType }: { contentType: undefined | string }) => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
|
||||
getClipboardContent().then(clipboard => {
|
||||
if (!clipboard) return;
|
||||
|
@ -254,12 +279,13 @@ const CompanionDelegates = () => {
|
|||
);
|
||||
});
|
||||
},
|
||||
[handleOpenURL],
|
||||
[handleOpenURL, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const handleAppStateChange = useCallback(
|
||||
async (nextAppState: AppStateStatus | undefined) => {
|
||||
if (wallets.length === 0) return;
|
||||
if (!shouldActivateListeners || wallets.length === 0) return;
|
||||
|
||||
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
|
||||
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
|
||||
updateExchangeRate();
|
||||
|
@ -299,10 +325,12 @@ const CompanionDelegates = () => {
|
|||
appState.current = nextAppState;
|
||||
}
|
||||
},
|
||||
[processPushNotifications, showClipboardAlert, wallets],
|
||||
[processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const addListeners = useCallback(() => {
|
||||
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
|
||||
|
||||
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
|
||||
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
|
||||
|
||||
|
@ -310,18 +338,16 @@ const CompanionDelegates = () => {
|
|||
urlSubscription,
|
||||
appStateSubscription,
|
||||
};
|
||||
}, [handleOpenURL, handleAppStateChange]);
|
||||
}, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscriptions = addListeners();
|
||||
|
||||
return () => {
|
||||
subscriptions.urlSubscription?.remove();
|
||||
subscriptions.appStateSubscription?.remove();
|
||||
subscriptions.urlSubscription?.remove?.();
|
||||
subscriptions.appStateSubscription?.remove?.();
|
||||
};
|
||||
}, [addListeners]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CompanionDelegates;
|
||||
export default useCompanionListeners;
|
|
@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
|
|||
const requiresBiometrics = [
|
||||
'WalletExportRoot',
|
||||
'WalletXpubRoot',
|
||||
'ViewEditMultisigCosignersRoot',
|
||||
'ViewEditMultisigCosigners',
|
||||
'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 { navigationRef } from '../NavigationService';
|
||||
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.
|
||||
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;
|
||||
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 {
|
||||
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) {
|
||||
if (Platform.OS === 'ios' && MenuElementsEmitter) {
|
||||
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
|
||||
eventEmitter = null;
|
||||
}
|
||||
|
||||
// Empty function that does nothing - used as default
|
||||
const noop = () => {};
|
||||
|
||||
const useMenuElements = () => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop);
|
||||
// Track if listeners have been set up
|
||||
const listenersInitialized = useRef<boolean>(false);
|
||||
const listenersRef = useRef<any[]>([]);
|
||||
|
||||
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
|
||||
if (typeof handler !== 'function') {
|
||||
/**
|
||||
* Safely navigate using multiple fallback approaches
|
||||
*/
|
||||
function safeNavigate(routeName: string, params?: Record<string, any>): void {
|
||||
try {
|
||||
if (navigationRef.current?.isReady()) {
|
||||
navigationRef.current.navigate(routeName as never, params as never);
|
||||
return;
|
||||
}
|
||||
|
||||
reloadTransactionsMenuActionRef.current = handler;
|
||||
globalReloadTransactionsFunction = handler;
|
||||
}, []);
|
||||
|
||||
const clearReloadTransactionsMenuAction = useCallback(() => {
|
||||
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
|
||||
}
|
||||
}, []);
|
||||
|
||||
const eventActions = useMemo(
|
||||
() => ({
|
||||
openSettings: () => {
|
||||
dispatchNavigate('Settings');
|
||||
},
|
||||
addWallet: () => {
|
||||
dispatchNavigate('AddWalletRoot');
|
||||
},
|
||||
importWallet: () => {
|
||||
dispatchNavigate('AddWalletRoot', 'ImportWallet');
|
||||
},
|
||||
reloadTransactions: () => {
|
||||
try {
|
||||
const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop;
|
||||
handler();
|
||||
} catch (error) {
|
||||
// Execution failed silently
|
||||
}
|
||||
},
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.dispatch(
|
||||
CommonActions.navigate({
|
||||
name: routeName,
|
||||
params,
|
||||
}),
|
||||
[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 = [];
|
||||
}
|
||||
|
||||
eventEmitter.removeAllListeners('openSettings');
|
||||
eventEmitter.removeAllListeners('addWalletMenuAction');
|
||||
eventEmitter.removeAllListeners('importWalletMenuAction');
|
||||
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
|
||||
} catch (error) {
|
||||
// Error cleanup silently ignored
|
||||
console.error(`[MenuElements] Navigation error:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup event listeners to prevent memory leaks
|
||||
function cleanupListeners(): void {
|
||||
if (subscriptions.length > 0) {
|
||||
subscriptions.forEach(subscription => {
|
||||
try {
|
||||
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;
|
||||
listenersInitialized.current = true;
|
||||
} catch (error) {
|
||||
// Listener setup failed silently
|
||||
}
|
||||
|
||||
return () => {
|
||||
try {
|
||||
listenersRef.current.forEach(listener => {
|
||||
if (listener && typeof listener.remove === 'function') {
|
||||
listener.remove();
|
||||
subscription.remove();
|
||||
} catch (e) {
|
||||
console.warn('[MenuElements] Error removing subscription:', e);
|
||||
}
|
||||
});
|
||||
listenersRef.current = [];
|
||||
listenersInitialized.current = false;
|
||||
} catch (error) {
|
||||
// Cleanup error silently ignored
|
||||
subscriptions = [];
|
||||
listenersInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeListeners(): void {
|
||||
if (!eventEmitter || listenersInitialized) return;
|
||||
|
||||
cleanupListeners();
|
||||
|
||||
// Navigation actions
|
||||
const globalActions = {
|
||||
navigateToSettings: (): void => {
|
||||
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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [walletsInitialized, eventActions]);
|
||||
|
||||
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 () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 {
|
||||
setReloadTransactionsMenuActionFunction,
|
||||
clearReloadTransactionsMenuAction,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
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 {
|
||||
setReloadTransactionsMenuActionFunction: (_func: any) => {},
|
||||
clearReloadTransactionsMenuAction: () => {},
|
||||
isMenuElementsSupported: true,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: false, // Not supported on platforms other than iOS
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1455,7 +1455,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
|
@ -1483,7 +1483,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1518,7 +1518,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
|
@ -1541,7 +1541,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1577,7 +1577,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1590,7 +1590,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1620,7 +1620,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1633,7 +1633,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
|
||||
|
@ -1664,7 +1664,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1683,7 +1683,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1720,7 +1720,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1739,7 +1739,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
|
||||
|
@ -1907,7 +1907,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1927,7 +1927,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1960,7 +1960,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1980,7 +1980,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
|
||||
|
@ -2012,7 +2012,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -2026,7 +2026,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -2061,7 +2061,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703157999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -2075,7 +2075,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.3;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;
|
||||
|
|
|
@ -257,11 +257,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter openSettings];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,11 +271,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter addWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,11 +285,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter importWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,11 +299,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter reloadTransactionsMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,14 +3,27 @@ import React
|
|||
|
||||
@objc(MenuElementsEmitter)
|
||||
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
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
MenuElementsEmitter._sharedInstance = self
|
||||
NSLog("[MenuElements] Swift: Initialized MenuElementsEmitter instance")
|
||||
MenuElementsEmitter.sharedInstance = self
|
||||
NSLog("[MenuElements] MenuElementsEmitter initialized")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSLog("[MenuElements] MenuElementsEmitter deallocated")
|
||||
// Ensure all event listeners are removed in deinit
|
||||
self.removeAllListeners()
|
||||
}
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
|
@ -22,54 +35,100 @@ class MenuElementsEmitter: RCTEventEmitter {
|
|||
}
|
||||
|
||||
@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() {
|
||||
hasListeners = true
|
||||
NSLog("[MenuElements] Swift: Started observing events")
|
||||
NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
|
||||
}
|
||||
|
||||
override func stopObserving() {
|
||||
hasListeners = false
|
||||
NSLog("[MenuElements] Swift: Stopped observing events")
|
||||
NSLog("[MenuElements] Stopped observing events")
|
||||
// Clear cache when stopping observation
|
||||
lastEventTime.removeAll()
|
||||
}
|
||||
|
||||
private func limitCacheSize() {
|
||||
if lastEventTime.count > maxCacheSize {
|
||||
// Remove oldest entries if cache is too large
|
||||
let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
|
||||
for i in 0..<(lastEventTime.count - maxCacheSize) {
|
||||
lastEventTime.removeValue(forKey: sortedKeys[i].key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if hasListeners && self.bridge != nil {
|
||||
NSLog("[MenuElements] Swift: Emitting event: %@", name)
|
||||
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 else { return }
|
||||
self.sendEvent(withName: name, body: nil)
|
||||
guard let self = self, self.bridge != nil, self.hasListeners else {
|
||||
NSLog("[MenuElements] Failed to emit event: \(name) - bridge or listeners not available")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
NSLog("[MenuElements] Swift: Cannot emit %@ event. %@", name, !hasListeners ? "No listeners" : "Bridge not ready")
|
||||
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() {
|
||||
NSLog("[MenuElements] Swift: openSettings called")
|
||||
NSLog("[MenuElements] openSettings method called")
|
||||
safelyEmitEvent(withName: "openSettings")
|
||||
}
|
||||
|
||||
@objc func addWalletMenuAction() {
|
||||
NSLog("[MenuElements] Swift: addWalletMenuAction called")
|
||||
NSLog("[MenuElements] addWalletMenuAction method called")
|
||||
safelyEmitEvent(withName: "addWalletMenuAction")
|
||||
}
|
||||
|
||||
@objc func importWalletMenuAction() {
|
||||
NSLog("[MenuElements] Swift: importWalletMenuAction called")
|
||||
NSLog("[MenuElements] importWalletMenuAction method called")
|
||||
safelyEmitEvent(withName: "importWalletMenuAction")
|
||||
}
|
||||
|
||||
@objc func reloadTransactionsMenuAction() {
|
||||
NSLog("[MenuElements] Swift: reloadTransactionsMenuAction called")
|
||||
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
NSLog("[MenuElements] Swift: Module invalidated")
|
||||
MenuElementsEmitter._sharedInstance = nil
|
||||
NSLog("[MenuElements] Module invalidated")
|
||||
if MenuElementsEmitter.sharedInstance === self {
|
||||
MenuElementsEmitter.sharedInstance = nil
|
||||
}
|
||||
removeAllListeners()
|
||||
super.invalidate()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,8 @@ class MarketAPI {
|
|||
return "https://www.bnr.ro/nbrfxrates.xml"
|
||||
case "Kraken":
|
||||
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
|
||||
default:
|
||||
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
|
||||
default: // CoinDesk
|
||||
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)")
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw CurrencyError(errorDescription: "Unsupported data source \(source)")
|
||||
default: // CoinDesk
|
||||
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';
|
||||
|
||||
export const FiatUnitSource = {
|
||||
|
@ -15,7 +16,8 @@ export const FiatUnitSource = {
|
|||
|
||||
const handleError = (source: string, ticker: string, error: 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 {
|
||||
bpi: {
|
||||
[ticker: string]: {
|
||||
rate_float: number;
|
||||
};
|
||||
};
|
||||
[ticker: string]: number;
|
||||
}
|
||||
|
||||
interface CoinGeckoResponse {
|
||||
|
@ -96,8 +94,10 @@ const RateExtractors = {
|
|||
|
||||
CoinDesk: async (ticker: string): Promise<number> => {
|
||||
try {
|
||||
const json = (await fetchRate(`https://api.coindesk.com/v1/bpi/currentprice/${ticker}.json`)) as CoinDeskResponse;
|
||||
const rate = Number(json?.bpi?.[ticker]?.rate_float);
|
||||
const json = (await fetchRate(
|
||||
`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');
|
||||
return rate;
|
||||
} catch (error: any) {
|
||||
|
|
|
@ -32,10 +32,8 @@ import AztecoRedeemStackRoot from './AztecoRedeemStack';
|
|||
import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
|
||||
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
|
||||
import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
|
||||
import ScanLndInvoiceRoot from './ScanLndInvoiceStack';
|
||||
import SendDetailsStack from './SendDetailsStack';
|
||||
import SignVerifyStackRoot from './SignVerifyStack';
|
||||
import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack';
|
||||
import WalletExportStack from './WalletExportStack';
|
||||
import WalletXpubStackRoot from './WalletXpubStack';
|
||||
import SettingsButton from '../components/icons/SettingsButton';
|
||||
|
@ -66,6 +64,8 @@ import ToolsScreen from '../screen/settings/tools';
|
|||
import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
|
||||
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
||||
import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
|
||||
import ScanLNDInvoiceRoot from './ScanLNDInvoiceStack';
|
||||
import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack';
|
||||
|
||||
const DetailViewStackScreensStack = () => {
|
||||
const theme = useTheme();
|
||||
|
@ -327,7 +327,7 @@ const DetailViewStackScreensStack = () => {
|
|||
<DetailViewStack.Screen name="AddWalletRoot" component={AddWalletStack} options={NavigationDefaultOptions} />
|
||||
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} 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} />
|
||||
{/* screens */}
|
||||
<DetailViewStack.Screen
|
||||
|
@ -342,8 +342,8 @@ const DetailViewStackScreensStack = () => {
|
|||
/>
|
||||
|
||||
<DetailViewStack.Screen
|
||||
name="ViewEditMultisigCosignersRoot"
|
||||
component={ViewEditMultisigCosignersStackRoot}
|
||||
name="ViewEditMultisigCosigners"
|
||||
component={ViewEditMultisigCosignersComponent}
|
||||
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
|
||||
initialParams={{ walletID: undefined, cosigners: undefined }}
|
||||
/>
|
||||
|
@ -364,7 +364,7 @@ const DetailViewStackScreensStack = () => {
|
|||
options={navigationStyle({
|
||||
headerBackVisible: false,
|
||||
gestureEnabled: false,
|
||||
presentation: 'containedModal',
|
||||
presentation: 'fullScreenModal',
|
||||
title: loc.wallets.manage_title,
|
||||
statusBarStyle: 'auto',
|
||||
})(theme)}
|
||||
|
|
|
@ -47,7 +47,7 @@ export type DetailViewStackParamList = {
|
|||
AddWalletRoot: undefined;
|
||||
SendDetailsRoot: SendDetailsParams;
|
||||
LNDCreateInvoiceRoot: undefined;
|
||||
ScanLndInvoiceRoot: {
|
||||
ScanLNDInvoiceRoot: {
|
||||
screen: string;
|
||||
params: {
|
||||
paymentHash: string;
|
||||
|
@ -79,7 +79,7 @@ export type DetailViewStackParamList = {
|
|||
ReleaseNotes: undefined;
|
||||
ToolsScreen: undefined;
|
||||
SettingsPrivacy: undefined;
|
||||
ViewEditMultisigCosignersRoot: { walletID: string; cosigners: string[] };
|
||||
ViewEditMultisigCosigners: { walletID: string; cosigners: string[]; onBarScanned?: string };
|
||||
WalletXpubRoot: undefined;
|
||||
SignVerifyRoot: {
|
||||
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';
|
||||
|
||||
// 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 Success = lazy(() => import('../screen/send/success'));
|
||||
const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay'));
|
||||
const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess'));
|
||||
|
||||
export const ScanLndInvoiceComponent = () => (
|
||||
export const ScanLNDInvoiceComponent = () => (
|
||||
<Suspense fallback={<LazyLoadingIndicator />}>
|
||||
<ScanLndInvoice />
|
||||
<ScanLNDInvoice />
|
||||
</Suspense>
|
||||
);
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
import React, { lazy, Suspense } from 'react';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import React from 'react';
|
||||
import DevMenu from '../components/DevMenu';
|
||||
import MainRoot from './index';
|
||||
const CompanionDelegates = lazy(() => import('../components/CompanionDelegates'));
|
||||
import useCompanionListeners from '../hooks/useCompanionListeners';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<MainRoot />
|
||||
{walletsInitialized && (
|
||||
<Suspense>
|
||||
<CompanionDelegates />
|
||||
</Suspense>
|
||||
)}
|
||||
{__DEV__ && <DevMenu />}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -4,24 +4,19 @@ import React from 'react';
|
|||
import navigationStyle from '../components/navigationStyle';
|
||||
import { useTheme } from '../components/themes';
|
||||
import loc from '../loc';
|
||||
import {
|
||||
LnurlPayComponent,
|
||||
LnurlPaySuccessComponent,
|
||||
ScanLndInvoiceComponent,
|
||||
SelectWalletComponent,
|
||||
SuccessComponent,
|
||||
} from './LazyLoadScanLndInvoiceStack';
|
||||
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
|
||||
import { LnurlPayComponent, LnurlPaySuccessComponent, ScanLNDInvoiceComponent, SuccessComponent } from './LazyLoadScanLNDInvoiceStack';
|
||||
import { SelectWalletComponent } from './LazyLoadLNDCreateInvoiceStack';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
const ScanLndInvoiceRoot = () => {
|
||||
const ScanLNDInvoiceRoot = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||
<Stack.Screen
|
||||
name="ScanLndInvoice"
|
||||
component={ScanLndInvoiceComponent}
|
||||
name="ScanLNDInvoice"
|
||||
component={ScanLNDInvoiceComponent}
|
||||
options={navigationStyle({ headerBackVisible: false, title: loc.send.header, statusBarStyle: 'light' })(theme)}
|
||||
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;
|
||||
};
|
||||
Success: {
|
||||
fee: number;
|
||||
fee?: number;
|
||||
amount: number;
|
||||
amountUnit?: BitcoinUnit;
|
||||
txid?: string;
|
||||
invoiceDescription?: string;
|
||||
};
|
||||
SelectWallet: {
|
||||
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",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bluewallet",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -72,7 +72,7 @@
|
|||
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"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-gesture-handler": "2.23.1",
|
||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||
|
@ -22087,8 +22087,7 @@
|
|||
},
|
||||
"node_modules/react-native-camera-kit": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#1e1921223bc9da636f9889d96b03df5f77dc7bf1",
|
||||
"integrity": "sha512-jwVriBGZai7b4TCM0JXR0xqBY0HPtu2NSQQMETTNLyTjYYqkHEK2uaWkq/GY5B93gbAnTGJ5bRyQAqfWkPjDEw==",
|
||||
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#3193427143b73a6f304198b1123b2e8b90a90862",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bluewallet",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.5",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -140,7 +140,7 @@
|
|||
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"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-gesture-handler": "2.23.1",
|
||||
"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 { 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 { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
|
||||
|
@ -19,26 +19,35 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
|||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
|
||||
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 { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||
const { colors } = useTheme();
|
||||
const route = useRoute();
|
||||
const { walletID, uri, invoice } = useRoute().params;
|
||||
/** @type {LightningCustodianWallet} */
|
||||
const [wallet, setWallet] = useState(
|
||||
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN),
|
||||
const route = useRoute<RouteProps>();
|
||||
const { walletID, uri, invoice } = route.params || {};
|
||||
const [wallet, setWallet] = useState<LightningCustodianWallet | undefined>(
|
||||
(wallets.find(item => item.getID() === walletID) as LightningCustodianWallet) ||
|
||||
(wallets.find(item => item.chain === Chain.OFFCHAIN) as LightningCustodianWallet),
|
||||
);
|
||||
const { navigate, setParams, goBack, pop } = useNavigation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
|
||||
const [destination, setDestination] = useState('');
|
||||
const [unit, setUnit] = useState(BitcoinUnit.SATS);
|
||||
const [decoded, setDecoded] = useState();
|
||||
const [amount, setAmount] = useState();
|
||||
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState();
|
||||
const [expiresIn, setExpiresIn] = useState();
|
||||
const { navigate, setParams, goBack, pop } = useExtendedNavigation<NavigationProps>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState<boolean>(false);
|
||||
const [destination, setDestination] = useState<string>('');
|
||||
const [unit, setUnit] = useState<BitcoinUnit>(BitcoinUnit.SATS);
|
||||
const [decoded, setDecoded] = useState<DecodedInvoice | undefined>();
|
||||
const [amount, setAmount] = useState<string | undefined>();
|
||||
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState<boolean | undefined>();
|
||||
const [expiresIn, setExpiresIn] = useState<string | undefined>();
|
||||
const stylesHook = StyleSheet.create({
|
||||
walletWrapLabel: {
|
||||
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(() => {
|
||||
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
|
||||
}, [walletID]);
|
||||
|
@ -75,7 +78,10 @@ const ScanLndInvoice = () => {
|
|||
if (!wallet) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
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
|
||||
}, [wallet]),
|
||||
|
@ -96,66 +102,68 @@ const ScanLndInvoice = () => {
|
|||
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
|
||||
console.log(data);
|
||||
|
||||
let newDecoded;
|
||||
let newDecoded: DecodedInvoice;
|
||||
try {
|
||||
newDecoded = wallet.decodeInvoice(data);
|
||||
|
||||
let newExpiresIn = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
|
||||
if (+new Date() > newExpiresIn) {
|
||||
const expiryTimeMs = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
|
||||
let newExpiresIn: string;
|
||||
|
||||
if (+new Date() > expiryTimeMs) {
|
||||
newExpiresIn = loc.lnd.expired;
|
||||
} 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 });
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
setParams({ uri: undefined, invoice: data });
|
||||
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === '0');
|
||||
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === 0);
|
||||
setDestination(data);
|
||||
setIsLoading(false);
|
||||
setAmount(newDecoded.num_satoshis);
|
||||
setAmount(newDecoded.num_satoshis.toString());
|
||||
setExpiresIn(newExpiresIn);
|
||||
setDecoded(newDecoded);
|
||||
} catch (Err) {
|
||||
} catch (Err: any) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
Keyboard.dismiss();
|
||||
setParams({ uri: undefined });
|
||||
setTimeout(() => presentAlert({ message: Err.message }), 10);
|
||||
setTimeout(() => presentAlert({ message: Err.message, hapticFeedback: HapticFeedbackTypes.NotificationError }), 10);
|
||||
setIsLoading(false);
|
||||
setAmount();
|
||||
setDestination();
|
||||
setExpiresIn();
|
||||
setDecoded();
|
||||
setAmount(undefined);
|
||||
setDestination('');
|
||||
setExpiresIn(undefined);
|
||||
setDecoded(undefined);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [uri]);
|
||||
|
||||
const _keyboardDidShow = () => {
|
||||
const _keyboardDidShow = (): void => {
|
||||
setRenderWalletSelectionButtonHidden(true);
|
||||
};
|
||||
|
||||
const _keyboardDidHide = () => {
|
||||
const _keyboardDidHide = (): void => {
|
||||
setRenderWalletSelectionButtonHidden(false);
|
||||
};
|
||||
|
||||
const processInvoice = data => {
|
||||
useKeyboard({ onKeyboardDidShow: _keyboardDidShow, onKeyboardDidHide: _keyboardDidHide });
|
||||
|
||||
const processInvoice = (data: string): void => {
|
||||
if (Lnurl.isLnurl(data)) return processLnurlPay(data);
|
||||
if (Lnurl.isLightningAddress(data)) return processLnurlPay(data);
|
||||
setParams({ uri: data });
|
||||
};
|
||||
|
||||
const processLnurlPay = data => {
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
screen: 'LnurlPay',
|
||||
params: {
|
||||
const processLnurlPay = (data: string): void => {
|
||||
navigate('LnurlPay', {
|
||||
lnurl: data,
|
||||
walletID: walletID || wallet.getID(),
|
||||
},
|
||||
walletID: walletID || wallet?.getID() || '',
|
||||
});
|
||||
};
|
||||
|
||||
const pay = async () => {
|
||||
if (!decoded) {
|
||||
if (!decoded || !wallet || !amount || !invoice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -167,22 +175,22 @@ const ScanLndInvoice = () => {
|
|||
}
|
||||
}
|
||||
|
||||
let amountSats = amount;
|
||||
let amountSats: number = parseInt(amount, 10);
|
||||
switch (unit) {
|
||||
case BitcoinUnit.SATS:
|
||||
amountSats = parseInt(amountSats, 10); // nop
|
||||
// amount is already in sats
|
||||
break;
|
||||
case BitcoinUnit.BTC:
|
||||
amountSats = btcToSatoshi(amountSats);
|
||||
amountSats = btcToSatoshi(amount);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
amountSats = btcToSatoshi(fiatToBTC(amountSats));
|
||||
amountSats = btcToSatoshi(fiatToBTC(Number(amount)));
|
||||
break;
|
||||
}
|
||||
setIsLoading(true);
|
||||
|
||||
const newExpiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
|
||||
if (+new Date() > newExpiresIn) {
|
||||
const expiryTimeMs = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
|
||||
if (+new Date() > expiryTimeMs) {
|
||||
setIsLoading(false);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
return presentAlert({ message: loc.lnd.errorInvoiceExpired });
|
||||
|
@ -197,7 +205,7 @@ const ScanLndInvoice = () => {
|
|||
|
||||
try {
|
||||
await wallet.payInvoice(invoice, amountSats);
|
||||
} catch (Err) {
|
||||
} catch (Err: any) {
|
||||
console.log(Err.message);
|
||||
setIsLoading(false);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
|
@ -212,7 +220,7 @@ const ScanLndInvoice = () => {
|
|||
fetchAndSaveWalletTransactions(wallet.getID());
|
||||
};
|
||||
|
||||
const processTextForInvoice = text => {
|
||||
const processTextForInvoice = (text: string): void => {
|
||||
if (
|
||||
(text && text.toLowerCase().startsWith('lnb')) ||
|
||||
text.toLowerCase().startsWith('lightning:lnb') ||
|
||||
|
@ -227,7 +235,7 @@ const ScanLndInvoice = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const shouldDisablePayButton = () => {
|
||||
const shouldDisablePayButton = (): boolean => {
|
||||
if (!decoded) {
|
||||
return true;
|
||||
} else {
|
||||
|
@ -235,16 +243,15 @@ const ScanLndInvoice = () => {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
return !(amount > 0);
|
||||
// return decoded.num_satoshis <= 0 || isLoading || isNaN(decoded.num_satoshis);
|
||||
return !(parseInt(amount, 10) > 0);
|
||||
};
|
||||
|
||||
const naviageToSelectWallet = () => {
|
||||
const naviageToSelectWallet = (): void => {
|
||||
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
|
||||
};
|
||||
|
||||
const renderWalletSelectionButton = () => {
|
||||
if (renderWalletSelectionButtonHidden) return;
|
||||
const renderWalletSelectionButton = (): JSX.Element | undefined => {
|
||||
if (renderWalletSelectionButtonHidden || !wallet) return;
|
||||
const walletLabel = wallet.getLabel();
|
||||
return (
|
||||
<View style={styles.walletSelectRoot}>
|
||||
|
@ -267,25 +274,27 @@ const ScanLndInvoice = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const getFees = () => {
|
||||
const min = Math.floor(decoded.num_satoshis * 0.003);
|
||||
const max = Math.floor(decoded.num_satoshis * 0.01) + 1;
|
||||
const getFees = (): string => {
|
||||
if (!decoded) return '';
|
||||
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}`;
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
const onBlur = (): void => {
|
||||
processTextForInvoice(destination);
|
||||
};
|
||||
|
||||
const onWalletSelect = selectedWallet => {
|
||||
const onWalletSelect = (selectedWallet: TWallet): void => {
|
||||
setParams({ walletID: selectedWallet.getID() });
|
||||
pop();
|
||||
};
|
||||
|
||||
const onBarScanned = useCallback(
|
||||
value => {
|
||||
(value: string): void => {
|
||||
if (!value) return;
|
||||
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
|
||||
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, (completionValue: any) => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
navigate(...completionValue);
|
||||
});
|
||||
|
@ -301,6 +310,12 @@ const ScanLndInvoice = () => {
|
|||
}
|
||||
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
|
||||
|
||||
const onChangeText = (text: string): void => {
|
||||
const trimmedText = text.trim();
|
||||
setDestination(trimmedText);
|
||||
processTextForInvoice(trimmedText);
|
||||
};
|
||||
|
||||
if (wallet === undefined || !wallet) {
|
||||
return (
|
||||
<View style={[styles.loadingIndicator, stylesHook.root]}>
|
||||
|
@ -334,11 +349,7 @@ const ScanLndInvoice = () => {
|
|||
|
||||
<BlueCard>
|
||||
<AddressInput
|
||||
onChangeText={text => {
|
||||
text = text.trim();
|
||||
setDestination(text);
|
||||
}}
|
||||
onBarScanned={data => processTextForInvoice(data.data)}
|
||||
onChangeText={onChangeText}
|
||||
address={destination}
|
||||
isLoading={isLoading}
|
||||
placeholder={loc.lnd.placeholder}
|
||||
|
@ -381,7 +392,7 @@ const ScanLndInvoice = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ScanLndInvoice;
|
||||
export default ScanLNDInvoice;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
walletSelectRoot: {
|
|
@ -109,7 +109,7 @@ const LNDCreateInvoice = () => {
|
|||
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
|
||||
// invoices (including through lnurl-withdraw)
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'LnurlPay',
|
||||
params: {
|
||||
lnurl: data,
|
||||
|
|
|
@ -149,7 +149,7 @@ const LnurlPay: React.FC = () => {
|
|||
await _LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage);
|
||||
}
|
||||
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'LnurlPaySuccess',
|
||||
params: {
|
||||
paymentHash: decoded.payment_hash,
|
||||
|
|
|
@ -109,7 +109,7 @@ const LnurlPaySuccess: React.FC = () => {
|
|||
{repeatable ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'LnurlPay',
|
||||
params: {
|
||||
// @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 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 Base43 from '../../blue_modules/base43';
|
||||
import * as fs from '../../blue_modules/fs';
|
||||
|
@ -12,7 +12,6 @@ import Button from '../../components/Button';
|
|||
import { useTheme } from '../../components/themes';
|
||||
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
|
||||
import loc from '../../loc';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import CameraScreen from '../../components/CameraScreen';
|
||||
import SafeArea from '../../components/SafeArea';
|
||||
|
@ -57,7 +56,6 @@ const styles = StyleSheet.create({
|
|||
|
||||
const ScanQRCode = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { setIsDrawerShouldHide } = useSettings();
|
||||
const navigation = useExtendedNavigation();
|
||||
const route = useRoute<RouteProps>();
|
||||
const navigationState = navigation.getState();
|
||||
|
@ -96,16 +94,6 @@ const ScanQRCode = () => {
|
|||
return createHash('sha256').update(s).digest().toString('hex');
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setIsDrawerShouldHide(true);
|
||||
|
||||
return () => {
|
||||
setIsDrawerShouldHide(false);
|
||||
};
|
||||
}, [setIsDrawerShouldHide]),
|
||||
);
|
||||
|
||||
const _onReadUniformResourceV2 = (part: string) => {
|
||||
if (!decoder) decoder = new BlueURDecoder();
|
||||
try {
|
||||
|
|
|
@ -118,7 +118,8 @@ const Currency: React.FC = () => {
|
|||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
automaticallyAdjustContentInsets
|
||||
keyExtractor={(_item, index) => `${index}`}
|
||||
automaticallyAdjustKeyboardInsets
|
||||
keyExtractor={item => item.endPointKey}
|
||||
data={data}
|
||||
initialNumToRender={30}
|
||||
renderItem={renderItem}
|
||||
|
|
|
@ -515,7 +515,6 @@ const ElectrumSettings: React.FC = () => {
|
|||
onChangeText={text => setHost(text.trim())}
|
||||
editable={!isLoading}
|
||||
keyboardType="default"
|
||||
skipValidation
|
||||
onBlur={() => setIsAndroidAddressKeyboardVisible(false)}
|
||||
onFocus={() => setIsAndroidAddressKeyboardVisible(true)}
|
||||
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
|
||||
|
@ -611,9 +610,7 @@ const ElectrumSettings: React.FC = () => {
|
|||
onValueChange: onElectrumConnectionEnabledSwitchChange,
|
||||
value: isElectrumDisabled,
|
||||
testID: 'ElectrumConnectionEnabledSwitch',
|
||||
disabled: isLoading,
|
||||
}}
|
||||
disabled={isLoading}
|
||||
bottomDivider={false}
|
||||
subtitle={loc.settings.electrum_offline_description}
|
||||
/>
|
||||
|
|
|
@ -7,9 +7,11 @@ import {
|
|||
Alert,
|
||||
I18nManager,
|
||||
Animated,
|
||||
LayoutAnimation,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
LayoutAnimation,
|
||||
UIManager,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
|
||||
|
@ -99,19 +101,21 @@ type Action =
|
|||
interface State {
|
||||
searchQuery: string;
|
||||
isSearchFocused: boolean;
|
||||
order: Item[];
|
||||
tempOrder: Item[];
|
||||
wallets: TWallet[];
|
||||
originalWalletsOrder: Item[];
|
||||
currentWalletsOrder: Item[];
|
||||
availableWallets: TWallet[];
|
||||
txMetadata: TTXMetadata;
|
||||
initialWalletsBackup: TWallet[];
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
searchQuery: '',
|
||||
isSearchFocused: false,
|
||||
order: [],
|
||||
tempOrder: [],
|
||||
wallets: [],
|
||||
originalWalletsOrder: [],
|
||||
currentWalletsOrder: [],
|
||||
availableWallets: [],
|
||||
txMetadata: {},
|
||||
initialWalletsBackup: [],
|
||||
};
|
||||
|
||||
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
|
||||
|
@ -131,21 +135,22 @@ const reducer = (state: State, action: Action): State => {
|
|||
}));
|
||||
return {
|
||||
...state,
|
||||
wallets: action.payload.wallets,
|
||||
availableWallets: action.payload.wallets,
|
||||
txMetadata: action.payload.txMetadata,
|
||||
order: initialWalletsOrder,
|
||||
tempOrder: initialWalletsOrder,
|
||||
originalWalletsOrder: initialWalletsOrder,
|
||||
currentWalletsOrder: initialWalletsOrder,
|
||||
initialWalletsBackup: deepCopyWallets(action.payload.wallets),
|
||||
};
|
||||
}
|
||||
case SET_FILTERED_ORDER: {
|
||||
const query = action.payload.toLowerCase();
|
||||
const filteredWallets = state.wallets
|
||||
const filteredWallets = state.availableWallets
|
||||
.filter(wallet => wallet.getLabel()?.toLowerCase().includes(query))
|
||||
.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
|
||||
|
||||
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
|
||||
.getTransactions()
|
||||
.filter((tx: Transaction) =>
|
||||
|
@ -158,14 +163,16 @@ const reducer = (state: State, action: Action): State => {
|
|||
|
||||
return {
|
||||
...state,
|
||||
tempOrder: filteredOrder,
|
||||
currentWalletsOrder: filteredOrder,
|
||||
};
|
||||
}
|
||||
case SAVE_CHANGES: {
|
||||
const savedWallets = deepCopyWallets(action.payload);
|
||||
return {
|
||||
...state,
|
||||
wallets: deepCopyWallets(action.payload),
|
||||
tempOrder: state.tempOrder.map(item =>
|
||||
availableWallets: savedWallets,
|
||||
initialWalletsBackup: savedWallets,
|
||||
currentWalletsOrder: state.currentWalletsOrder.map(item =>
|
||||
item.type === ItemType.WalletSection
|
||||
? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! }
|
||||
: item,
|
||||
|
@ -173,13 +180,15 @@ const reducer = (state: State, action: Action): State => {
|
|||
};
|
||||
}
|
||||
case SET_TEMP_ORDER: {
|
||||
return { ...state, tempOrder: action.payload };
|
||||
return { ...state, currentWalletsOrder: action.payload };
|
||||
}
|
||||
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 {
|
||||
...state,
|
||||
tempOrder: updatedOrder,
|
||||
currentWalletsOrder: updatedOrder,
|
||||
};
|
||||
}
|
||||
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 { colors, closeImage } = useTheme();
|
||||
const { wallets: storedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
|
||||
const { wallets: persistedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
|
||||
const { setIsDrawerShouldHide } = useSettings();
|
||||
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList
|
||||
const { navigate, setOptions, goBack } = useExtendedNavigation();
|
||||
const initialWalletsRef = useRef<TWallet[]>(deepCopyWallets(persistedWallets));
|
||||
const { navigate, setOptions, goBack, dispatch: navigationDispatch } = useExtendedNavigation();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
|
||||
const bounceAnim = useBounceAnimation(state.searchQuery);
|
||||
|
@ -204,76 +217,123 @@ const ManageWallets: React.FC = () => {
|
|||
color: colors.foregroundColor,
|
||||
},
|
||||
};
|
||||
const [data, setData] = useState(state.tempOrder);
|
||||
const [uiData, setUiData] = useState(state.currentWalletsOrder);
|
||||
|
||||
const listRef = useRef<FlatList<Item> | null>(null);
|
||||
const [saveInProgress, setSaveInProgress] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setData(state.tempOrder);
|
||||
}, [state.tempOrder]);
|
||||
setUiData(state.currentWalletsOrder);
|
||||
}, [state.currentWalletsOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
|
||||
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: initialWalletsRef.current, txMetadata } });
|
||||
}, [txMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearchQuery) {
|
||||
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
|
||||
} 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(() => {
|
||||
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
|
||||
}, [state.tempOrder]);
|
||||
const currentWalletIds = state.currentWalletsOrder
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data.getID());
|
||||
|
||||
usePreventRemove(hasUnsavedChanges, async () => {
|
||||
await new Promise<void>(resolve => {
|
||||
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
||||
{ text: loc._.cancel, style: 'cancel', onPress: () => resolve() },
|
||||
{ text: loc._.ok, style: 'default', onPress: () => resolve() },
|
||||
]);
|
||||
});
|
||||
});
|
||||
const originalWalletIds = state.initialWalletsBackup.map(wallet => wallet.getID());
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
||||
const newWalletOrder = state.tempOrder
|
||||
if (currentWalletIds.length !== originalWalletIds.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
setWalletsWithNewOrder(newWalletOrder);
|
||||
|
||||
dispatch({ type: SAVE_CHANGES, payload: newWalletOrder });
|
||||
|
||||
walletsRef.current = deepCopyWallets(newWalletOrder);
|
||||
|
||||
state.tempOrder.forEach(item => {
|
||||
if (item.type === ItemType.WalletSection && !newWalletOrder.some(wallet => wallet.getID() === item.data.getID())) {
|
||||
handleWalletDeletion(item.data.getID());
|
||||
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(() => {
|
||||
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
||||
const reorderedWallets = state.currentWalletsOrder
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data);
|
||||
|
||||
const walletsToDelete = state.initialWalletsBackup.filter(
|
||||
originalWallet => !reorderedWallets.some(wallet => wallet.getID() === originalWallet.getID()),
|
||||
);
|
||||
|
||||
setWalletsWithNewOrder(reorderedWallets);
|
||||
dispatch({ type: SAVE_CHANGES, payload: reorderedWallets });
|
||||
initialWalletsRef.current = deepCopyWallets(reorderedWallets);
|
||||
|
||||
walletsToDelete.forEach(wallet => {
|
||||
handleWalletDeletion(wallet.getID());
|
||||
});
|
||||
|
||||
setSaveInProgress(true);
|
||||
} else {
|
||||
dispatch({ type: SET_SEARCH_QUERY, payload: '' });
|
||||
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(
|
||||
() => (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.close}
|
||||
style={styles.button}
|
||||
style={[styles.button, buttonOpacity]}
|
||||
onPress={goBack}
|
||||
disabled={saveInProgress}
|
||||
testID="NavigationCloseButton"
|
||||
>
|
||||
<Image source={closeImage} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
[goBack, closeImage],
|
||||
[buttonOpacity, goBack, saveInProgress, closeImage],
|
||||
);
|
||||
|
||||
const SaveButton = useMemo(
|
||||
|
@ -290,7 +350,6 @@ const ManageWallets: React.FC = () => {
|
|||
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
|
||||
placeholder: loc.wallets.manage_wallets_search_placeholder,
|
||||
};
|
||||
|
||||
setOptions({
|
||||
headerLeft: () => HeaderLeftButton,
|
||||
headerRight: () => SaveButton,
|
||||
|
@ -329,19 +388,30 @@ const ManageWallets: React.FC = () => {
|
|||
[bounceAnim],
|
||||
);
|
||||
|
||||
const handleDeleteWallet = useCallback(
|
||||
async (wallet: TWallet) => {
|
||||
const deletionSucceeded = await handleWalletDeletion(wallet.getID());
|
||||
if (deletionSucceeded) {
|
||||
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
|
||||
}
|
||||
const handleDeleteWallet = useCallback(async (wallet: TWallet) => {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 300,
|
||||
create: {
|
||||
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(
|
||||
(wallet: TWallet) => {
|
||||
const updatedOrder = state.tempOrder.map(item => {
|
||||
const updatedOrder = state.currentWalletsOrder.map(item => {
|
||||
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
|
||||
item.data.hideBalance = !item.data.hideBalance;
|
||||
return {
|
||||
|
@ -351,11 +421,10 @@ const ManageWallets: React.FC = () => {
|
|||
}
|
||||
return item;
|
||||
});
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||
},
|
||||
[state.tempOrder],
|
||||
[state.currentWalletsOrder],
|
||||
);
|
||||
|
||||
const navigateToWallet = useCallback(
|
||||
|
@ -373,13 +442,19 @@ const ManageWallets: React.FC = () => {
|
|||
const renderItem = useCallback(
|
||||
(info: DragListRenderItemInfo<Item>) => {
|
||||
const { item, onDragStart, isActive } = info;
|
||||
|
||||
const compatibleState = {
|
||||
wallets: state.availableWallets,
|
||||
searchQuery: state.searchQuery,
|
||||
};
|
||||
|
||||
return (
|
||||
<ManageWalletsListItem
|
||||
item={item}
|
||||
onPressIn={undefined}
|
||||
onPressOut={undefined}
|
||||
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
|
||||
state={state}
|
||||
state={compatibleState}
|
||||
navigateToWallet={navigateToWallet}
|
||||
renderHighlightedText={renderHighlightedText}
|
||||
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(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
const copy = [...state.order];
|
||||
const removed = copy.splice(fromIndex, 1);
|
||||
copy.splice(toIndex, 0, removed[0]);
|
||||
const updatedOrder = [...state.currentWalletsOrder];
|
||||
const removed = updatedOrder.splice(fromIndex, 1);
|
||||
updatedOrder.splice(toIndex, 0, removed[0]);
|
||||
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
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,
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||
},
|
||||
});
|
||||
},
|
||||
[state.order, state.txMetadata],
|
||||
[state.currentWalletsOrder],
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
|
||||
|
||||
const renderHeader = useMemo(() => {
|
||||
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]) =>
|
||||
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
|
||||
);
|
||||
|
@ -425,7 +500,7 @@ const ManageWallets: React.FC = () => {
|
|||
!hasWallets &&
|
||||
!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 (
|
||||
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
|
||||
|
@ -437,7 +512,7 @@ const ManageWallets: React.FC = () => {
|
|||
automaticallyAdjustKeyboardInsets
|
||||
automaticallyAdjustsScrollIndicatorInsets
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={data}
|
||||
data={uiData}
|
||||
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
|
||||
keyExtractor={keyExtractor}
|
||||
onReordered={onReordered}
|
||||
|
@ -460,35 +535,37 @@ const styles = StyleSheet.create({
|
|||
padding: 16,
|
||||
},
|
||||
noResultsText: {
|
||||
fontSize: 19,
|
||||
fontWeight: 'bold',
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 34,
|
||||
},
|
||||
highlightedContainer: {
|
||||
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: {
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 19,
|
||||
},
|
||||
dimmedText: {
|
||||
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 { RouteProp, useFocusEffect, useRoute, usePreventRemove, CommonActions } from '@react-navigation/native';
|
||||
import { RouteProp, useFocusEffect, useRoute, usePreventRemove, StackActions } from '@react-navigation/native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
|
@ -18,7 +18,15 @@ import {
|
|||
import { Badge, Icon } from '@rneui/themed';
|
||||
import { isDesktop } from '../../blue_modules/environment';
|
||||
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 presentAlert from '../../components/Alert';
|
||||
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
|
||||
|
@ -40,14 +48,14 @@ import { useStorage } from '../../hooks/context/useStorage';
|
|||
import ToolTipMenu from '../../components/TooltipMenu';
|
||||
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import SafeArea from '../../components/SafeArea';
|
||||
import { TWallet } from '../../class/wallets/types';
|
||||
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
|
||||
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
|
||||
|
||||
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
|
||||
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
|
||||
type RouteParams = RouteProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
|
||||
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
|
||||
|
||||
const ViewEditMultisigCosigners: React.FC = () => {
|
||||
const hasLoaded = useRef(false);
|
||||
|
@ -169,9 +177,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
|||
setIsSaveButtonDisabled(true);
|
||||
setWalletsWithNewOrder(newWallets);
|
||||
setTimeout(() => {
|
||||
dispatch(
|
||||
CommonActions.navigate({ name: 'WalletTransactions', params: { walletID: wallet.getID(), walletType: MultisigHDWallet.type } }),
|
||||
);
|
||||
const popTo = StackActions.popTo('WalletTransactions', {
|
||||
walletID,
|
||||
walletType: wallet.type,
|
||||
});
|
||||
dispatch(popTo);
|
||||
}, 500);
|
||||
}, 100);
|
||||
};
|
||||
|
@ -560,6 +570,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
|||
|
||||
{!isLoading && (
|
||||
<>
|
||||
<BlueSpacing40 />
|
||||
<AddressInputScanButton
|
||||
beforePress={async () => {
|
||||
await provideMnemonicsModalRef.current?.dismiss();
|
||||
|
@ -568,7 +579,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
|||
type="link"
|
||||
onChangeText={setImportText}
|
||||
/>
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing40 />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -95,7 +95,8 @@ const WalletDetails: React.FC = () => {
|
|||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [handleWalletDeletion, wallet]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const presentWalletHasBalanceAlert = useCallback(async () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
|
||||
|
@ -305,11 +306,8 @@ const WalletDetails: React.FC = () => {
|
|||
});
|
||||
};
|
||||
const navigateToViewEditCosigners = () => {
|
||||
navigate('ViewEditMultisigCosignersRoot', {
|
||||
screen: 'ViewEditMultisigCosigners',
|
||||
params: {
|
||||
navigate('ViewEditMultisigCosigners', {
|
||||
walletID,
|
||||
},
|
||||
});
|
||||
};
|
||||
const navigateToXPub = () =>
|
||||
|
@ -446,7 +444,7 @@ const WalletDetails: React.FC = () => {
|
|||
return (
|
||||
<>
|
||||
<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
|
||||
try {
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
findNodeHandle,
|
||||
FlatList,
|
||||
I18nManager,
|
||||
InteractionManager,
|
||||
LayoutAnimation,
|
||||
PixelRatio,
|
||||
ScrollView,
|
||||
|
@ -53,12 +52,14 @@ const buttonFontSize =
|
|||
? 22
|
||||
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||
|
||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
|
||||
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
|
||||
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { params, name } = useRoute<RouteProps>();
|
||||
|
@ -98,7 +99,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
if (wallet?.chain === Chain.ONCHAIN) {
|
||||
navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters });
|
||||
} else {
|
||||
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: parameters });
|
||||
navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: parameters });
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
@ -256,11 +257,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
);
|
||||
|
||||
const navigateToViewEditCosigners = useCallback(() => {
|
||||
navigate('ViewEditMultisigCosignersRoot', {
|
||||
screen: 'ViewEditMultisigCosigners',
|
||||
params: {
|
||||
navigate('ViewEditMultisigCosigners', {
|
||||
walletID,
|
||||
},
|
||||
});
|
||||
}, [navigate, walletID]);
|
||||
|
||||
|
@ -321,7 +319,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
|
||||
const sendButtonPress = () => {
|
||||
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()) {
|
||||
|
@ -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(
|
||||
useCallback(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setReloadTransactionsMenuActionFunction(() => refreshTransactions);
|
||||
});
|
||||
if (wallet) {
|
||||
const screenKey = `WalletTransactions-${walletID}`;
|
||||
|
||||
return () => {
|
||||
task.cancel();
|
||||
console.debug('Next screen is focused, clearing reloadTransactionsMenuActionFunction');
|
||||
setReloadTransactionsMenuActionFunction(() => {});
|
||||
unregisterTransactionsHandler(screenKey);
|
||||
};
|
||||
}, [setReloadTransactionsMenuActionFunction, refreshTransactions]),
|
||||
}
|
||||
}, [wallet, walletID, unregisterTransactionsHandler]),
|
||||
);
|
||||
|
||||
const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0);
|
||||
|
|
|
@ -98,7 +98,7 @@ const WalletsList: React.FC = () => {
|
|||
const { isLargeScreen } = useIsLargeScreen();
|
||||
const walletsCarousel = useRef<any>();
|
||||
const currentWalletIndex = useRef<number>(0);
|
||||
const { setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction } = useMenuElements();
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
|
||||
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
|
||||
const { width } = useWindowDimensions();
|
||||
|
@ -159,19 +159,41 @@ const WalletsList: React.FC = () => {
|
|||
}
|
||||
}, [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(
|
||||
useCallback(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setReloadTransactionsMenuActionFunction(onRefresh);
|
||||
verifyBalance();
|
||||
setSelectedWalletID(undefined);
|
||||
});
|
||||
|
||||
return () => {
|
||||
task.cancel();
|
||||
clearReloadTransactionsMenuAction();
|
||||
};
|
||||
}, [onRefresh, setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction, verifyBalance, setSelectedWalletID]),
|
||||
}, [verifyBalance, setSelectedWalletID]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -207,6 +229,7 @@ const WalletsList: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
refreshTransactions();
|
||||
// es-lint-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`,
|
||||
},
|
||||
expected: [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
||||
},
|
||||
|
@ -162,9 +162,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
|||
url: `bluewallet:lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
|
||||
},
|
||||
expected: [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
||||
},
|
||||
|
@ -248,9 +248,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
|||
url: 'lnaddress@zbd.gg',
|
||||
},
|
||||
expected: [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: 'lnaddress@zbd.gg',
|
||||
},
|
||||
|
@ -518,13 +518,13 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
|
|||
navigate: (...args) => {
|
||||
navigateWasCalled = true;
|
||||
assert.deepStrictEqual(args, [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
params: {
|
||||
uri: 'lightning:LNBC1855790N1PNUPWSFPP5P5RVQJA067PV6NJQ3EFKLP78TN6MHUK842ZFGDCTXRDSGNTY765QDZ62PSKJEPQW3HJQSNPD36XJCEQFPHKUETEVFSKGEM9WGSRYVPJXSSZSNMJV3JHYGZFGSAZQARFVD4K2AR5V95KCMMJ9YCQZPUXQZ6GSP53E4EX9YTD2MGDN2C2CFA0J0SM3E7PVLPJ208H5LMYPNJMGZ7RLGS9QXPQYSGQ6GQMEQXJKKF2DHXJK8XQ4WGLM5NTE3RKEXGYQC6HYGFKS9SHHA6HL9X4339MXHNNQFSH7TS62PU8T9RSWTK6HQ4LV4GW3DPD25DQ8UQQYC909N',
|
||||
walletID: 'bfcacb7288cf43c6c02a1154c432ec155b813798fa4e87cd2c1e5531d6363f71',
|
||||
},
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue