import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Alert, Keyboard, LayoutAnimation, Platform, ScrollView, StyleSheet, Switch, TextInput, View } from 'react-native'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; import triggerHapticFeedback, { HapticFeedbackTypes, triggerSelectionHapticFeedback } from '../../blue_modules/hapticFeedback'; import { BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import presentAlert from '../../components/Alert'; import Button from '../../components/Button'; import loc from '../../loc'; 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 { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { Divider } from '@rneui/themed'; import { Header } from '../../components/Header'; import AddressInput from '../../components/AddressInput'; import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency'; import { Action } from '../../components/types'; import ListItem, { PressableWrapper } from '../../components/ListItem'; import HeaderMenuButton from '../../components/HeaderMenuButton'; import { useSettings } from '../../hooks/context/useSettings'; import { suggestedServers, hardcodedPeers, presentResetToDefaultsAlert } from '../../blue_modules/BlueElectrum'; type RouteProps = RouteProp; export interface ElectrumServerItem { host: string; tcp?: number; ssl?: number; } const SET_PREFERRED_PREFIX = 'set_preferred_'; const ElectrumSettings: React.FC = () => { const { colors } = useTheme(); const params = useRoute().params; const { server } = params; const navigation = useExtendedNavigation(); const [isLoading, setIsLoading] = useState(true); const [serverHistory, setServerHistory] = useState>(new Set()); const [config, setConfig] = useState<{ connected?: number; host?: string; port?: string }>({}); const [host, setHost] = useState(''); const [port, setPort] = useState(); const [sslPort, setSslPort] = useState(undefined); const [isAndroidNumericKeyboardFocused, setIsAndroidNumericKeyboardFocused] = useState(false); const [isAndroidAddressKeyboardVisible, setIsAndroidAddressKeyboardVisible] = useState(false); const { setIsElectrumDisabled, isElectrumDisabled } = useSettings(); 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, }, }); const configIntervalRef = React.useRef(null); const fetchData = React.useCallback(async () => { console.log('Fetching data...'); const preferredServer = await BlueElectrum.getPreferredServer(); const savedHost = preferredServer?.host; const savedPort = preferredServer?.tcp; const savedSslPort = preferredServer?.ssl; const serverHistoryStr = (await DefaultPreference.get(BlueElectrum.ELECTRUM_SERVER_HISTORY)) as string; console.log('Preferred server:', preferredServer); console.log('Server history string:', serverHistoryStr); const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : []; const filteredServerHistory = new Set( parsedServerHistory.filter( v => v.host && (v.tcp || v.ssl) && !suggestedServers.some(suggested => suggested.host === v.host && suggested.tcp === v.tcp && suggested.ssl === v.ssl) && !hardcodedPeers.some(peer => peer.host === v.host), ), ); console.log('Filtered server history:', filteredServerHistory); setHost(savedHost || ''); setPort(savedPort ? Number(savedPort) : undefined); setSslPort(savedSslPort ? Number(savedSslPort) : undefined); setServerHistory(filteredServerHistory); setConfig(await BlueElectrum.getConfig()); configIntervalRef.current = setInterval(async () => { setConfig(await BlueElectrum.getConfig()); }, 500); setIsLoading(false); return () => { if (configIntervalRef.current) clearInterval(configIntervalRef.current); }; }, []); useEffect(() => { fetchData(); return () => { if (configIntervalRef.current) clearInterval(configIntervalRef.current); }; }, [fetchData]); useEffect(() => { 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]); const save = useCallback( async (v?: ElectrumServerItem) => { Keyboard.dismiss(); setIsLoading(true); try { const serverHost = v?.host || host; const serverPort = v?.tcp || port?.toString() || ''; const serverSslPort = v?.ssl || sslPort?.toString() || ''; if (serverHost && (serverPort || serverSslPort)) { await DefaultPreference.setName(GROUP_IO_BLUEWALLET); await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, serverHost); await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, serverPort); await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, serverSslPort); const serverExistsInHistory = Array.from(serverHistory).some( s => s.host === serverHost && s.tcp === Number(serverPort) && s.ssl === Number(serverSslPort), ); if (!serverExistsInHistory && (serverPort || serverSslPort) && !hardcodedPeers.some(peer => peer.host === serverHost)) { const newServerHistory = new Set(serverHistory); newServerHistory.add({ host: serverHost, tcp: Number(serverPort), ssl: Number(serverSslPort) }); await DefaultPreference.set(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(Array.from(newServerHistory))); setServerHistory(newServerHistory); } } triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); presentAlert({ message: loc.settings.electrum_saved }); fetchData(); } catch (error) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: (error as Error).message }); } finally { setIsLoading(false); } }, [host, port, sslPort, fetchData, serverHistory], ); const selectServer = useCallback( (value: string) => { const parsedServer = JSON.parse(value) as ElectrumServerItem; setHost(parsedServer.host); setPort(parsedServer.tcp); setSslPort(parsedServer.ssl); save(parsedServer); }, [save], ); const presentSelectServerAlert = useCallback( (value: ElectrumServerItem) => { triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy); Alert.alert( loc.settings.electrum_preferred_server, loc.formatString(loc.settings.set_as_preferred_electrum, { host: value.host, port: String(value.ssl ?? value.tcp) }), [ { text: loc._.ok, onPress: () => { selectServer(JSON.stringify(value)); }, style: 'default', }, { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, ], { cancelable: false }, ); }, [selectServer], ); const onPressMenuItem = useCallback( (id: string) => { if (id.startsWith(SET_PREFERRED_PREFIX)) { const rawServer = JSON.parse(id.replace(SET_PREFERRED_PREFIX, '')); presentSelectServerAlert(rawServer); } else { switch (id) { case CommonToolTipActions.ResetToDefault.id: presentResetToDefaultsAlert().then(reset => { if (reset) { triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); presentAlert({ message: loc.settings.electrum_saved }); fetchData(); } }); break; default: try { selectServer(id); } catch (error) { console.warn('Unknown menu item selected:', id); } break; } } }, [presentSelectServerAlert, fetchData, selectServer], ); type TCreateServerActionParameters = { value: ElectrumServerItem; seenHosts: Set; isPreferred?: boolean; isConnectedTo?: boolean; isSuggested?: boolean; }; const createServerAction = useCallback( ({ value, seenHosts, isPreferred = false, isConnectedTo = false, isSuggested = false }: TCreateServerActionParameters) => { const hostKey = `${value.host}:${value.ssl ?? value.tcp}`; if (seenHosts.has(hostKey)) return null; seenHosts.add(hostKey); return { id: `${SET_PREFERRED_PREFIX}${JSON.stringify(value)}`, text: Platform.OS === 'android' ? `${value.host}:${value.ssl ?? value.tcp}` : value.host, icon: isPreferred ? { iconValue: Platform.OS === 'ios' ? 'star.fill' : 'star_off' } : undefined, menuState: isConnectedTo, disabled: isPreferred, subtitle: value.ssl ? `${loc._.ssl_port}: ${value.ssl}` : `${loc._.port}: ${value.tcp}`, subactions: isSuggested || isPreferred ? [] : [ ...(host === value.host && (port === value.tcp || sslPort === value.ssl) ? [] : [ { id: `${SET_PREFERRED_PREFIX}${JSON.stringify(value)}`, text: loc.settings.set_as_preferred, subtitle: value.ssl ? `${loc._.ssl_port}: ${value.ssl}` : `${loc._.port}: ${value.tcp}`, }, ]), ], } as Action; }, [host, port, sslPort], ); const generateToolTipActions = useCallback(() => { const actions: Action[] = []; const seenHosts = new Set(); const suggestedServersAction: Action = { id: 'suggested_servers', text: loc._.suggested, displayInline: true, subtitle: loc.settings.electrum_suggested_description, subactions: suggestedServers .map(value => createServerAction({ value, seenHosts, isPreferred: host === value.host && (port === value.tcp || sslPort === value.ssl), isConnectedTo: config?.host === value.host && (config.port === value.tcp || config.port === value.ssl), isSuggested: true, }), ) .filter((action): action is Action => action !== null), }; actions.push(suggestedServersAction); if (serverHistory.size > 0) { const serverSubactions: Action[] = Array.from(serverHistory) .map(value => createServerAction({ value, seenHosts, isPreferred: host === value.host && (port === value.tcp || sslPort === value.ssl), isConnectedTo: config?.host === value.host && (config.port === value.tcp || config.port === value.ssl), isSuggested: false, }), ) .filter((action): action is Action => action !== null); actions.push({ id: 'server_history', text: loc.settings.electrum_history, displayInline: serverHistory.size <= 5 && serverHistory.size > 0, subactions: serverSubactions, hidden: serverHistory.size === 0, }); } const resetToDefaults = { ...CommonToolTipActions.ResetToDefault }; resetToDefaults.hidden = !host; actions.push(resetToDefaults); return actions; }, [config?.host, config.port, createServerAction, host, port, serverHistory, sslPort]); const HeaderRight = useMemo( () => , [onPressMenuItem, generateToolTipActions], ); useEffect(() => { navigation.setOptions({ headerRight: isElectrumDisabled ? null : () => HeaderRight, }); }, [HeaderRight, isElectrumDisabled, navigation]); const checkServer = async () => { setIsLoading(true); try { const features = await BlueElectrum.serverFeatures(); triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); presentAlert({ message: JSON.stringify(features, null, 2) }); } catch (error) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: (error as Error).message }); } setIsLoading(false); }; 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 () => { navigation.navigate('ScanQRCode'); }; useEffect(() => { const data = params.onBarScanned; if (data) { onBarScanned(data); navigation.setParams({ onBarScanned: undefined }); } }, [navigation, params.onBarScanned]); const onSSLPortChange = (value: boolean) => { Keyboard.dismiss(); if (value) { setPort(undefined); setSslPort(port); } else { setPort(sslPort); setSslPort(undefined); } }; const onElectrumConnectionEnabledSwitchChange = async (value: boolean) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); try { triggerSelectionHapticFeedback(); await BlueElectrum.setDisabled(value); setIsElectrumDisabled(value); } catch (error) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: (error as Error).message }); } }; const preferredServerIsEmpty = !host || (!port && !sslPort); const renderElectrumSettings = () => { return ( <>
{config.connected === 1 ? loc.settings.electrum_connected : loc.settings.electrum_connected_not} {config.host}:{config.port}
{loc.settings.electrum_preferred_server_description} setHost(text.trim())} editable={!isLoading} onBarScanned={importScan} keyboardType="default" onBlur={() => setIsAndroidAddressKeyboardVisible(false)} onFocus={() => setIsAndroidAddressKeyboardVisible(true)} inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID} isLoading={isLoading} /> { const parsed = Number(text.trim()); if (Number.isNaN(parsed)) { // Handle invalid input sslPort === undefined ? setPort(undefined) : setSslPort(undefined); return; } sslPort === undefined ? setPort(parsed) : 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}