UNPKG

@talkjs/react-native

Version:

Official TalkJS SDK for React Native

181 lines (177 loc) 6.84 kB
"use strict"; // The React import below may be reported as unused however it is required for // the Fragment returned. import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Keyboard, KeyboardAvoidingView, Linking, Platform } from 'react-native'; import { WebView as RNWebView } from 'react-native-webview'; import { CONTAINER, WEBVIEW_ERROR, TalkEvents, WEBVIEW_LOADED_MESSAGE } from './constants'; import { jsx as _jsx } from "react/jsx-runtime"; function getErrorHandlingScript() { return ` // Retrieve Iframe console.error messages const observer = new MutationObserver((mutationList) => { const iframe = mutationList[0].addedNodes[0]; if (iframe) { const innerConsole = iframe.contentWindow.console; const innerError = innerConsole.error.bind(innerConsole); innerConsole.error = (...args) => { innerError(...args); sendToReactNative('${WEBVIEW_ERROR}', args.join(' ')); }; } }); observer.observe(document.getElementById('${CONTAINER}'), { childList: true, subtree: true, }); // Retrieve console.error messages const error = window.console.error.bind(window.console); console.error = (...args) => { error(...args); sendToReactNative('${WEBVIEW_ERROR}', args.join(' ')); }; window.onerror = function (err) { sendToReactNative('${WEBVIEW_ERROR}', err); }; true; `; } function WebView(props, ref) { const isLoadedRef = useRef(false); const [isEnabled, setIsEnabled] = useState(true); const webViewMessageHandler = useCallback(webViewEvent => { const eventObj = JSON.parse(webViewEvent.nativeEvent.data); const event = eventObj.event; if (TalkEvents.includes(event)) { props.talkHandlers.process(eventObj.data); return; } props.customHandlers.process(event, eventObj.data); }, [props.talkHandlers, props.customHandlers]); useEffect(() => { let content = 'width=device-width, initial-scale=1.0'; if (props.disableZoom) { content += ', user-scalable=no'; } const injectJavascript = ` document.querySelector('meta[name="viewport"]').setAttribute("content", "${content}"); true; `; if (isLoadedRef.current) { ref.current.injectJavaScript(injectJavascript); } else { props.pendingScripts.push(injectJavascript); } }, [props.disableZoom]); const source = useMemo(() => { return { baseUrl: 'https://app.talkjs.com', html: ` <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script> window.Talk = {v: 3}; function sendToReactNative(event, data = {}) { const message = { data, event }; window.ReactNativeWebView.postMessage(JSON.stringify(message)); } </script> <script src="https://cdn.talkjs.com/talk.js"></script> </head> <body style="margin: 0px;"> <div> <div id='${CONTAINER}' style="width: 100%; height: 100%"> </div> </div> </body> </html> ` }; }, []); const onShouldStartLoadWithRequest = useCallback(request => { if (Platform.OS === 'ios' && request.navigationType === 'click') { Linking.openURL(request.url); // Open link in separate Safari window. return false; // Stops webview from loading the clicked link. } return true; }, []); const originWhitelist = useMemo(() => ['*'], []); const webViewStyle = useMemo(() => ({ backgroundColor: 'transparent' }), []); const keyboardStyle = useMemo(() => ({ display: 'flex', flex: 1, position: 'absolute', top: 0, left: 0, width: props.isHeadless ? 0 : '100%', height: props.isHeadless ? 0 : '100%', opacity: props.isHeadless ? 0 : 1, zIndex: 2 }), [props.isHeadless]); // (2024 Update): So initial assumption was that this function gets called once // after DOMContentLoaded has been triggered. However, I discovered that on 32 bit // Android, `onPageFinished` and subsequently this gets triggered twice. const onLoad = useCallback(e => { if (e.nativeEvent.loading === false) { isLoadedRef.current = true; const _ref = ref.current; if (__DEV__) { // Add WebView errors handler props.customHandlers.add(WEBVIEW_ERROR, data => console.error(data)); _ref.injectJavaScript(getErrorHandlingScript()); } props.pendingScripts.forEach(script => _ref.injectJavaScript(script)); _ref.injectJavaScript(`sendToReactNative('${WEBVIEW_LOADED_MESSAGE}'); true;`); } }, [props.customHandlers]); useLayoutEffect(() => { // (April 2025): Android 15 enforces edge-to-edge by default for apps targeting // API level 35 and above. This introduces issues where the MessageField gets // covered by the keyboard. `KeyboardAvoidingView` fixes this but adds an // extra padding when the keyboard is closed. To fix this, we disable `KeyboardAvoidingView` // when the keyboard is closed and vice versa. // See: https://github.com/facebook/react-native/issues/29614#issuecomment-1567199087 const keyboardDidHideSubscription = Keyboard.addListener('keyboardDidHide', () => { if (Platform.OS === 'android') setIsEnabled(false); }); const keyboardDidShowSubscription = Keyboard.addListener('keyboardDidShow', () => { if (Platform.OS === 'android') setIsEnabled(true); }); return () => { keyboardDidShowSubscription.remove(); keyboardDidHideSubscription.remove(); }; }, []); return /*#__PURE__*/_jsx(KeyboardAvoidingView, { style: keyboardStyle, behavior: Platform.OS === 'ios' ? 'padding' : 'height', enabled: isEnabled, keyboardVerticalOffset: props.keyboardVerticalOffset, children: /*#__PURE__*/_jsx(RNWebView, { style: webViewStyle, androidLayerType: 'hardware', ref: ref, allowsInlineMediaPlayback: true, source: source, bounces: false, onMessage: webViewMessageHandler, onShouldStartLoadWithRequest: onShouldStartLoadWithRequest, onLoad: onLoad, hideKeyboardAccessoryView: props.hideKeyboardAccessoryView, originWhitelist: originWhitelist, javaScriptEnabled: true, domStorageEnabled: true, sharedCookiesEnabled: true, mediaCapturePermissionGrantType: 'grantIfSameHostElseDeny', webviewDebuggingEnabled: __DEV__, applicationNameForUserAgent: 'TalkJS_React_Native/0.16.1', scrollEnabled: false }) }); } const forwardedWebView = /*#__PURE__*/forwardRef(WebView); export { forwardedWebView as WebView }; //# sourceMappingURL=WebView.js.map