How to use PostMessage in a React Native webview? - reactjs

I'm trying to use the postmessage to a page opened in a webview inside a React Native App. I tried many times, but still wasn't able to send it.
I can listen to messages from the webpage normally. I just can't send anything back.
I'm currently using react-native-webview 11.6.5
export default function WebPage() {
const webviewRef = useRef();
const onMessage = (event) => {
//receive message from the web page. working here until here
const data = JSON.parse(event.nativeEvent.data);
//reply the message
webviewRef.current.postMessage(
JSON.stringify({reply: 'reply'}),
'*'
)
}
return <View>
<WebView
ref={webviewRef}
originWhitelist={['*']}
source={{ uri: 'https://robertnyman.com/html5/postMessage/postMessage.html' }}
domStorageEnabled
javaScriptEnabled
onMessage={onMessage}
/>
</View>
}
Any ideas what am I doing wrong?
UPDATE:
Thanks to #Ahmed Gaber help I was able to find this issue https://github.com/react-native-webview/react-native-webview/issues/809 and discover they're changing postMessage to injectJavaScript.
So I updated my code onMessage to the following:
const onMessage = (event) => {
const data = JSON.parse(event.nativeEvent.data);
//reply the message
webviewRef.current.injectJavaScript(
`window.postMessage(
{
reply: 'reply'
}
);`
)
}

To send data from app to webview, use injectedJavaScript
To send data from webview to app, use postMessage
To receive data in webview sent by postMessage, use onMessage
// This Js function will be injected into the web page after the document finishes loading.
// This function will Post a message to WebView.
const INJECTED_JAVASCRIPT = `(function() {
window.ReactNativeWebView.postMessage(JSON.stringify({key : "value"}));
})();`;
<WebView
source={{ uri: 'https://reactnative.dev' }}
injectedJavaScript={INJECTED_JAVASCRIPT}
onMessage={(event) => {
const data = JSON.parse(event.nativeEvent.data);
alert(data.key);
}}
/>;

React native code
function getInjectableJSMessage(message) {
return `
(function() {
document.dispatchEvent(new MessageEvent('message', {
data: ${JSON.stringify(message)}
}));
})();
`;
}
function sendDataToWebView() {
webviewRef.current?.injectJavaScript(
getInjectableJSMessage("Hello")
);
}
React web app code
React.useEffect(() => {
function handleEvent(message) {
console.log(message.data);
}
document.addEventListener("message", handleEvent);
return () =>
document.removeEventListener("message", handleEvent);
}, []);

Related

How do I implement a callback function between a web configured in reactjs with nextjs(WEB) and a react-native(RN) webview?

I am configuring a web composed of Reactjs as a webview in ReactNative.
I want to get a callback after a call from react to reactnative.
reference source from git
I implemented the desired callback in pure web, not in reactjs, referring to the above source.
However, since the same applies to React, the source is not applied.
Can I create the same function to do the webviewbridge of the above source in react?
If so, is there any source or link I can refer to?
For reference, ReactJS consists of NextJS.
Some of my sources are:
-nextjs folder source(pure js inject)
return (
<CustomLayout>
<Script src="/resources/js/webViewBridge.js"></Script>
<MyContent>
<CustomLayout>
MyContent.js(react)
const MyContent = () =>{
...
// ReactNative Webview Call Function(Common)
const sendRNMessage = (type:string, callback:any, errback:any) => {
if (window.ReactNativeWebView) {
// MOBILE CASE
window.counter++;
console.log(window.counter);
// *** webviewbridge send functiion call!!! ***
window.webViewBridge.send(type
, window.counter
, callback
, errback);
} else {
// NOT MOBILE CASE
alert(ERROR_TYPES.notMobile); // message : Not Mobile, Use Mobile
window.counter++;
console.log(window.counter);
}
};
const function1 = () =>{
//
sendRNMessage(MESSAGE_TYPE.get_items, (ok:any)=>{
alert('callback Success!')
alert(ok);
})}(err:any)=>{
alert(err);
})
...
return (
<>
<div>
<button
type="button"
onClick={(e) => {
function1(e);
}}
>
Function1 Call BUTTON
</button>
</div>
</>
);
};

Initialize / Eager Load React Component in Background on App Start for later usage

I'm writing a React App, where I have a Fallback component, which gets displayed when something goes wrong, for example: network is down, API isn't reachable, unknown route, etc.
This component will fetch some URLs of cat pictures and displays a slide show.
But of course this isn't possible when the network is down.
Though I'd like to somehow create and initialize this component in the background when the App starts, so everything is ready in the case of emergency.
Additional info: The Fallback component will be used as child component of different views. So it's not possible to simply mount it in App.jsx and use CSS visibility: hidden / visible to hide and display it.
Is this possible and does someone know how to do it?
EDIT: Example code
const Fallback = () => {
// will contain urls like:
// - https://cats.example.org/images/foo.jpg
// - https://cats.example.org/images/bar.png
// - https://cats.example.org/images/42.gif
const [urls, setUrls] = useState([]);
useEffect(() => {
fetch('https://catpictures.example.org')
.then(response => response.json())
.then(data => setUrls(data));
}, []);
// this should be cached somehow:
return (
<div>
{urls.map(url =>
<img src={url} />
}
</div>
);
}
You can do this and I've done it in big production apps by simply creating a new Image() and setting the src. The image will be preloaded when the component is first rendered.
const LoadingComponent() {
useEffect(() => {
const img = new Image();
img.src = imgUrl;
}, []);
return null; // doesn't matter if you display the image or not, the image has been preloaded
}
It could even become a hook such as useImagePreloader(src) but that's up to you.
Here is a Sandbox with a working version.
Steps to try it:
Create an incognito window, open devtools and check the network tab, search for "imgur". The image is loaded.
Set the network offline or disconnect from your WIFI.
Click on the Show Image button. The image will display correctly.
This solution will always work provided your cache settings for images are set correctly (usually they are). If not, you can save the images to blobs and get a URL to that blob, that will work 100% of times.
As you noted that you need an array of images, you can do that same code inside a loop and it will work just fine, images will still be cached:
const images = ['first.jpg', 'second.png', 'etc.gif'];
images.forEach(imageUrl => {
const img = new Image();
img.src = image
});
How about a service worker that would cache your assets and then serve them when offline? Then you could send a message with the URL to change to notify your "app" it is back online with some new content.
Thre is a working example here: https://serviceworke.rs/strategy-cache-update-and-refresh_demo.html
var CACHE = 'cache-update-and-refresh';
self.addEventListener('install', function(evt) {
console.log('The service worker is being installed.');
evt.waitUntil(caches.open(CACHE).then(function (cache) {
cache.addAll([
'./controlled.html',
'./asset'
]);
}));
});
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request);
});
}
function update(request) {
return caches.open(CACHE).then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response.clone()).then(function () {
return response;
});
});
});
}
function refresh(response) {
return self.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
var message = {
type: 'refresh',
url: response.url,
eTag: response.headers.get('ETag')
};
client.postMessage(JSON.stringify(message));
});
});
}
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
evt.respondWith(fromCache(evt.request));
evt.waitUntil(
update(evt.request)
.then(refresh)
);
});
Local Storage
On page/App load;
Get each image
Save base64 data to localStorage
On network fail
Render <FallBack />
<FallBack />
Read localStorage
Render base64 images
Small example
We use fetch to get the cat images in the <App/> component
Save those to the localStorage
(NOTE: StackSnippet doesn't allow localStorage, so please test it on JSFiddle)
We use a useState to 'fake' the network status
// Init
const { useState } = React;
// <Fallback />
const Fallback = () => {
// Get all localstorage items
let ls = { ...localStorage };
// Get all starting with 'image_'
ls = Object.keys(ls).filter(key => key.startsWith('image_'));
// Render images
return (
<div>
<p>{'FallBack'}</p>
{
(ls.length < 1)
? 'Unable to find cached images!'
: (
ls.map((key) => {
// Get current key from localstorage
const base64 = localStorage.getItem(key);
// Render image
return <img src={base64} />;
})
)
}
</div>
);
}
// <App />
const App = ({title}) => {
const [network, setNetwork] = useState(true);
const [urls, setUrls] = useState([ 'https://placekitten.com/200/300', 'https://placekitten.com/200/300']);
// Render Fallback on lost network
if (!network) {
return <Fallback />;
}
// While network is active, get the images
urls.forEach((url, index) => {
fetch(url)
.then(response => response.blob())
.then(blob => {
// Convert BLOB to base64
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
// Write base64 to localstorage
var base64data = reader.result;
localStorage.setItem('image_' + index, base64data);
console.log('Saving image ' + index + ' to localstorage');
};
});
})
return (
<div>
<p>{'App'}</p>
<p>Press me to turn of the internet</p>
<button onClick={() => setNetwork(false)}>{'Click me'}</button>
</div>
);
};
// Render <App />
ReactDOM.render(<App />, document.getElementById("root"));
JSFiddle Demo
Pros;
LocalStorage will not be cleared, if the same app is loaded the next day, we don't need to get those images again
Cons;
There's a size limit for LocalStorage
You can manually add a resource to the cache by using a preloaded <link>:
<link rel="preload" as="image" href="https://cats.example.org/images/foo.jpg">
Put this inside your index.html and use it from cache when needed by using the same href.

expo SMS.sendSMSAsync inside a loop pops sms modal and immediately closing

I'm trying to send a text message using expo-sms, the problem is when the sms modal is open to send the message - it immediately closing and the result of SMS.sendSMSAsync is cancelled.
my code is this:
<TouchableOpacity onPress={() => handleSend(contacts, text)}>{...}</TouchableOpacity>
async function handleSend(contacts, text) {
try {
const numbersArr = contacts.map(c => c.number)
while (numbersArr.length > 0) {
const number = numbersArr.pop()
const { result } = await SMS.sendSMSAsync(number, text)
// 'result' here is 'canceled' for each number
}
showMessagesSuccessfullySentMessage()
} catch (e) {
console.error(e)
showSmsSendingProblemError()
}
}
SMS sending is available, of course, I do the check in advanced using useEffect when the component is mounted:
useEffect(() => {
;(async function() {
const isAvailable = await SMS.isAvailableAsync()
setIsSmsAllowed(isAvailable)
})()
}, [])
what am I doing wrong? what did I miss?
Moreover, when I write a brand new app from scratch with the following code, everything works just perfect:
const numbersArr = ['1234', '12345', '123456', '1234567'] // dummy numbers here
export default function App() {
async function handleSend () {
const isAvailable = await SMS.isAvailableAsync()
if (isAvailable) {
while (numbersArr.length > 0) {
const n = numbersArr.pop()
const result = await SMS.sendSMSAsync(n, 'My sample HelloWorld message')
console.log('results', result)
}
} else {
// misfortune... there's no SMS available on this device
}
}
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
<Button title="Send" onPress={handleSend}/>
</View>
)
}
some technical data and versions:
tested on iPhone 11 Pro
"expo": "~36.0.0"
"expo-sms": "~8.0.0"
We don't have any known issues with Expo SMS at the moment. I wrote a small example Snack, you can see it here. I've tested the dialog on both Android and iOS.
If you think this is an issue with Expo, please open an issue at expo/expo.
the problem was that my selected dummy contacts was accidentally created with the wrong phone number field, eventually resulted with number=undefined being called on SMS.sendSMSAsync(undefined, 'My sample HelloWorld message')
Obviously I created those dummy contacts to avoid testing the app on my real contacts...
(sorry for bothering the network)
Opened an issue to expo to discuss the option to throw error or at least a warning in dev stage to warn for undefined/null value being passed to SMS.sendSMSAsync

How to receive message posted in the iframe to an ssr react app

am trying to post a message into the domain which is a server side rendering react app. after posting the message I cannot receive the message in the server side
Though I can able to post the message, in server side rendering I could not access the window. Is there any fix for this
const attributes = {
src: "demo.com",
width: "100%",
height: "175",
frameBorder: 1, // show frame border just for fun...
};
const postMessageData = "hello iframe";
const onReceiveMessage = () => {
console.log("onReceiveMessage");
};
const onReady = () => {
console.log("onReady");
};
return (
<IframeComm
attributes={attributes}
postMessageData={postMessageData}
handleReady={onReady}
handleReceiveMessage={onReceiveMessage}
/>
);
Parent App:
postMessageData = {Data}
I need to get this post message in the domain which am integrating using iframe

React Native - Set localStorage in WebView

I would like to set localStorage in the WebView component before loading the page.
https://facebook.github.io/react-native/docs/webview
My use case for this is that my RN app would like to open a page on it's accompanying website. The website authenticates on load by checking for a token in localStorage. If the token isn't there they will be prompted to login. Seen as the user has already logged in on the app I would prefer it if they could avoid logging in again in the WebView.
You might have overcome the issue. If not here is my solution.
You can use injectedJavaScript property of WebView and add javascript code to add the token on page load.
Example code snippet.
My Custom Javascript code that i need to inject.
let myInjectedJs = `(function(){ let tk = window.localStorage.getItem('tokenKey');
if(!tk || (tk && tk != '${token}')){
window.localStorage.setItem('tokenKey', '${token}');
window.location.reload();
}
})();`;
Code Explanation
Function is called as soon as loaded. Checks if tokenKey is already set. If not set we are setting it to the new token ${token} and reloading the page.
Note: We need to pass function as a string to injectedJavaScript in webview.
Using myInjectedJs in WebView.
<WebView
ref={webView => { this.refWeb = webView; }}
javaScriptEnabled={true}
injectedJavaScript={myInjectedJs}
...
Hope this solves your problem.
injectedJavaScript and window.ReactNativeWebView.postMessage should do the job.
import { WebView } from 'react-native-webview';
const INJECTED_JAVASCRIPT = `(function() {
const tokenLocalStorage = window.localStorage.getItem('token');
window.ReactNativeWebView.postMessage(tokenLocalStorage);
})();`;
export default function App() {
const onMessage = (payload) => {
console.log('payload', payload);
};
return (
<View style={styles.container}>
<StatusBar style="auto" />
<WebView
source={{ uri: 'https://somewebsite.com/login' }}
injectedJavaScript={INJECTED_JAVASCRIPT}
onMessage={onMessage}
/>
</View>
);
}
Payload data:
Object {
"nativeEvent": Object {
"canGoBack": true,
"canGoForward": false,
"data": "your_localstorage_value",
"loading": false,
"target": 3,
"title": "Coil",
"url": "https://somewebsite.com/home",
},
}
Works on both Android and iOS.

Resources