This commit is contained in:
Marcos Rodriguez Velez 2024-09-26 02:36:51 -04:00
parent 7ab10db864
commit c675bf290b
7 changed files with 306 additions and 200 deletions

View File

@ -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,

View File

@ -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;

View File

@ -31,6 +31,7 @@ export const BlueDefaultTheme = {
outgoingForegroundColor: '#d0021b',
successColor: '#37c0a1',
failedColor: '#ff0000',
placeholderTextColor: '#81868e',
shadowColor: '#000000',
inverseForegroundColor: '#ffffff',
hdborderColor: '#68BBE1',

View File

@ -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}",

View File

@ -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;
}
};

View File

@ -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: {

View File

@ -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,
},
});