Receiving message from injected JavaScript in a React Native WebView - javascript

In my React Native App, I'm using a WebView to display a google ad (AdSense) by using the "injectedJavascript" prop. The problem is I can't know the height of the ad in advance. So I give it a random height at first and when its style is updated, I plan to set its height correctly.
I assume I have to get the height in the injected JS code, and then use the "window.postMessage()" method to send it to the WebView through the "onMessage" prop.
MutationObserver combined with promises seem very appropriate for this case. For now, I'd like to just receive the message from the webview. So this is my code right now but no message is sent :
export default class App extends Component {
constructor(props) {
super(props);
}
_onMessage(e) {
console.warn(e.nativeEvent.data);
}
render() {
const jsCode = `
window._googCsa('ads', pageOptions, adblock1);
function waitForAdSense(id) {
var config = {
attributes: true,
attributeFilter: ['style'],
};
return new Promise((resolve, reject) => {
var adSenseElement = document.body.getElementById(id);
if (adSenseElement.style) {
resolve(adSenseElement.style.height);
return;
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'style') {
observer.disconnect();
resolve(adSenseElement);
return;
}
});
});
observer.observe(adSenseElement, config);
});
}
waitForAdSense('afscontainer1').then(height => {
window.postMessage(height, '*');
});
`;
return (
<ScrollView>
<WebView
key={'AdSense'}
ref={'webview2'}
style={{ height: 300 }}
source={{
uri: isAndroid
? 'file:///android_asset/widget/adSense.html'
: './widget/index.html',
}}
javaScriptEnabled={true}
mixedContentMode="compatibility"
injectedJavaScript={jsCode}
scrollEnabled={false}
domStorageEnabled={true}
onMessage={this._onMessage}
scalesPageToFit
startInLoadingState={true}
automaticallyAdjustContentInsets={false}
/>
;
</ScrollView>
);
}
}
Though, I can make it work with this code but setTimeout is not the best solution :
window._googCsa('ads', pageOptions, adblock1);
var adSenseContainer = document.getElementById("afscontainer1");
setTimeout(function(){ window.postMessage(adSenseContainer.style.height, '*'); },1000);
Do you have any ideas ? I think my function waitForAdSense() might be bugged somehow. Thanks in advance !

The best solution would be to use AdMob instead of AdSense on a React Native mobile application. It is somewhat pointless to mess around with these issues as the AdSense javascript wasn't made with this use case in mind. Here is a library made to easily integrate AdMob within your app.

I managed to make it work with this changes in the injected code :
window._googCsa('ads', pageOptions, adblock1);
function waitForAdSense() {
var config = {
attributes: true,
attributeFilter: ["style"]
};
var adSenseContainer = document.getElementById("afscontainer1");
return new Promise((resolve, reject) => {
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.attributeName === 'style' && adSenseContainer.style.height) {
var height = adSenseContainer.style.height;
resolve(height);
observer.disconnect();
return;
};
});
});
observer.observe(adSenseContainer, config);
});
};
waitForAdSense().then(height => {
window.postMessage(height, '*');
});
Thanks for the advices !

Related

What does this getFirebaseRoot error occur on InterstitialAd?

Creating a react-native app and trying to ad ads through admob and firebase, but getting an error I couldn't find much on.
Relevant parts of my App.js:
...
import {
InterstitialAd,
TestIds,
AdEventType,
} from '#react-native-firebase/admob';
...
showAds = () => {
let interstitial = InterstitialAd.createForAdRequest(TestIds.INTERSTITIAL);
let interstitialListener = interstitial.onAdEvent(type => {
if (type === AdEventType.LOADED) {
interstitial.show();
}
});
interstitial.load();
return () => {
interstitialListener = null;
};
};
onEvent = e => {
if (e.type === 'game-over') {
this.setState({
running: false,
});
this.showAds();
};
UPDATE:
Following this guide instead but getting another error.
Yeah sorry to tell you but #react-native-firebase/admob is now deprecated The last supported version was 11.5.0. I wasted time with this too because this old website (https://rnfb-docs.netlify.app) says it exist. When really you should use this one (https://rnfirebase.io). What I did was used (https://www.applovin.com/) for ads it was really easy to setup.

ReactJS: Data not binding from continuous response

While integrating the aws ivs streaming channel with quiz related metadata, at that time getting the console.log of the metadata records and while passing those records into another component it is not handling any how.
A playground that i have created into codesandobx
PlayerComponent
function PlayerComponent(options) {
useEffect(() => {
const script = document.createElement("script");
script.src = "https://player.live-video.net/1.0.0/amazon-ivs-player.min.js";
script.async = true;
document.body.appendChild(script);
script.onload = (IVSPlayer) => {
if (IVSPlayer.isPlayerSupported) {
const player = IVSPlayer.create();
player.attachHTMLVideoElement(document.getElementById("video-player"));
player.load(
"https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.xhP3ExfcX8ON.m3u8"
);
player.play();
player.addEventListener(
IVSPlayer.PlayerEventType.TEXT_METADATA_CUE,
(cue) => {
const metadataText = cue.text;
setMetaData(metadataText);
console.log("PlayerEvent - METADATA: ", metadataText);
}
);
}
};
return () => {
document.body.removeChild(script);
};
}, []);
return (
<div ref={divEl}>
<video id="video-player" ref={videoEl} autoPlay controls></video>
{metaData !== undefined ? <QuizComponent metadata={metaData} /> : ""}
</div>
);
}
On QuizComponent would like to render those metadata
export default function QuizComponent(props) {
const questionData = props;
return (
<React.Fragment>
<h2>{questionData.metadata.question}</h2>
</React.Fragment>
);
}
But any how not able to render the data into component.
Ref example of what I am going to implement.
https://codepen.io/amazon-ivs/pen/XWmjEKN?editors=0011
I found the problem. Basically you are referring IVSPlayer as if it was the argument of the arrow function you passed to script onload, while the argument instead is an event (the onload event).
Solution: const {IVSPlayer} = window;. Infact docs say
Once amazon-ivs-player.min.js is loaded, it adds an IVSPlayer variable to the global context.
Docs also explain how to setup with NPM which you may be interested in.
I updated my playground here.
I also suggest you to edit the version of the player as the last one is 1.2.0.

react native webview postmessage doesn't trigger onmessage

React Native Version : 0.60
Library used : react-native-webview (version : 9.0.1)
Issue :
When I send postMessage (this.webView.postMessage('somedata')) from my react-native event handler to webview, the onMessage is not triggered. But onMessage does get triggered on events sent by the webview.
Solutions tried :
Added both document.eventListener and window.eventListener. Still the onMessage is NOT trigged when I postMessage from react-native.
Any help would be greatly appreciated.
Code :
// On Click, the following function will run
_sendPostMessage = () => {
//data sent to webview
this.webView.postMessage("getAuthToken");
}
// Inside render() function
<WebView
ref={(webView) => this.webView = webView}
style={styles.webView}
source={{ uri: event.ticket_form_url, headers: headers }}
injectedJavaScript={jsCode}
onMessage={this.onMessage}
renderLoading={this._renderActivityIndicator}
startInLoadingState={true}
/>
// Injected javscript code below
var jsCode = `
(function() {
window.postMessage = function(data) {
window.ReactNativeWebView.postMessage(data);
};
})()
(function() {
var originalPostMessage = window.postMessage;
var patchedPostMessage = function(message, targetOrigin, transfer) {
originalPostMessage(message, targetOrigin, transfer);
};
patchedPostMessage.toString = function() {
return String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
};
window.postMessage = patchedPostMessage;
})();
window.addEventListener("message", function(event) {
var uat = ''
if (window.userAuthenticationToken) {
uat = window.userAuthenticationToken()
}
window.postMessage('setAuthToken,' + uat)
});
document.addEventListener("message", function(event) {
var uat = ''
if (window.userAuthenticationToken) {
uat = window.userAuthenticationToken()
}
window.postMessage('setAuthToken,' + uat)
});
`;
// onMessage
onMessage = (event) => {
//data received from webview
console.log('Reached onMessage', event.nativeEvent.data);
var response = event.nativeEvent.data
var data = response.split(',')
}
Hello you must use post message as injectedJavascript. For example,
<WebView
source={{ uri:'https://stackoverflow.com' }}
injectedJavaScript="window.ReactNativeWebView.postMessage(document.title)"
onMessage={event => setTitle(event.nativeEvent.data)}
/>
You can use this package for webview for beauty UI.
https://github.com/ilkerkesici/react-native-beauty-webview

Decode QR Code Image from Camera Roll React Native

I'm trying to implement a feature in my app where I the user can select a picture from their camera roll and the app will decode a QR code in the image is detected.
I'm currently using react-native-camera-roll-picker: https://github.com/jeanpan/react-native-camera-roll-picker
and react-native-qrcode-local-image: https://github.com/remobile/react-native-qrcode-local-image
The problem is the local QR code image library wants me to pass a local path and isn't compatible with the native uri provided by react-native-camera-roll-picker. I would use another library for decoding the image QR code but this one appears to be the only one that works on iOS and Android and scans from existing images rather than from the actual camera.
I've also tried implementing react-native-fetch-blob in order to temporarily save the camera roll image locally, but that's been giving me trouble as well: https://github.com/wkh237/react-native-fetch-blob
This is my current attempt in a function that I call in the "callback" prop for react-native-camera-roll-picker (with previous attempts commented out):
_pickedImage(array,currentImg) {
console.log(currentImg)
var path = RNFS.DocumentDirectoryPath + '/pickedqr';
let rnfbURI = RNFetchBlob.wrap(RNFetchBlob.fs.asset(currentImg.uri))
const Blob = RNFetchBlob.polyfill.Blob
Blob.build(rnfbURI, {type:'image/jpg'}).then((b) => {
tmpBlob = b;
RNFetchBlob.fs.readFile(tmpBlob, 'base64').then((data) => {
console.log("Base64", data)
QRDecoder.decode(`data:image/gif;base64,${data}`, (error, result)=>{
console.log("Code", result)
console.log("Error", error)
});
});
})
/*fullPath = currentImg.uri.replace("assets-library://", "cdvfile://localhost/assets-library/")
QRDecoder.decode(fullPath, (error, result)=>{
console.log("Code", result)
console.log("Error", error)
});*/
/*let blb = Blob.build( rnfbURI, { type: 'image/jpg'})
console.log(blb)*/
/*RNFetchBlob.fs.readFile(rnfbURI, 'base64').then((data) => {
console.log("Base64", data)
QRDecoder.decode(`data:image/gif;base64,${data}`, (error, result)=>{
console.log("Code", result)
console.log("Error", error)
});
})*/
}
I'm at a total loss at the moment so any methods or insight would be much appreciated.
you can use react-native-qrcode-scanner to scan QR from images or directly through the camera.
INSTALLATION:
install dependency by using this command:
yarn add react-native-camera react-native-qr-scanner
link those libraries by using:
react-native link react-native-camera && react-native-qr-scanner
you need to add the permission to your AndroidManifest.xml of your project. This should be found in your android/app/src/main/AndroidManifest.xml Add the following:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE"/>
With iOS 10 and higher, you need to add the "Privacy - Camera Usage Description" key to the info.plist of your project. This should be found in your_project/ios/your_project/Info.plist. Add the following code:
<key>NSCameraUsageDescription</key>
<string/>
<key>NSPhotoLibraryUsageDescription</key>
<string/>
<key>NSMicrophoneUsageDescription</key>
<string/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string/>
Usage:
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, TouchableOpacity} from 'react-native';
import {QRreader} from 'react-native-qr-scanner';
import ImagePicker from 'react-native-image-picker';
export default class Scanner extends Component {
constructor(props) {
super(props);
this.state = {
reader: {
message: null,
data: null
}
};
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity onPress={()=>{
this.openPhoto();
}}>
<Text style={{marginTop: 20}}>打开相册识别二维码</Text>
</TouchableOpacity>
<View>
{!this.state.reader? <Text>{!this.state.reader.message?'':`${this.state.reader.message}`}</Text>: <Text>{!this.state.reader.message?'':`${this.state.reader.message}:${this.state.reader.data}`}</Text>}
</View>
</View>
);
}
openPhoto(){
console.log('ImagePicker');
ImagePicker.launchImageLibrary({}, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled image picker');
}
else if (response.error) {
console.log('ImagePicker Error: ', response.error);
}
else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
}
else {
if(response.uri){
var path = response.path;
if(!path){
path = response.uri;
}
QRreader(path).then((data)=>{
this.setState({reader: {
message: '识别成功',
data: data
}});
// 十秒后自动清空
setTimeout(() => {
this.setState({reader: {
message: null,
data: null
}})
}, 10000);
}).catch((err)=>{
this.setState({reader: {
message: '识别失败',
data: null
}});
});
}
}
});
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff'
}
});
you can read more about this library here: https://www.npmjs.com/package/react-native-qr-scanner
You can use react-native-camera to solve this issue easily.
Here's the simple code snippet for that.
handleCodeDetected = (data) => {
// do whatever you want to do with data
}
...
...
<RNCamera
type={RNCamera.Constants.Type.back}
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
onBarCodeRead={this.handleCodeDetected}
style={styles.preview}
/>
This answer solved it for me.
Created native method to covert uri in path, here is my code:
#ReactMethod
public void getRealPathFromURI(String contentUriString, Promise promise) {
Uri contentUri = Uri.parse(contentUriString);
Cursor cursor = null;
try {
String wholeID = DocumentsContract.getDocumentId(contentUri);
String id = wholeID.split(":")[1];
String[] column = { MediaStore.Images.Media.DATA };
String sel = MediaStore.Images.Media._ID + "=?";
cursor = reactContext.getContentResolver().
query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
column, sel, new String[]{ id }, null);
int columnIndex = cursor.getColumnIndex(column[0]);
String filePath = "";
if (cursor.moveToFirst()) {
filePath = cursor.getString(columnIndex);
}
promise.resolve(filePath);
} catch (Throwable e) {
promise.reject(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
And js:
DocumentPicker.pick({ type }).then((document: DocumentPickerResponse) => {
MyNativeModule.getRealPathFromURI(document.uri).then((path) => {
QRCodeLocalImage.decode(path, (err, res) => {
callback(err, { qrData: res });
});
});
});
You can just use react-native-rn-zxing:
npm i react-native-rn-zxing
then link it :
react-native link react-native-rn-zxing
Usage :
import RnZxing from 'react-native-rn-zxing';
...
// Pass the callback as a parameter
RnZxing.showQrReader(this.onBarcodeScanned);
...
// Define the callback function to handle data after scan
onBarcodeScanned = (data) => {
this.setState({data: data});
};
You can use rn-qr-generator
It can detect QR code in image
RNQRGenerator.detect({
uri: PATH_TO_IMAGE, // local path of the image. Can be skipped if base64 is passed.
base64: imageBase64String, // If uri is passed this option will be skipped.
})
.then(response => {
const { values } = response; // Array of detected QR code values. Empty if nothing found.
})

How to add a loading indicator on every Relay.js network request

I'm currently building a Relay/React web app. It's been amazingly simple with one exception. I've not been able to figure out how I can get a global notification when any of my components are making network requests. I'm hopping to add a spinner to the top of my app when ever there's network activity, because some of my mutations take a long time to load.
This was my attempt at solving this problem, but it only works on new route page loads.
function renderer(info)
{
let {props, error, element} = info;
if (error) {
return (<ServerError errors={error}/>);
} else {
if (props) {
return React.cloneElement(element, props);
} else {
return (<Loading />);
}
}
}
ReactDOM.render(
<Router
history={browserHistory}
render={applyRouterMiddleware(useRelay)}
environment={Relay.Store}
>
<Route
path="/"
queries={ViewerQuery}
component={App}
>
<IndexRoute
queries={ViewerQuery}
component={Libraries}
render={renderer}
/>
<Route path="*" component={Error}/>
</Route>
</Router>
Ideally I could get some callback that I can pass to my App component, which renders all my pages headers and footers. Any help with this would be greatly appreciated. I've been all over the internet for a while trying to find a good solution to this.
You could also try adding a custom Relay Network Layer that renders the loading indicator component on every request. I think the main concern to take into account for the "global" loading indicator is about design not only of the indicator but also about its impact on the UI globally. Is it going to block the UI?, just one part of it?, will it displace the other elements up/down?, etc.
In the meantime, you can do something like:
handleSubmit(event) {
event.preventDefault();
this.setState({isLoading: true});
this.props.relay.commitUpdate(
new LoginMutation({
email: this.refs.email.value,
password: this.refs.password.value
}), {
onSuccess: response => {
Auth.login(response.login.accessToken);
const { location, router } = this.props;
if (location.state && location.state.nextPathname) {
router.replace(location.state.nextPathname)
} else {
router.replace('/')
}
},
onFailure: transaction => {
this.setState({hasError: true});
this.setState({isLoading: false});
}
}
);
}
The above is snippet is taken from here. You can see the rest of the logic in that repo.
You can create a custom spinner component in react and based on your data load status you can either show or hide the spinner.
An example for this can be -
Your Spinner component could be like this -
let SpinMe
= (
<div className="spinner-container">
<div className="loader">
<svg className="circular">
<circle className = "path"
cx = "50"
cy = "50"
r = "20"
fill = "none"
strokeWidth = "3"
strokeMiterLimit = "10"
/>
</svg>
</div>
</div>
);
This spinner component should have somewhat higher z-index than other component so that while loading is occurring user can not click other components or interact with other components.
Also in styling show some transparent dark background of spinner.
e.g
.spinner-container {
position : absolute;
background-color : rgba(12, 12, 12, 0.61);
width : 100%;
min-height : 100%;
background-size : 100% 100%;
text-align : center;
z-index : 3;
top : 0;
left : 0;
}
Now Your another component where you want to use the spinner and in this component you want to make network request.
import React from 'react';
class SpinTestComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading:false
};
}
sendNetworkRequest(URL, dataToSend) {
return $.ajax({
type : 'POST',
url : URL,
data : JSON.stringify(dataToSend),
dataType : 'json'
});
}
componentDidMount() {
const URL = "test url";
const dataToSend = { param1:"val", param2:"val" };
this.setState({isLoading:true});
this.sendNetworkRequest(dataToSend)
.then(
() => {
// success response now remove spinner
this.setState({isLoading:false});
},
() => {
// error response again remove spinner and
// show error message to end user
this.setState({isLoading:false});
});
}
render() {
return (
<div>
{ this.state.isLoading ? <SpinMe/> : null }
<div>
<h1>
Remaining Component structure
or Jsx
</h1>
<p>
To be show after loading is done.
</p>
</div>
</div>
);
}
}
export default SpinTestComponent;
I ended up extending Relay's default network layer so that I could make some Flux events that my main root component listens to. This allows me to have all my loading and error message handling in one place if I so desire.
Here's my final solution. Not sure that it's the cleanest it could be, but it works quite well.
import Relay from "react-relay";
import RelayMutationRequest from "react-relay/lib/RelayMutationRequest";
import AppDispatcher from "./AppDispatcher";
export default class CustomNetworkLayer extends Relay.DefaultNetworkLayer {
constructor(uri, init)
{
super(uri, init);
}
networkLoadingEvent()
{
AppDispatcher.dispatch({
actionType: 'LOADING'
});
}
networkDoneLoadingEvent()
{
AppDispatcher.dispatch({
actionType: 'DONE_LOADING'
});
}
networkErrorEvent(error)
{
AppDispatcher.dispatch({
actionType: 'ERROR',
payload: error
});
}
sendQueries(requests)
{
this.networkLoadingEvent();
return Promise.all(requests.map(request => (
this._sendQuery(request).then(
result => result.json()
).then(payload =>
{
if (payload.hasOwnProperty('errors')) {
const error = CustomNetworkLayer.createRequestError(request, '200', payload);
this.networkErrorEvent(payload.errors[0].message);
request.reject(error);
} else {
if (!payload.hasOwnProperty('data')) {
const error = new Error(
'Server response was missing for query ' +
`\`${request.getDebugName()}\`.`
);
this.networkErrorEvent(error);
request.reject(error);
} else {
this.networkDoneLoadingEvent();
request.resolve({response: payload.data});
}
}
}).catch(
error =>
{
this.networkErrorEvent(error);
request.reject(error)
}
)
)));
}
sendMutation(request)
{
this.networkLoadingEvent();
return this._sendMutation(request).then(
result => result.json()
).then(payload =>
{
if (payload.hasOwnProperty('errors')) {
const error = CustomNetworkLayer.createRequestError(request, '200', payload);
this.networkErrorEvent(payload.errors[0].message);
request.reject(error);
} else {
this.networkDoneLoadingEvent();
request.resolve({response: payload.data});
}
}).catch(
error =>
{
this.networkErrorEvent(error);
request.reject(error)
}
);
}
/**
* Formats an error response from GraphQL server request.
*/
static formatRequestErrors(request, errors)
{
const CONTEXT_BEFORE = 20;
const CONTEXT_LENGTH = 60;
const queryLines = request.getQueryString().split('\n');
return errors.map(({locations, message}, ii) =>
{
const prefix = (ii + 1) + '. ';
const indent = ' '.repeat(prefix.length);
//custom errors thrown in graphql-server may not have locations
const locationMessage = locations ?
('\n' + locations.map(({column, line}) =>
{
const queryLine = queryLines[line - 1];
const offset = Math.min(column - 1, CONTEXT_BEFORE);
return [
queryLine.substr(column - 1 - offset, CONTEXT_LENGTH),
' '.repeat(Math.max(0, offset)) + '^^^',
].map(messageLine => indent + messageLine).join('\n');
}).join('\n')) :
'';
return prefix + message + locationMessage;
}).join('\n');
}
static createRequestError(request, responseStatus, payload)
{
const requestType =
request instanceof RelayMutationRequest ? 'mutation' : 'query';
const errorReason = typeof payload === 'object' ?
CustomNetworkLayer.formatRequestErrors(request, payload.errors) :
`Server response had an error status: ${responseStatus}`;
const error = new Error(
`Server request for ${requestType} \`${request.getDebugName()}\` ` +
`failed for the following reasons:\n\n${errorReason}`
);
error.source = payload;
error.status = responseStatus;
return error;
}
}
then in my index.js file I do this:
Relay.injectNetworkLayer(new CustomNetworkLayer("/graphql",
{
fetchTimeout: 35000, // timeout after 35 seconds
retryDelays: [2000] // retry after 2 seconds
}));
quick note: AppDispatcher is just a flux js dispatcher, and I'm listening to those events in my main wrapper component.
hope this helps someone else out. I certainly spent too much time on this.
Also thank you to everyone who helped me come to this solution.

Categories

Resources