diff --git a/components/Context/LargeScreenProvider.tsx b/components/Context/LargeScreenProvider.tsx index b07f5f86e..be03fba0c 100644 --- a/components/Context/LargeScreenProvider.tsx +++ b/components/Context/LargeScreenProvider.tsx @@ -3,8 +3,11 @@ import { Dimensions } from 'react-native'; import { isDesktop, isTablet } from '../../blue_modules/environment'; +type ScreenSize = 'Handheld' | 'LargeScreen' | undefined; + interface ILargeScreenContext { isLargeScreen: boolean; + setLargeScreenValue: (value: ScreenSize) => void; } export const LargeScreenContext = createContext(undefined); @@ -15,7 +18,7 @@ interface LargeScreenProviderProps { export const LargeScreenProvider: React.FC = ({ children }) => { const [windowWidth, setWindowWidth] = useState(Dimensions.get('window').width); - const screenWidth: number = useMemo(() => Dimensions.get('screen').width, []); + const [largeScreenValue, setLargeScreenValue] = useState(undefined); useEffect(() => { const updateScreenUsage = (): void => { @@ -30,13 +33,23 @@ export const LargeScreenProvider: React.FC = ({ childr }, [windowWidth]); const isLargeScreen: boolean = useMemo(() => { + if (largeScreenValue === 'LargeScreen') { + return true; + } else if (largeScreenValue === 'Handheld') { + return false; + } + const screenWidth: number = Dimensions.get('screen').width; const halfScreenWidth = windowWidth >= screenWidth / 2; - const condition = (isTablet && halfScreenWidth) || isDesktop; - console.debug( - `LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTablet}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, - ); - return condition; - }, [windowWidth, screenWidth]); + return (isTablet && halfScreenWidth) || isDesktop; + }, [windowWidth, largeScreenValue]); - return {children}; + const contextValue = useMemo( + () => ({ + isLargeScreen, + setLargeScreenValue, + }), + [isLargeScreen, setLargeScreenValue], + ); + + return {children}; }; diff --git a/components/DevMenu.tsx b/components/DevMenu.tsx new file mode 100644 index 000000000..4b1490f84 --- /dev/null +++ b/components/DevMenu.tsx @@ -0,0 +1,187 @@ +import React, { useEffect } from 'react'; +import { DevSettings, Alert, Platform, AlertButton } from 'react-native'; +import { useStorage } from '../hooks/context/useStorage'; +import { HDSegwitBech32Wallet } from '../class'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { useIsLargeScreen } from '../hooks/useIsLargeScreen'; +import { TWallet } from '../class/wallets/types'; + +const getRandomLabelFromSecret = (secret: string): string => { + const words = secret.split(' '); + const firstWord = words[0]; + const lastWord = words[words.length - 1]; + return `[Developer] ${firstWord} ${lastWord}`; +}; + +const showAlertWithWalletOptions = ( + wallets: TWallet[], + title: string, + message: string, + onWalletSelected: (wallet: TWallet) => void, + filterFn?: (wallet: TWallet) => boolean, +) => { + const filteredWallets = filterFn ? wallets.filter(filterFn) : wallets; + + const showWallet = (index: number) => { + if (index >= filteredWallets.length) return; + const wallet = filteredWallets[index]; + + if (Platform.OS === 'android') { + // Android: Use a limited number of buttons since the alert dialog has a limit + Alert.alert( + `${title}: ${wallet.getLabel()}`, + `${message}\n\nSelected Wallet: ${wallet.getLabel()}\n\nWould you like to select this wallet or see the next one?`, + [ + { + text: 'Select This Wallet', + onPress: () => onWalletSelected(wallet), + }, + { + text: 'Show Next Wallet', + onPress: () => showWallet(index + 1), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ], + { cancelable: true }, + ); + } else { + const options: AlertButton[] = filteredWallets.map(w => ({ + text: w.getLabel(), + onPress: () => onWalletSelected(w), + })); + + options.push({ + text: 'Cancel', + style: 'cancel', + }); + + Alert.alert(title, message, options, { cancelable: true }); + } + }; + + if (filteredWallets.length > 0) { + showWallet(0); + } else { + Alert.alert('No wallets available'); + } +}; + +const DevMenu: React.FC = () => { + const { wallets, addWallet } = useStorage(); + const { setLargeScreenValue } = useIsLargeScreen(); + + useEffect(() => { + if (__DEV__) { + // Clear existing Dev Menu items to prevent duplication + DevSettings.addMenuItem('Reset Dev Menu', () => { + DevSettings.reload(); + }); + + DevSettings.addMenuItem('Add New Wallet', async () => { + const wallet = new HDSegwitBech32Wallet(); + await wallet.generate(); + const label = getRandomLabelFromSecret(wallet.getSecret()); + wallet.setLabel(label); + addWallet(wallet); + + Clipboard.setString(wallet.getSecret()); + Alert.alert('New Wallet created!', `Wallet secret copied to clipboard.\nLabel: ${label}`); + }); + + DevSettings.addMenuItem('Copy Wallet Secret', () => { + if (wallets.length === 0) { + Alert.alert('No wallets available'); + return; + } + + showAlertWithWalletOptions(wallets, 'Copy Wallet Secret', 'Select the wallet to copy the secret', wallet => { + Clipboard.setString(wallet.getSecret()); + Alert.alert('Wallet Secret copied to clipboard!'); + }); + }); + + DevSettings.addMenuItem('Copy Wallet ID', () => { + if (wallets.length === 0) { + Alert.alert('No wallets available'); + return; + } + + showAlertWithWalletOptions(wallets, 'Copy Wallet ID', 'Select the wallet to copy the ID', wallet => { + Clipboard.setString(wallet.getID()); + Alert.alert('Wallet ID copied to clipboard!'); + }); + }); + + DevSettings.addMenuItem('Copy Wallet Xpub', () => { + if (wallets.length === 0) { + Alert.alert('No wallets available'); + return; + } + + showAlertWithWalletOptions( + wallets, + 'Copy Wallet Xpub', + 'Select the wallet to copy the Xpub', + wallet => { + const xpub = wallet.getXpub(); + if (xpub) { + Clipboard.setString(xpub); + Alert.alert('Wallet Xpub copied to clipboard!'); + } else { + Alert.alert('This wallet does not have an Xpub.'); + } + }, + wallet => typeof wallet.getXpub === 'function', + ); + }); + + DevSettings.addMenuItem('Purge Wallet Transactions', () => { + if (wallets.length === 0) { + Alert.alert('No wallets available'); + return; + } + + showAlertWithWalletOptions(wallets, 'Purge Wallet Transactions', 'Select the wallet to purge transactions', wallet => { + const msg = 'Transactions purged successfully!'; + + if (wallet.type === HDSegwitBech32Wallet.type) { + wallet._txs_by_external_index = {}; + wallet._txs_by_internal_index = {}; + } + + // @ts-ignore: Property '_hdWalletInstance' does not exist on type 'Wallet'. Pls help + if (wallet._hdWalletInstance) { + // @ts-ignore: Property '_hdWalletInstance' does not exist on type 'Wallet'. Pls help + wallet._hdWalletInstance._txs_by_external_index = {}; + // @ts-ignore: Property '_hdWalletInstance' does not exist on type 'Wallet'. Pls help + wallet._hdWalletInstance._txs_by_internal_index = {}; + } + + Alert.alert(msg); + }); + }); + + DevSettings.addMenuItem('Force Large Screen Interface', () => { + setLargeScreenValue('LargeScreen'); + Alert.alert('Large Screen Interface forced.'); + }); + + DevSettings.addMenuItem('Force Handheld Interface', () => { + setLargeScreenValue('Handheld'); + Alert.alert('Handheld Interface forced.'); + }); + + DevSettings.addMenuItem('Reset Screen Interface', () => { + setLargeScreenValue(undefined); + Alert.alert('Screen Interface reset to default.'); + }); + } + }, [wallets, addWallet, setLargeScreenValue]); + + return null; +}; + +export default DevMenu; diff --git a/components/WalletsCarousel.tsx b/components/WalletsCarousel.tsx index 7c2fc6f06..8e42187bf 100644 --- a/components/WalletsCarousel.tsx +++ b/components/WalletsCarousel.tsx @@ -61,7 +61,7 @@ const NewWalletPanel: React.FC = ({ onPress }) => { const { colors } = useTheme(); const { width } = useWindowDimensions(); const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82; - const isLargeScreen = useIsLargeScreen(); + const { isLargeScreen } = useIsLargeScreen(); const nStylesHooks = StyleSheet.create({ container: isLargeScreen ? { @@ -192,7 +192,7 @@ export const WalletCarouselItem: React.FC = React.memo( const { walletTransactionUpdateStatus } = useStorage(); const { width } = useWindowDimensions(); const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82; - const isLargeScreen = useIsLargeScreen(); + const { isLargeScreen } = useIsLargeScreen(); const onPressedIn = useCallback(() => { if (animationsEnabled) { diff --git a/hooks/useIsLargeScreen.ts b/hooks/useIsLargeScreen.ts index 14ec0cd2d..135b89684 100644 --- a/hooks/useIsLargeScreen.ts +++ b/hooks/useIsLargeScreen.ts @@ -1,10 +1,18 @@ import { useContext } from 'react'; import { LargeScreenContext } from '../components/Context/LargeScreenProvider'; -export const useIsLargeScreen = (): boolean => { +interface UseIsLargeScreenResult { + isLargeScreen: boolean; + setLargeScreenValue: (value: 'Handheld' | 'LargeScreen' | undefined) => void; +} + +export const useIsLargeScreen = (): UseIsLargeScreenResult => { const context = useContext(LargeScreenContext); if (context === undefined) { throw new Error('useIsLargeScreen must be used within a LargeScreenProvider'); } - return context.isLargeScreen; -}; + return { + isLargeScreen: context.isLargeScreen, + setLargeScreenValue: context.setLargeScreenValue, + }; +}; \ No newline at end of file diff --git a/navigation/DrawerRoot.tsx b/navigation/DrawerRoot.tsx index 479a6c3af..9880fa99c 100644 --- a/navigation/DrawerRoot.tsx +++ b/navigation/DrawerRoot.tsx @@ -14,7 +14,7 @@ const DrawerListContent = (props: any) => { }; const DrawerRoot = () => { - const isLargeScreen = useIsLargeScreen(); + const { isLargeScreen } = useIsLargeScreen(); const drawerStyle: DrawerNavigationOptions = useMemo( () => ({ diff --git a/navigation/MasterView.tsx b/navigation/MasterView.tsx index 18212ef14..75ba403bb 100644 --- a/navigation/MasterView.tsx +++ b/navigation/MasterView.tsx @@ -3,6 +3,7 @@ import 'react-native-gesture-handler'; // should be on top import React, { lazy, Suspense } from 'react'; import MainRoot from '../navigation'; import { useStorage } from '../hooks/context/useStorage'; +import DevMenu from '../components/DevMenu'; const CompanionDelegates = lazy(() => import('../components/CompanionDelegates')); const MasterView = () => { @@ -16,6 +17,7 @@ const MasterView = () => { )} + {__DEV__ && } ); }; diff --git a/package-lock.json b/package-lock.json index d97218b05..0b1f60dd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "coinselect": "3.1.13", "crypto-js": "4.2.0", "dayjs": "1.11.13", - "detox": "20.25.6", + "detox": "20.26.1", "ecpair": "2.0.1", "ecurve": "1.0.6", "electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc", @@ -7141,9 +7141,9 @@ } }, "node_modules/detox": { - "version": "20.25.6", - "resolved": "https://registry.npmjs.org/detox/-/detox-20.25.6.tgz", - "integrity": "sha512-UouUZ9Xa7WHzVIkv7QgAbG7aym0S7hQboiJJVw2ZfVUhdn4P3mfM6YED/g+fpRxVxiZDFCIziuIbOajToU8yUg==", + "version": "20.26.1", + "resolved": "https://registry.npmjs.org/detox/-/detox-20.26.1.tgz", + "integrity": "sha512-wGaqNpxTyFJzIcgIhmTeevsmjRIgvMNbi0TagmQPpE+RdiA59QQUkbS0o3FMCieZHxAr5XaIDwyh9ps0m1RAiw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -7153,6 +7153,7 @@ "caf": "^15.0.1", "chalk": "^4.0.0", "child-process-promise": "^2.2.0", + "detox-copilot": "^0.0.0", "execa": "^5.1.1", "find-up": "^5.0.0", "fs-extra": "^11.0.0", @@ -7198,6 +7199,12 @@ } } }, + "node_modules/detox-copilot": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.0.tgz", + "integrity": "sha512-BasCw/JXlplL1UZfe19xJYT0i0JU4tdGPWutsmybGy6166Jvj2ryikbgoE1ls44F+41p9Y0Yei0cdBC7KawXeQ==", + "license": "MIT" + }, "node_modules/detox/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", diff --git a/package.json b/package.json index 4818d93dc..82d61558b 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "clean:ios": "rm -fr node_modules && rm -fr ios/Pods && npm i && cd ios && pod update && cd ..; npm start -- --reset-cache", "releasenotes2json": "./scripts/release-notes.sh > release-notes.txt; node -e 'console.log(JSON.stringify(require(\"fs\").readFileSync(\"release-notes.txt\", \"utf8\")));' > release-notes.json", "branch2json": "./scripts/current-branch.sh > current-branch.json", - "start": "node node_modules/react-native/local-cli/cli.js start", + "start": "react-native start", "android": "react-native run-android", "android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android", "ios": "react-native run-ios", @@ -110,7 +110,7 @@ "coinselect": "3.1.13", "crypto-js": "4.2.0", "dayjs": "1.11.13", - "detox": "20.25.6", + "detox": "20.26.1", "ecpair": "2.0.1", "ecurve": "1.0.6", "electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc", diff --git a/screen/wallets/SelectWallet.tsx b/screen/wallets/SelectWallet.tsx index 1846de39a..e63683e85 100644 --- a/screen/wallets/SelectWallet.tsx +++ b/screen/wallets/SelectWallet.tsx @@ -74,8 +74,8 @@ const SelectWallet: React.FC = () => { const onPress = (item: TWallet) => { triggerHapticFeedback(HapticFeedbackTypes.Selection); - if (isModal) { - onWalletSelect?.(item, { navigation: { pop, navigate } }); + if (onWalletSelect) { + onWalletSelect(item, { navigation: { pop, navigate } }); } else { navigate(previousRouteName, { walletID: item.getID(), merge: true }); } diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index 5cc9f859a..b7750f158 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -40,6 +40,8 @@ import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamL import { Transaction, TWallet } from '../../class/wallets/types'; import getWalletTransactionsOptions from '../../navigation/helpers/getWalletTransactionsOptions'; import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder'; +import selectWallet from '../../helpers/select-wallet'; +import assert from 'assert'; const buttonFontSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22 @@ -177,34 +179,35 @@ const WalletTransactions: React.FC = ({ route }) => { }; const onWalletSelect = async (selectedWallet: TWallet) => { - if (selectedWallet) { - navigate('WalletTransactions', { - walletType: wallet?.type, - walletID: wallet?.getID(), - key: `WalletTransactions-${wallet?.getID()}`, - }); - if (wallet?.type === LightningCustodianWallet.type) { - let toAddress; - if (wallet?.refill_addressess.length > 0) { - toAddress = wallet.refill_addressess[0]; - } else { - try { - await wallet?.fetchBtcAddress(); - toAddress = wallet?.refill_addressess[0]; - } catch (Err) { - return presentAlert({ message: (Err as Error).message, type: AlertType.Toast }); - } - } - navigate('SendDetailsRoot', { - screen: 'SendDetails', - params: { - memo: loc.lnd.refill_lnd_balance, - address: toAddress, - walletID: selectedWallet.getID(), - }, - }); + assert(wallet?.type === LightningCustodianWallet.type, `internal error, wallet is not ${LightningCustodianWallet.type}`); + navigate('WalletTransactions', { + walletType: wallet?.type, + walletID: wallet?.getID(), + key: `WalletTransactions-${wallet?.getID()}`, + }); // navigating back to ln wallet screen + + // getting refill address, either cached or from the server: + let toAddress; + if (wallet?.refill_addressess.length > 0) { + toAddress = wallet.refill_addressess[0]; + } else { + try { + await wallet?.fetchBtcAddress(); + toAddress = wallet?.refill_addressess[0]; + } catch (Err) { + return presentAlert({ message: (Err as Error).message, type: AlertType.Toast }); } } + + // navigating to pay screen where user can pay to refill address: + navigate('SendDetailsRoot', { + screen: 'SendDetails', + params: { + memo: loc.lnd.refill_lnd_balance, + address: toAddress, + walletID: selectedWallet.getID(), + }, + }); }; const navigateToViewEditCosigners = () => { @@ -222,7 +225,7 @@ const WalletTransactions: React.FC = ({ route }) => { if (availableWallets.length === 0) { presentAlert({ message: loc.lnd.refill_create }); } else { - navigate('SelectWallet', { onWalletSelect, chainType: Chain.ONCHAIN }); + selectWallet(navigate, name, Chain.ONCHAIN).then(onWalletSelect); } } else if (id === actionKeys.RefillWithExternalWallet) { navigate('ReceiveDetailsRoot', { diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 6e399aa61..3c18970e2 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -95,7 +95,7 @@ type RouteProps = RouteProp; const WalletsList: React.FC = () => { const [state, dispatch] = useReducer>(reducer, initialState); const { isLoading } = state; - const isLargeScreen = useIsLargeScreen(); + const { isLargeScreen } = useIsLargeScreen(); const walletsCarousel = useRef(); const currentWalletIndex = useRef(0); const { @@ -465,7 +465,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - marginHorizontal: 16, + paddingHorizontal: 16, }, listHeaderText: { fontWeight: 'bold',