@talkjs/react-native
Version:
Official TalkJS SDK for React Native
181 lines (177 loc) • 6.84 kB
JavaScript
"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