import { StatusBar } from 'expo-status-bar'; import { useEffect, useMemo, useRef, useState } from 'react'; import { BackHandler, Linking, Platform, StyleSheet, View } from 'react-native'; import type { WebViewMessageEvent } from 'react-native-webview'; import { WebView } from 'react-native-webview'; import { configureMobileHostBridgeNavigation, handleMobileHostBridgeMessage, MOBILE_HOST_CAPABILITIES, } from './src/mobileHostBridge'; import { buildMobileShellUrlFromDeepLink } from './src/mobileShellDeepLink'; import { resolveMobileShellExternalUrl, shouldOpenInMobileShellWebView, } from './src/mobileShellNavigation'; import { buildMobileShellUrl } from './src/mobileShellUrl'; const defaultWebUrl = 'http://127.0.0.1:3000/'; const hostVersion = '0.1.0'; function buildHostBridgeMessageScript(message: unknown) { return `window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify( JSON.stringify(message), )} })); true;`; } export default function App() { const webViewRef = useRef(null); const [canGoBack, setCanGoBack] = useState(false); const baseWebUrl = process.env.EXPO_PUBLIC_GENARRATIVE_WEB_URL || defaultWebUrl; const mobileShellUrlOptions = useMemo( () => ({ platform: Platform.OS === 'ios' ? 'ios' as const : 'android' as const, hostVersion, capabilities: MOBILE_HOST_CAPABILITIES, }), [], ); const [webUrl, setWebUrl] = useState(() => buildMobileShellUrl(baseWebUrl, mobileShellUrlOptions), ); const allowedWebOrigin = useMemo(() => new URL(webUrl).origin, [webUrl]); useEffect(() => { configureMobileHostBridgeNavigation({ allowedOrigin: allowedWebOrigin, openWebViewUrl(url) { setWebUrl(url); }, }); return () => configureMobileHostBridgeNavigation(null); }, [allowedWebOrigin]); useEffect(() => { let disposed = false; const openDeepLink = (url: string | null | undefined) => { const nextUrl = buildMobileShellUrlFromDeepLink( url, baseWebUrl, mobileShellUrlOptions, ); setWebUrl(nextUrl); }; void Linking.getInitialURL().then((url) => { if (!disposed) { openDeepLink(url); } }); const subscription = Linking.addEventListener('url', (event) => { openDeepLink(event.url); }); return () => { disposed = true; subscription.remove(); }; }, [baseWebUrl, mobileShellUrlOptions]); useEffect(() => { const subscription = BackHandler.addEventListener( 'hardwareBackPress', () => { if (!canGoBack) { return false; } webViewRef.current?.goBack(); return true; }, ); return () => subscription.remove(); }, [canGoBack]); const handleMessage = (event: WebViewMessageEvent) => { void handleMobileHostBridgeMessage(event.nativeEvent.data, (response) => { webViewRef.current?.injectJavaScript( buildHostBridgeMessageScript(response), ); }); }; const handleShouldStartLoad = (request: { url: string }) => { if (shouldOpenInMobileShellWebView(request.url, allowedWebOrigin)) { return true; } const externalUrl = resolveMobileShellExternalUrl(request.url); if (externalUrl) { void Linking.openURL(externalUrl).catch(() => undefined); } return false; }; return ( { setCanGoBack(event.canGoBack); webViewRef.current?.injectJavaScript( buildHostBridgeMessageScript({ bridge: 'GenarrativeHostBridge', version: 1, event: 'navigation.canGoBack', payload: { canGoBack: event.canGoBack, }, }), ); }} setSupportMultipleWindows={false} /> ); } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: '#fffdf9', }, });