MovixOpenSource/app/src/components/WebViewBrowser.tsx

142 lines
4.3 KiB
TypeScript

import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
} from 'react';
import { Linking, Platform } from 'react-native';
import { WebView, type WebViewNavigation } from 'react-native-webview';
import type {
WebViewErrorEvent,
WebViewMessageEvent,
ShouldStartLoadRequest,
} from 'react-native-webview/lib/WebViewTypes';
import { handleBridgeMessage } from '../services/bridge';
import { buildInjectedJavaScript } from '../injection/inject';
import { CONFIG } from '../config';
export interface WebViewBrowserRef {
goBack: () => void;
goForward: () => void;
reload: () => void;
loadUrl: (url: string) => void;
injectJavaScript: (script: string) => void;
}
interface WebViewBrowserProps {
url: string;
onNavigationStateChange?: (state: WebViewNavigation) => void;
onError?: (error: string) => void;
onLoadEnd?: () => void;
}
const injectedJS = buildInjectedJavaScript();
const WebViewBrowser = forwardRef<WebViewBrowserRef, WebViewBrowserProps>(
({ url, onNavigationStateChange, onError, onLoadEnd }, ref) => {
const webViewRef = useRef<WebView>(null);
useImperativeHandle(ref, () => ({
goBack: () => webViewRef.current?.goBack(),
goForward: () => webViewRef.current?.goForward(),
reload: () => webViewRef.current?.reload(),
loadUrl: (newUrl: string) => {
webViewRef.current?.injectJavaScript(
`window.location.href = ${JSON.stringify(newUrl)}; true;`,
);
},
injectJavaScript: (script: string) => {
webViewRef.current?.injectJavaScript(script);
},
}));
const onMessage = useCallback((event: WebViewMessageEvent) => {
handleBridgeMessage(event.nativeEvent.data, webViewRef);
}, []);
const onHttpError = useCallback(
(event: any) => {
onError?.(
`HTTP ${event.nativeEvent.statusCode}: ${event.nativeEvent.url}`,
);
},
[onError],
);
const onShouldStartLoadWithRequest = useCallback(
(request: ShouldStartLoadRequest) => {
const { url, navigationType } = request;
if (
url.startsWith('https://') ||
url.startsWith('http://') ||
url.startsWith('about:') ||
url.startsWith('blob:')
) {
return true;
}
// Ouvre uniquement les deep links déclenchés par un vrai clic utilisateur.
// Les redirections automatiques (pubs, iframes) sont silencieusement bloquées.
if (navigationType === 'click') {
Linking.openURL(url).catch(() => {});
}
return false;
},
[],
);
const onWebViewError = useCallback(
(event: WebViewErrorEvent) => {
onError?.(event.nativeEvent.description);
},
[onError],
);
const userAgent =
Platform.OS === 'ios' ? CONFIG.USER_AGENT_IOS : CONFIG.USER_AGENT;
return (
<WebView
ref={webViewRef}
source={{ uri: url }}
style={{ flex: 1, backgroundColor: '#0a0a0a' }}
// Injection du bridge + userscript avant le chargement
injectedJavaScriptBeforeContentLoaded={injectedJS}
// Réinjection après chaque navigation
injectedJavaScriptBeforeContentLoadedForMainFrameOnly={true}
// Bridge messages
onMessage={onMessage}
// Navigation
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onNavigationStateChange={onNavigationStateChange}
// Errors
onError={onWebViewError}
onHttpError={onHttpError}
onLoadEnd={onLoadEnd}
// Config
userAgent={userAgent}
javaScriptEnabled={true}
domStorageEnabled={true}
mediaPlaybackRequiresUserAction={false}
allowsInlineMediaPlayback={true}
allowsFullscreenVideo={true}
allowsBackForwardNavigationGestures={true}
// Sécurité
originWhitelist={['https://*', 'http://*', 'about:*', 'blob:*']}
mixedContentMode="compatibility"
// Cache
cacheEnabled={true}
// Désactive le zoom pour un rendu app-like
scalesPageToFit={true}
// Android
overScrollMode="never"
thirdPartyCookiesEnabled={true}
// iOS
sharedCookiesEnabled={true}
contentMode="mobile"
/>
);
},
);
WebViewBrowser.displayName = 'WebViewBrowser';
export default WebViewBrowser;