mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-12 10:30:36 +01:00
Merge pull request #7084 from BlueWallet/blocxkex
FIX: Allow user to define a custom block explorer URL #7077
This commit is contained in:
commit
03a96b428b
16 changed files with 478 additions and 26 deletions
|
@ -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<boolean>} 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;
|
||||
|
|
|
@ -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<void>;
|
||||
isDrawerShouldHide: boolean;
|
||||
setIsDrawerShouldHide: (value: boolean) => void;
|
||||
selectedBlockExplorer: BlockExplorer;
|
||||
setBlockExplorerStorage: (explorer: BlockExplorer) => Promise<boolean>;
|
||||
}
|
||||
|
||||
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<SettingsContextType>(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<boolean>(false);
|
||||
|
||||
const [selectedBlockExplorer, setSelectedBlockExplorer] = useState<BlockExplorer>(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<boolean> => {
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
89
components/SettingsBlockExplorerCustomUrlListItem.tsx
Normal file
89
components/SettingsBlockExplorerCustomUrlListItem.tsx
Normal file
|
@ -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<TextInput>;
|
||||
}
|
||||
|
||||
const SettingsBlockExplorerCustomUrlItem: React.FC<SettingsBlockExplorerCustomUrlItemProps> = ({
|
||||
isCustomEnabled,
|
||||
onSwitchToggle,
|
||||
customUrl,
|
||||
onCustomUrlChange,
|
||||
onSubmitCustomUrl,
|
||||
inputRef,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{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, { color: colors.text }]}
|
||||
placeholderTextColor={colors.placeholderTextColor}
|
||||
textContentType="URL"
|
||||
clearButtonMode="while-editing"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
underlineColorAndroid="transparent"
|
||||
onSubmitEditing={onSubmitCustomUrl}
|
||||
editable={isCustomEnabled}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
|
@ -43,7 +43,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
const { navigate } = useExtendedNavigation<NavigationProps>();
|
||||
const menuRef = useRef<ToolTipMenuProps>();
|
||||
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<TransactionListItemProps> = 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) => {
|
||||
|
|
|
@ -31,6 +31,7 @@ export const BlueDefaultTheme = {
|
|||
outgoingForegroundColor: '#d0021b',
|
||||
successColor: '#37c0a1',
|
||||
failedColor: '#ff0000',
|
||||
placeholderTextColor: '#81868e',
|
||||
shadowColor: '#000000',
|
||||
inverseForegroundColor: '#ffffff',
|
||||
hdborderColor: '#68BBE1',
|
||||
|
|
|
@ -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}",
|
||||
|
|
79
models/blockExplorer.ts
Normal file
79
models/blockExplorer.ts
Normal file
|
@ -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<boolean> => {
|
||||
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<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;
|
||||
}
|
||||
};
|
|
@ -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)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="SettingsBlockExplorer"
|
||||
component={BlockExplorerSettingsComponent}
|
||||
options={navigationStyle({ title: loc.settings.block_explorer })(theme)}
|
||||
/>
|
||||
|
||||
<DetailViewStack.Screen name="About" component={AboutComponent} options={navigationStyle({ title: loc.settings.about })(theme)} />
|
||||
<DetailViewStack.Screen
|
||||
name="DefaultView"
|
||||
|
|
|
@ -54,6 +54,7 @@ export type DetailViewStackParamList = {
|
|||
About: undefined;
|
||||
DefaultView: undefined;
|
||||
ElectrumSettings: undefined;
|
||||
SettingsBlockExplorer: undefined;
|
||||
EncryptStorage: undefined;
|
||||
Language: undefined;
|
||||
LightningSettings: {
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { lazy, Suspense } from 'react';
|
|||
import Currency from '../screen/settings/Currency';
|
||||
import Language from '../screen/settings/Language';
|
||||
import { LazyLoadingIndicator } from './LazyLoadingIndicator'; // Assume you have this component for loading indication
|
||||
import SettingsBlockExplorer from '../screen/settings/SettingsBlockExplorer';
|
||||
|
||||
const Settings = lazy(() => import('../screen/settings/Settings'));
|
||||
const GeneralSettings = lazy(() => import('../screen/settings/GeneralSettings'));
|
||||
|
@ -46,6 +47,12 @@ export const NetworkSettingsComponent = () => (
|
|||
</Suspense>
|
||||
);
|
||||
|
||||
export const BlockExplorerSettingsComponent = () => (
|
||||
<Suspense fallback={<LazyLoadingIndicator />}>
|
||||
<SettingsBlockExplorer />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
export const AboutComponent = () => (
|
||||
<Suspense fallback={<LazyLoadingIndicator />}>
|
||||
<About />
|
||||
|
|
|
@ -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<string | undefined>();
|
||||
const { colors } = useTheme();
|
||||
const [broadcastResult, setBroadcastResult] = useState<string>(BROADCAST_RESULT.none);
|
||||
const { selectedBlockExplorer } = useSettings();
|
||||
|
||||
const stylesHooks = StyleSheet.create({
|
||||
input: {
|
||||
|
@ -158,13 +160,13 @@ const Broadcast: React.FC = () => {
|
|||
<BlueSpacing20 />
|
||||
</BlueCard>
|
||||
)}
|
||||
{BROADCAST_RESULT.success === broadcastResult && tx && <SuccessScreen tx={tx} />}
|
||||
{BROADCAST_RESULT.success === broadcastResult && tx && <SuccessScreen tx={tx} url={`${selectedBlockExplorer}/tx/${tx}`} />}
|
||||
</View>
|
||||
</SafeArea>
|
||||
);
|
||||
};
|
||||
|
||||
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 }) => {
|
|||
<BlueSpacing20 />
|
||||
<BlueTextCentered>{loc.settings.success_transaction_broadcasted}</BlueTextCentered>
|
||||
<BlueSpacing10 />
|
||||
<BlueButtonLink title={loc.settings.open_link_in_explorer} onPress={() => Linking.openURL(`https://mempool.space/tx/${tx}`)} />
|
||||
<BlueButtonLink title={loc.settings.open_link_in_explorer} onPress={() => Linking.openURL(url)} />
|
||||
</View>
|
||||
</BlueCard>
|
||||
</View>
|
||||
|
|
|
@ -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 = () => {
|
|||
<HandOffComponent
|
||||
title={loc.transactions.details_title}
|
||||
type={HandOffActivityType.ViewInBlockExplorer}
|
||||
url={`https://mempool.space/tx/${txid}`}
|
||||
url={`${selectedBlockExplorer}/tx/${txid}`}
|
||||
/>
|
||||
)}
|
||||
</SafeArea>
|
||||
|
|
|
@ -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 (
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets>
|
||||
<ListItem title={loc.settings.block_explorer} onPress={navigateToBlockExplorerSettings} testID="BlockExplorerSettings" chevron />
|
||||
<ListItem title={loc.settings.network_electrum} onPress={navigateToElectrumSettings} testID="ElectrumSettings" chevron />
|
||||
<ListItem title={loc.settings.lightning_settings} onPress={navigateToLightningSettings} testID="LightningSettings" chevron />
|
||||
{Notifications.isNotificationsCapable && (
|
||||
{isNotificationsCapable && (
|
||||
<ListItem
|
||||
title={loc.settings.notifications}
|
||||
onPress={() => navigate('NotificationSettings')}
|
||||
onPress={() => navigation.navigate('NotificationSettings')}
|
||||
testID="NotificationSettings"
|
||||
chevron
|
||||
/>
|
219
screen/settings/SettingsBlockExplorer.tsx
Normal file
219
screen/settings/SettingsBlockExplorer.tsx
Normal file
|
@ -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<BlockExplorerItem> {
|
||||
title?: string;
|
||||
data: BlockExplorerItem[];
|
||||
}
|
||||
|
||||
const SettingsBlockExplorer: React.FC = () => {
|
||||
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 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<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 (
|
||||
<View style={styles.container}>
|
||||
<Header leftText={title} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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.root, { backgroundColor: colors.background }]}
|
||||
stickySectionHeadersEnabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsBlockExplorer;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
paddingTop: 24,
|
||||
},
|
||||
rowHeight: {
|
||||
minHeight: 60,
|
||||
},
|
||||
});
|
|
@ -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<NavigationProps>();
|
||||
const { hash, walletID } = useRoute<RouteProps>().params;
|
||||
const { saveToDisk, txMetadata, counterpartyMetadata, wallets, getTransactions } = useStorage();
|
||||
const { selectedBlockExplorer } = useSettings();
|
||||
const [from, setFrom] = useState<string[]>([]);
|
||||
const [to, setTo] = useState<string[]>([]);
|
||||
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 = () => {
|
|||
<HandOffComponent
|
||||
title={loc.transactions.details_title}
|
||||
type={HandOffActivityType.ViewInBlockExplorer}
|
||||
url={`https://mempool.space/tx/${tx.hash}`}
|
||||
url={`${selectedBlockExplorer}/tx/${tx.hash}`}
|
||||
/>
|
||||
<BlueCard>
|
||||
<View>
|
||||
|
|
|
@ -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<TransactionStatusProps['navigation']>();
|
||||
const { colors } = useTheme();
|
||||
const wallet = useRef(wallets.find(w => w.getID() === walletID));
|
||||
const { selectedBlockExplorer } = useSettings();
|
||||
const fetchTxInterval = useRef<NodeJS.Timeout>();
|
||||
const stylesHook = StyleSheet.create({
|
||||
value: {
|
||||
|
@ -481,7 +483,7 @@ const TransactionStatus = () => {
|
|||
<HandOffComponent
|
||||
title={loc.transactions.details_title}
|
||||
type={HandOffActivityType.ViewInBlockExplorer}
|
||||
url={`https://mempool.space/tx/${tx.hash}`}
|
||||
url={`${selectedBlockExplorer}/tx/${tx.hash}`}
|
||||
/>
|
||||
|
||||
<View style={styles.container}>
|
||||
|
|
Loading…
Add table
Reference in a new issue