React Native WebView - Open link in external browser only in case of user interaction - react-native

I have the following WebView component that is used multiple times for various embeds inside news articles
<WebView
useWebKit={true}
ref={(ref) => this.webview = ref}
javaScriptEnabled={true}
injectedJavaScript={"(" + injectedScript + ")()"}
onMessage={this.onMessage}
onNavigationStateChange={this.onNavigationStateChange}
{...this.props}
/>
and the following navigation state change listener, used for opening links in an external browser
onNavigationStateChange = (event) => {
this.webview.stopLoading();
Linking.openURL(event.url);
}
The problem is that the navigation change listener doesn't seem capable of differentiating between user interaction and automatic page redirects (which social media embeds will often do). How can I open the link in an external browser ONLY for user interaction?

I don't know exactly if automatic page redirects trigger click events, but they should not, so you can listen to only click events, which I suppose can only be triggered by the user.
onNavigationStateChange = (navState) =>
{
if (navState.navigationType === 'click') {
// User clicked something
}
}

Related

Logout user by sending them to a Logout Webview

I'm having a difficult time trying to figure out how to log out both the user on the expo app as well as logging them out from the react-native-webview.
The user can log out of the expo app by pressing logout (which just handles the auth logic), however; the user is still logged in inside the webview (which is the shop uri).
At the moment, I came up with two possible ways: either set incognito to true on the webview, or send the user to a Screen which returns another webview to the shopify's logout uri.
// sending user to shopify logout webview
//navigator
<Stack.Navigator>
<Stack.Screen
name="AccountDetails"
component={AccountDetailsScreen}
/>
<Stack.Screen
name="ShopWebview"
component={ShopWebview}
/>
<Stack.Screen
name="LogOut"
component={LogOutWebView}
/>
</Stack.Navigator>
// from `AccountDetails` component, user presses handleLogout()
const handleLogOut = () => navigation.navigate('LogOut');
// shopify logout webview
export const LogOutWebView = () => {
return (
<WebView
source={{ uri: 'https://www.quarantotto.co.jp/account/logout' }}
/>
);
};
This seems to work w/ android, but it doesn't seem to work on iOS? I'm not quite sure why that is.
I can still set the incognito to true likeso:
// ShopWebview
<WebView
incognito
source={{ uri: 'someshop.com/' }}
/>
and this would help me not store the logged in user's session, but not quite sure if this is the proper way to handle auth.
If I can use the first method of sending the user to a webview that handles logout when a user visits it, that would be great. But not sure why it's not working from ios.
any ideas?
UPDATE: possible working solution?
Whenever the user logs out, they are sent back to a screen w/ login or register.
The issue was that whenever the user pressed login, since their session was cached, they are redirect to their account screen (which is what I don't want since if a user presses logout, I want them to be logged out and have to log back in).
what I have done is just inject some js in the LoginWebview
const handleNavigationStateChange = (newNavState) => {
if (webViewRef.current) {
webViewRef.current.injectJavaScript(scriptFetchCustomer());
}
if (newNavState.url.includes('logout')) {
setTimeout(() => {
const redirectTo = `window.location = "${someshop.com/login}"`;
webViewRef.current.injectJavaScript(redirectTo);
}, 500);
}
};
seems to work, but I don't enjoy using the settimeout. any recommendations?
second update... doesn't work on android
Android isn't working properly. I've set the source to the shopify's /account/logout uri, but inside handleNavigationStateChange the newNavState's url isn't the logout as it's set on the source.
....
Last update:
It finally works as I wanted to. I had to check for the url changes inside onNavigationStateChange of the webview. Then I could do what I wanted. I basically sent user to /account/logout, then set current logged in user state back into null which fires a redirect to login screen.
Hope it helps someone.

Accessing an intranet Sharepoint page using React Native webview

using some basic code, I am trying to access a sharepoint page:
import { WebView } from 'react-native-webview'
const ExampleScreen = ({ navigation }) => {
return (
<WebView style={{flex: 1}}
source={{uri: 'https://example.intra.net/sites/1/ProjectName/SitePages/Nameofpage.aspx'}}/>
)
}
export default ExampleScreen;
When I pull up the page on Safari, I can see it, because I'm logged in. However when I try to look at the page in the Expo Go test build, it says "401 Unauthorized". What is the process for authenticating something like this in a native app? I am a novice and I don't understand how it works.

How to setup sendSignInLinkToEmail() from Firebase in react-native?

Working on a react-native project using #react-native-firebase/app v6 we recently integrated signing in with a "magic link" using auth.sendSignInLinkToEmail
We couldn't find a good example on how to setup everything in react-native and had different problems like
- auth/invalid-dynamic-link-domain - The provided dynamic link domain is not configured or authorized for the current project.
- auth/unauthorized-continue-uri - Domain not whitelisted by project
Searching for information and implementing the "magic link login" I've prepared a guide on how to have this setup in react-native
Firebase project configuration
Open the Firebase console
Prepare firebase instance (Email Link sign-in)
open the Auth section.
On the Sign in method tab, enable the Email/Password provider. Note that email/password sign-in must be enabled to use email link sign-in.
In the same section, enable Email link (passwordless sign-in) sign-in method.
On the Authorized domains tab (just bellow)
Add any domains that will be used
For example the domain for the url from ActionCodeSettings needs to be included here
Configuring Firebase Dynamic Links
For IOS - you need to have an ios app configured - Add an app or specify the following throughout the firebase console
Bundle ID
App Store ID
Apple Developer Team ID
For Android - you just need to have an Android app configured with a package name
Enable Firebase Dynamic Links - open the Dynamic Links section
“Firebase Auth uses Firebase Dynamic Links when sending a link that is meant to be opened in a mobile application.
In order to use this feature, Dynamic Links need to be configured in the Firebase Console.”
(ios only) You can verify that your Firebase project is properly configured to use Dynamic Links in your iOS app by opening
the following URL: https://your_dynamic_links_domain/apple-app-site-association
It should show something like:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "AP_ID123.com.example.app",
"paths": [
"NOT /_/", "/"
]
}
]
}
}
IOS Xcode project configuration for universal links
Open the Xcode project and go to the Info tab create a new URL type to be used for Dynamic Links.
Enter a unique value in Identifier field and set the URL scheme field to be your bundle identifier, which is the default URL scheme used by Dynamic Links.
In the Capabilities tab, enable Associated Domains and add the following to the Associated Domains list: applinks:your_dynamic_links_domain
(!) This should be only the domain - no https:// prefix
Android
Android doesn’t need additional configuration for default or custom domains.
Packages
A working react-native project setup with react-native-firebase is required, this is thoroughly covered in the library own documentation, here are the specific packages we used
Note: using the dynamicLinks package can be replaced with react-native's own Linking module and the code would be almost identical
Exact packages used:
"#react-native-firebase/app": "^6.7.1",
"#react-native-firebase/auth": "^6.7.1",
"#react-native-firebase/dynamic-links": "^6.7.1",
Sending the link to the user email
The module provides a sendSignInLinkToEmail method which accepts an email and action code configuration.
Firebase sends an email with a magic link to the provided email. Following the link has different behavior depending on the action code configuration.
The example below demonstrates how you could setup such a flow within your own application:
EmailLinkSignIn.jsx
import React, { useState } from 'react';
import { Alert, AsyncStorage, Button, TextInput, View } from 'react-native';
import auth from '#react-native-firebase/auth';
const EmailLinkSignIn = () => {
const [email, setEmail] = useState('');
return (
<View>
<TextInput value={email} onChangeText={text => setEmail(text)} />
<Button title="Send login link" onPress={() => sendSignInLink(email)} />
</View>
);
};
const BUNDLE_ID = 'com.example.ios';
const sendSignInLink = async (email) => {
const actionCodeSettings = {
handleCodeInApp: true,
// URL must be whitelisted in the Firebase Console.
url: 'https://www.example.com/magic-link',
iOS: {
bundleId: BUNDLE_ID,
},
android: {
packageName: BUNDLE_ID,
installApp: true,
minimumVersion: '12',
},
};
// Save the email for latter usage
await AsyncStorage.setItem('emailForSignIn', email);
await auth().sendSignInLinkToEmail(email, actionCodeSettings);
Alert.alert(`Login link sent to ${email}`);
/* You can also show a prompt to open the user's mailbox using 'react-native-email-link'
* await openInbox({ title: `Login link sent to ${email}`, message: 'Open my mailbox' }); */
};
export default EmailLinkSignIn;
We're setting handleCodeInApp to true since we want the link from the email to open our app and be handled there. How to configure and handle this is described in the next section.
The url parameter in this case is a fallback in case the link is opened from a desktop or another device that does not
have the app installed - they will be redirected to the provided url and it is a required parameter. It's also required to
have that url's domain whitelisted from Firebase console - Authentication -> Sign in method
You can find more details on the supported options here: ActionCodeSettings
Handling the link inside the app
Native projects needs to be configured so that the app can be launched by an universal link as described
above
You can use the built in Linking API from react-native or the dynamicLinks #react-native-firebase/dynamic-links to intercept and handle the link inside your app
EmailLinkHandler.jsx
import React, { useState, useEffect } from 'react';
import { ActivityIndicator, AsyncStorage, StyleSheet, Text, View } from 'react-native';
import auth from '#react-native-firebase/auth';
import dynamicLinks from '#react-native-firebase/dynamic-links';
const EmailLinkHandler = () => {
const { loading, error } = useEmailLinkEffect();
// Show an overlay with a loading indicator while the email link is processed
if (loading || error) {
return (
<View style={styles.container}>
{Boolean(error) && <Text>{error.message}</Text>}
{loading && <ActivityIndicator />}
</View>
);
}
// Hide otherwise. Or show some content if you are using this as a separate screen
return null;
};
const useEmailLinkEffect = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const handleDynamicLink = async (link) => {
// Check and handle if the link is a email login link
if (auth().isSignInWithEmailLink(link.url)) {
setLoading(true);
try {
// use the email we saved earlier
const email = await AsyncStorage.getItem('emailForSignIn');
await auth().signInWithEmailLink(email, link.url);
/* You can now navigate to your initial authenticated screen
You can also parse the `link.url` and use the `continueurl` param to go to another screen
The `continueurl` would be the `url` passed to the action code settings */
}
catch (e) {
setError(e);
}
finally {
setLoading(false);
}
}
};
const unsubscribe = dynamicLinks().onLink(handleDynamicLink);
/* When the app is not running and is launched by a magic link the `onLink`
method won't fire, we can handle the app being launched by a magic link like this */
dynamicLinks().getInitialLink()
.then(link => link && handleDynamicLink(link));
// When the component is unmounted, remove the listener
return () => unsubscribe();
}, []);
return { error, loading };
};
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFill,
backgroundColor: 'rgba(250,250,250,0.33)',
justifyContent: 'center',
alignItems: 'center',
},
});
const App = () => (
<View>
<EmailLinkHandler />
<AppScreens />
</View>
);
You can use the component in the root of your app as in this example
Or you can use it as a separate screen/route - in that case the user should be redirected to it after
the sendSignInLinkToEmail action
Upon successful sign-in, any onAuthStateChanged listeners will trigger with the new authentication state of the user. The result from the signInWithEmailLink can also be used to retrieve information about the user that signed in
Testing the email login link in the simulator
Have the app installed on the running simulator
Go through the flow that will send the magic link to the email
Go to your inbox and copy the link address
Open a terminal and paste the following code
xcrun simctl openurl booted {paste_the_link_here}
This will start the app if it’s not running
It will trigger the onLink hook (if you have a listener for it like above)
References
Deep Linking In React Native Using Firebase Dynamic Links
React Native Firebase - Dynamic Links
React Native Firebase - auth - signInWithEmailLink
firebase.google.com - Passing State In Email Actions
firebase.google.com - Authenticate with Firebase Using Email Link in JavaScript

webview onNavigationStateChange returns url of previous page

In react native webview, when I navigate to a page the onNavigationStateChange returns url of the previous page? I think its an issue and posted it to github react native issues.
<WebView
ref={r => this.webview = r}
style = {{marginTop : 0}}
onNavigationStateChange=
{this._onNavigationStateChange.bind(this)}
startInLoadingState = {true}
source = {{uri: 'https://www.youtube.com' }}
/>
_onNavigationStateChange(webViewState)
{
this.setState(
{
youtube_video_url: webViewState.url
})
}
When I print this youtube_video_url, it is of the previous page or previous browsed video.
Sadly, all the webview methods like onNavigationStateChange, onLoad, onLoadStart etc are not working as expected in cases of single page application websites or websites that don't trigger a window.load event.
Normaly onNavigationStateChange gets triggered in the beginning of a request with {loading: true, url: "someUrl", ...} and once after the loading of the url with {loading: false, url: "someUrl", ...}.
There is no global workaround (to my knowledge) that will work correctly for all websites and for both iOs/Android but maybe this can help in your case:
https://snack.expo.io/H1idX8vpM
Basically you inject a custom javascript in the webview that notifies the WebView which is the loaded page. You can enhance it if you want to catch custom window.history manipulation events.

react native navigationExperimental NavigationCardStack renderScene didn't re-render for previous routes

I already checked similar existing issues on stack-overflow or gihub, like:
NavigationCard will only re-render when the route changes, Help: renderScene is executed just once and does not refresh updated props, etc. But my case is different.
Page list:
sign in page
home page: user can only see this page after sign in;
Transition logic:
In "sign in page", after sign in, it shall go to "home page"
In "home page", there is a sign out button, after user click, it shall go back to "sign in" page.
My implementation
I created a top level component called App, and the code looks like the below:
// app.js
class App extends Component {
componentWillMount() {
const {doNavigateReplaceAtIndex} = this.props;
let {isSignedIn} = this.props;
doNavigateReplaceAtIndex(isSignedIn? 'home': 'sign-in');
}
render() {
const {globalNavigationState} = this.props;
return (
<NavigationCardStack
navigationState={globalNavigationState}
style={styles.container}
onNavigate={()=>{}}
renderScene={this._renderScene}
/>
);
}
_renderScene({scene}) {
const { route } = scene;
switch(route.key) {
case 'home':
return <Home />
case 'sign-in':
return <SignIn />
}
}
}
export default connect (
state =>({
isSignedIn: !! state.auth.token,
token: state.auth.token,
globalNavigationState: state.globalNavigation
}),
dispatch => ({
doNavigateReplaceAtIndex: (key) => dispatch(navigateReplaceAtIndex(0, key))
})
)(App);
// sign in page
// after signin, it will doNavigateReplaceAtIndex(0, 'home');
// home page
// after clicking "sign out", it will doNavigateReplaceAtIndex(0, 'sign-in');
// doNavigateReplaceAtIndex basically is just call NavigationStateUtils.replaceAtIndex
Symptoms
At beginning, it shows sign in page, after signing in, it goes to home page, it is good. In home page, when click the sign out button, it didn't move anywhere, but when refresh, it shows sign in page.
What I got so far
It is not because of this issue: NavigationCard will only re-render
when the route
changes,
because I debugged into rn source code, the shouldComponentUpdate didn't block;
I am not sure if I didn't right for doNavigateReplaceAtIndex, usually we use push or pop, but my case I cannot use push/pop, because after sign in, we should not allow use to go back sign in page by clicking "BACK" button on Android.
I think the issue may because of NavigationScenesReducer(which is called by NavigationAnimatedView used in NavigationCardStack), it will mark all previous routes as stale, and will not show it them.
Any help will be welcome, thanks.
My environment
react native: 0.29.1
react native cli: 1.0.0
node: 5.6.0
OS: ios 9.3