You have an idea. You already had a responsive web app, it works well for mobile. It leads you to the thought of having a MVP mobile app to engage users: an app is built by embedded web-view. You also know React Native can help you build a cross-platform app. Very good solution: a React Native app with few WebView screens that point to correct links. Life is so easy, right? Unfortunately, it’s not.
Problems
You are right. It’s a great solution until you need to implement the authentication. There are at least 2 problems if your app requires user login.
Firstly, “remember me” doesn’t work although it works well on web. You need to enter credential whenever the app opens (in cold start).
Secondly, 3rd party authenticators like Google, Facebook… doesn’t work. You tap on “Login by Facebook”, Facebook login page is opened inside the app. Although you already logged in on Facebook app or OS browser, you need to enter Facebook credential whenever the app opens.
The reason is that WebView doesn’t share any data with OS browser. Each time the app starts, a “fresh” browser is initiated. It means there is no cookies, token, history… are stored by default.
There is only one weird function on your app while the rest are working perfectly. Yes, only one. But it kills your whole app. Do you know anyone who is willing to type username & password anytime he opens the app? Yes, if the app relates to very sensitive personal data like personal finance. But your app doesn’t.
Solutions
There are many solutions. I will describe the simplest one with less code change in your existing web app.
Facebook login
Let’s firstly solve the problem with 3rd party authenticators. I use Facebook as an example, you could treat others in the same way. The steps could be:
- Handle event in WebView to detect user tapping on Facebook login button
- Open client Facebook login, get the access token
- Pass token to server to authenticate user
First step is setting up the Facebook SDK, please follow here: https://github.com/facebook/react-native-fbsdk
Second step is handling user tapping on Facebook login link on app, onNavigationStateChange of WebView is the good function to work on. Passing the token to server could be simply made by GET calling from WebView.
The code looks like:
import React from 'react'; import { View, ActivityIndicator } from 'react-native'; import { WebView } from 'react-native-webview' import { LoginManager, AccessToken } from "react-native-fbsdk"; class App extends React.Component { webViewRef; constructor(props) { super(props); this.state = { facebookToken: null }; } onNavigationStateChange(navState) { var url = navState.url; var self = this; if (url.includes('facebook.com')) { webViewRef.stopLoading(); } if (url.includes('https://facebook.com') || url.includes('https://www.facebook.com')) { LoginManager.logOut() LoginManager.logInWithReadPermissions(["email", "public_profile", "user_link"]).then( function (result) { if (result.isCancelled) { if (webViewRef.canGoBack()) webViewRef.goBack(); self.setState({ facebookToken: null }) } else { AccessToken.getCurrentAccessToken().then( (data) => { self.setState({ facebookToken: data.accessToken.toString(), }) } ) } }, function (error) { if (webViewRef.canGoBack()) webViewRef.goBack(); self.setState({ facebookToken: null }) } ); } } render() { return ( <View> <WebView ref={WEBVIEW_REF => (webViewRef = WEBVIEW_REF)} source={{ uri: (this.state.facebookToken) ? "https://yourcompany.com/FacebookNativeLogin?accessToken=" + this.state.facebookToken : "https://yourcompany.com/Login" }} onNavigationStateChange={this.onNavigationStateChange.bind(this)} /> </View> ); } } export default App;
There are some notes:
- Line 20: we should let WebView stop loading to avoid strange UI: a popup of Facebook login on app on the top of Facebook web login screen (on app). It also stops WebView being redirected from https://facebook.com to https://m.facebook.com that lets the popup show twice.
- Line 24: Facebook LoginManager caches the access token, calling logOut() before logInWithReadPermissions() helps app detects if user logout the app from other Facebook areas (web etc).
- Line 64: you need to implement FacebookNativeLogin on server side then setting cookies etc.
Now it works? Yes, but not really. User is still required to press Facebook login when he opens the app. If you want “auto login”, componentDidMount could be a simple solution.
componentDidMount() { var self = this; AccessToken.getCurrentAccessToken().then( (data) => { if (data && data.accessToken) { self.setState({ facebookToken: data.accessToken.toString(), }) } else { self.setState({ facebookToken: null }); } } );
Remember me
Now it’s time for “remember me” function. The common solution is using the cookies. If you already had it running for web, your hands wouldn’t be much dirty. There is a simple solution with CookieManager, you could find it here: https://github.com/joeferraro/react-native-cookies
Well, let’s combine it with componentDidMount above:
componentDidMount() { CookieManager.get('https://yourcompany.com').then((res) => { if (res['.App.ApplicationCookie']) { this.setState({ loggedIn: true, facebookToken: null }); } else { var self = this; AccessToken.getCurrentAccessToken().then( (data) => { if (data && data.accessToken) { self.setState({ loggedIn: true, facebookToken: data.accessToken.toString(), }) } else { self.setState({ loggedIn: false, facebookToken: null }); } } ) } }); }
Some notes:
- Line 3: replace it with your correct cookies
- Line 5: I introduce loggedIn state because Facebook login is separated with your system username/password login. It lets you update uri in WebView of render() function above with a condition combined by loggedIn and facebookToken. But it’s not difficult, right? Hmm, loggedIn isn’t a good name.
Done? Almost. You could see it works really well on Android but not on iOS. In cold start, CookieManager.get() returns value on Android but doesn’t on iOS. Then storing and restoring cookies programmatically is a solution. AsyncStorage could help https://facebook.github.io/react-native/docs/asyncstorage.html. Just add the few lines of code to onNavigationStateChange to store the cookie and set it back on componentDidMount. Fortunately, CookieManager.set() only works on iOS.
Know issues
I hope it help you overcome the common issue of web-view app. I don’t want to put the setting cookies into existing functions because they are too long now. You should refactor it after getting it run.
Is the solution simple? Yes. Is it perfect? No. There are some small issues:
- We call GET to FacebookNativeLogin with accessToken. Might POST be a bit better?
- What if the cookies is expired? Is it good to still keep user login? A solution is storing username/password by AsyncStorage and injecting Javascript to auto fill username/password text fields. But I don’t prefer storing any data like that.
But if you are interested in those topics, I will go into the example next post.