React Native Webview onShouldStartLoadWithRequest does not detect history.push()? - reactjs

I'm trying to make my React web application native using React Native Webview. In order to implement native social oauth, I made a screen that requires users sign in and after the users sign in, a webView with my web application url is rendered:
const App = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
// Check if the app has valid tokens
}, []);
return (
<AuthContext.Provider
value={{
isLoggedIn,
login: () => setIsLoggedIn(true),
logout: () => setIsLoggedIn(false),
}}>
{isLoggedIn ? (
<SafeAreaView style={styles.container}>
<MyWebView />
</SafeAreaView>
) : (
<LoginScreen />
)}
</AuthContext.Provider>
);
};
If users navigate to a page in my domain, my app renders it in MyWebView, otherwise my app opens the os browser with the external url. Also, I want my app to confirm and set isLoggedIn to false so that the app stops rendering MyWebView and goes back to LoginScreen, if users try to navigate to ${BASE_URL}/logout, :
const MyWebView = () => {
const { logout: appLogout } = useContext(AuthContext);
const askForLogout = () => {
const title = 'Logout';
const message = 'Do you want to sign out?';
return new Promise((resolve, reject) => {
Alert.alert(
title,
message,
[
{ text: 'cancel', onPress: () => resolve(false) },
{ text: 'OK', onPress: () => resolve(true) },
],
{ cancelable: false },
);
reject('Error');
});
};
const onLogout = async () => {
try {
const shouldLogout = await askForLogout();
if (!shouldLogout) {
return;
}
await logout(); // This expires tokens
appLogout();
} catch (e) {
alertError();
}
};
return (
<WebView
source={{ uri: BASE_URL }}
sharedCookiesEnabled={true}
startInLoadingState={true}
renderLoading={() => <ActivityIndicator />}
onShouldStartLoadWithRequest={navState => {
const { url } = navState;
if (url.startsWith(BASE_URL)) {
if (url.includes('/logout')) {
onLogout();
return false;
}
return true;
} else {
// External Links
Linking.openURL(url);
return false;
}
}}
/>
);
};
export default MyWebView;
The problem is, onShouldStartLoadWithRequest sometimes (almost always) does not detect navigations using history.push(path) so my app cannot detect whether users navigate to /logout and call the logout function. onNavigationStateChange can detect history.push(path), but with onNavigationStateChange my app cannot stops external links from being rendered in MyWebView (This is bad because some external links use insecure HTTP, causing NSURLErrorDomain)
Any help would be really appreciated. Thank you!

Related

React Admin resource name with path variable

Is it possible to specify a Resource name in React Admin with a path variable?
Problem
When the user authenticate, if admin, should see a list of email accounts. Clicking on the email it redirects to the path experts/${email}/requests.
To map this path I need to define a new Resource for each email. The problem of this is that I have to do it before the authentication.
What I would like to have, is a path variable for the resource name. e.g. experts/:email/requests.
Example of the current implementation
function App() {
const [emails, setEmails] = useState<string[]>([]);
useEffect(() => {
(async () => {
const experts = await firebase.firestore().collection("experts").get();
const docIds: string[] = experts.docs.map((doc) => doc.id);
setEmails(docIds);
})();
}, []);
return (
<Admin
authProvider={authProvider}
dataProvider={dataProvider}
>
{(props) => {
if (props.admin) {
return [
emails.map((email) => {
return (
<Resource
key={email}
options={{ label: `${email}` }}
name={`experts/${email}/requests`}
list={RequestList}
edit={RequestEdit}
/>
);
}),
<Resource name={`experts`} list={ExpertList} />,
];
} else {
return [
<Resource
options={{ label: `${props.email}` }}
name={`experts/${props.email}/requests`}
list={RequestList}
edit={RequestEdit}
/>,
];
}
}}
</Admin>
);
}
Instead of a path variable (not possible with React Admin) I decided to override the login of the Firebase authProvider to fetch the data right after the user log in.
The timeout is needed because it can happen that the localStorage is setup after the first rendering.
let authProvider = FirebaseAuthProvider(firebaseConfig, options);
const login = authProvider.login;
authProvider = {
...authProvider,
login: async (params) => {
try {
const user = await login(params);
const experts = await firebase.firestore().collection("experts").get();
const docIds: string[] = experts.docs.map((doc) => doc.id);
localStorage.setItem("emails", JSON.stringify(docIds));
await timeout(300);
return user;
} catch (error) {
console.error("error", error);
}
},
};

How to prevent UI flickering while conditionally rendering components?

Consider the following code:
const Home = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
setUser(authUser);
} else {
setUser(null)
}
});
return () => unsubscribe();
}, []);
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
The Login component has all the functions which handles all the Sign Up, Login and Third-Party Authentications using Firebase.
The problems are:
When I reload the page and if the user is already logged in, it shows the component for some time, and then renders the component, which gives a bad UX.
Also, when I sign in using Google or Facebook, again this component is rendered before finally rendering the component.
Please throw some light into this issue. Your help will be highly appreciated!
Edit:
Problem 1 is solved, but problem 2 is not. Here is the relevant code for problem 2:
Login.js
<div style={{ marginBottom: "2%" }}>
<GoogleSignup />
</div>
GoogleSignup.js
import { GoogleLoginButton } from "react-social-login-buttons";
import firebase from "firebase";
import fire from "../fire";
const GoogleSignup = ({ extensionId }) => {
const OnSubmitButton = async () => {
var provider = new firebase.auth.GoogleAuthProvider();
fire
.auth()
.signInWithPopup(provider)
.then((result) => {
const credential = result.credential;
const token = credential.accessToken;
const user = result.user;
})
.catch((error) => {
console.log(error);
});
};
return (
<div>
<GoogleLoginButton
style={{ fontSize: "17px" }}
text={"Continue with Google"}
align={"center"}
onClick={OnSubmitButton}
/>
</div>
);
};
export default GoogleSignup;
These lines:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
setUser(authUser);
} else {
setUser(null)
}
});
return () => unsubscribe();
}, []);
can be replaced with just:
useEffect(() => auth.onAuthStateChanged(setUser), []);
Next, instead of passing in just null to the useState, pass in current user.
const [user, setUser] = useState(null);
becomes
const [user, setUser] = useState(auth.currentUser);
This results in:
const Home = () => {
const [user, setUser] = useState(auth.currentUser);
useEffect(() => auth.onAuthStateChanged(setUser), []);
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
Personally, I tend to use undefined/null/firebase.auth.User using:
const Home = () => {
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const loadingUser = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
if (loadingUser)
return null; // or show loading icon, etc.
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
After the popup has closed, Firebase Authentication still needs to handle the authentication flow of exchanging the provider's authentication token for a Firebase User token. While this is taking place, you should show some form of loading screen in your component. In the below code sample, I change the "Continue with Google" text to "Signing in..." and disable the onClick events for each button while the sign in process takes place.
import { GoogleLoginButton } from "react-social-login-buttons";
import firebase from "firebase";
import fire from "../fire";
const PROVIDER_ID_GOOGLE = firebase.auth.GoogleAuthProvider.PROVIDER_ID;
const ignoreOnClick = () => {};
const GoogleSignup = ({ extensionId }) => {
const [activeSignInMethod, setActiveSignInMethod] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (activeSignInMethod === null)
return; // do nothing.
let disposed = false, provider;
switch (activeSignInMethod) {
case PROVIDER_ID_GOOGLE:
provider = new firebase.auth.GoogleAuthProvider();
break;
default:
// this is here to help catch when you've added a button
// but forgot to add the provider as a case above
setError("Unsupported authentication provider");
return;
}
fire.auth()
.signInWithPopup(provider)
.then((result) => {
// const credential = result.credential;
// const token = credential.accessToken;
// const user = result.user;
if (!disposed) {
setError(null);
setActiveSignInMethod(null);
}
})
.catch((error) => {
console.error(`Failed to sign in using ${activeSignInMethod}`, error);
if (!disposed) {
setError("Failed to sign in!");
setActiveSignInMethod(null);
}
});
return () => disposed = true; // <- this is to prevent any "updating destroyed component" errors
}, [activeSignInMethod]);
return (
{ error && (<div key="error">{error}</div>) }
<div key="signin-list">
<GoogleLoginButton
style={{ fontSize: "17px" }}
text={
activeSignInMethod == PROVIDER_ID_GOOGLE
? "Signing in..."
: "Continue with Google"
}
align={"center"}
onClick={
activeSignInMethod === null
? () => setActiveSignInMethod(PROVIDER_ID_GOOGLE)
: ignoreOnClick
}
/>
</div>
);
};
export default GoogleSignup;

KeyCloak React refreshToken expired token

I want to implement authorization in my client-side application but I've got problem with update Token in React Application with Keycloak.
App.js
import keycloak from "../../keycloak";
const App = () => {
const handleOnEvent = async (event,error) => {
if(event === 'onTokenExpired'){
keycloak.updateToken(300).then(
(response) => {
//I want to update my existing Token
alert("response: ", response )
})
.catch(error => {
console.log("error: ", error)
})
}
}
return (
<>
<ReactKeycloakProvider
authClient={keycloak}
onEvent={(event,error) => handleOnEvent(event,error)}>
<AppRouter/>
</ReactKeycloakProvider>
</>)
}
export default App;
Header
const Header = () => {
const {keycloak,initialized} = useKeycloak()
useEffect(() => {
if(keycloak.authenticated){
alert(JSON.stringify(keycloak))
localStorage.setItem("keycloakToken", keycloak.token); //set keycloak token to localStorag
localStorage.setItem("keycloakRefreshToken", keycloak.refreshToken); // set refresh token
setJWTToken(keycloak.token) //set to axios Authorization Bearer
}
},[keycloak.authenticated])
return(
<>
{
keycloak && !keycloak.authenticated && <UnloggedHeader keycloak={keycloak}/>
}
{
keycloak && keycloak.authenticated && <LoggedHeader keycloak={keycloak}/>
}
</>
)
}
export default Header
UnloggedHeader
function UnloggedHeader({keycloak}){
const signIn = () => {
keycloak.login()
}
return (
<div style={{minWidth: '1100px'}}>
<AppBar position="sticky" color='transparent'>
<Toolbar>
<Button onClick={signIn} variant="contained" color="primary">Login</Button>
<Typography variant="body1" component="h6">Unlogged</Typography>
</Toolbar>
</AppBar>
</div>
);
}
export default UnloggedHeader
LoggedHeader
function LoggedHeader({keycloak}){
let history = useHistory()
const [anchorEl, setAnchorEl] = React.useState(null);
const isMenuOpen = Boolean(anchorEl);
const handleProfileMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const [userInfo,setUserInfo] = useState()
useEffect(() => {
keycloak.loadUserInfo().then(userInfo => {
setUserInfo(userInfo)
localStorage.setItem("username", userInfo.preferred_username); // set username of user
})
},[])
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleUserLogoutClick = () => {
keycloak.logout()
history.push("/")
}
return (
<div style={{minWidth: '1100px'}}>
<AppBar position="sticky" color='transparent'>
<Toolbar>
<Typography variant="body1" component="h6">{userInfo !== undefined ? userInfo.preferred_username : "EMPTY"}</Typography>
<ExpandMoreIcon/>
<Button onClick={handleUserLogoutClick} variant="contained" color="primary">Log out</Button>
</Toolbar>
</AppBar>
{renderMenu}
</div>
);
}
export default LoggedHeader
keycloak.js
import Keycloak from 'keycloak-js'
const keycloakConfig = {
url: 'http://10.192.168.72:8080/auth/',
realm: 'Realm12',
clientId: 'client',
}
const keycloak = new Keycloak(keycloakConfig);
export default keycloak
What I need provide to ReactKeycloakProvider to get new access_token when was expired ?
How based on refreshToken value get accessToken? I don't know which method or endpoint due to get this value. I can't find this kind of problem in network.
Please help me !
You can use event onTokens on Provider
<ReactKeycloakProvider
authClient={keycloak}
onTokens={({ token }) => {
// dispatch(setToken(token));
localStorage.setItem("keycloakToken", token);
}}
<AppRouter/>
</ReactKeycloakProvider>
And to trigger the update method, you can listen the event in your app router like this
export default function AppRouter() {
const { initialized, keycloak } = useKeycloak<KeycloakInstance>();
useEffect(() => {
if (keycloak && initialized) {
keycloak.onTokenExpired = () => keycloak.updateToken(600);
}
return () => {
if (keycloak) keycloak.onTokenExpired = () => {};
};
}, [initialized, keycloak]);
return (
<MyPreferedRouter>
<Switch />
</MyPreferedRouter>
);
}
Is working on #react-keycloak/ssr and i used this implementation with redux to have the token in the store
Don't forget to adapt keycloak.updateToken(600);
600 is number of seconds your minValidity
I made some investigation in this point because I couldn't get new token by refresh token, this is what worked with me
I used Keycloak end point:
https://<yourAuthLink>/auth/realms/<relmName>/protocol/openid-connect/token
with headers object
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}
and the body will be like that :
body: "client_id"=<clientId>&"grant_type"="refresh_token"&"refresh_token"=<refreshToken>&"client_secret"=<clientSecret>
this will return response which has access_token which you use as token and refresh_token to use it again before expiration time
it is useful link for this type of endpoint and headers
We use this flow
useEffect(() => {
dispatch(keycloak.token);
// and then save it to localStorage
}, [keycloak.token]);
useEffect(() => {
// jast in case
if(!initialized)
return;
if(!keycloak.authenticated)
return;
keycloak.onTokenExpired = () => {
keycloak.updateToken(50);
};
}, [keycloak.authenticated]);
But here I have a question: if the user sleep for a long time and then need to do some API request, so here I have to ask for refreshed token before request
but useKeycloak hook doesn't work in this case

How i can re-render Drawer in React Native?

I'm implementing for the first time the login with Mysql and php in react native, and everything works correctly but the only problem i have is that the content of the Drawer is not updated until i restart the app. I'm implementing AsyncStorage to save if the user is logged or not, if he is logged then return the My Profile button and if it does not return the Sign In button. I have tried this: props.navigation.state.params.refresh(); instead props.navigation.navigate('home'); but does not work for me. Any suggestion? I'm doing something wrong?
Login Function
const login = async() => {
if (email, password) {
signInApi(email, password).then(response => {
if (response != 'error') {
setLogged(true);
props.navigation.navigate('home');
}else if(response === 'error'){
setLogged(false);
}
});
}else{
Alert.alert('Complete Form');
}
}
DrawerContent
export default function DrawerContent(props){
const {navigation} = props;
const [isLogged, setisLogged] = useState(null);
const onChangeScreen = (screen) => {
navigation.navigate(screen);
};
const checkLogged = async () => {
const response = await getLogged();
setisLogged(response);
}
useEffect(() => {
checkLogged();
}
}, []);
renderItem = () => {
if (!isLogged || isLogged === 'false') {
return (
<Button onPress={() => onChangeScreen("signin")}>
Sign In
</Button>
);
}else{
return (
<Button onPress={() => onChangeScreen("profile")}>
My Profile
</Button>
);
}
};
return (
{this.renderItem()}
);
Navigation Drawer
import DrawerContent from './DrawerContent';
const Drawer = createDrawerNavigator();
const DrawerNav = () => {
return (
<Drawer.Navigator initialRouteName="app" drawerContent={(props) => <DrawerContent {...props} />}>
<Drawer.Screen name="app" component={StackNavigation} />
</Drawer.Navigator>
);
};
export default DrawerNav;

Why is this React Component rendering first?

I could use your input on a quick question about Component loads.
The Goal
Return the <Login /> Component if the user isn't logged in, and the App if they are.
Expected Behavior
When a user is logged in, they see the App.
Observed Behavior
The <Login /> Component flickers (renders) for a moment, then the user sees the App.
My goal is to eliminate this flicker!
Code Samples
Index.js
export default function Index() {
let [isLoading, setIsLoading] = useState(true)
const router = useRouter()
// User object comes in from an Auth Context Provider
const { user } = useContext(AuthContext)
const { email } = user
useEffect(() => {
if (user) {
setIsLoading(false)
}
}, [])
// Returns the App if logged in, login screen if not
const getLoggedIn = () => {
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
}
return (
<Box className="App">
{ isLoading
? <div className={classes.root}>
<LinearProgress />
</div>
: getLoggedIn()
}
</Box>
)
}
Auth Context
Note: I'm using Firebase for auth.
// Listens to auth state changes when App mounts
useEffect(() => {
// Calls setUser state update method on callback
const unsubscribe = onAuthStateChange(setUser)
return () => {
unsubscribe()
}
}, [])
// Brings data from auth to Auth Context user state via callback
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
Stack
"next": "^8.1.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
Thanks so much for taking a look.
I had this exact problem and resolved it by storing the user in local storage
then on app start up do this:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')))
and it'll use the details from localstorage and you wont see a flicker
(it's because onauthstate takes longer to kick in)
So I figured out a sort of 'hacky' way around this. One needs to set the value of the boolean on which the initial load of the App depends...
const getLoggedIn = () => {
// Right here
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
...before making any asynchronous calls in the AuthContext. Like this:
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
// sets loggedIn to true to prevent flickering to login screen on load
callback({ loggedIn: true })
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
I hope this helps someone.

Resources