← back to Beverlyhillsvideos App
components/BHVWebView.tsx
199 lines
import React, { useRef, useState, useCallback } from 'react';
import {
View,
StyleSheet,
ActivityIndicator,
Text,
TouchableOpacity,
Share,
Platform,
} from 'react-native';
import { WebView, WebViewNavigation } from 'react-native-webview';
import { Linking } from 'react-native';
import { Colors } from '@/constants/Colors';
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
const BASE_DOMAIN = 'beverlyhillsvideos.com';
const BASE_URL = `https://${BASE_DOMAIN}`;
function isInternalUrl(url: string): boolean {
try {
const parsed = new URL(url);
return (
parsed.hostname === BASE_DOMAIN ||
parsed.hostname === `www.${BASE_DOMAIN}`
);
} catch {
return false;
}
}
interface Props {
initialUrl: string;
shareTitle?: string;
}
export default function BHVWebView({ initialUrl, shareTitle }: Props) {
const webViewRef = useRef<WebView>(null);
const [loading, setLoading] = useState(true);
const [canGoBack, setCanGoBack] = useState(false);
const { isConnected } = useNetworkStatus();
const handleNavigationChange = useCallback((nav: WebViewNavigation) => {
setCanGoBack(nav.canGoBack);
const url = nav.url;
// External links: tel:, mailto:, off-domain — open in system browser
if (
url.startsWith('tel:') ||
url.startsWith('mailto:') ||
url.startsWith('sms:') ||
(!isInternalUrl(url) && !url.startsWith('about:') && !url.startsWith('data:'))
) {
// Prevent WebView navigation for external URLs
// We use onShouldStartLoadWithRequest for this (see below)
}
}, []);
const handleShouldStartLoad = useCallback(
({ url }: { url: string }): boolean => {
if (
url.startsWith('tel:') ||
url.startsWith('mailto:') ||
url.startsWith('sms:')
) {
Linking.openURL(url).catch(() => {});
return false;
}
if (!url.startsWith('about:') && !url.startsWith('data:') && !isInternalUrl(url)) {
Linking.openURL(url).catch(() => {});
return false;
}
return true;
},
[]
);
const handleShare = useCallback(async () => {
try {
await Share.share({
title: shareTitle ?? 'Beverly Hills Videos',
message: `${shareTitle ?? 'Beverly Hills Videos'} — ${BASE_URL}`,
url: BASE_URL,
});
} catch {
// user dismissed
}
}, [shareTitle]);
if (!isConnected) {
return (
<View style={styles.offlineContainer}>
<Text style={styles.offlineIcon}>wifi_off</Text>
<Text style={styles.offlineTitle}>No Connection</Text>
<Text style={styles.offlineBody}>
Connect to the internet and tap Retry to load Beverly Hills Videos.
</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={() => webViewRef.current?.reload()}
activeOpacity={0.8}
>
<Text style={styles.retryText}>Retry</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
{/* Share header button injected via parent layout, but we expose a method */}
<WebView
ref={webViewRef}
source={{ uri: initialUrl }}
style={styles.webview}
onLoadStart={() => setLoading(true)}
onLoadEnd={() => setLoading(false)}
onNavigationStateChange={handleNavigationChange}
onShouldStartLoadWithRequest={handleShouldStartLoad}
pullToRefreshEnabled
allowsBackForwardNavigationGestures={Platform.OS === 'ios'}
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false}
javaScriptEnabled
domStorageEnabled
startInLoadingState={false}
decelerationRate="normal"
originWhitelist={['https://*', 'http://*', 'tel:*', 'mailto:*']}
contentInsetAdjustmentBehavior="automatic"
injectedJavaScriptBeforeContentLoaded={`(function(){
var s=document.createElement('style');
s.innerHTML='.adsbygoogle,ins.adsbygoogle,.ad-wrap{display:none!important}';
(document.head||document.documentElement).appendChild(s);
window.adsbygoogle=window.adsbygoogle||[]; window.adsbygoogle.push=function(){};
})(); true;`}
/>
{loading && (
<View style={styles.loadingOverlay} pointerEvents="none">
<ActivityIndicator size="large" color={Colors.green} />
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.cream,
},
webview: {
flex: 1,
backgroundColor: Colors.cream,
},
loadingOverlay: {
...StyleSheet.absoluteFill,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: Colors.cream,
},
offlineContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: Colors.cream,
padding: 32,
},
offlineIcon: {
fontSize: 48,
marginBottom: 16,
color: Colors.textSecondary,
},
offlineTitle: {
fontSize: 22,
fontWeight: '600',
color: Colors.ink,
marginBottom: 8,
textAlign: 'center',
},
offlineBody: {
fontSize: 15,
color: Colors.textSecondary,
textAlign: 'center',
lineHeight: 22,
marginBottom: 32,
},
retryButton: {
backgroundColor: Colors.green,
paddingVertical: 14,
paddingHorizontal: 40,
borderRadius: 8,
},
retryText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
letterSpacing: 0.5,
},
});