mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
wip
This commit is contained in:
parent
7ab10db864
commit
c675bf290b
@ -14,7 +14,7 @@ import { useStorage } from '../../hooks/context/useStorage';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
import { TotalWalletsBalanceKey, TotalWalletsBalancePreferredUnit } from '../TotalWalletsBalance';
|
||||
import { LayoutAnimation } from 'react-native';
|
||||
import { BLOCK_EXPLORERS, getBlockExplorer, saveBlockExplorer } from '../../models/blockExplorer';
|
||||
import { BLOCK_EXPLORERS, getBlockExplorerUrl, saveBlockExplorer, BlockExplorer, normalizeUrl } from '../../models/blockExplorer';
|
||||
|
||||
// DefaultPreference and AsyncStorage get/set
|
||||
|
||||
@ -86,8 +86,8 @@ interface SettingsContextType {
|
||||
setTotalBalancePreferredUnitStorage: (unit: BitcoinUnit) => Promise<void>;
|
||||
isDrawerShouldHide: boolean;
|
||||
setIsDrawerShouldHide: (value: boolean) => void;
|
||||
selectedBlockExplorer: string;
|
||||
setBlockExplorerStorage: (url: string) => Promise<boolean>;
|
||||
selectedBlockExplorer: BlockExplorer;
|
||||
setBlockExplorerStorage: (explorer: BlockExplorer) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const defaultSettingsContext: SettingsContextType = {
|
||||
@ -115,8 +115,8 @@ const defaultSettingsContext: SettingsContextType = {
|
||||
setTotalBalancePreferredUnitStorage: async (unit: BitcoinUnit) => {},
|
||||
isDrawerShouldHide: false,
|
||||
setIsDrawerShouldHide: () => {},
|
||||
selectedBlockExplorer: BLOCK_EXPLORERS.DEFAULT,
|
||||
setBlockExplorerStorage: async () => false,
|
||||
selectedBlockExplorer: BLOCK_EXPLORERS.default,
|
||||
setBlockExplorerStorage: async (explorer: BlockExplorer) => false,
|
||||
};
|
||||
|
||||
export const SettingsContext = createContext<SettingsContextType>(defaultSettingsContext);
|
||||
@ -147,7 +147,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
||||
// Toggle Drawer (for screens like Manage Wallets or ScanQRCode)
|
||||
const [isDrawerShouldHide, setIsDrawerShouldHide] = useState<boolean>(false);
|
||||
|
||||
const [selectedBlockExplorer, setSelectedBlockExplorer] = useState<string>(BLOCK_EXPLORERS.DEFAULT);
|
||||
const [selectedBlockExplorer, setSelectedBlockExplorer] = useState<BlockExplorer>(BLOCK_EXPLORERS.default);
|
||||
|
||||
const languageStorage = useAsyncStorage(STORAGE_KEY);
|
||||
const { walletsInitialized } = useStorage();
|
||||
@ -218,11 +218,15 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
||||
setTotalBalancePreferredUnitState(unit);
|
||||
})
|
||||
.catch(error => console.error('Error fetching total balance preferred unit:', error));
|
||||
|
||||
getBlockExplorer()
|
||||
getBlockExplorerUrl()
|
||||
.then(url => {
|
||||
console.debug('SettingsContext blockExplorer:', url);
|
||||
setSelectedBlockExplorer(url);
|
||||
const predefinedExplorer = Object.values(BLOCK_EXPLORERS).find(explorer => normalizeUrl(explorer.url) === normalizeUrl(url));
|
||||
if (predefinedExplorer) {
|
||||
setSelectedBlockExplorer(predefinedExplorer);
|
||||
} else {
|
||||
setSelectedBlockExplorer({ key: 'custom', name: 'Custom', url });
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error fetching block explorer settings:', error));
|
||||
|
||||
@ -310,14 +314,13 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
||||
setTotalBalancePreferredUnitState(unit);
|
||||
}, []);
|
||||
|
||||
const setBlockExplorerStorage = useCallback(async (url: string): Promise<boolean> => {
|
||||
const success = await saveBlockExplorer(url);
|
||||
const setBlockExplorerStorage = useCallback(async (explorer: BlockExplorer): Promise<boolean> => {
|
||||
const success = await saveBlockExplorer(explorer.url);
|
||||
if (success) {
|
||||
setSelectedBlockExplorer(url);
|
||||
setSelectedBlockExplorer(explorer);
|
||||
}
|
||||
return success;
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
preferredFiatCurrency,
|
||||
|
@ -1,125 +1,89 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, TextInput, View, TouchableOpacity } from 'react-native';
|
||||
import { ListItem as RNElementsListItem } from '@rneui/themed';
|
||||
import { StyleSheet, TextInput, View, Switch } from 'react-native';
|
||||
import { ListItem } from '@rneui/themed';
|
||||
import { useTheme } from './themes';
|
||||
import loc from '../loc';
|
||||
|
||||
interface SettingsBlockExplorerCustomUrlListItemProps {
|
||||
title: string;
|
||||
customUrl?: string;
|
||||
onCustomUrlChange?: (url: string) => void;
|
||||
onSubmitCustomUrl?: () => void;
|
||||
selected: boolean;
|
||||
onPress: () => void;
|
||||
checkmark?: boolean;
|
||||
isLoading?: boolean;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
interface SettingsBlockExplorerCustomUrlItemProps {
|
||||
isCustomEnabled: boolean;
|
||||
onSwitchToggle: (value: boolean) => void;
|
||||
customUrl: string;
|
||||
onCustomUrlChange: (url: string) => void;
|
||||
onSubmitCustomUrl: () => void;
|
||||
inputRef?: React.RefObject<TextInput>;
|
||||
}
|
||||
|
||||
const SettingsBlockExplorerCustomUrlListItem: React.FC<SettingsBlockExplorerCustomUrlListItemProps> = ({
|
||||
title,
|
||||
const SettingsBlockExplorerCustomUrlItem: React.FC<SettingsBlockExplorerCustomUrlItemProps> = ({
|
||||
isCustomEnabled,
|
||||
onSwitchToggle,
|
||||
customUrl,
|
||||
onCustomUrlChange,
|
||||
onSubmitCustomUrl,
|
||||
selected,
|
||||
onPress,
|
||||
checkmark = false,
|
||||
isLoading = false,
|
||||
onFocus,
|
||||
onBlur,
|
||||
inputRef,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const styleHook = StyleSheet.create({
|
||||
uri: {
|
||||
borderColor: colors.formBorder,
|
||||
borderBottomColor: colors.formBorder,
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
},
|
||||
containerStyle: {
|
||||
backgroundColor: colors.background,
|
||||
minHeight: selected ? 140 : 60,
|
||||
},
|
||||
checkmarkContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
checkmarkStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<RNElementsListItem containerStyle={styleHook.containerStyle} bottomDivider>
|
||||
<RNElementsListItem.Content>
|
||||
<RNElementsListItem.Title style={[styles.title, { color: colors.text }]}>{title}</RNElementsListItem.Title>
|
||||
</RNElementsListItem.Content>
|
||||
{checkmark && (
|
||||
<View style={styleHook.checkmarkContainer}>
|
||||
<RNElementsListItem.CheckBox
|
||||
iconRight
|
||||
iconType="octaicon"
|
||||
checkedIcon="check"
|
||||
checked
|
||||
containerStyle={styleHook.checkmarkStyle}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</RNElementsListItem>
|
||||
<>
|
||||
<ListItem containerStyle={[styles.container, { backgroundColor: colors.background }]} bottomDivider>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={[styles.title, { color: colors.text }]}>{loc.settings.block_explorer_preferred}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<Switch
|
||||
accessible
|
||||
accessibilityRole="switch"
|
||||
accessibilityState={{ checked: isCustomEnabled }}
|
||||
onValueChange={onSwitchToggle}
|
||||
value={isCustomEnabled}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{selected && (
|
||||
<View style={[styles.uri, styleHook.uri]}>
|
||||
{isCustomEnabled && (
|
||||
<View style={[styles.uriContainer, { borderColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor }]}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={customUrl}
|
||||
placeholder={loc._.enter_url}
|
||||
onChangeText={onCustomUrlChange}
|
||||
numberOfLines={1}
|
||||
style={styles.uriText}
|
||||
placeholderTextColor="#81868e"
|
||||
editable={!isLoading}
|
||||
style={[styles.uriText, { color: colors.text }]}
|
||||
placeholderTextColor={colors.placeholderTextColor}
|
||||
textContentType="URL"
|
||||
autoFocus
|
||||
clearButtonMode="while-editing"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
underlineColorAndroid="transparent"
|
||||
onSubmitEditing={onSubmitCustomUrl}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
testID="CustomURIInput"
|
||||
editable={isCustomEnabled}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsBlockExplorerCustomUrlItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 60,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
uri: {
|
||||
uriContainer: {
|
||||
flexDirection: 'row',
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0.5,
|
||||
minHeight: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
marginHorizontal: 15,
|
||||
marginVertical: 10,
|
||||
paddingHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
uriText: {
|
||||
flex: 1,
|
||||
color: '#81868e',
|
||||
marginHorizontal: 8,
|
||||
minHeight: 36,
|
||||
height: 36,
|
||||
},
|
||||
});
|
||||
|
||||
export default SettingsBlockExplorerCustomUrlListItem;
|
||||
|
@ -31,6 +31,7 @@ export const BlueDefaultTheme = {
|
||||
outgoingForegroundColor: '#d0021b',
|
||||
successColor: '#37c0a1',
|
||||
failedColor: '#ff0000',
|
||||
placeholderTextColor: '#81868e',
|
||||
shadowColor: '#000000',
|
||||
inverseForegroundColor: '#ffffff',
|
||||
hdborderColor: '#68BBE1',
|
||||
|
12
loc/en.json
12
loc/en.json
@ -10,7 +10,6 @@
|
||||
"never": "Never",
|
||||
"of": "{number} of {total}",
|
||||
"ok": "OK",
|
||||
"default": "Default",
|
||||
"enter_url": "Enter URL",
|
||||
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it.",
|
||||
"yes": "Yes",
|
||||
@ -27,7 +26,8 @@
|
||||
"pick_file": "Choose a file",
|
||||
"enter_amount": "Enter amount",
|
||||
"qr_custom_input_button": "Tap 10 times to enter custom input",
|
||||
"unlock": "Unlock"
|
||||
"unlock": "Unlock",
|
||||
"suggested": "Suggested"
|
||||
},
|
||||
"azteco": {
|
||||
"codeIs": "Your voucher code is",
|
||||
@ -207,11 +207,11 @@
|
||||
"about_review": "Leave us a review",
|
||||
"performance_score": "Performance score: {num}",
|
||||
"run_performance_test": "Test performance",
|
||||
"enter_custom_url": "Enter custom block explorer URL",
|
||||
"about_selftest": "Run self-test",
|
||||
"block_explorer_invalid_custom_url": "The custom URL provided is invalid. Please enter a valid URL starting with http:// or https://.",
|
||||
"block_explorer_invalid_custom_url": "The URL provided is invalid. Please enter a valid URL starting with http:// or https://.",
|
||||
"about_selftest_electrum_disabled": "Self-testing is not available with Electrum Offline Mode. Please disable offline mode and try again.",
|
||||
"about_selftest_ok": "All internal tests have passed successfully. The wallet works well.",
|
||||
|
||||
"about_sm_github": "GitHub",
|
||||
"about_sm_discord": "Discord Server",
|
||||
"about_sm_telegram": "Telegram channel",
|
||||
@ -264,8 +264,8 @@
|
||||
"encrypt_storage_explanation_description_line2": "However, it's important to know that this encryption only protects the access to your wallets stored on the device's keychain. It doesn't put a password or any extra protection on the wallets themselves.",
|
||||
"i_understand": "I understand",
|
||||
"block_explorer": "Block Explorer",
|
||||
"block_explorer_custom": "Custom Block Explorer",
|
||||
"block_explorer_error_saving_custom": "Error saving custom block explorer",
|
||||
"block_explorer_preferred": "Use preferred block explorer",
|
||||
"block_explorer_error_saving_custom": "Error saving preferred block explorer",
|
||||
"encrypt_title": "Security",
|
||||
"encrypt_tstorage": "Storage",
|
||||
"encrypt_use": "Use {type}",
|
||||
|
@ -1,41 +1,79 @@
|
||||
// blockExplorer.ts
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
import { GROUP_IO_BLUEWALLET } from '../blue_modules/currency';
|
||||
import loc from '../loc';
|
||||
|
||||
export const BLOCK_EXPLORER = 'blockExplorer';
|
||||
export interface BlockExplorer {
|
||||
key: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const BLOCK_EXPLORERS = {
|
||||
DEFAULT: 'https://mempool.space',
|
||||
BLOCKCHAIR: 'https://blockchair.com',
|
||||
BLOCKSTREAM: 'https://blockstream.info',
|
||||
CUSTOM: 'custom',
|
||||
export const BLOCK_EXPLORERS: { [key: string]: BlockExplorer } = {
|
||||
default: { key: 'default', name: 'Mempool.space', url: 'https://mempool.space' },
|
||||
blockchair: { key: 'blockchair', name: 'Blockchair', url: 'https://blockchair.com/bitcoin' },
|
||||
blockstream: { key: 'blockstream', name: 'Blockstream.info', url: 'https://blockstream.info' },
|
||||
custom: { key: 'custom', name: 'Custom', url: '' }, // Custom URL will be handled separately
|
||||
};
|
||||
|
||||
export const blockExplorers = [
|
||||
{ name: `${loc._.default} - Mempool.space`, key: 'default', url: BLOCK_EXPLORERS.DEFAULT },
|
||||
{ name: 'Blockchair', key: 'blockchair', url: BLOCK_EXPLORERS.BLOCKCHAIR },
|
||||
{ name: 'Blockstream.info', key: 'blockstream', url: BLOCK_EXPLORERS.BLOCKSTREAM },
|
||||
{ name: loc.settings.block_explorer_custom, key: 'custom', url: null },
|
||||
];
|
||||
export const getBlockExplorersList = (): BlockExplorer[] => {
|
||||
return Object.values(BLOCK_EXPLORERS);
|
||||
};
|
||||
|
||||
export const getBlockExplorer = async (): Promise<string> => {
|
||||
export const normalizeUrl = (url: string): string => {
|
||||
return url.replace(/\/+$/, '');
|
||||
};
|
||||
|
||||
export const isValidUrl = (url: string): boolean => {
|
||||
const pattern = /^(https?:\/\/)/;
|
||||
return pattern.test(url);
|
||||
};
|
||||
|
||||
export const findMatchingExplorerByDomain = (url: string): BlockExplorer | null => {
|
||||
const domain = getDomain(url);
|
||||
for (const explorer of Object.values(BLOCK_EXPLORERS)) {
|
||||
if (getDomain(explorer.url) === domain) {
|
||||
return explorer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getDomain = (url: string): string => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const selectedExplorer = await DefaultPreference.get(BLOCK_EXPLORER);
|
||||
return selectedExplorer || BLOCK_EXPLORERS.DEFAULT; // Return the selected explorer or default to mempool.space
|
||||
} catch (error) {
|
||||
console.error('Error getting block explorer:', error);
|
||||
return BLOCK_EXPLORERS.DEFAULT;
|
||||
const hostname = new URL(url).hostname;
|
||||
return hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const BLOCK_EXPLORER_STORAGE_KEY = 'blockExplorer';
|
||||
|
||||
export const saveBlockExplorer = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.set(BLOCK_EXPLORER, url);
|
||||
await DefaultPreference.set(BLOCK_EXPLORER_STORAGE_KEY, url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving block explorer:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeBlockExplorer = async (): Promise<boolean> => {
|
||||
try {
|
||||
await DefaultPreference.clear(BLOCK_EXPLORER_STORAGE_KEY);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error removing block explorer:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBlockExplorerUrl = async (): Promise<string> => {
|
||||
try {
|
||||
const url = await DefaultPreference.get(BLOCK_EXPLORER_STORAGE_KEY);
|
||||
return url ?? BLOCK_EXPLORERS.default.url;
|
||||
} catch (error) {
|
||||
console.error('Error getting block explorer:', error);
|
||||
return BLOCK_EXPLORERS.default.url;
|
||||
}
|
||||
};
|
||||
|
@ -51,10 +51,10 @@ export type DetailViewStackParamList = {
|
||||
PlausibleDeniability: undefined;
|
||||
Licensing: undefined;
|
||||
NetworkSettings: undefined;
|
||||
SettingsBlockExplorer: undefined;
|
||||
About: undefined;
|
||||
DefaultView: undefined;
|
||||
ElectrumSettings: undefined;
|
||||
SettingsBlockExplorer: undefined;
|
||||
EncryptStorage: undefined;
|
||||
Language: undefined;
|
||||
LightningSettings: {
|
||||
|
@ -1,111 +1,205 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { FlatList, Alert, StyleSheet, TextInput } from 'react-native';
|
||||
import React, { useRef, useCallback, useState, useEffect } from 'react';
|
||||
import { SectionList, StyleSheet, TextInput, SectionListRenderItemInfo, SectionListData, View, LayoutAnimation } from 'react-native';
|
||||
import ListItem from '../../components/ListItem';
|
||||
import SettingsBlockExplorerCustomUrlListItem from '../../components/SettingsBlockExplorerCustomUrlListItem';
|
||||
import loc from '../../loc';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import {
|
||||
getBlockExplorersList,
|
||||
BlockExplorer,
|
||||
isValidUrl,
|
||||
normalizeUrl,
|
||||
BLOCK_EXPLORERS,
|
||||
removeBlockExplorer,
|
||||
} from '../../models/blockExplorer';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import { blockExplorers } from '../../models/blockExplorer';
|
||||
import SettingsBlockExplorerCustomUrlItem from '../../components/SettingsBlockExplorerCustomUrlListItem';
|
||||
import { Header } from '../../components/Header';
|
||||
|
||||
const normalizeUrl = (url: string) => url.replace(/\/+$/, '');
|
||||
type BlockExplorerItem = BlockExplorer | string;
|
||||
|
||||
const isValidUrl = (url: string) => {
|
||||
const urlPattern = /^(https?:\/\/)/;
|
||||
return urlPattern.test(url);
|
||||
};
|
||||
interface SectionData extends SectionListData<BlockExplorerItem> {
|
||||
title?: string;
|
||||
data: BlockExplorerItem[];
|
||||
}
|
||||
|
||||
const SettingsBlockExplorer: React.FC = () => {
|
||||
const { selectedBlockExplorer, setBlockExplorerStorage } = useSettings();
|
||||
const [customUrlInput, setCustomUrlInput] = useState<string>(selectedBlockExplorer || '');
|
||||
const [prevSelectedBlockExplorer, setPrevSelectedBlockExplorer] = useState<string>(selectedBlockExplorer); // Use prevSelectedBlockExplorer
|
||||
const [isCustomSelected, setIsCustomSelected] = useState<boolean>(false);
|
||||
const customUrlInputRef = useRef<TextInput>(null);
|
||||
const { colors } = useTheme();
|
||||
const { selectedBlockExplorer, setBlockExplorerStorage } = useSettings();
|
||||
const customUrlInputRef = useRef<TextInput>(null);
|
||||
const [customUrl, setCustomUrl] = useState<string>(selectedBlockExplorer.key === 'custom' ? selectedBlockExplorer.url : '');
|
||||
const [isCustomEnabled, setIsCustomEnabled] = useState<boolean>(selectedBlockExplorer.key === 'custom');
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
||||
const isSelectedExplorer = (url: string | null) => {
|
||||
if (!url && selectedBlockExplorer === customUrlInput) return true;
|
||||
return normalizeUrl(selectedBlockExplorer) === normalizeUrl(url || '');
|
||||
};
|
||||
const predefinedExplorers = getBlockExplorersList().filter(explorer => explorer.key !== 'custom');
|
||||
|
||||
const handleExplorerPress = async (key: string, url: string | null) => {
|
||||
if (key === 'custom') {
|
||||
setIsCustomSelected(true);
|
||||
setPrevSelectedBlockExplorer(selectedBlockExplorer); // Store previous selection
|
||||
return;
|
||||
}
|
||||
setCustomUrlInput('');
|
||||
setIsCustomSelected(false);
|
||||
const success = await setBlockExplorerStorage(url!);
|
||||
if (!success) {
|
||||
Alert.alert(loc.errors.error, loc.settings.block_explorer_error_saving_custom);
|
||||
}
|
||||
};
|
||||
const sections: SectionData[] = [
|
||||
{
|
||||
title: loc._.suggested,
|
||||
data: predefinedExplorers,
|
||||
},
|
||||
{
|
||||
title: loc.wallets.details_advanced,
|
||||
data: ['custom'],
|
||||
},
|
||||
];
|
||||
|
||||
const handleCustomUrlChange = (url: string) => {
|
||||
setCustomUrlInput(url);
|
||||
};
|
||||
const handleExplorerPress = useCallback(
|
||||
async (explorer: BlockExplorer) => {
|
||||
const success = await setBlockExplorerStorage(explorer);
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
setIsCustomEnabled(false);
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
|
||||
}
|
||||
},
|
||||
[setBlockExplorerStorage],
|
||||
);
|
||||
|
||||
const handleSubmitCustomUrl = async () => {
|
||||
if (!customUrlInput || !isValidUrl(customUrlInput)) {
|
||||
Alert.alert(loc.errors.error, loc.settings.block_explorer_invalid_custom_url);
|
||||
const handleCustomUrlChange = useCallback((url: string) => {
|
||||
setCustomUrl(url);
|
||||
}, []);
|
||||
|
||||
const handleSubmitCustomUrl = useCallback(async () => {
|
||||
if (isSubmitting) return;
|
||||
setIsSubmitting(true);
|
||||
const customUrlNormalized = normalizeUrl(customUrl);
|
||||
|
||||
if (!isValidUrl(customUrlNormalized)) {
|
||||
presentAlert({ message: loc.settings.block_explorer_invalid_custom_url });
|
||||
customUrlInputRef.current?.focus();
|
||||
await setBlockExplorerStorage(prevSelectedBlockExplorer); // Revert to previous block explorer
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const success = await setBlockExplorerStorage(customUrlInput);
|
||||
if (!success) {
|
||||
Alert.alert(loc.errors.error, loc.settings.block_explorer_error_saving_custom);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomUrlBlur = async () => {
|
||||
if (isValidUrl(customUrlInput)) {
|
||||
setIsCustomSelected(false);
|
||||
const customExplorer: BlockExplorer = {
|
||||
key: 'custom',
|
||||
name: 'Custom',
|
||||
url: customUrlNormalized,
|
||||
};
|
||||
|
||||
const success = await setBlockExplorerStorage(customExplorer);
|
||||
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
} else {
|
||||
await handleSubmitCustomUrl(); // Revert to previous block explorer if invalid
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
|
||||
}
|
||||
};
|
||||
setIsSubmitting(false);
|
||||
}, [customUrl, setBlockExplorerStorage, isSubmitting]);
|
||||
|
||||
const handleCustomUrlFocus = () => {
|
||||
setIsCustomSelected(true);
|
||||
};
|
||||
const handleCustomSwitchToggle = useCallback(
|
||||
async (value: boolean) => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
setIsCustomEnabled(value);
|
||||
if (value) {
|
||||
await removeBlockExplorer();
|
||||
customUrlInputRef.current?.focus();
|
||||
} else {
|
||||
const defaultExplorer = BLOCK_EXPLORERS.default;
|
||||
const success = await setBlockExplorerStorage(defaultExplorer);
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSubmitting) {
|
||||
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[setBlockExplorerStorage, isSubmitting],
|
||||
);
|
||||
|
||||
const renderItem = ({ item }: { item: { name: string; key: string; url: string | null } }) => {
|
||||
if (item.key === 'custom') {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isCustomEnabled) {
|
||||
const customUrlNormalized = normalizeUrl(customUrl);
|
||||
if (!isValidUrl(customUrlNormalized)) {
|
||||
(async () => {
|
||||
const success = await setBlockExplorerStorage(BLOCK_EXPLORERS.default);
|
||||
if (!success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [customUrl, isCustomEnabled, setBlockExplorerStorage]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, section }: SectionListRenderItemInfo<BlockExplorerItem, SectionData>) => {
|
||||
if (section.title === loc._.suggested) {
|
||||
const explorer = item as BlockExplorer;
|
||||
const isSelected = !isCustomEnabled && normalizeUrl(selectedBlockExplorer.url || '') === normalizeUrl(explorer.url || '');
|
||||
return (
|
||||
<ListItem
|
||||
title={explorer.name}
|
||||
onPress={() => handleExplorerPress(explorer)}
|
||||
checkmark={isSelected}
|
||||
disabled={isCustomEnabled}
|
||||
containerStyle={[{ backgroundColor: colors.background }, styles.rowHeight]}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SettingsBlockExplorerCustomUrlItem
|
||||
isCustomEnabled={isCustomEnabled}
|
||||
onSwitchToggle={handleCustomSwitchToggle}
|
||||
customUrl={customUrl}
|
||||
onCustomUrlChange={handleCustomUrlChange}
|
||||
onSubmitCustomUrl={handleSubmitCustomUrl}
|
||||
inputRef={customUrlInputRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedBlockExplorer,
|
||||
isCustomEnabled,
|
||||
handleExplorerPress,
|
||||
colors.background,
|
||||
handleCustomSwitchToggle,
|
||||
customUrl,
|
||||
handleCustomUrlChange,
|
||||
handleSubmitCustomUrl,
|
||||
],
|
||||
);
|
||||
|
||||
// @ts-ignore: renderSectionHeader type is not correct
|
||||
const renderSectionHeader = useCallback(({ section }) => {
|
||||
const { title } = section;
|
||||
if (title) {
|
||||
return (
|
||||
<SettingsBlockExplorerCustomUrlListItem
|
||||
title={item.name}
|
||||
selected={isCustomSelected}
|
||||
customUrl={customUrlInput}
|
||||
onCustomUrlChange={handleCustomUrlChange}
|
||||
onSubmitCustomUrl={handleSubmitCustomUrl}
|
||||
onPress={() => handleExplorerPress(item.key, item.url)}
|
||||
onFocus={handleCustomUrlFocus}
|
||||
onBlur={handleCustomUrlBlur}
|
||||
inputRef={customUrlInputRef}
|
||||
checkmark={isSelectedExplorer(null)}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
<Header leftText={title} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
title={item.name}
|
||||
onPress={() => handleExplorerPress(item.key, item.url)}
|
||||
checkmark={isSelectedExplorer(item.url)}
|
||||
containerStyle={{ backgroundColor: colors.background }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={blockExplorers}
|
||||
keyExtractor={item => item.key}
|
||||
<SectionList<BlockExplorerItem, SectionData>
|
||||
sections={sections}
|
||||
keyExtractor={(item, index) => {
|
||||
if (typeof item === 'string') {
|
||||
return `custom-${index}`;
|
||||
} else {
|
||||
return item.key;
|
||||
}
|
||||
}}
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
automaticallyAdjustContentInsets
|
||||
style={[styles.container, { backgroundColor: colors.background }]}
|
||||
style={[styles.root, { backgroundColor: colors.background }]}
|
||||
stickySectionHeadersEnabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -113,7 +207,13 @@ const SettingsBlockExplorer: React.FC = () => {
|
||||
export default SettingsBlockExplorer;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
paddingTop: 24,
|
||||
},
|
||||
rowHeight: {
|
||||
minHeight: 60,
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user