159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
import { action, observable, runInAction } from 'mobx';
|
|
import { Platform } from 'react-native';
|
|
import NetInfo, {
|
|
NetInfoState,
|
|
NetInfoSubscription
|
|
} from '@react-native-community/netinfo';
|
|
|
|
import SettingsStore from './SettingsStore';
|
|
|
|
const POLL_INTERVAL = 15000; // 15s
|
|
const VERIFY_TIMEOUT_MS = 5000;
|
|
const DEFAULT_REACHABILITY_HOST = 'mempool.space';
|
|
const PLATFORM_REACHABILITY_URL =
|
|
Platform.OS === 'ios'
|
|
? 'https://www.apple.com/library/test/success.html'
|
|
: 'https://www.google.com/generate_204';
|
|
const FALLBACK_REACHABILITY_URLS = [
|
|
'https://pay.zeusln.app/api/rates?storeId=Fjt7gLnGpg4UeBMFccLquy3GTTEz4cHU4PZMU63zqMBo',
|
|
PLATFORM_REACHABILITY_URL
|
|
];
|
|
|
|
export default class ConnectivityStore {
|
|
@observable public isOffline: boolean = false;
|
|
|
|
private netInfoUnsubscribe: NetInfoSubscription | null = null;
|
|
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
private verifyInFlight: boolean = false;
|
|
private reconnectCallbacks: Array<() => void> = [];
|
|
private settingsStore: SettingsStore;
|
|
|
|
constructor(settingsStore: SettingsStore) {
|
|
this.settingsStore = settingsStore;
|
|
}
|
|
|
|
private getReachabilityUrl = (): string => {
|
|
const { privacy } = this.settingsStore.settings;
|
|
const custom = privacy?.defaultBlockExplorer === 'Custom';
|
|
const host =
|
|
custom && privacy?.customBlockExplorer
|
|
? privacy.customBlockExplorer
|
|
: privacy?.defaultBlockExplorer || DEFAULT_REACHABILITY_HOST;
|
|
|
|
if (custom && host.indexOf('://') !== -1) {
|
|
const hostUrl = host.split('#')[0];
|
|
return `${hostUrl}/api/blocks/tip/height`;
|
|
}
|
|
|
|
return `https://${host}/api/blocks/tip/height`;
|
|
};
|
|
|
|
public onReconnect = (callback: () => void) => {
|
|
this.reconnectCallbacks.push(callback);
|
|
};
|
|
|
|
private probeUrl = async (url: string): Promise<boolean> => {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);
|
|
try {
|
|
await fetch(url, {
|
|
method: 'HEAD',
|
|
signal: controller.signal
|
|
});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
};
|
|
|
|
private verifyConnectivity = async (): Promise<boolean> => {
|
|
const urls = [this.getReachabilityUrl(), ...FALLBACK_REACHABILITY_URLS];
|
|
const results = await Promise.all(
|
|
urls.map((url) => this.probeUrl(url))
|
|
);
|
|
return results.some((ok) => ok);
|
|
};
|
|
|
|
/**
|
|
* Core logic: probe fallback URLs and update isOffline accordingly.
|
|
* Called by the poll interval and on NetInfo state changes.
|
|
*/
|
|
private check = () => {
|
|
if (this.verifyInFlight) return;
|
|
this.verifyInFlight = true;
|
|
this.verifyConnectivity().then((online) => {
|
|
this.verifyInFlight = false;
|
|
const wasOffline = this.isOffline;
|
|
runInAction(() => {
|
|
this.isOffline = !online;
|
|
});
|
|
if (online && wasOffline) {
|
|
this.reconnectCallbacks.forEach((cb) => cb());
|
|
}
|
|
});
|
|
};
|
|
|
|
private updateState = (state: NetInfoState) => {
|
|
// isConnected === false is a reliable native signal — mark immediately
|
|
if (state.isConnected === false) {
|
|
runInAction(() => {
|
|
this.isOffline = true;
|
|
});
|
|
return;
|
|
}
|
|
|
|
// isInternetReachable === true is reliable — mark online immediately
|
|
if (state.isInternetReachable === true) {
|
|
const wasOffline = this.isOffline;
|
|
runInAction(() => {
|
|
this.isOffline = false;
|
|
});
|
|
if (wasOffline) {
|
|
this.reconnectCallbacks.forEach((cb) => cb());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// null or false — verify ourselves with fallback probes
|
|
this.check();
|
|
};
|
|
|
|
@action
|
|
public start = () => {
|
|
if (this.netInfoUnsubscribe) return;
|
|
if (this.settingsStore.settings?.networking?.disableOfflineCheck)
|
|
return;
|
|
|
|
NetInfo.configure({
|
|
reachabilityUrl: this.getReachabilityUrl(),
|
|
reachabilityTest: async (response) => response.status === 200,
|
|
useNativeReachability: false
|
|
});
|
|
|
|
this.netInfoUnsubscribe = NetInfo.addEventListener(this.updateState);
|
|
this.pollInterval = setInterval(() => {
|
|
NetInfo.fetch().then(this.updateState);
|
|
}, POLL_INTERVAL);
|
|
};
|
|
|
|
@action
|
|
public stop = () => {
|
|
if (this.netInfoUnsubscribe) {
|
|
this.netInfoUnsubscribe();
|
|
this.netInfoUnsubscribe = null;
|
|
}
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval);
|
|
this.pollInterval = null;
|
|
}
|
|
};
|
|
|
|
@action
|
|
public reset = () => {
|
|
this.stop();
|
|
this.isOffline = false;
|
|
};
|
|
}
|