diff --git a/blue_modules/notifications.js b/blue_modules/notifications.js index dc97ad4db..3fbb66c96 100644 --- a/blue_modules/notifications.js +++ b/blue_modules/notifications.js @@ -31,8 +31,6 @@ function Notifications(props) { return false; }; - Notifications.isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android'; - /** * Calls `configure`, which tries to obtain push token, save it, and registers all associated with * notifications callbacks @@ -131,7 +129,7 @@ function Notifications(props) { * @returns {Promise} TRUE if permissions were obtained, FALSE otherwise */ Notifications.tryToObtainPermissions = async function (anchor) { - if (!Notifications.isNotificationsCapable) return false; + if (!isNotificationsCapable) return false; if (await Notifications.getPushToken()) { // we already have a token, no sense asking again, just configure pushes to register callbacks and we are done if (!alreadyConfigured) configureNotifications(); // no await so it executes in background while we return TRUE and use token @@ -441,4 +439,6 @@ function Notifications(props) { return null; } +export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android'; + export default Notifications; diff --git a/components/Context/SettingsProvider.tsx b/components/Context/SettingsProvider.tsx index 444e98c10..bbe01aa49 100644 --- a/components/Context/SettingsProvider.tsx +++ b/components/Context/SettingsProvider.tsx @@ -14,6 +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, getBlockExplorerUrl, saveBlockExplorer, BlockExplorer, normalizeUrl } from '../../models/blockExplorer'; // DefaultPreference and AsyncStorage get/set @@ -85,6 +86,8 @@ interface SettingsContextType { setTotalBalancePreferredUnitStorage: (unit: BitcoinUnit) => Promise; isDrawerShouldHide: boolean; setIsDrawerShouldHide: (value: boolean) => void; + selectedBlockExplorer: BlockExplorer; + setBlockExplorerStorage: (explorer: BlockExplorer) => Promise; } const defaultSettingsContext: SettingsContextType = { @@ -112,6 +115,8 @@ const defaultSettingsContext: SettingsContextType = { setTotalBalancePreferredUnitStorage: async (unit: BitcoinUnit) => {}, isDrawerShouldHide: false, setIsDrawerShouldHide: () => {}, + selectedBlockExplorer: BLOCK_EXPLORERS.default, + setBlockExplorerStorage: async (explorer: BlockExplorer) => false, }; export const SettingsContext = createContext(defaultSettingsContext); @@ -142,6 +147,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil // Toggle Drawer (for screens like Manage Wallets or ScanQRCode) const [isDrawerShouldHide, setIsDrawerShouldHide] = useState(false); + const [selectedBlockExplorer, setSelectedBlockExplorer] = useState(BLOCK_EXPLORERS.default); + const languageStorage = useAsyncStorage(STORAGE_KEY); const { walletsInitialized } = useStorage(); @@ -211,6 +218,18 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setTotalBalancePreferredUnitState(unit); }) .catch(error => console.error('Error fetching total balance preferred unit:', error)); + getBlockExplorerUrl() + .then(url => { + console.debug('SettingsContext blockExplorer:', 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)); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -295,6 +314,13 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setTotalBalancePreferredUnitState(unit); }, []); + const setBlockExplorerStorage = useCallback(async (explorer: BlockExplorer): Promise => { + const success = await saveBlockExplorer(explorer.url); + if (success) { + setSelectedBlockExplorer(explorer); + } + return success; + }, []); const value = useMemo( () => ({ preferredFiatCurrency, @@ -321,6 +347,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setTotalBalancePreferredUnitStorage, isDrawerShouldHide, setIsDrawerShouldHide, + selectedBlockExplorer, + setBlockExplorerStorage, }), [ preferredFiatCurrency, @@ -347,6 +375,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setTotalBalancePreferredUnitStorage, isDrawerShouldHide, setIsDrawerShouldHide, + selectedBlockExplorer, + setBlockExplorerStorage, ], ); diff --git a/components/SettingsBlockExplorerCustomUrlListItem.tsx b/components/SettingsBlockExplorerCustomUrlListItem.tsx new file mode 100644 index 000000000..15b04bbf7 --- /dev/null +++ b/components/SettingsBlockExplorerCustomUrlListItem.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { StyleSheet, TextInput, View, Switch } from 'react-native'; +import { ListItem } from '@rneui/themed'; +import { useTheme } from './themes'; +import loc from '../loc'; + +interface SettingsBlockExplorerCustomUrlItemProps { + isCustomEnabled: boolean; + onSwitchToggle: (value: boolean) => void; + customUrl: string; + onCustomUrlChange: (url: string) => void; + onSubmitCustomUrl: () => void; + inputRef?: React.RefObject; +} + +const SettingsBlockExplorerCustomUrlItem: React.FC = ({ + isCustomEnabled, + onSwitchToggle, + customUrl, + onCustomUrlChange, + onSubmitCustomUrl, + inputRef, +}) => { + const { colors } = useTheme(); + + return ( + <> + + + {loc.settings.block_explorer_preferred} + + + + + {isCustomEnabled && ( + + + + )} + + ); +}; + +export default SettingsBlockExplorerCustomUrlItem; + +const styles = StyleSheet.create({ + container: { + minHeight: 60, + paddingVertical: 10, + }, + title: { + fontSize: 16, + fontWeight: '500', + }, + uriContainer: { + flexDirection: 'row', + borderWidth: 1, + borderRadius: 4, + marginHorizontal: 15, + marginVertical: 10, + paddingHorizontal: 10, + alignItems: 'center', + }, + uriText: { + flex: 1, + minHeight: 36, + }, +}); diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index 67941cff0..1d5085e60 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -43,7 +43,7 @@ export const TransactionListItem: React.FC = React.mem const { navigate } = useExtendedNavigation(); const menuRef = useRef(); const { txMetadata, counterpartyMetadata, wallets } = useStorage(); - const { language } = useSettings(); + const { language, selectedBlockExplorer } = useSettings(); const containerStyle = useMemo( () => ({ backgroundColor: 'transparent', @@ -253,16 +253,16 @@ export const TransactionListItem: React.FC = React.mem const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]); const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]); const handleOnViewOnBlockExplorer = useCallback(() => { - const url = `https://mempool.space/tx/${item.hash}`; + const url = `${selectedBlockExplorer}/tx/${item.hash}`; Linking.canOpenURL(url).then(supported => { if (supported) { Linking.openURL(url); } }); - }, [item.hash]); + }, [item.hash, selectedBlockExplorer]); const handleCopyOpenInBlockExplorerPress = useCallback(() => { - Clipboard.setString(`https://mempool.space/tx/${item.hash}`); - }, [item.hash]); + Clipboard.setString(`${selectedBlockExplorer}/tx/${item.hash}`); + }, [item.hash, selectedBlockExplorer]); const onToolTipPress = useCallback( (id: any) => { diff --git a/components/themes.ts b/components/themes.ts index bb91e61bc..d285b35b8 100644 --- a/components/themes.ts +++ b/components/themes.ts @@ -31,6 +31,7 @@ export const BlueDefaultTheme = { outgoingForegroundColor: '#d0021b', successColor: '#37c0a1', failedColor: '#ff0000', + placeholderTextColor: '#81868e', shadowColor: '#000000', inverseForegroundColor: '#ffffff', hdborderColor: '#68BBE1', diff --git a/loc/en.json b/loc/en.json index 18135d7bb..10327e269 100644 --- a/loc/en.json +++ b/loc/en.json @@ -10,6 +10,7 @@ "never": "Never", "of": "{number} of {total}", "ok": "OK", + "enter_url": "Enter URL", "storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it.", "yes": "Yes", "no": "No", @@ -25,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", @@ -206,8 +208,10 @@ "performance_score": "Performance score: {num}", "run_performance_test": "Test performance", "about_selftest": "Run self-test", + "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", @@ -259,6 +263,9 @@ "encrypt_storage_explanation_description_line1": "Enabling Storage Encryption adds an extra layer of protection to your app by securing the way your data is stored on your device. This makes it harder for anyone to access your information without permission.", "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_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}", diff --git a/models/blockExplorer.ts b/models/blockExplorer.ts new file mode 100644 index 000000000..1b4a9cc94 --- /dev/null +++ b/models/blockExplorer.ts @@ -0,0 +1,79 @@ +// blockExplorer.ts +import DefaultPreference from 'react-native-default-preference'; + +export interface BlockExplorer { + key: string; + name: string; + url: string; +} + +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 getBlockExplorersList = (): BlockExplorer[] => { + return Object.values(BLOCK_EXPLORERS); +}; + +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 { + 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 => { + try { + 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 => { + 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 => { + 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; + } +}; diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 5d8e34b06..c9915756a 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -31,6 +31,7 @@ import AddWalletStack from './AddWalletStack'; import AztecoRedeemStackRoot from './AztecoRedeemStack'; import { AboutComponent, + BlockExplorerSettingsComponent, CurrencyComponent, DefaultViewComponent, ElectrumSettingsComponent, @@ -304,6 +305,12 @@ const DetailViewStackScreensStack = () => { component={NetworkSettingsComponent} options={navigationStyle({ title: loc.settings.network })(theme)} /> + + import('../screen/settings/Settings')); const GeneralSettings = lazy(() => import('../screen/settings/GeneralSettings')); @@ -46,6 +47,12 @@ export const NetworkSettingsComponent = () => ( ); +export const BlockExplorerSettingsComponent = () => ( + }> + + +); + export const AboutComponent = () => ( }> diff --git a/screen/send/Broadcast.tsx b/screen/send/Broadcast.tsx index cd39c947e..4b434ed47 100644 --- a/screen/send/Broadcast.tsx +++ b/screen/send/Broadcast.tsx @@ -23,6 +23,7 @@ import { useTheme } from '../../components/themes'; import { scanQrHelper } from '../../helpers/scan-qr'; import loc from '../../loc'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; +import { useSettings } from '../../hooks/context/useSettings'; const BROADCAST_RESULT = Object.freeze({ none: 'Input transaction hex', @@ -39,6 +40,7 @@ const Broadcast: React.FC = () => { const [txHex, setTxHex] = useState(); const { colors } = useTheme(); const [broadcastResult, setBroadcastResult] = useState(BROADCAST_RESULT.none); + const { selectedBlockExplorer } = useSettings(); const stylesHooks = StyleSheet.create({ input: { @@ -158,13 +160,13 @@ const Broadcast: React.FC = () => { )} - {BROADCAST_RESULT.success === broadcastResult && tx && } + {BROADCAST_RESULT.success === broadcastResult && tx && } ); }; -const SuccessScreen: React.FC<{ tx: string }> = ({ tx }) => { +const SuccessScreen: React.FC<{ tx: string; url: string }> = ({ tx, url }) => { if (!tx) { return null; } @@ -177,7 +179,7 @@ const SuccessScreen: React.FC<{ tx: string }> = ({ tx }) => { {loc.settings.success_transaction_broadcasted} - Linking.openURL(`https://mempool.space/tx/${tx}`)} /> + Linking.openURL(url)} /> diff --git a/screen/send/success.js b/screen/send/success.js index d8351bb04..6be333ca6 100644 --- a/screen/send/success.js +++ b/screen/send/success.js @@ -13,12 +13,14 @@ import loc from '../../loc'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import HandOffComponent from '../../components/HandOffComponent'; import { HandOffActivityType } from '../../components/types'; +import { useSettings } from '../../hooks/context/useSettings'; const Success = () => { const pop = () => { getParent().pop(); }; const { colors } = useTheme(); + const { selectedBlockExplorer } = useSettings(); const { getParent } = useNavigation(); const { amount, fee, amountUnit = BitcoinUnit.BTC, invoiceDescription = '', onDonePressed = pop, txid } = useRoute().params; const stylesHook = StyleSheet.create({ @@ -52,7 +54,7 @@ const Success = () => { )} diff --git a/screen/settings/NetworkSettings.js b/screen/settings/NetworkSettings.tsx similarity index 54% rename from screen/settings/NetworkSettings.js rename to screen/settings/NetworkSettings.tsx index b8bf06bab..40123ae9e 100644 --- a/screen/settings/NetworkSettings.js +++ b/screen/settings/NetworkSettings.tsx @@ -1,30 +1,34 @@ -import { useNavigation } from '@react-navigation/native'; import React from 'react'; import { ScrollView } from 'react-native'; - -import Notifications from '../../blue_modules/notifications'; +import { isNotificationsCapable } from '../../blue_modules/notifications'; import ListItem from '../../components/ListItem'; import loc from '../../loc'; +import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; -const NetworkSettings = () => { - const { navigate } = useNavigation(); +const NetworkSettings: React.FC = () => { + const navigation = useExtendedNavigation(); const navigateToElectrumSettings = () => { - navigate('ElectrumSettings'); + navigation.navigate('ElectrumSettings'); }; const navigateToLightningSettings = () => { - navigate('LightningSettings'); + navigation.navigate('LightningSettings'); + }; + + const navigateToBlockExplorerSettings = () => { + navigation.navigate('SettingsBlockExplorer'); }; return ( + - {Notifications.isNotificationsCapable && ( + {isNotificationsCapable && ( navigate('NotificationSettings')} + onPress={() => navigation.navigate('NotificationSettings')} testID="NotificationSettings" chevron /> diff --git a/screen/settings/SettingsBlockExplorer.tsx b/screen/settings/SettingsBlockExplorer.tsx new file mode 100644 index 000000000..4d1ef8558 --- /dev/null +++ b/screen/settings/SettingsBlockExplorer.tsx @@ -0,0 +1,219 @@ +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 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 SettingsBlockExplorerCustomUrlItem from '../../components/SettingsBlockExplorerCustomUrlListItem'; +import { Header } from '../../components/Header'; + +type BlockExplorerItem = BlockExplorer | string; + +interface SectionData extends SectionListData { + title?: string; + data: BlockExplorerItem[]; +} + +const SettingsBlockExplorer: React.FC = () => { + const { colors } = useTheme(); + const { selectedBlockExplorer, setBlockExplorerStorage } = useSettings(); + const customUrlInputRef = useRef(null); + const [customUrl, setCustomUrl] = useState(selectedBlockExplorer.key === 'custom' ? selectedBlockExplorer.url : ''); + const [isCustomEnabled, setIsCustomEnabled] = useState(selectedBlockExplorer.key === 'custom'); + const [isSubmitting, setIsSubmitting] = useState(false); + + const predefinedExplorers = getBlockExplorersList().filter(explorer => explorer.key !== 'custom'); + + const sections: SectionData[] = [ + { + title: loc._.suggested, + data: predefinedExplorers, + }, + { + title: loc.wallets.details_advanced, + data: ['custom'], + }, + ]; + + 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 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(); + setIsSubmitting(false); + return; + } + + const customExplorer: BlockExplorer = { + key: 'custom', + name: 'Custom', + url: customUrlNormalized, + }; + + const success = await setBlockExplorerStorage(customExplorer); + + if (success) { + triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + } else { + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ message: loc.settings.block_explorer_error_saving_custom }); + } + setIsSubmitting(false); + }, [customUrl, setBlockExplorerStorage, isSubmitting]); + + 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], + ); + + 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) => { + if (section.title === loc._.suggested) { + const explorer = item as BlockExplorer; + const isSelected = !isCustomEnabled && normalizeUrl(selectedBlockExplorer.url || '') === normalizeUrl(explorer.url || ''); + return ( + handleExplorerPress(explorer)} + checkmark={isSelected} + disabled={isCustomEnabled} + containerStyle={[{ backgroundColor: colors.background }, styles.rowHeight]} + /> + ); + } else { + return ( + + ); + } + }, + [ + 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 ( + +
+ + ); + } + return null; + }, []); + + return ( + + 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.root, { backgroundColor: colors.background }]} + stickySectionHeadersEnabled={false} + /> + ); +}; + +export default SettingsBlockExplorer; + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + container: { + paddingTop: 24, + }, + rowHeight: { + minHeight: 60, + }, +}); diff --git a/screen/transactions/TransactionDetails.tsx b/screen/transactions/TransactionDetails.tsx index 9374b519e..094907b3e 100644 --- a/screen/transactions/TransactionDetails.tsx +++ b/screen/transactions/TransactionDetails.tsx @@ -19,6 +19,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import { useStorage } from '../../hooks/context/useStorage'; import { HandOffActivityType } from '../../components/types'; +import { useSettings } from '../../hooks/context/useSettings'; const actionKeys = { CopyToClipboard: 'copyToClipboard', @@ -63,6 +64,7 @@ const TransactionDetails = () => { const { setOptions, navigate } = useExtendedNavigation(); const { hash, walletID } = useRoute().params; const { saveToDisk, txMetadata, counterpartyMetadata, wallets, getTransactions } = useStorage(); + const { selectedBlockExplorer } = useSettings(); const [from, setFrom] = useState([]); const [to, setTo] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -159,7 +161,7 @@ const TransactionDetails = () => { ); const handleOnOpenTransactionOnBlockExplorerTapped = () => { - const url = `https://mempool.space/tx/${tx?.hash}`; + const url = `${selectedBlockExplorer}/tx/${tx?.hash}`; Linking.canOpenURL(url) .then(supported => { if (supported) { @@ -184,7 +186,7 @@ const TransactionDetails = () => { }; const handleCopyPress = (stringToCopy: string) => { - Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx?.hash}`); + Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `${selectedBlockExplorer}/tx/${tx?.hash}`); }; if (isLoading || !tx) { @@ -255,7 +257,7 @@ const TransactionDetails = () => { diff --git a/screen/transactions/TransactionStatus.tsx b/screen/transactions/TransactionStatus.tsx index 5094c606a..8b45bb9d5 100644 --- a/screen/transactions/TransactionStatus.tsx +++ b/screen/transactions/TransactionStatus.tsx @@ -21,6 +21,7 @@ import { useStorage } from '../../hooks/context/useStorage'; import { HandOffActivityType } from '../../components/types'; import HeaderRightButton from '../../components/HeaderRightButton'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; +import { useSettings } from '../../hooks/context/useSettings'; enum ButtonStatus { Possible, @@ -97,6 +98,7 @@ const TransactionStatus = () => { const { navigate, setOptions, goBack } = useNavigation(); const { colors } = useTheme(); const wallet = useRef(wallets.find(w => w.getID() === walletID)); + const { selectedBlockExplorer } = useSettings(); const fetchTxInterval = useRef(); const stylesHook = StyleSheet.create({ value: { @@ -481,7 +483,7 @@ const TransactionStatus = () => {