From a2d4bfe3eec80fee762f4e88d44cd96ed806093c Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Mon, 23 Sep 2024 21:36:45 -0400 Subject: [PATCH 01/63] REF: ElectrumSettings to TSX --- components/AddressInput.tsx | 164 +------- components/AddressInputScanButton.tsx | 172 ++++++++ loc/en.json | 7 +- navigation/DetailViewScreensStack.tsx | 1 + navigation/DetailViewStackParamList.ts | 3 +- navigation/LazyLoadSettingsStack.tsx | 2 +- screen/settings/ElectrumSettings.tsx | 536 ++++++++++++++++++++++++ screen/settings/electrumSettings.js | 543 ------------------------- tests/e2e/bluewallet.spec.js | 10 +- typings/CommonToolTipActions.ts | 5 + 10 files changed, 743 insertions(+), 700 deletions(-) create mode 100644 components/AddressInputScanButton.tsx create mode 100644 screen/settings/ElectrumSettings.tsx delete mode 100644 screen/settings/electrumSettings.js diff --git a/components/AddressInput.tsx b/components/AddressInput.tsx index fbff8e926..e1b1b922e 100644 --- a/components/AddressInput.tsx +++ b/components/AddressInput.tsx @@ -1,13 +1,8 @@ -import React, { useCallback, useMemo } from 'react'; -import { Image, Keyboard, Platform, StyleSheet, Text, TextInput, View } from 'react-native'; - -import { scanQrHelper } from '../helpers/scan-qr'; +import React from 'react'; +import { Keyboard, StyleSheet, TextInput, View } from 'react-native'; import loc from '../loc'; import { useTheme } from './themes'; -import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs'; -import Clipboard from '@react-native-clipboard/clipboard'; -import presentAlert from './Alert'; -import ToolTipMenu from './TooltipMenu'; +import { AddressInputScanButton } from './AddressInputScanButton'; interface AddressInputProps { isLoading?: boolean; @@ -20,6 +15,7 @@ interface AddressInputProps { editable?: boolean; inputAccessoryViewID?: string; onBlur?: () => void; + onFocus?: () => void; keyboardType?: | 'default' | 'numeric' @@ -47,6 +43,7 @@ const AddressInput = ({ editable = true, inputAccessoryViewID, onBlur = () => {}, + onFocus = () => {}, keyboardType = 'default', }: AddressInputProps) => { const { colors } = useTheme(); @@ -56,11 +53,8 @@ const AddressInput = ({ borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor, }, - scan: { - backgroundColor: colors.scanLabel, - }, - scanText: { - color: colors.inverseForegroundColor, + input: { + color: colors.foregroundColor, }, }); @@ -69,64 +63,6 @@ const AddressInput = ({ Keyboard.dismiss(); }; - const toolTipOnPress = useCallback(async () => { - await scanButtonTapped(); - Keyboard.dismiss(); - if (launchedBy) scanQrHelper(launchedBy).then(value => onBarScanned({ data: value })); - }, [launchedBy, onBarScanned, scanButtonTapped]); - - const onMenuItemPressed = useCallback( - (action: string) => { - if (onBarScanned === undefined) throw new Error('onBarScanned is required'); - switch (action) { - case actionKeys.ScanQR: - scanButtonTapped(); - if (launchedBy) { - scanQrHelper(launchedBy) - .then(value => onBarScanned({ data: value })) - .catch(error => { - presentAlert({ message: error.message }); - }); - } - - break; - case actionKeys.CopyFromClipboard: - Clipboard.getString() - .then(onChangeText) - .catch(error => { - presentAlert({ message: error.message }); - }); - break; - case actionKeys.ChoosePhoto: - showImagePickerAndReadImage() - .then(value => { - if (value) { - onChangeText(value); - } - }) - .catch(error => { - presentAlert({ message: error.message }); - }); - break; - case actionKeys.ImportFile: - showFilePickerAndReadFile() - .then(value => { - if (value.data) { - onChangeText(value.data); - } - }) - .catch(error => { - presentAlert({ message: error.message }); - }); - break; - } - Keyboard.dismiss(); - }, - [launchedBy, onBarScanned, onChangeText, scanButtonTapped], - ); - - const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]); - return ( {editable ? ( - - - - {loc.send.details_scan} - - + ) : null} ); @@ -174,7 +102,6 @@ const styles = StyleSheet.create({ borderBottomWidth: 0.5, minHeight: 44, height: 44, - marginHorizontal: 20, alignItems: 'center', marginVertical: 8, borderRadius: 4, @@ -183,66 +110,7 @@ const styles = StyleSheet.create({ flex: 1, marginHorizontal: 8, minHeight: 33, - color: '#81868e', - }, - scan: { - height: 36, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderRadius: 4, - paddingVertical: 4, - paddingHorizontal: 8, - marginHorizontal: 4, - }, - scanText: { - marginLeft: 4, }, }); -const actionKeys = { - ScanQR: 'scan_qr', - CopyFromClipboard: 'copy_from_clipboard', - ChoosePhoto: 'choose_photo', - ImportFile: 'import_file', -}; - -const actionIcons = { - ScanQR: { - iconValue: Platform.OS === 'ios' ? 'qrcode' : 'ic_menu_camera', - }, - ImportFile: { - iconValue: 'doc', - }, - ChoosePhoto: { - iconValue: Platform.OS === 'ios' ? 'photo' : 'ic_menu_gallery', - }, - Clipboard: { - iconValue: Platform.OS === 'ios' ? 'doc' : 'ic_menu_file', - }, -}; - -const actions = [ - { - id: actionKeys.ScanQR, - text: loc.wallets.list_long_scan, - icon: actionIcons.ScanQR, - }, - { - id: actionKeys.CopyFromClipboard, - text: loc.wallets.list_long_clipboard, - icon: actionIcons.Clipboard, - }, - { - id: actionKeys.ChoosePhoto, - text: loc.wallets.list_long_choose, - icon: actionIcons.ChoosePhoto, - }, - { - id: actionKeys.ImportFile, - text: loc.wallets.import_file, - icon: actionIcons.ImportFile, - }, -]; - export default AddressInput; diff --git a/components/AddressInputScanButton.tsx b/components/AddressInputScanButton.tsx new file mode 100644 index 000000000..c43d29b2a --- /dev/null +++ b/components/AddressInputScanButton.tsx @@ -0,0 +1,172 @@ +import React, { useCallback, useMemo } from 'react'; +import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; +import ToolTipMenu from './TooltipMenu'; +import loc from '../loc'; +import { scanQrHelper } from '../helpers/scan-qr'; +import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs'; +import presentAlert from './Alert'; +import { useTheme } from './themes'; + +interface AddressInputScanButtonProps { + isLoading: boolean; + launchedBy?: string; + scanButtonTapped: () => void; + onBarScanned: (ret: { data?: any }) => void; + onChangeText: (text: string) => void; +} + +export const AddressInputScanButton = ({ + isLoading, + launchedBy, + scanButtonTapped, + onBarScanned, + onChangeText, +}: AddressInputScanButtonProps) => { + const { colors } = useTheme(); + const stylesHook = StyleSheet.create({ + scan: { + backgroundColor: colors.scanLabel, + }, + scanText: { + color: colors.inverseForegroundColor, + }, + }); + + const toolTipOnPress = useCallback(async () => { + await scanButtonTapped(); + Keyboard.dismiss(); + if (launchedBy) scanQrHelper(launchedBy).then(value => onBarScanned({ data: value })); + }, [launchedBy, onBarScanned, scanButtonTapped]); + + const onMenuItemPressed = useCallback( + (action: string) => { + if (onBarScanned === undefined) throw new Error('onBarScanned is required'); + switch (action) { + case actionKeys.ScanQR: + scanButtonTapped(); + if (launchedBy) { + scanQrHelper(launchedBy) + .then(value => onBarScanned({ data: value })) + .catch(error => { + presentAlert({ message: error.message }); + }); + } + break; + case actionKeys.CopyFromClipboard: + Clipboard.getString() + .then(onChangeText) + .catch(error => { + presentAlert({ message: error.message }); + }); + break; + case actionKeys.ChoosePhoto: + showImagePickerAndReadImage() + .then(value => { + if (value) { + onChangeText(value); + } + }) + .catch(error => { + presentAlert({ message: error.message }); + }); + break; + case actionKeys.ImportFile: + showFilePickerAndReadFile() + .then(value => { + if (value.data) { + onChangeText(value.data); + } + }) + .catch(error => { + presentAlert({ message: error.message }); + }); + break; + } + Keyboard.dismiss(); + }, + [launchedBy, onBarScanned, onChangeText, scanButtonTapped], + ); + + const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]); + + return ( + + + + {loc.send.details_scan} + + + ); +}; + +const styles = StyleSheet.create({ + scan: { + height: 36, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderRadius: 4, + paddingVertical: 4, + paddingHorizontal: 8, + marginHorizontal: 4, + }, + scanText: { + marginLeft: 4, + }, +}); + +const actionKeys = { + ScanQR: 'scan_qr', + CopyFromClipboard: 'copy_from_clipboard', + ChoosePhoto: 'choose_photo', + ImportFile: 'import_file', +}; + +const actionIcons = { + ScanQR: { + iconValue: Platform.OS === 'ios' ? 'qrcode' : 'ic_menu_camera', + }, + ImportFile: { + iconValue: 'doc', + }, + ChoosePhoto: { + iconValue: Platform.OS === 'ios' ? 'photo' : 'ic_menu_gallery', + }, + Clipboard: { + iconValue: Platform.OS === 'ios' ? 'doc' : 'ic_menu_file', + }, +}; + +const actions = [ + { + id: actionKeys.ScanQR, + text: loc.wallets.list_long_scan, + icon: actionIcons.ScanQR, + }, + { + id: actionKeys.CopyFromClipboard, + text: loc.wallets.list_long_clipboard, + icon: actionIcons.Clipboard, + }, + { + id: actionKeys.ChoosePhoto, + text: loc.wallets.list_long_choose, + icon: actionIcons.ChoosePhoto, + }, + { + id: actionKeys.ImportFile, + text: loc.wallets.import_file, + icon: actionIcons.ImportFile, + }, +]; diff --git a/loc/en.json b/loc/en.json index 9ecf5079e..aca900843 100644 --- a/loc/en.json +++ b/loc/en.json @@ -243,13 +243,12 @@ "set_electrum_server_as_default": "Set {server} as the default Electrum server?", "set_lndhub_as_default": "Set {url} as the default LNDhub server?", "electrum_settings_server": "Electrum Server", - "electrum_settings_explain": "Leave blank to use the default.", "electrum_status": "Status", - "electrum_clear_alert_title": "Clear history?", + "electrum_preferred_server": "Preferred Server", + "electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.", "electrum_clear_alert_title": "Clear history?", "electrum_clear_alert_message": "Do you want to clear electrum servers history?", "electrum_clear_alert_cancel": "Cancel", "electrum_clear_alert_ok": "Ok", - "electrum_select": "Select", "electrum_reset": "Reset to default", "electrum_unable_to_connect": "Unable to connect to {server}.", "electrum_history": "Server history", @@ -645,4 +644,4 @@ "notif_tx": "Notification transaction", "not_found": "Payment code not found" } -} +} \ No newline at end of file diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 5d8e34b06..d0cb7fb17 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -314,6 +314,7 @@ const DetailViewStackScreensStack = () => { name="ElectrumSettings" component={ElectrumSettingsComponent} options={navigationStyle({ title: loc.settings.electrum_settings_server })(theme)} + initialParams={{ server: undefined }} /> import('../screen/settings/Licensing')); const NetworkSettings = lazy(() => import('../screen/settings/NetworkSettings')); const About = lazy(() => import('../screen/settings/About')); const DefaultView = lazy(() => import('../screen/settings/DefaultView')); -const ElectrumSettings = lazy(() => import('../screen/settings/electrumSettings')); +const ElectrumSettings = lazy(() => import('../screen/settings/ElectrumSettings')); const EncryptStorage = lazy(() => import('../screen/settings/EncryptStorage')); const LightningSettings = lazy(() => import('../screen/settings/LightningSettings')); const NotificationSettings = lazy(() => import('../screen/settings/notificationSettings')); diff --git a/screen/settings/ElectrumSettings.tsx b/screen/settings/ElectrumSettings.tsx new file mode 100644 index 000000000..84a37bc37 --- /dev/null +++ b/screen/settings/ElectrumSettings.tsx @@ -0,0 +1,536 @@ +import React, { useState, useEffect, useContext, useMemo, useCallback } from 'react'; +import { + Alert, + Keyboard, + LayoutAnimation, + Platform, + ScrollView, + StyleSheet, + Switch, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import * as BlueElectrum from '../../blue_modules/BlueElectrum'; +import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; +import { BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; +import presentAlert, { AlertType } from '../../components/Alert'; +import Button from '../../components/Button'; +import ListItem, { PressableWrapper } from '../../components/ListItem'; +import { scanQrHelper } from '../../helpers/scan-qr'; +import loc from '../../loc'; +import { StorageContext } from '../../components/Context/StorageProvider'; +import { + DoneAndDismissKeyboardInputAccessory, + DoneAndDismissKeyboardInputAccessoryViewID, +} from '../../components/DoneAndDismissKeyboardInputAccessory'; +import DefaultPreference from 'react-native-default-preference'; + +import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory'; +import { useTheme } from '../../components/themes'; +import { RouteProp, useRoute } from '@react-navigation/native'; +import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; +import ToolTipMenu from '../../components/TooltipMenu'; +import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; +import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; +import { Icon } from '@rneui/themed'; +import { Header } from '../../components/Header'; +import AddressInput from '../../components/AddressInput'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency'; + +type RouteProps = RouteProp; + +export interface ElectrumServerItem { + host: string; + port?: number; + sslPort?: number; +} + +const ElectrumSettings: React.FC = () => { + const { colors } = useTheme(); + const { server } = useRoute().params; + const { setOptions } = useExtendedNavigation(); + const [isLoading, setIsLoading] = useState(true); + const [isOfflineMode, setIsOfflineMode] = useState(true); + const [serverHistory, setServerHistory] = useState([]); + const [config, setConfig] = useState<{ connected?: number; host?: string; port?: string }>({}); + const [host, setHost] = useState(''); + const [port, setPort] = useState(); + const [sslPort, setSslPort] = useState(undefined); + const [intervalId, setIntervalId] = useState(null); + const [isAndroidNumericKeyboardFocused, setIsAndroidNumericKeyboardFocused] = useState(false); + const [isAndroidAddressKeyboardVisible, setIsAndroidAddressKeyboardVisible] = useState(false); + const { setIsElectrumDisabled } = useContext(StorageContext); + + const stylesHook = StyleSheet.create({ + inputWrap: { + borderColor: colors.formBorder, + backgroundColor: colors.inputBackgroundColor, + }, + containerConnected: { + backgroundColor: colors.feeLabel, + }, + containerDisconnected: { + backgroundColor: colors.redBG, + }, + textConnected: { + color: colors.feeValue, + }, + textDisconnected: { + color: colors.redText, + }, + hostname: { + color: colors.foregroundColor, + }, + inputText: { + color: colors.foregroundColor, + }, + usePort: { + color: colors.foregroundColor, + }, + explain: { + color: colors.feeText, + }, + }); + + useEffect(() => { + const fetchData = async () => { + // Initial fetch of preferences and server settings + const savedHost = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_HOST); + const savedPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_TCP_PORT); + const savedSslPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SSL_PORT); + const serverHistoryStr = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SERVER_HISTORY); + + const offlineMode = await BlueElectrum.isDisabled(); + const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : []; + + // Set state + setHost(savedHost || ''); + setPort(savedPort ? Number(savedPort) : undefined); + setSslPort(savedSslPort ? Number(savedSslPort) : undefined); + setServerHistory(parsedServerHistory); + setIsOfflineMode(offlineMode); + + // Fetch the Electrum configuration + setConfig(await BlueElectrum.getConfig()); + + // Start a periodic update of the Electrum configuration + const configInterval = setInterval(async () => { + setConfig(await BlueElectrum.getConfig()); + }, 500); + + setIntervalId(configInterval); + setIsLoading(false); + }; + + // Execute the data fetching + fetchData(); + + // Clean up interval on component unmount + return () => { + if (intervalId) clearInterval(intervalId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // Handle server separately to avoid reset on other state + if (server) { + triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy); + Alert.alert( + loc.formatString(loc.settings.set_electrum_server_as_default, { server: (server as ElectrumServerItem).host }), + '', + [ + { + text: loc._.ok, + onPress: () => { + onBarScanned(JSON.stringify(server)); + }, + style: 'default', + }, + { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, + ], + { cancelable: false }, + ); + } + }, [server]); // Only runs when server changes + + const clearHistory = async () => { + setIsLoading(true); + await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify([])); + setServerHistory([]); + setIsLoading(false); + }; + + const serverExists = useCallback( + (value: ElectrumServerItem) => { + return serverHistory.some(s => `${s.host}${s.port}${s.sslPort}` === `${value.host}${value.port}${value.sslPort}`); + }, + [serverHistory], + ); + + const save = useCallback(async () => { + setIsLoading(true); + + try { + if (!host && !port && !sslPort) { + await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_HOST); + await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_TCP_PORT); + await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_SSL_PORT); + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + await DefaultPreference.clear(BlueElectrum.ELECTRUM_HOST); + await DefaultPreference.clear(BlueElectrum.ELECTRUM_TCP_PORT); + await DefaultPreference.clear(BlueElectrum.ELECTRUM_SSL_PORT); + } else { + await AsyncStorage.setItem(BlueElectrum.ELECTRUM_HOST, host); + await AsyncStorage.setItem(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || ''); + await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || ''); + if (!serverExists({ host, port, sslPort })) { + const newServerHistory = [...serverHistory, { host, port, sslPort }]; + await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(newServerHistory)); + } + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, host); + await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || ''); + await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || ''); + } + } catch (error) { + presentAlert({ message: (error as Error).message, type: AlertType.Toast }); + } + setIsLoading(false); + }, [host, port, sslPort, serverExists, serverHistory]); + + const resetToDefault = useCallback(() => { + setPort(undefined); + setHost(''); + setSslPort(undefined); + save(); + }, [save]); + + const onPressMenuItem = useCallback( + (id: string) => { + switch (id) { + case CommonToolTipActions.ResetToDefault.id: + resetToDefault(); + break; + } + }, + [resetToDefault], + ); + + const toolTipActions = useMemo(() => [CommonToolTipActions.ResetToDefault], []); + + const HeaderRight = useMemo( + () => ( + + + + ), + [colors.foregroundColor, onPressMenuItem, toolTipActions], + ); + + useEffect(() => { + setOptions({ + headerRight: isOfflineMode ? null : () => HeaderRight, + }); + }, [HeaderRight, isOfflineMode, setOptions]); + + const checkServer = async () => { + setIsLoading(true); + const features = await BlueElectrum.serverFeatures(); + triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); + presentAlert({ message: JSON.stringify(features, null, 2) }); + setIsLoading(false); + }; + + const selectServer = useCallback( + (value: string) => { + const parsedServer = JSON.parse(value) as ElectrumServerItem; + setHost(parsedServer.host); + setPort(parsedServer.port); + setSslPort(parsedServer.sslPort); + save(); + }, + [save], + ); + + const clearHistoryAlert = () => { + triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy); + Alert.alert(loc.settings.electrum_clear_alert_title, loc.settings.electrum_clear_alert_message, [ + { text: loc.settings.electrum_clear_alert_cancel, onPress: () => console.log('Cancel Pressed'), style: 'cancel' }, + { text: loc.settings.electrum_clear_alert_ok, onPress: () => clearHistory() }, + ]); + }; + + const onBarScanned = (value: string) => { + let v = value; + if (value && DeeplinkSchemaMatch.getServerFromSetElectrumServerAction(value)) { + v = DeeplinkSchemaMatch.getServerFromSetElectrumServerAction(value) as string; + } + const [scannedHost, scannedPort, type] = v?.split(':') ?? []; + setHost(scannedHost); + if (type === 's') { + setSslPort(Number(scannedPort)); + setPort(undefined); + } else { + setPort(Number(scannedPort)); + setSslPort(undefined); + } + }; + + const importScan = async () => { + const scanned = await scanQrHelper('ElectrumSettings', true); + if (scanned) { + onBarScanned(scanned); + } + }; + + const onSSLPortChange = (value: boolean) => { + if (value) { + setPort(undefined); + setSslPort(port); + } else { + setPort(sslPort); + setSslPort(undefined); + } + }; + + const onElectrumConnectionEnabledSwitchChange = async (value: boolean) => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + + if (value) { + await BlueElectrum.setDisabled(true); + setIsElectrumDisabled(true); + BlueElectrum.forceDisconnect(); + } else { + await BlueElectrum.setDisabled(false); + setIsElectrumDisabled(false); + BlueElectrum.connectMain(); + } + setIsOfflineMode(value); + }; + + const ElectrumServerItems = useMemo( + () => + serverHistory.map((value, i) => ( + selectServer(JSON.stringify(value))} + key={i} + title={`${value.host}:${value.port || value.sslPort}`} + disabled={isLoading || (host === value.host && (port === value.port || sslPort === value.sslPort))} + checkmark={host === value.host && (port === value.port || sslPort === value.sslPort)} + /> + )), + [host, isLoading, port, selectServer, serverHistory, sslPort], + ); + + const renderElectrumSettings = () => { + return ( + <> +
+ + + + + {config.connected === 1 ? loc.settings.electrum_connected : loc.settings.electrum_connected_not} + + + + + + {config.host}:{config.port} + + + + +
+ + + setHost(text.trim())} + editable={!isLoading} + onBarScanned={importScan} + keyboardType="default" // Adjust if needed + onBlur={() => setIsAndroidAddressKeyboardVisible(false)} + onFocus={() => setIsAndroidAddressKeyboardVisible(true)} // Add if you have an onFocus prop + inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID} + isLoading={isLoading} + /> + + + + { + const parsed = Number(text.trim()); + if (sslPort === undefined) { + setPort(parsed); + setSslPort(undefined); + } else { + setPort(undefined); + setSslPort(parsed); + } + }} + numberOfLines={1} + style={[styles.inputText, stylesHook.inputText]} + editable={!isLoading} + placeholderTextColor="#81868e" + underlineColorAndroid="transparent" + autoCorrect={false} + autoCapitalize="none" + keyboardType="number-pad" + inputAccessoryViewID={DismissKeyboardInputAccessoryViewID} + testID="PortInput" + onFocus={() => setIsAndroidNumericKeyboardFocused(true)} + onBlur={() => setIsAndroidNumericKeyboardFocused(false)} + /> + + {loc.settings.use_ssl} + + + + + {loc.settings.electrum_preferred_server_description} + + {isLoading ? :